【Threejs进阶教程-着色器篇】3. Uniform的基本用法2与基本地球昼夜效果

前两篇地址,请按顺序学习

【Threejs进阶教程-着色器篇】1. Shader入门(ShadertoyShader和ThreejsShader入门)
【Threejs进阶教程-着色器篇】2. Uniform的基本用法与Uniform的调试

本篇使用到的资源

threejs开发包中
three/examples/textures/plantes/earth_atmos_2048.jpg 注意这张图片后缀是jpg!
three/examples/textures/plantes/earth_lights_2048.png
three/examples/textures/transition/transition5.png

用uniform传递纹理

有些时候不是说所有的图形效果都需要用数学去实现,还可以使用贴图

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        body{
            width:100vw;
            height: 100vh;
            overflow: hidden;
            margin: 0;
            padding: 0;
            border: 0;
        }
    </style>
</head>
<body>
<script type="importmap">
			{
				"imports": {
					"three": "../three/build/three.module.js",
					"three/addons/": "../three/examples/jsm/"
				}
			}
		</script>

<script type="x-shader/x-vertex" id="vertexShader">
    varying vec2 vUv;
    void main(){
        vUv = vec2(uv.x,uv.y);
        vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
        gl_Position = projectionMatrix * mvPosition;
    }

</script>
<script type="x-shader/x-fragment" id="fragmentShader">
    varying vec2 vUv;
    uniform sampler2D uDiffuse1;
    void main(){
        vec4 col = texture2D(uDiffuse1,vUv);
        gl_FragColor = col;
    }
</script>

<script type="module">

    import * as THREE from "../three/build/three.module.js";
    import {OrbitControls} from "../three/examples/jsm/controls/OrbitControls.js";

    window.addEventListener('load',e=>{
        init();
        addMesh();
        render();
    })

    let scene,renderer,camera;
    let orbit;

    function init(){

        scene = new THREE.Scene();
        renderer = new THREE.WebGLRenderer({
            alpha:true,
            antialias:true
        });
        renderer.setSize(window.innerWidth,window.innerHeight);
        document.body.appendChild(renderer.domElement);

        camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,0.1,2000);
        camera.add(new THREE.PointLight());
        camera.position.set(15,15,15);
        scene.add(camera);

        orbit = new OrbitControls(camera,renderer.domElement);
        orbit.enableDamping = true;
        scene.add(new THREE.GridHelper(10,10));
    }

    let uniforms = {
        uDiffuse1:{value:null}
    }

    function addMesh() {

        let textureLoader = new THREE.TextureLoader();

        uniforms.uDiffuse1.value = textureLoader.load('./earth_atmos_2048.jpg');

        let geometry = new THREE.SphereGeometry(5,32,32);
        let material = new THREE.ShaderMaterial({
            uniforms,
            vertexShader:document.getElementById('vertexShader').textContent,
            fragmentShader:document.getElementById('fragmentShader').textContent,
        })
        let mesh = new THREE.Mesh(geometry,material);
        scene.add(mesh);
    }


    function render() {
        renderer.render(scene,camera);
        orbit.update();
        requestAnimationFrame(render);
    }

</script>
</body>
</html>

代码效果
在这里插入图片描述

代码分析

texture类型的uniform

shader允许传递一个texture类型的对象到uniform,这里在threejs中,对应的是 THREE.Texture类型的对象,也就是TexthreLoader读取出来的图片,并转换成的texture实例

注意,一般情况下,图片的使用要考虑异步,本地化的读取效率非常高,不会出现渲染延迟,线上的话,可能会让地球变成一个黑色或者白色的球体

这个uniform的写法有两种,一种是像上面一样先赋值null,然后再读取,另一种是在读取的时候创建uniform的key,任选一个自己喜欢的风格即可,没有太多的要求,只是注意,给uniform赋值要这样写

//如果采用第二种写法,可以这样写
uniforms.uDiffuse1 = {value:texture}

