ThreejsShader入门
- 关于本Shader教程
- 认识Shader
- 再次劝退数学不好的人
- 从ShaderToy开始
- [最基本的ThreeJS Shader](#最基本的ThreeJS Shader)
- ThreejsShader和ShadertoyShader的巧妙使用
- 本系列教程更新速度可能较慢,还请耐心等待
关于本Shader教程
- 本教程着重讲解Shadertoy的shader和Threejs的Shader,与原生WebGLShader略有不同,如果需要学习原生WebGL的shader,请参考《WebGL编程指南》
- 本人的shader水平也比较基础,文章中所写代码,不一定是最佳的代码,思路也不一定是最好的思路,所以一切本人的Shader教程下,所有的代码及思路以及学习建议均仅供参考,且目前本教程可能不适用于WebGPU,如果有大佬路过看到本人文章,觉得有可以指点之处,可以在下面留言,我们一起进步
- 数学水平不行的人,尤其是高中数学都及格不了的,不建议入坑Shader
- 本教程会在讲解片元着色器时,使用Shadertoy来编写demo,所以教程中会出现一部分Shadertoy的代码
- 本段内容将会出现在本人所有的【进阶教程-着色器篇】的文章中
认识Shader
能百度到的我这里就不废话了,自行百度 WebGL,OpenGL,OpenGL ES,GLSL,这些可以帮助你了解shader的历史和发展以及版本关系
Shader和Threejs的关系
shader是Threejs的底层,在前后关系上,Shader要更底层,Threejs的渲染器部分,主要基于Shader来开发
WebGLShader
最原始版本的Shader,在学习《WebGL编程指南》时会经常接触
ThreejsShader
ThreejsShader,特指 THREE.ShaderMaterial和THREE.RawShaderMaterial,这个是Threejs最核心的内容之一,基本上所有的特效物体,特效粒子,都可以使用Shader来制作,在threejs的ShaderMaterial中,内置了很多方便的常用的变量,提供给你访问,如果你不想使用Threejs内置的变量,那么用THREE.RawShaderMaterial即可
ShadertoyShader
基于Shadertoy网站的Shader,由Shadertoy网站提供了大量的内置变量,以编写片元着色器为主
(但是shadertoy上的大佬,早就不满足于只写片元着色器了,在里面加入了各种各样牛逼的写法,什么光线追踪,3d化的各种效果,还有人在里面做游戏的,也有人就纯靠代码写出来超级逼真的场景,比如说iq大佬,而且全部开源,是个非常适合学Shader的地方)
本系列教程,以ShadertoyShader和ThreejsShader为主,可能部分写法不太适用于其他的shader
其他Shader
cesium,babylon,pixi均内置了自己的shader系统,但是所有的shader,基本语法都是一样的,只有内置变量的区别,所以学会了原生Shader基本上可以通吃整个webgl圈子的shader
所以如果你想要学习原生的Shader,建议从《WebGL开发指南》开始学起
再次劝退数学不好的人
Shader难度很高,对数学能力要求很高,如果你已经做好了心理准备,那么我们接下来,就准备开始吧
从ShaderToy开始
但是,这里本人要改一下,改的更简单一点,方便入门
js
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy;
fragColor = vec4(uv.x,0.0,0.0,1.0);
}
把这一段代码,替换到Shadertoy中,然后点击编辑窗左下角的播放键,即可得到左边的渐变红色效果
我们来对上述代码做一个分析
Shader的代码是强类型
很多人直接就反应了,这个代码根本看不懂,因为根本就不是 JS语言
首先,我们看到第一行,代码是==void mainImage( out vec4 fragColor, in vec2 fragCoord ) ==
有没有发现,这个代码很像是我们学习的C语言的HelloWorld
glsl是强类型语言,所以我们无论什么时候,都要遵循强类型语言的规则
现阶段,我们只需要死记硬背,第一行是Shadertoy必须要写的内容即可
然后我们开始分析第二行代码,vec2 uv = fragCoord/iResolution.xy;
这里声明了一个变量,vec2 uv
glsl的类型,变量,内置函数,关键字
这里本人就懒得手敲了,《WebGL编程指南》187页~215页,非常多,这里本人仅截图出来第一张 ,后面的请自行去群里下载电子书,或者买一本实体书来阅读
什么群?基本上所有的Threejs/WebGL的qq群文件里都有相关电子书,非常好找,随便加一个就行了
内置函数在 427~436页,内置函数表在后续的开发中会经常查阅,所以建议学习Shader的人自行购买一本实体书
还有,内置变量啊,内置函数啊,现阶段就看一遍就行了,知道哪些是关键字,避开就行了
实在懒得看书的就看我这里的解释吧。。。
这里仅介绍本篇教程中用到的变量和对象
vec2,其实是一个缩写,完整单词是Vector2,二维向量,这里的uv实际上就是一个二维向量,一个二维向量由 x,y两个数据构成
然后后面的 fragCoord/iResolution.xy,这里,现阶段不做讲解,只需要记住,shadertory第二行必须是这个就行了,后面在threejs的开发中,这一行也不会发生大的改变,如果你需要更详细的了解和学习Shadertoy,再去考虑研究 fragCoord和iResolution的作用,最终的计算结果,就是uv
注意:shadertoy,以及后续所有的Shader代码每一行结尾必须加分号!!!!!!!!
关于uv
uv这个概念,在前面介绍纹理篇已经简单介绍过了,所以还不懂uv概念的,回去补补课了
在Shadertoy中,已经通过内置的计算,把uv给你端上来了,但是shadertoy的uv和我们传统意义上的uv的直观感受不太一样,一般我们在讲解uv的时候,长宽是一样长的,但是shadertoy中不同
基于UV的颜色处理
接下来进入最重要的一行了 fragColor = vec4(uv.x,0.0,0.0,1.0);
首先,在Shadertoyu中,fragColor 代表最终输出,也就是说,无论你怎么编写这里的代码,都必须要有一个最终输出,这个最终输出需要你传入一个 vec4类型的变量,就是最终颜色,对应颜色的rgba
vec4在用作最终变量的时候,4个值分别对应 红色百分比,绿色百分比,蓝色百分比,透明度
我们从代码中可以得到,我们把uv的x值,作为红色的百分比值,传入到了最终输出中,然后绿色和蓝色保持为0,透明度为1,所以整个效果都呈现红色,只有强弱的区别
但是,这个效果并不是纯红色,因为uv的值是在变的,画面最左侧的uv的u值(代码中是uv.x),是0,左右侧的u值为1,所以最终呈现出了这样的一个渐变红色的效果
我们把uv.x放到第二个参数上,看看效果
我们把uv.y 作为参数传进去,看看效果
这样我们就完成了Shader的入门级案例,基于uv的颜色渐变
最基本的ThreeJS Shader
Shader最最基本的案例,我们已经完成了,那么现在我们进入下一阶段,将这个效果应用于Threejs
首先,我们依然要准备一个demo
js
<!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;
void main(){
gl_FragColor = vec4(vUv.x,0.0,0.0,1.0);
}
</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(0,0,15);
scene.add(camera);
orbit = new OrbitControls(camera,renderer.domElement);
orbit.enableDamping = true;
scene.add(new THREE.GridHelper(10,10));
}
let uniforms = {
}
function addMesh() {
let geometry = new THREE.PlaneGeometry(10,10);
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>
demo效果如下
中间的那条线,是在代码中创建的网格辅助线
ShaderMaterial介绍
ShaderMaterial官方文档
RawShaderMaterial官方文档
大多数情况下都用Shadermaterial,内置的东西已经足够用了,RawShaderMaterial只是用了更原生的方式,来编写Shader
现阶段给你文档也不一定看得懂,只需要记得,下面提到的,现阶段死记硬背的东西,就死记硬背下来,按照这个格式写就行了
代码拆解
创建ShaderMaterial,至少要有三个要点
uniforms(参数集)(本篇不讲)
uniforms是一个对象,而且,threejs对uniforms有严格要求,比如说下面的写法:
let uniforms = {
opacity:{ value: 1.0 },
aPosition:{value: new THREE.Vector3()}
}
uniforms的参数,必须是 [key] : { value : [value] },后续会详细讲解uniforms的用法用途
vertexShader(顶点着色器)(现阶段不讲)
threejs这里需要的是一个字符串,这个字符串,就是glsl的代码
现阶段,默认顶点着色器代码固定 ,且只需要知道,创建ShaderMaterial必须要有一个顶点着色器即可 ,通过document.getElementById('vertexShader').textContent
来获取上面id为 vertexShader的dom,并通过.textContent,来获取到内部编写的文本代码
html
<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>
fragmentShader(片元着色器)代码讲解
现阶段主要编写的代码,通过
document.getElementById('fragmentShader').textContent
来获取上面id为 fragmentShader的dom,并通过.textContent,来获取到内部编写的文本代码
html
<script type="x-shader/x-fragment" id="fragmentShader">
varying vec2 vUv;
void main(){
gl_FragColor = vec4(vUv.x,0.0,0.0,1.0);
}
</script>
这里我们先介绍片元着色器的代码,等后续进入到顶点着色器的篇章中,再详细介绍顶点着色器
首先,varying vec2 vUv; ,现阶段视为片元着色器的固定代码,只需要关注下面的即可
我们对比一下和ShaderToy的代码结构
首先,这里我们依然是需要一个main,但是在Threejs的Shader中,写法变更为 main(),而且不需要传入参数(为什么不需要传入参数,这个后续讲解ShadertoyShader时会讲)
然后同样的,threejs中,也需要一个最终输出,只不过,这个最终输出写为 gl_FragColor,参数与Shadertoy一致
我们的代码,采用在Shadertoy的代码的第一版,依然是用vUv.x去控制红色
在Threejs中,uv是内置变量,所以我们为了避免冲突,在这里写成vUv,实际作用与在Shadertoy里面的uv是一致的
现阶段的效果进阶
我们现在改一下代码,改动一点点即可
html
<script type="x-shader/x-fragment" id="fragmentShader">
varying vec2 vUv;
void main(){
gl_FragColor = vec4(1.0 - vUv.y,0.0,0.0,1.0 - vUv.y);
}
</script>
然后改动一下下面的ShaderMaterial
js
let material = new THREE.ShaderMaterial({
uniforms,
vertexShader:document.getElementById('vertexShader').textContent,
fragmentShader:document.getElementById('fragmentShader').textContent,
transparent:true,
side:THREE.DoubleSide
});
这样,我们就得出了一个这样的效果
然后,我们在代码中,创建4个一样的平面,然后做一下旋转,拼成一个围墙
js
function addMesh() {
let geometry = new THREE.PlaneGeometry(10,10);
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,5,0);
mesh1.rotation.y = Math.PI/2;
scene.add(mesh1);
let mesh2 = mesh.clone();
mesh2.position.set(-5,5,0);
mesh2.rotation.y = Math.PI/2;
scene.add(mesh2);
let mesh3 = mesh.clone();
mesh3.position.set(0,5,-5);
scene.add(mesh3);
let mesh4 = mesh.clone();
mesh4.position.set(0,5,5);
scene.add(mesh4);
}
这个就是最基础的光栅栏效果,各位可以自行修改在片元着色器中的颜色,来修改颜色
但是,这个光栅栏不是最终版本,请阅读后续教程来继续优化效果
ThreejsShader和ShadertoyShader的巧妙使用
上面我们展示了,如何编写最基本的shader,不难看出,我们在编辑片元着色器的时候,是可以借助Shadertoy来编辑的,很多shadertoy的2d效果,也可以被搬到Threejs中现用,但是仅限于你要看懂,且以uv为基准的那些,所以,如果你想要编辑一些2D的,以及动态贴图效果,都可以借助Shadertoy来研究和学习