当Three.js内置的材质不能满足需求时,就需要通过编写着色器来实现了
也可能是出于性能原因。像MeshStandardMaterial这样的材料非常复杂,涉及大量的代码和计算。如果我们编写自己的着色器,我们可以将功能和计算保持在最低限度。我们可以更好地控制性能。
编写指定要着色器也是向渲染结果添加后处理效果的绝佳方式
要创建一个着色器,我们需要创建一个特定的材质。这个特定的材质可以时着色器材质ShaderMaterial或者原始材质RawShaderMaterial, 它们的区别是ShaderMaterial
会自动将一些代码添加到着色器代码中,而RawShaderMaterial
则不会。
这里从比较原始的RawShaderMaterial
实验:
// Geometry
const geometry = new THREE.PlaneGeometry(1, 1, 32, 32)
// Material
const material = new THREE.RawShaderMaterial({
vertexShader: `
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position;
void main(){
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
precision mediump float;
void main(){
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`
})
// Mesh
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
更好的写法是将着色器拆分写到不同文件中
这里可以分别创建vertex.glsl
、fragment.glsl
文件,将组织到某个文件夹下,因为一个项目通常有很多个着色器代码。
在vite中import shader文件(.glsl)问题
在vite中import shader文件(.glsl)问题,不需要安装插件,只需在导入文件的结尾添加一个?raw符号,vite会将改文件的内容解析为字符串导入
import vertexShader from "./../shader/particels_vs.glsl?raw"
import fragmentShader from "./../shader/particels_fs.glsl?raw"
使用其他打包构建工具估计也需要进行配置才能使用.glsl文件
例如在webpack中则需要添加:一个rules
{
test: /\.(glsl|vs|fs|vert|frag)$/,
type: 'asset/source',
generator:
{
filename: 'assets/images/[hash][ext]'
}
}
在其他常见材质中使用的属性(例如线框、侧面、透明或平面着色)仍然可用于 RawShaderMaterial
const material = new THREE.RawShaderMaterial({
vertexShader: testVertexShader,
fragmentShader: testFragmentShader,
wireframe: true //网格线条
})
但是map
贴图、alphaMap
、opacity
、color
等属性将不再起作用,因为我们需要自己在着色器中编写这些功能。
首先看看MVP矩阵,这里的MVP矩阵于WEBGL中有些不同
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
modelMatrix
将应用与Mesh相关的所有变换。如果我们缩放、旋转或移动网格,这些转换将包含在 中 modelMatrix 并应用于 positionviewMatrix
将应用相对于相机的变换。如果我们将相机向左旋转,顶点应该在右侧。如果我们沿网格体的方向移动相机,顶点应该会变大,依此类推。projectionMatrix
会将我们的坐标转换为最终的剪辑空间坐标。参考:LearnOpenGL - Coordinate Systems
关于片段着色器的精度:
当我们使用ShaderMaterial
而不是RawShaderMaterial
时,这部分会自动处理。
开始上点费脑的内置函数
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position;
void main(){
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
modelPosition.z += sin(modelPosition.x * 10.0) * 0.1;
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
}
可以直接将要添加的Attributes属性直接添加到 BufferGeometry 中,例如下面传递一些随机值到顶点着色器重
const count = geometry.attributes.position.count //获取几何体的顶点数
const randoms = new Float32Array(count)
for(let i = 0; i < count; i++){
randoms[i] = Math.random()
}
geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1))//传递
然后就可以直接在顶点着色器中使用了:
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute float aRandom;
attribute vec3 position;
void main(){
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
// modelPosition.z += sin(modelPosition.x * 10.0) * 0.1;
modelPosition.z += aRandom * 0.1;
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
}
varying float vRandom;
void main(){
// ...
vRandom = aRandom;
}
precision mediump float;
varying float vRandom;
void main(){
gl_FragColor = vec4(0.5, vRandom, 1.0, 1.0);
}
与传递attribute不同的是传递Uniforms直接通过RawShaderMaterial的属性传递即可
const material = new THREE.RawShaderMaterial({
vertexShader: testVertexShader,
fragmentShader: testFragmentShader,
uniforms:{
uFrequency: { value: new THREE.Vector2(10, 5) } //传递一个波动频率
}
})
使用
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform float uFrequency;
attribute float aRandom;
attribute vec3 position;
varying float vRandom;
void main(){
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
modelPosition.z += sin(modelPosition.x * uFrequency.x) * 0.1;
modelPosition.z += sin(modelPosition.y * uFrequency.y) * 0.1;
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
vRandom = aRandom;
}
因为这些值是直接在在传递到着色器的,那么就可以动态控制传递过去,比如通过dat.gui控制
gui.add(material.uniforms.uFrequency.value, 'x').min(0).max(20).step(0.01).name('frequencyX')
gui.add(material.uniforms.uFrequency.value, 'y').min(0).max(20).step(0.01).name('frequencyY')
还可以通过模型构建类似于shadtory里的一个自带的uTime变量,让图形动起来
const material = new THREE.RawShaderMaterial({
vertexShader: testVertexShader,
fragmentShader: testFragmentShader,
uniforms:{
uFrequency: { value: new THREE.Vector2(10, 5) },
uTime: { value: 0 }
}
})
在绘制函数中填值即可
const tick = () =>{
const elapsedTime = clock.getElapsedTime()
// Update material
material.uniforms.uTime.value = elapsedTime
// ...
}
// ...
uniform float uTime;
// ...
void main(){
// ...
modelPosition.z += sin(modelPosition.x * uFrequency.x + uTime) * 0.1;
modelPosition.z += sin(modelPosition.y * uFrequency.y + uTime) * 0.1;
// ...
}
这样就动起来了
后面还可以传递更多的uniform,比如color等这里就不演示了
这里讨论的是给RawShaderMaterial材质添加纹理图片,这里加载映射纹理同原始的webgl一样,需要uv坐标texture2D(uTexture, vUv);
这里可以直接将加载好的纹理通过uniform传递到片段着色器:
const flagTexture = textureLoader.load('/textures/flag-french.jpg')
const material = new THREE.RawShaderMaterial({
// ...
uniforms:{
// ...
uTexture: { value: flagTexture }
}
})
这里因为geometry会自动构建uv坐标到attribute上,所以这里就可以直接在顶点着色器中获取到
// ...
attribute vec2 uv;
varying vec2 vUv;
void main(){
// ...
vUv = uv;
}
precision mediump float;
uniform vec3 uColor;
uniform sampler2D uTexture;
varying vec2 vUv;
void main(){
vec4 textureColor = texture2D(uTexture, vUv);
gl_FragColor = textureColor;
}
ShaderMaterial相比于ShaderMaterial要方便一些,内置了一些变量
如下变量就不需要在着色器中定义就可以直接在glsl中使用了
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position;
attribute vec2 uv;
precision mediump float;
本文部分内容为Three.js Journey课程的学习笔记