在shader中接收uniform

    varying vec2 vUv;
    uniform sampler2D uDiffuse1; //这里在shader中,对应sampler2D这个类型
    void main(){
        vec4 col = texture2D(uDiffuse1,vUv); // 对图片逐uv取色
        gl_FragColor = col;
    }

texture2D()

texture2D函数的参数有两个,第一个是一个vec2的对象,第二个是一个sampler2D类型的数据
一般前者我们都使用uv,这个就是最基本的贴图代码,读取图片的uv并将颜色给到指定uv的顶点处

我们把几何体换成正方向平面,这样看的更明显一些

在这里插入图片描述
可以看出,在正方形平面的贴图上,图片出现了压缩,这是因为图片本身并不是一个正方形,但是以逐uv的形式来读取了这张图片,所以最终造成了压缩的问题

处理图片压缩

这里我们在shader中,把vUv.x 放大即可

    varying vec2 vUv;
    uniform sampler2D uDiffuse1;
    void main(){
        vec2 uUv = vec2(vUv.x / 2.0, vUv.y);
        vec4 col = texture2D(uDiffuse1,uUv);
        gl_FragColor = col;
    }

我们把uv.x除以2.0,这样我们就只加载 0 < uv.x < 0.5范围内的图片, 0< uv.y < 1的图片,所以我们可以看到,y轴没有变化,而x轴拉回去了
我们也可以继续修改,看看对uv的xy都乘或除2.0的效果怎么样

在这里插入图片描述
发现新的问题了,我们在除以2.0的情况下,我们只取了图片的左下角,但是乘以2.0的时候,并不如我们想的结果那样,平铺四张图

所以这里我们要对纹理做一下处理

修改wrapS和wrapT

        uniforms.uDiffuse1.value = textureLoader.load('./earth_atmos_2048.jpg');
        uniforms.uDiffuse1.value.wrapT = THREE.RepeatWrapping;
        uniforms.uDiffuse1.value.wrapS = THREE.RepeatWrapping;
    varying vec2 vUv;
    uniform sampler2D uDiffuse1;
    void main(){
        vec2 uUv = vec2(vUv.x * 2.0, vUv.y * 2.0);
        vec4 col = texture2D(uDiffuse1,uUv);
        gl_FragColor = col;
    }

在这里插入图片描述

这样,我们就解决了没有平铺图片的问题,这是一种通过shader的方式,来改变图片平铺方式的解决办法

切换成夜景

首先我们引入第二张图片,并改回球体和uv,并额外添加一个 uChange属性

修改代码

    let uniforms = {
        uDiffuse1:{value:null},
        uDiffuse2:{value:null},
        uChange:{value:0.0}
    }

    function addMesh() {

        let textureLoader = new THREE.TextureLoader();

        uniforms.uDiffuse1.value = textureLoader.load('./earth_atmos_2048.jpg');
        uniforms.uDiffuse1.value.wrapT = THREE.RepeatWrapping;
        uniforms.uDiffuse1.value.wrapS = THREE.RepeatWrapping;

        uniforms.uDiffuse2.value = textureLoader.load('./earth_lights_2048.png');
        uniforms.uDiffuse2.value.wrapT = THREE.RepeatWrapping;
        uniforms.uDiffuse2.value.wrapS = THREE.RepeatWrapping;

        let geometry = new THREE.SphereGeometry(5,32,32);
        let material = new THREE.ShaderMaterial({
            uniforms,
            vertexShader:document.getElementById('vertexShader').textContent,
            fragmentShader:document.getElementById('fragmentShader').textContent,
        })
        let mesh = new THREE.Mesh(geometry,material);
        scene.add(mesh);

        let gui = new GUI();
        gui.add(uniforms.uChange,'value',0,1).name('渐变');
    }

