Uniform的基本用法2
- 关于本Shader教程
- 前两篇地址,请按顺序学习
- 本篇使用到的资源
- 用uniform传递纹理
- 代码分析
- texture类型的uniform
- 在shader中接收uniform
- texture2D()
- 处理图片压缩
- 修改wrapS和wrapT
- 切换成夜景
- 效果切换
- Mix()
- 昼夜切换升级
- 改动代码
- 效果分析
- 解决球体分界线太过明显的问题
- 让昼夜动起来
- 改动代码
- 最终效果
- 案例完整源码
- 如有不明白的,可以在下方留言或者加群
关于本Shader教程
- 本教程着重讲解Shadertoy的shader和Threejs的Shader,与原生WebGLShader略有不同,如果需要学习原生WebGL的shader,请参考《WebGL编程指南》
- 本人的shader水平也比较基础,文章中所写代码,不一定是最佳的代码,思路也不一定是最好的思路,所以一切本人的Shader教程下,所有的代码及思路以及学习建议均仅供参考,且目前本教程可能不适用于WebGPU,如果有大佬路过看到本人文章,觉得有可以指点之处,可以在下面留言,我们一起进步
- 数学水平不行的人,尤其是高中数学都及格不了的,不建议入坑Shader
- 本教程会在讲解片元着色器时,使用Shadertoy来编写demo,所以教程中会出现一部分Shadertoy的代码
- 本段内容将会出现在本人所有的【进阶教程-着色器篇】的文章中
前两篇地址,请按顺序学习
【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然后绝对值,再乘2vec4 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,欢迎大家来群里交流