1. 什么是shader?
- WebGL中重要的一部分内容
- 开发原生WebGL必须学习的知识
比如你不依赖任何库去开发,例如绘制三角形啥的,就必须要掌握shader。
下面简述一下什么是shader?
- shader也可以说是一种程序,使用GLSL编写。
- 编写的shader会发送到GPU
- shader会定位几何体的每个顶点
- shader会对几何体的每个像素进行着色
第4点用像素可能不太准确,因为在渲染的时候,每个点不一定对应显示器的每个像素,使用fragment(片段)更合适一点。
我们在shader中需要编写很多内容,诸如
- 顶点定位
- 几何体的变换
- 摄像机的信息
- 颜色
- 纹理
- 灯光
- 雾
- etc
编写完这些内容后,GPU会按照shader的操作处理这些内容。
shader可以分为两种类型
- vertex shader(顶点shader),作用是定位几何体的每个顶点。
- fragment shader(片段shader),作用是对几何体的每个片段进行着色。
2. 为什么我们需要编写shader?
- three.js material是有限制的
- 我们自己编写的shader可以很简单并且性能表现更好
- 自己编写shader,我们可以对其做后期处理
3. 尝试使用RawShaderMaterial编写我们的第一个shader
3.1 使用vertextShader和fragmentShader属性
const material = new THREE.RawShaderMaterial({
vertextShader: ``,
fragmentShader: ``
})
尝试编写shader
// 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);
现在我们自己编写的shader起作用了,后面我们会解释上述代码的含义。
3.2 将上述程序中的shader代码分离到不同文件中
// src/shaders/test/vertex.glsl
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position;
void main()
{
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
}
// src/shaders/test/fragment.glsl
precision mediump float;
void main()
{
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
安装VSCode插件,使glsl语言高亮
使用分离的glsl文件
import testVertexShader from "./shaders/test/vertex.glsl";
import testGragmentShader from "./shaders/test/fragment.glsl";
// Material
const material = new THREE.RawShaderMaterial({
vertexShader: testVertexShader,
fragmentShader: testGragmentShader,
});
此时会提示编译错误,此时我们需要配置一下webpack
// shaders
{
test: /\.(glsl|vs|fs|vert|grag)$/,
exclude: /node_modules/,
use: ["raw-loader"],
},
现在就完成了glsl文件分离啦
其他通用属性,像wireframe,side,transparent,flatShading仍然可以配置
// Material
const material = new THREE.RawShaderMaterial({
vertexShader: testVertexShader,
fragmentShader: testGragmentShader,
wireframe: true
});
但是一些属性,像map,alphaMap,opacity,color等不能配置,这些我们需要自己写在glsl文件中。
4. 简述GLSL
我们编写的shader语言叫做GLSL,它很类似于C语言,GLSL其使用C语言作为基础高阶着色语言,避免了使用汇编语言或硬件规格语言的复杂性。
4.1 不能打印
诸如,print, log等打印语句是不生效的
4.2 忽略缩进
缩进是不影响程序运行结果的
4.3 分号是必须的
4.4 变量
glsl 和 c语言类似,是强类型语言
// 浮点数
float a = -0.123;
float b = 1.0;
// 整数
int c = 123;
// 可以在运算时进行类型转换
float d = b * float(c);
// 布尔值
bool e = true;
bool f = false;
// 二维向量
vet2 g = vec2(-1.0, 2.0);
vet2 h = vec2(0.0);
h.x = 1.0;
vet2 i = vec2(1.0, 2.0);
i *= 2;
// 三维向量
vec3 j = vec3(1.0, 2.0, 3.0);
vec3 k = vec3(0.0);
// 可以使用r代替x,g代替y,b代替z
k.r = 0.5;
k.g = 0.2;
vet3 l = vec3(1.0, 2.0, 3.0);
vec2 m = l.xy; // vec2 = vec2(1.0, 2.0)
// 四维向量
vec4 n = vec4(1.0, 2.0, 3.0, 4.0);
vec4 o = vec4(n.zw, vec2(5.0, 5.0));
除了这些类型还有mat2, mat3, mat4, sampler2D,后续再介绍这些,先从基本的入手。
4.5 函数
float caluate() {
float a = 1.0;
float b = 2.0;
return a + b;
}
float add(float a, float b) {
return a + b;
}
4.6 内置函数
内置函数有很多,例如 sin, cos, max, min, pow, exp, mod, clamp等,下面提供了一些网站,可以方便查询有哪些内置函数
5. 理解GLSL
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position;
void main() {
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
}
5.1 主函数 main
- 主函数会自动执行
- 不需要返回任何内容
void main() {
}
5.2 gl_Position
- 已经存在,不需要声明
- 包含屏幕中顶点的位置信息
我们可以改变其x和y属性
void main() {
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
gl_Position.x += 0.5;
gl_Position.y += 0.5;
}
但要注意,这是移动物体的错误方式,不要使用。
5.3 attribute position
attribute vec3 position;
convert it to vec4
gl_Position = /* ... */ * vec4(position, 1.0);
5.4 matrices uniforms
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
每一个矩阵都会转换位置,知道得到正确的正标
- 3个矩阵
- 使用
uniform是因为它们应用在几何体的每个顶点上 modelMatrix相当于几何体位置的转换viewMatrix相当于摄像机的转换projectionMatrix,投影矩阵,相当于最终显示的位置
此外,我们还可以写简洁版本,将modelMatrix和viewMatrix合并为modelViewMatrix
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
attribute vec3 position;
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
还可以根据矩阵分离一下我们的代码
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position;
void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition
gl_Position = projectedPosition
}
现在可以修改modelPosition的 x 和 y 属性来调整位置。
void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
// modelPosition.y += 1.0;
modelPosition.z += sin(modelPosition.x * 10.0)* 0.1;
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
}
现在我们就绘制了一个类似挥舞的旗帜。而这不使用shader很难做到。
5.5 理解fragment shader
precision mediump float;
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
precision
float的精度可以是
- highp(会丢失性能)
- mediump
- lowp
通常使用mediump
gl_FragColor
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
// 对应的是 r, g, b, alpha
5.6 创建自定义属性
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));
应用自定义属性到vertex shader
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
attribute vec3 position;
attribute float aRandom;
void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
modelPosition.z += aRandom* 0.1;
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
}
在vertex shader中使用varying定义属性,并尝试在fragment shader中使用
// vertex shader
attribute float aRandom;
varying float vRandom;
void main() {
// ...
vRandom = aRandom;
}
// fragment shader
precision mediump float;
varying float vRandom;
void main() {
gl_FragColor = vec4(vRandom, 0.0, 0.0, 1.0);
}
现在我们成功对每个顶点进行着色了。
uniforms
uniform很有用,例如
- 可以使相同的着色器呈现不同的结果
- 可以调整数值
- 制作动画
给 material 添加 uniform属性
const material = new THREE.RawShaderMaterial({
vertexShader: testVertexShader,
fragmentShader: testGragmentShader,
uniforms: {
uFrequency: { value: 10 },
},
});
在vertx shader中使用
// vertex shader
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform float uFrequency;
attribute vec3 position;
void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
modelPosition.z += sin(modelPosition.x * uFrequency) * 0.1;
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
}
此外我们还可以把值替换成Vector2来控制每个轴
const material = new THREE.RawShaderMaterial({
vertexShader: testVertexShader,
fragmentShader: testGragmentShader,
uniforms: {
uFrequency: { value: new THREE.Vector2(10, 5) },
},
});
// vertex shader
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform vec2 uFrequency;
attribute vec3 position;
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;
}
制作舞动的旗帜
首先添加uTime属性
// Material
const material = new THREE.RawShaderMaterial({
vertexShader: testVertexShader,
fragmentShader: testGragmentShader,
uniforms: {
uFrequency: { value: new THREE.Vector2(10, 5) },
uTime: { value: 0 },
},
});
const tick = () => {
const elapsedTime = clock.getElapsedTime();
material.uniforms.uTime.value = elapsedTime;
};
在vertex shader中使用
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform vec2 uFrequency;
uniform float uTime;
attribute vec3 position;
void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
modelPosition.z += sin(modelPosition.x * uFrequency.x - uTime) * 0.1;
modelPosition.z += sin(modelPosition.y * uFrequency.y - uTime) * 0.1;
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
}
现在舞动的旗帜就完成啦
注意在tick函数中,我们使用的是elapsedTime,而不是Date.now(),不使用的原因是因为它太大了,因为对shader来说,Date.now()太大了
给旗帜添加纹理
const flagTexture = textureLoader.load("/textures/flag-china.jpg");
const material = new THREE.RawShaderMaterial({
vertexShader: testVertexShader,
fragmentShader: testGragmentShader,
uniforms: {
uFrequency: { value: new THREE.Vector2(10, 5) },
uTime: { value: 0 },
uTexture: { value: flagTexture },
},
});
// vartex shader
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform vec2 uFrequency;
uniform float uTime;
attribute vec3 position;
attribute vec2 uv; // 传递uv坐标
varying vec2 vUv; // 传递给fragment shader
void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
modelPosition.z += sin(modelPosition.x * uFrequency.x - uTime) * 0.1;
modelPosition.z += sin(modelPosition.y * uFrequency.y - uTime) * 0.1;
modelPosition.y *= 0.75;
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
vUv = uv;
}
precision mediump float;
uniform sampler2D uTexture; // 使用smapler2D来接受纹理
varying vec2 vUv;
void main() {
vec4 textureColor = texture2D(uTexture, vUv);
gl_FragColor = textureColor;
}
好啦,现在舞动的国旗就画好啦~
demo预览地址
最后
⚽️本文主要介绍了shader的基础知识,以及如何使用shader绘制国旗
⚾如果你对本专栏感兴趣欢迎点赞关注+收藏,后面会持续更新,更多精彩知识正在等你!😘
🏉此外笔者还有其他专栏,欢迎阅读~
🏐玩转CSS之美
🎳深入浅出JavaScript