片元着色器代码

    varying vec2 vUv;
    uniform sampler2D uDiffuse1;
    uniform sampler2D uDiffuse2;
    uniform float uChange;
    void main(){
        vec4 col = texture2D(uDiffuse2,vUv);
        gl_FragColor = col;
    }

在这里插入图片描述

效果切换

我们现在要做的是,如果uChange = 1 ,则显示白天的贴图,如果uChange = 0,则显示夜晚贴图

    varying vec2 vUv;
    uniform sampler2D uDiffuse1;
    uniform sampler2D uDiffuse2;
    uniform float uChange;
    void main(){
        vec4 col1 = texture2D(uDiffuse1,vUv);
        vec4 col2 = texture2D(uDiffuse2,vUv);
        gl_FragColor = vec4(
            col1.r * uChange + col2.r * (1.0 - uChange),
            col1.g * uChange + col2.g * (1.0 - uChange),
            col1.b * uChange + col2.b * (1.0 - uChange),
            col1.a * uChange + col2.a * (1.0 - uChange)
        );
    }

我们这样想,既然change = 0.1的时候,那么此时图片1的颜色值为最淡,然后图片2的颜色值为最深,对应rgb三个颜色都是这样的结果

在这里插入图片描述

Mix()

但是其实,这个算法,官方早就给你想好了,我们只需使用mix即可,下面两种写法的效果完全一致

//旧代码
        gl_FragColor = vec4(
            col1.r * uChange + col2.r * (1.0 - uChange),
            col1.g * uChange + col2.g * (1.0 - uChange),
            col1.b * uChange + col2.b * (1.0 - uChange),
            col1.a * uChange + col2.a * (1.0 - uChange)
        );
//新代码
gl_FragColor = mix(col2,col1,uChange);

mix是个非常非常常用的函数,主要用来线性混合数据,mix可以适用于很多种类型的数据,也有很多种用法,后续会经常用到和提到mix
在这里插入图片描述

昼夜切换升级

细心的朋友注意到了,这个切换效果,是全地球一起在切换,而并不是在模拟日出日落的那种昼夜切换,所以这个时候我们就要用到特殊的一种混合模式,这里我们加载第三张图,且拿掉uChange这个uniform

改动代码

我们加入第三张贴图,且加入时间变量iTime
由于在shadertoy中的时间变量也是iTime,所以后续所有的教程中,出现的时间变量都会命名为iTime

    let uniforms = {
        uDiffuse1:{value:null},
        uDiffuse2:{value:null},
        uChangeTexture:{value:null},
        iTime:{value:0.01}
    }

    function addMesh() {

        let textureLoader = new THREE.TextureLoader();

        uniforms.uDiffuse1.value = textureLoader.load('./earth_atmos_2048.jpg');
        uniforms.uDiffuse1.value.wrapT = THREE.RepeatWrapping;
        uniforms.uDiffuse1.value.wrapS = THREE.RepeatWrapping;

        uniforms.uDiffuse2.value = textureLoader.load('./earth_lights_2048.png');
        uniforms.uDiffuse2.value.wrapT = THREE.RepeatWrapping;
        uniforms.uDiffuse2.value.wrapS = THREE.RepeatWrapping;

        uniforms.uChangeTexture.value = textureLoader.load('./transition5.png');
        uniforms.uChangeTexture.value.wrapT = THREE.RepeatWrapping;
        uniforms.uChangeTexture.value.wrapS = THREE.RepeatWrapping;


        let geometry = new THREE.SphereGeometry(5,32,32);
        let material = new THREE.ShaderMaterial({
            uniforms,
            vertexShader:document.getElementById('vertexShader').textContent,
            fragmentShader:document.getElementById('fragmentShader').textContent,
        })
        let mesh = new THREE.Mesh(geometry,material);
        scene.add(mesh);
    }


    function render() {
        renderer.render(scene,camera);
        orbit.update();
        requestAnimationFrame(render);
        uniforms.iTime.value += 0.01;
    }

