第一个 shader 程序
shader 是用 GLSL(OpenGL Shading Language) 编写的程序,他会被发送到GPU运行。原生 WebGL 的一部分,如果不依赖任何库使用 WebGl 的话,需要自己动手创建 Shader。shader 会为几何体的每个顶点(vertex)定位,以及为几何体的所有可见片段(fragment)着色。
我们会为shader发送大量的数据,比如顶点坐标、网格变换信息、摄像机的信息以及几何体的顶点颜色、纹理等等。然后交给CPU,利用GLSL编写的着色器程序处理这些数据,最终在屏幕上定位顶点,并为每个可见片段着色
着色器有两种类型
- vertex shader
- fragment shader
Vertex Shader
顶点着色器会为每个几何体的顶点定位,当顶点着色器为顶点定位完之后,GPU 便知道哪些几何体的像素是可见的,然后进入片段着色器(fragment shader)
fragment shader
片段着色器会为顶点着色,包括颜色,纹理,透明度等等。
https://learnopengl.com/Getting-started/Coordinate-Systems
为什么要自己编写着色器?为什么我们不能直接使用Threejs的内置材质?
- 材质不够灵活,一些形状通过内置的material无法实现
- 着色器高效,比Threejs需要性能开销更少
- 可以做后期处理
Threejs有两种着色器ShaderMaterial RawShaderMaterial。ShaderMaterial包含了必要的uniforms和attributes的声明,除此之外并无差别。RawShaderMaterial就是原始的,必须编写所有代码。
RawShaderMaterial
THREE.RawShaderMaterial创建原始着色器,RawShaderMaterial 不会自动注入 Three.js 的内置着色器代码(如光照、矩阵变换等),开发者需要手动管理所有 Uniforms 和 Attributes。
分别创建vertex.glsl和fragment.glsl文件, 使用 vite-plugin-glsl或者vite-plugin-glslify插件,可以将大的着色器分割为小的模块,可以重用着色器块,以及可以使用其他开发者编写的着色器块.
npm install vite-plugin-glsl在vite.config.js中导入glsl
import glsl from 'vite-plugin-glsl'export default { // ... plugins: [ glsl(), ],}使用
// Geometryconst geometry = new THREE.PlaneGeometry(1, 1, 32, 32)
const count = geometry.attributes.position.countconst randoms = new Float32Array(count)for (let i = 0; i < count; i++) { randoms[i] = Math.random()}geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1))// Materialconst material = new THREE.RawShaderMaterial({ vertexShader: testVertexShader, fragmentShader: testFragmentShader, transparent: true, side: THREE.DoubleSide, // wireframe: true, uniforms: { uFrequency: { value: new THREE.Vector2(10, 5) }, uTime: { value: 0 }, uColor: { value: new THREE.Color(0x7799cc) }, uTexture: { value: flagTexture }, },})在THREE.RawShaderMaterial中,THREEjs中的material同样可以使用wireframe, side, transparent, flatShading这些属性。但是像map,alphaMap,opacity等等都没法使用,因为这些事在顶点内部处理的,只有在fragmentShader中才能更改
const count = geometry.attributes.position.countconst randoms = new Float32Array(count)for (let i = 0; i < count; i++) { randoms[i] = Math.random()}geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1))上面创建了一个随机生成的名为aRandom的attribute,添加在geometry的attribute上,在vertex.glsl中可以通过attribute float aRandom;接收。
因为new THREE.BufferAttribute(randoms, 1)为 1,表明一个为一组。所以是float类型
通过gui直接操作shader中数据,可以通过更改uniforms中的值
gui.add(material.uniforms.uFrequency.value, 'x').min(0).max(20).step(0.01).name('uFrequency.x')gui.add(material.uniforms.uFrequency.value, 'y').min(0).max(20).step(0.01).name('uFrequency.y')gui.addColor(material.uniforms.uColor, 'value').name('uColor')vertex.glsl
他会对每个顶点执行这段代码
uniform mat4 projectionMatrix;uniform mat4 viewMatrix;uniform mat4 modelMatrix;uniform vec2 uFrequency;uniform float uTime;
attribute vec3 position;attribute vec2 uv;
varying vec2 vUv;varying float vElevation;
void main() { vec4 modelPosition = modelMatrix * vec4(position, 1.0); float elevation = sin(modelPosition.x * uFrequency.x - uTime) * 0.1; elevation += sin(modelPosition.y * uFrequency.y - uTime) * 0.1; modelPosition.z += elevation;
vec4 viewPosition = viewMatrix * modelPosition; vec4 projectionPosition = projectionMatrix * viewPosition;
gl_Position = projectionPosition; // gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
vUv = uv; vElevation = elevation;}gl_Position 为什么是一个vec4?
这是因为我们的坐标实际上是在裁剪空间里,裁剪空间不是一个二维空间,是一个四维的投影变换,将3D场景转换为2D屏幕坐标。当我们从camera观察其中一个物体时,最终渲染是在2d的canvas上的,除了知道他的顶点坐标(x,y,z),还需要知道他的深度,帮助计算机判断物体哪个顶点坐标在前,形成透视效果。在正交投影中,w 分量通常为 1,因此不影响结果。
uniform mat4 projectionMatrix;uniform mat4 viewMatrix;uniform mat4 modelMatrix;uniform 变量是全局常量,在整个渲染过程中保持不变。它的作用是从 CPU 侧的应用程序向 GPU 中的着色器程序传递数据,例如变换矩阵、光照参数、时间等。uniform 变量可以同时在顶点着色器和片段着色器中使用,并且一旦设置,在一次渲染调用中,所有顶点和片段的 uniform 变量值都是相同的,且在着色器内部不能被修改。
上面三个矩阵都是 uniform 的,不会改变,对几何体的每个顶点都采用相同的矩阵运算
-
uniform mat4 modelMatrix;模型矩阵,他将对每个顶点应用模型自身的变换(平移、旋转、缩放),将顶点从模型本地坐标系转换到世界坐标系。
mesh.position.x = 1就会变换这个矩阵,只需要提供(position, rotation, scale) -
uniform mat4 viewMatrix;相机矩阵,他定义相机的位姿(位置和朝向),将顶点从世界坐标系转换到相机坐标系。
同理,改变camera的 position, rotation, near, far都会变换这个矩阵
-
uniform mat4 projectionMatrix;投影矩阵,它负责将3D场景从相机视角(观察空间)投影到2D裁剪空间。
也可以传递自定义的属性。
uniform vec2 uFrequency;uniform float uTime;在RawShaderMaterial中通过uniforms可以传递uniform类型的数据
const material = new THREE.RawShaderMaterial({ // ... uniforms: { uFrequency: { value: new THREE.Vector2(10, 5) }, uTime: { value: 0 }, uColor: { value: new THREE.Color(0x7799cc) }, uTexture: { value: flagTexture }, },})上面就像顶点着色器和片段着色器传递了四个uniform类型的数据
attribute (属性)是顶点间变化的数据。主要功能是从顶点缓冲区读取每个顶点的数据。这些数据通常包含顶点的位置、法线、纹理坐标等信息。它是专门为顶点着色器设计的,因为它存储的是每个顶点特有的数据。在顶点着色器执行过程中,会逐个处理每个顶点的 attribute 数据。
attribute vec3 position;attribute vec2 uv;这个值从实例化的THREE.xxxGeometry中 geometry.attributes.position中的每个坐标点取得
attribute vec2 uv;同样的接收一个uv坐标,也可以通过geometry.setAttribute('random', new THREE.BufferAttribute(randoms, 1))添加一组名为 random的attribute,同样通过attribute float random;获取
varying vec2 vUv;varying float vElevation;varying 变量充当了顶点着色器和片段着色器之间的数据桥梁。顶点着色器会为每个顶点计算 varying 变量的值,在光栅化阶段,这些值会在三角形的各个片段之间进行插值。片段着色器会接收这些插值后的值,并以此进行后续的计算。简而言之,varying 变量将顶点级别的数据传递到了片段着色器。
上面代码就向片段着色器传递了 vec2 vUv和 float vElevation的值,在fragment.glsl中用相同的方式接收。
void main() { vec4 modelPosition = modelMatrix * vec4(position, 1.0); float elevation = sin(modelPosition.x * uFrequency.x - uTime) * 0.1; elevation += sin(modelPosition.y * uFrequency.y - uTime) * 0.1; modelPosition.z += elevation;
vec4 viewPosition = viewMatrix * modelPosition; vec4 projectionPosition = projectionMatrix * viewPosition;
gl_Position = projectionPosition; // gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
vUv = uv; vElevation = elevation;}main()函数是整个程序的入口,对每个顶点都会执行这段代码。
gl_Position是顶点着色器的内置输出变量,用于存储顶点的最终裁剪空间坐标。这个坐标会被后续的图形管线用于裁剪、透视除法和视口变换到屏幕空间。
每个矩阵都会对 gl_Position进行变换,最终时将顶点定位在裁剪空间内。所以在渲染时我们会应用不同的矩阵,直到获得裁剪空间的顶点坐标。
在每次顶点着色器运行结束时,OpenGL 期望坐标在特定范围内,并且任何超出此范围的坐标都将被剪裁。被剪切的坐标将被丢弃,因此剩余的坐标最终将在屏幕上显示为片段。这也是 clip space (裁剪空间)名称的由来。
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);因为矩阵乘法是右乘计算的,实际计算顺序:modelMatrix → viewMatrix → projectionMatrix。组合顺序不能变,等价于下面的代码
vec4 modelPosition = modelMatrix * vec4(position, 1.0);vec4 viewPosition = viewMatrix * modelPosition;vec4 projectionPosition = projectionMatrix * viewPosition;
gl_Position = projectionPosition;fragment.glsl
precision mediump float;
uniform vec3 uColor;// 声明一个 uniform sampler2D,用于表示可以在片段着色器中采样的纹理uniform sampler2D uTexture;
varying vec2 vUv;varying float vElevation;
void main() { // 第一个参数是纹理,第二个参数是在纹理的哪个位置着色也就是uv坐标 vec4 textrueColor = texture2D(uTexture, vUv); textrueColor.rgb *= vElevation * 2.0 + 0.5; // [0.3, 0.7] // gl_FragColor = vec4(uColor, 1.0);
gl_FragColor = vec4(textrueColor);}precision mediump float;定义浮点数的精度:highp,mediump,lowp
为什么需要?当你在处理非常大、非常小的数值时,或者需要大幅缩小场景,然后放大到场景中的一个很小的物体上,可能会影响渲染效果。
highp会影响性能,而且低端设备上可能无法支持;lowp可能因为缺乏精度引发错误。所以通常使用 mediump
uniform sampler2D uTexture;申明一个 uniform的 sampler2D(2D textrue)类型,存储了纹理数据
varying vec2 vUv;varying float vElevation;从 vertex.glsl 中接收插值vec2 vUv和float vElevation
fragment shader 不能直接获取attribute,如果要获取uv坐标,需要通过 vertex shader,发送给 fragment shader 接收,并且要取别名不能重复声明变量。
vec4 textrueColor = texture2D(uTexture, vUv);texture2D(sampler2D sampler, vec2 uv) 是一个用于从2D纹理中采样颜色值的内置函数,根据给定的纹理坐标从绑定的纹理中获取对应的颜色数据。为3D模型的每个顶点指定UV坐标,告诉渲染器:“将纹理图像中(u, v)位置的颜色,应用到该顶点对应的模型表面区域”。
输入两个参数,第一个参数是纹理,第二个参数是在纹理的哪个位置着色也就是uv坐标;输出一个 vec4 类型的颜色值(RGBA格式)
gl_FragColor = vec4(uColor, 1.0); // 应用 material.uniforms.uColor 的颜色
gl_FragColor = vec4(textrueColor); // 应用采样后的纹理颜色gl_FragColor = vec4(r, g, b, a)给每个可见片段上色,直接决定了渲染到屏幕(或帧缓冲)的像素颜色。若要更改透明度(a)时,需要给material设置 transparent: true
如果想查看某些值,我们可以发送给gl_FragColor显示,例如查看uv gl_FragColor = vec4(vUv, 1.0, 1.0);
ShaderMaterial
ShaderMaterial和RawShaderMaterial基本相同,只不过ShaderMaterial提前加入了一些声明:
uniform mat4 projectionMatrix;uniform mat4 viewMatrix;uniform mat4 modelMatrix;
attribute vec3 position;attribute vec2 uv;
precision mediump float;RawShaderMaterial中所有uniforms和attributes需用户手动声明和赋值。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!














