Uniform的基本用法与Uniform的调试
- 关于本Shader教程
- 优化上一篇的效果
- 优化光栅栏高度
- 让透明度和颜色变的更平滑
- pow()函数
- 借助数学工具更好的理解函数
- Unifoms简介
- 编写uniforms
- 修改片元着色器代码
- 借助lil.gui调试uniforms
- 使用uniform控制颜色
- 继续在uniforms添加颜色
- 在着色器中接收颜色
- 使用lil.gui来调试颜色
- 最终效果
- Uniform数据类型对应
- 让光栅栏动起来
- 加入持续变化的变量
- 让vTime持续变化
- 修改Shader部分代码响应变化
- 三角函数sin() 和 绝对值函数abs()
- 最终效果
- 常用的片元着色器开发流程
- 先使用Shadertoy开发基本效果
- 将Shadertoy中的数据常量以及颜色,在Threejs的Shader中,修改为uniform传递
- 最后将ShaderMaterial应用到你的Mesh上即可
- 本篇全部源码
关于本Shader教程
- 本教程着重讲解Shadertoy的shader和Threejs的Shader,与原生WebGLShader略有不同,如果需要学习原生WebGL的shader,请参考《WebGL编程指南》
- 本人的shader水平也比较基础,文章中所写代码,不一定是最佳的代码,思路也不一定是最好的思路,所以一切本人的Shader教程下,所有的代码及思路以及学习建议均仅供参考,且目前本教程可能不适用于WebGPU,如果有大佬路过看到本人文章,觉得有可以指点之处,可以在下面留言,我们一起进步
- 数学水平不行的人,尤其是高中数学都及格不了的,不建议入坑Shader
- 本教程会在讲解片元着色器时,使用Shadertoy来编写demo,所以教程中会出现一部分Shadertoy的代码
- 本段内容将会出现在本人所有的【进阶教程-着色器篇】的文章中
优化上一篇的效果
上一篇的效果,其实并不怎么好看,所以我们先做一下优化
优化光栅栏高度
首先,我们在创建Plane的时候,修改plane的高度,让它看起来更像是围墙,栅栏的样子
function addMesh() {let planeHeight = 5;let geometry = new THREE.PlaneGeometry(10,planeHeight);let material = new THREE.ShaderMaterial({uniforms,vertexShader:document.getElementById('vertexShader').textContent,fragmentShader:document.getElementById('fragmentShader').textContent,transparent:true,side:THREE.DoubleSide})let mesh = new THREE.Mesh(geometry,material);let mesh1 = mesh.clone();mesh1.position.set(5,planeHeight/2,0);mesh1.rotation.y = Math.PI/2;scene.add(mesh1);let mesh2 = mesh.clone();mesh2.position.set(-5,planeHeight/2,0);mesh2.rotation.y = Math.PI/2;scene.add(mesh2);let mesh3 = mesh.clone();mesh3.position.set(0,planeHeight/2,-5);scene.add(mesh3);let mesh4 = mesh.clone();mesh4.position.set(0,planeHeight/2,5);scene.add(mesh4);}
修改后的效果
让透明度和颜色变的更平滑
首先我们介绍本篇Shader的第一个函数,pow函数
pow()函数
pow函数在《webgl编程指南》429页有相关文档
简单说
float a = 2.0; //注意,float类型必须写成1.0,否则会判断为 int类型的数据
pow(a, 2.0); //结果是 a^2 = 2.0 ^ 2.0 = 4.0
pow(a, 3.0); //结果是 a^3 = 2.0 ^ 3.0 = 8.0
注意,pow函数的第二个参数,是float类型,也就意味着,不仅仅可以使用整数,还可以使用小数,负数等等
借助数学工具更好的理解函数
这是本人找的一个非常不错的数学学习工具
我们来借助这个工具,来复习一下指数函数
可以看出,指数函数的图像是这样,那么我们把指数函数带入到我们的Shader中是什么样子呢
我们把上一篇的代码的基础部分,改成
<script type="x-shader/x-fragment" id="fragmentShader">varying vec2 vUv;void main(){float vy = 1.0 - vUv.y;vy = pow(vy,4.0);gl_FragColor = vec4(vy,0.0,0.0,vy);}
</script>
这里我们发现,红色的区域明显变小了
这里我们的pow()的参数1,是vUv.y,参数2是个固定常量2.0,所以,此时在函数图像上,vUv.y 对应的x值,而计算结果,对应的是y值,接下来我们看一下在Shadertoy中的效果
我们把写的公式,带入到desmos中生成图像
这里,我们的vy对应desmos里的x,pow的计算结果对应desmos里的y
可以看出,在desmos里,当x逐渐增大的时候,结果值会无限趋近于0,而且前期变化幅度很大,后期变化幅度小
也就意味着对应在Shadertoy中,uv.y = 0时颜色最深,uv.y=1时完全没有颜色,且前面颜色渐变变化很明显,后面颜色变化不够明显
我们可以把pow函数的第二个参数直接改到10,来让变化更明显一些
所以,我们可以通过pow,来控制光栅栏的有颜色部分的宽度
Unifoms简介
Uniforms在《WebGL编程指南》中介绍的比较多,有想了解Uniform完整介绍的请自行参阅此书,这里笔者仅介绍怎么用
uniforms的主要用途,是传递一个参数给到glsl,此参数限定类型,最常用的参数类型就是float,vec2,vec3, vec4等
这里介绍如何利用uniform传递一个float类型的数据,给到glsl
- 在js部分的代码中,创建一个对象,这里通常命名为uniforms
- 在uniforms中,添加一个属性,命名为key,key的值也是一个对象,对象内容为: {value: [value]},后面的value是你需要传递给glsl的数值(不要问我为什么这么麻烦,threejs就这么规定的)
- 在着色器代码的上面,追加代码 uniform float key; 你的数据类型是什么,这里就要声明为什么类型,且命名一定要与unifomrs中的[key]命名一致,一定不要漏分号,否则会报错
这里我们演示一下如何用uniform来控制光栅栏的光效高度
编写uniforms
let uniforms = {vPow:{value:2.0}}function addMesh() {let planeHeight = 5;let geometry = new THREE.PlaneGeometry(10,planeHeight);let material = new THREE.ShaderMaterial({//uniforms在这里传入到js的ShaderMaterial中uniforms, //这里使用了对象的新语法,uniforms:uniforms可以简写为 uniforms,vertexShader:document.getElementById('vertexShader').textContent,fragmentShader:document.getElementById('fragmentShader').textContent,transparent:true,side:THREE.DoubleSide})let mesh = new THREE.Mesh(geometry,material);let mesh1 = mesh.clone();mesh1.position.set(5,planeHeight/2,0);mesh1.rotation.y = Math.PI/2;scene.add(mesh1);let mesh2 = mesh.clone();mesh2.position.set(-5,planeHeight/2,0);mesh2.rotation.y = Math.PI/2;scene.add(mesh2);let mesh3 = mesh.clone();mesh3.position.set(0,planeHeight/2,-5);scene.add(mesh3);let mesh4 = mesh.clone();mesh4.position.set(0,planeHeight/2,5);scene.add(mesh4);}
修改片元着色器代码
<script type="x-shader/x-fragment" id="fragmentShader">varying vec2 vUv;uniform float vPow; //使用uniform关键字引入float类型的vPow,注意不带svoid main(){float vy = 1.0 - vUv.y;vy = pow(vy,vPow);gl_FragColor = vec4(vy,0.0,0.0,vy);}
</script>
验证效果后,基本上与原效果一致,这里就不放图了
借助lil.gui调试uniforms
//引入lil.gui并初始化import {GUI} from "../three/examples/jsm/libs/lil-gui.module.min.js";let gui = new GUI();
添加gui控制代码
function addMesh() {//...这里省略上面的代码gui.add(uniforms.vPow,'value',0,10).name('光栅栏高度');
}
我们也修改一下相机的位置,让相机初始视角高一点,省的操作
camera.position.set(10,10,10);
最终效果
使用uniform控制颜色
继续在uniforms添加颜色
其他代码不变
let uniforms = {vPow:{value:2.0},vColor:{value:new THREE.Color("#ff0000")}}
颜色在threejs中,可以作为vec3类型的数据,传递给glsl,必须是 THREE.Color()类型的颜色对象,才可以作为glsl的传递参数
在着色器中接收颜色
<script type="x-shader/x-fragment" id="fragmentShader">varying vec2 vUv;uniform float vPow;//这里对应vPowuniform vec3 vColor;//这里对应vColorvoid main(){float vy = 1.0 - vUv.y;vy = pow(vy,vPow);gl_FragColor = vec4(vColor,vy);}
</script>
这里,我们就不用vy来控制红色高度了,仅需要用透明度控制高度即可
在glsl的语法中,没有color类型的数据,要使用vec3类型的来接收颜色数据
在glsl的语法中,vec4的构造器,允许传入 一个vec3类型的和一个float类型的
使用lil.gui来调试颜色
gui.add(uniforms.vPow,'value',0,10).name('光栅栏高度');let colors = {vColor:"#ff0000"};gui.addColor(colors,'vColor').name('光栅栏颜色').onChange(v=>{uniforms.vColor.value = new THREE.Color(v);})
如果忘了lil.gui怎么用,可以回顾一下本人的基础教程,基础教程中有很详细的用法说明
最终效果
Uniform数据类型对应
这里我们截取Threejs的文档,ThreejsShaderMaterial文档
让光栅栏动起来
加入持续变化的变量
let uniforms = {vTime:{value:0},vPow:{value:2.0},vColor:{value:new THREE.Color("#ff0000")}}
让vTime持续变化
这里我们在Render函数中,让vTime持续变化,每帧增加0.01
function render() {renderer.render(scene,camera);orbit.update();requestAnimationFrame(render);uniforms.vTime.value += 0.01;}
修改Shader部分代码响应变化
<script type="x-shader/x-fragment" id="fragmentShader">varying vec2 vUv;uniform float vTime;uniform float vPow;uniform vec3 vColor;void main(){float vy = 1.0 - vUv.y;vy = pow(vy,vPow - abs(sin(vTime)));//让vPow跟随时间变化gl_FragColor = vec4(vColor,vy);}
</script>
三角函数sin() 和 绝对值函数abs()
这俩函数完全不需要解释吧
上述的计算结果,会让vPow后面增加一个周期性变化的效果
最终效果
常用的片元着色器开发流程
一般情况下,我们在开发片元着色器的时候,可以先考虑在shadertoy上来编写效果,然后将写好的效果
比如说上面的效果
先使用Shadertoy开发基本效果
Shadertoy内置了一个时间变量iTime,每帧会增加0.01,所以使用上面的写法效果是完全一致的
注意!在Shadertoy中,由于正常情况下没有参照物,所以默认控制红色,而很少控制透明度,注意两边的代码的区别
将Shadertoy中的数据常量以及颜色,在Threejs的Shader中,修改为uniform传递
最后将ShaderMaterial应用到你的Mesh上即可
本篇全部源码
<!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 float vTime;uniform float vPow;uniform vec3 vColor;void main(){float vy = 1.0 - vUv.y;vy = pow(vy,vPow - abs(sin(vTime)));gl_FragColor = vec4(vColor,vy);}
</script><script type="module">import * as THREE from "../three/build/three.module.js";import {OrbitControls} from "../three/examples/jsm/controls/OrbitControls.js";import {GUI} from "../three/examples/jsm/libs/lil-gui.module.min.js";let gui = new GUI();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(10,10,10);scene.add(camera);orbit = new OrbitControls(camera,renderer.domElement);orbit.enableDamping = true;scene.add(new THREE.GridHelper(10,10));}let uniforms = {vTime:{value:0.01},vPow:{value:2.0},vColor:{value:new THREE.Color("#ff0000")}}function addMesh() {let planeHeight = 5;let geometry = new THREE.PlaneGeometry(10,planeHeight);let material = new THREE.ShaderMaterial({//uniforms在这里传入到js的ShaderMaterial中uniforms, //这里使用了对象的新语法,uniforms:uniforms可以简写为 uniforms,vertexShader:document.getElementById('vertexShader').textContent,fragmentShader:document.getElementById('fragmentShader').textContent,transparent:true,side:THREE.DoubleSide})let mesh = new THREE.Mesh(geometry,material);let mesh1 = mesh.clone();mesh1.position.set(5,planeHeight/2,0);mesh1.rotation.y = Math.PI/2;scene.add(mesh1);let mesh2 = mesh.clone();mesh2.position.set(-5,planeHeight/2,0);mesh2.rotation.y = Math.PI/2;scene.add(mesh2);let mesh3 = mesh.clone();mesh3.position.set(0,planeHeight/2,-5);scene.add(mesh3);let mesh4 = mesh.clone();mesh4.position.set(0,planeHeight/2,5);scene.add(mesh4);gui.add(uniforms.vPow,'value',0,10).name('光栅栏高度');let colors = {vColor:"#ff0000"};gui.addColor(colors,'vColor').name('光栅栏颜色').onChange(v=>{uniforms.vColor.value = new THREE.Color(v);})}function render() {renderer.render(scene,camera);orbit.update();requestAnimationFrame(render);uniforms.vTime.value += 0.01;}</script>
</body>
</html>