片元着色器改动

    varying vec2 vUv;
    uniform sampler2D uDiffuse1;
    uniform sampler2D uDiffuse2;
    uniform sampler2D uChangeTexture;
    void main(){
        vec4 col1 = texture2D(uDiffuse1,vUv);
        vec4 col2 = texture2D(uDiffuse2,vUv);
        vec4 col3 = texture2D(uChangeTexture,vUv);
        gl_FragColor = mix(col2,col1,col3.r);
    }

效果分析

在这里插入图片描述
在这里插入图片描述

这样,我们看到的效果,就是一边是白天,一边是黑夜的效果了

我们切换回plane,来看看效果演变
在这里插入图片描述
在这里插入图片描述
可以看出,最黑的地方,最终使用的是夜晚的图片,也就是说此处的值,r值是最低的
最白的地方,最终使用的是白天的图片,也就是说此处的值,r值是最高的
中间的部分,随着上图的颜色变化而变化,白色越强的地方,白天图片的强度越高
黑色越强的地方,黑夜图片的强度越高

解决球体分界线太过明显的问题

但是这里有个很明显的问题,就是应用到球体上之后,左右两侧的颜色差距太大,导致了明显的分界线

我们切换回球体,然后对uv做一下处理

首先,我们的图片,是左边黑右边白,那么我们试着移动一下图片的像素,给uv.x - 0.5

此时图片会变成下面圈出来的这一块
在这里插入图片描述
然后,接下来,我们让负数变正数,则左边的这一块变成了,对abs(uv.x - 0.5)
在这里插入图片描述
原先按照正常的取值流程,红框的最左边是-0.5,但是我们给它变成正的了,所以后面会产生镜像效果

但是这一块黑色区域太大,所以我们要把最终结果再乘2,让图片截取到完全白色的区域

在这里插入图片描述
此时,uv.x的取值范围,就变成了 -1 ~1,就变成了上面的图片效果

然后我们带入到代码中试一下

<script type="x-shader/x-fragment" id="fragmentShader">
    varying vec2 vUv;
    uniform sampler2D uDiffuse1;
    uniform sampler2D uDiffuse2;
    uniform sampler2D uChangeTexture;
    void main(){
        vec4 col1 = texture2D(uDiffuse1,vUv);
        vec4 col2 = texture2D(uDiffuse2,vUv);
                                              //对vUv.x - 0.5然后绝对值,再乘2
        vec4 col3 = texture2D(uChangeTexture, vec2(abs(vUv.x - 0.5) * 2.0,vUv.y));
        gl_FragColor = mix(col2,col1,col3.r);
    }
</script>

在这里插入图片描述

让昼夜动起来

改动代码

    varying vec2 vUv;
    uniform sampler2D uDiffuse1;
    uniform sampler2D uDiffuse2;
    uniform sampler2D uChangeTexture;
    uniform float iTime;
    void main(){
        vec4 col1 = texture2D(uDiffuse1,vec2(vUv.x + iTime,vUv.y));
        vec4 col2 = texture2D(uDiffuse2,vec2(vUv.x + iTime,vUv.y));
        vec4 col3 = texture2D(uChangeTexture,vec2(abs(vUv.x - 0.5) * 2.0,vUv.y));
        gl_FragColor = mix(col2,col1,col3.r);
    }

由于col3中的vUv.x已经做了太多处理了,所以我们把跟随时间动的代码,放到了前面两张图上,让前面两张图动起来

然后发现动的实在是太快了,所以我们把运动速度也做了调整

    function render() {
        renderer.render(scene,camera);
        orbit.update();
        requestAnimationFrame(render);
        //旧代码 uniforms.iTime.value += 0.001;
        uniforms.iTime.value += 0.001;
    }

最终效果

在这里插入图片描述

完整效果由于gif图近10M,csdn承受不了,所以就不发了

案例完整源码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        body{
            width:100vw;
            height: 100vh;
            overflow: hidden;
            margin: 0;
            padding: 0;
            border: 0;
        }
    </style>
</head>
<body>

<script type="importmap">
			{
				"imports": {
					"three": "../three/build/three.module.js",
					"three/addons/": "../three/examples/jsm/"
				}
			}
		</script>

<script type="x-shader/x-vertex" id="vertexShader">
    varying vec2 vUv;
    void main(){
        vUv = vec2(uv.x,uv.y);
        vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
        gl_Position = projectionMatrix * mvPosition;
    }

</script>
<script type="x-shader/x-fragment" id="fragmentShader">
    varying vec2 vUv;
    uniform sampler2D uDiffuse1;
    uniform sampler2D uDiffuse2;
    uniform sampler2D uChangeTexture;
    uniform float iTime;
    void main(){
        vec4 col1 = texture2D(uDiffuse1,vec2(vUv.x + iTime,vUv.y));
        vec4 col2 = texture2D(uDiffuse2,vec2(vUv.x + iTime,vUv.y));
        vec4 col3 = texture2D(uChangeTexture,vec2(abs(vUv.x - 0.5) * 2.0,vUv.y));
        gl_FragColor = mix(col2,col1,col3.r);
    }
</script>

<script type="module">

    import * as THREE from "../three/build/three.module.js";
    import {OrbitControls} from "../three/examples/jsm/controls/OrbitControls.js";

    window.addEventListener('load',e=>{
        init();
        addMesh();
        render();
    })

    let scene,renderer,camera;
    let orbit;

    function init(){

        scene = new THREE.Scene();
        renderer = new THREE.WebGLRenderer({
            alpha:true,
            antialias:true
        });
        renderer.setSize(window.innerWidth,window.innerHeight);
        document.body.appendChild(renderer.domElement);

        camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,0.1,2000);
        camera.add(new THREE.PointLight());
        camera.position.set(15,15,15);
        scene.add(camera);

        orbit = new OrbitControls(camera,renderer.domElement);
        orbit.enableDamping = true;
        scene.add(new THREE.GridHelper(10,10));
    }

    let uniforms = {
        uDiffuse1:{value:null},
        uDiffuse2:{value:null},
        uChangeTexture:{value:null},
        iTime:{value:0.01}
    }

    function addMesh() {

        let textureLoader = new THREE.TextureLoader();

        uniforms.uDiffuse1.value = textureLoader.load('./earth_atmos_2048.jpg');
        uniforms.uDiffuse1.value.wrapT = THREE.RepeatWrapping;
        uniforms.uDiffuse1.value.wrapS = THREE.RepeatWrapping;

        uniforms.uDiffuse2.value = textureLoader.load('./earth_lights_2048.png');
        uniforms.uDiffuse2.value.wrapT = THREE.RepeatWrapping;
        uniforms.uDiffuse2.value.wrapS = THREE.RepeatWrapping;

        uniforms.uChangeTexture.value = textureLoader.load('./transition5.png');
        uniforms.uChangeTexture.value.wrapT = THREE.RepeatWrapping;
        uniforms.uChangeTexture.value.wrapS = THREE.RepeatWrapping;


        let geometry = new THREE.SphereGeometry(5,32,32);
        let material = new THREE.ShaderMaterial({
            uniforms,
            vertexShader:document.getElementById('vertexShader').textContent,
            fragmentShader:document.getElementById('fragmentShader').textContent,
        })
        let mesh = new THREE.Mesh(geometry,material);
        scene.add(mesh);
    }


    function render() {
        renderer.render(scene,camera);
        orbit.update();
        requestAnimationFrame(render);
        uniforms.iTime.value += 0.001;
    }

</script>
</body>
</html>

如有不明白的,可以在下方留言或者加群

如有其他不懂的问题,可以在下方留言,也可以加入qq群咨询,本人的群于2024/7/8日正式创建,群号867120877,欢迎大家来群里交流
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值