本文内容:介绍web着色器的基本概念,以及通过一个着色器demo来进一步理解着色器的用法和作用。
顶点着色器 & 片元着色器
首先我们需要知道threejs绘制3D物体包含了两个主要属性:顶点以及材质,可以理解为就是位置和颜色。着色器决定了3D物体每个顶点最终在屏幕上渲染出来的位置和颜色。
所以着色器分为顶点着色器(用于决定位置)
和片元着色器(用于决定颜色)
在具体的绘制过程先执行顶点着色器,再执行片元着色器,因此片元着色器中的变量需要从顶点着色器中传入。
顶点着色器示例:
attribute float size; //顶点大小,由geometry的属性传入
attribute vec3 customColor; //顶点自定义颜色,由geometry的属性传入
varying vec3 vColor; //插值颜色
void main() {
vColor = customColor; //插值颜色,由geometry的属性传入
gl_PointSize = 10 ; // 给内置变量gl_PointSize赋值像素大小
//gl_Position的计算总是固定为 投影矩阵*模型视图矩阵*位置向量
gl_Position = projectionMatrix*modelViewMatrix * vec4( position, 1.0 );
}
顶点着色器定义了顶点的渲染位置和点的渲染像素大小,对组成物体的每个顶点都执行一次。
投影矩阵:不同的观察者位置和视角看到的物体不一样,所以需要一个投影矩阵变换操作。
模型视图矩阵:分为模型矩阵和视图矩阵,模型矩阵决定物体在世界空间矩阵的位置,比如操作物品平移,便是变更了模型矩阵。视图矩阵是从不同角度看到物品到二维平面的投影不一样,增加视图矩阵才能构建物体的全貌。
通过投影矩阵*模型视图矩阵*位置向量来计算出每个顶点在三维矩阵中的位置。
片元着色器示例:
uniform vec3 color; //顶点颜色 ,由shader构造材质时引入
uniform sampler2D pointTexture; //顶点纹理采样器
varying vec3 vColor; //顶点插值颜色
void main() {
gl_FragColor = vec4( color * vColor, 1.0 ) ;
}
片元着色器定义了点的渲染结果像素的颜色值,对组成的每个像素执行一次。
逐顶点&逐片元
理解内置变量gl_Position
需要建立逐顶点
的概念,在javascript语言中出现一个变量赋值,可以理解为仅仅执行一次,但是对于着色器中不能直接这么理解,如果有多个顶点,则每个顶点都要执行一遍顶点着色器主函数main
中的程序。
对于内置变量gl_FragColor
而言,需要建立逐片元
的概念。顶点经过片元着色器片元化以后,得到一个个片元,或者说像素点,然后通过内置变量gl_FragColor
给每一个片元设置颜色值,所有片元可以使用同一个颜色值,也可能不是同一个颜色值,可以通过特定算法计算或者纹理像素采样。
关键字
varing 可用于在顶点着色器和片元着色器中传递变量的。
uniform 可读属性,不能在着色器代码中更改,用于从js中传递一些参数。
attribute 可从geometry中的attribute中获取相应的参数。一些比较复杂的参数可以通过这个参数传入,注意这里也要符合逐片元。
着色器用于图元的材质示例:
var material = new THREE.ShaderMaterial({
uniforms: {
color: { value: new THREE.Color(0xf0ffff) },
pointTexture: { value: new THREE.TextureLoader().load("../images/red_line.png") }
},
vertexShader: v,
fragmentShader: f,
// blending: THREE.AdditiveBlending,
depthTest: false,
depthWrite: false,
transparent: true
});
vertexShader是顶点着色器,fragmentShader是片元着色器,支持js脚本引入或字符串(在工程化中,字符串比较方便),弊病就是目前不知道如何调试着色器代码。
uniforms,用于定义传入着色器的变量,可以改变uniforms中的值来动态变换物体的位置形状。变量需要在着色器代码中定义同名变量,例如:uniform sampler2D pointTexture;
在mesh.vertices
中存储的position值,因为它们是模型空间矩阵的,而变换是在世界空间矩阵中进行的,所以在世界空间矩阵中进行的变换,不会更改着色器中的position的值,但是会变更gl_Position的值。gl_Position是模型顶点在世界空间矩阵最终的位置。
例子
下面以一个自定义的着色器的例子看看着色器的使用。
平面扩散波纹
创建顶点着色器和片元着色器
// 顶点着色器代码
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`;
// 片元着色器代码
const fragmentShader = `
varying vec2 vUv;
// 下面是传进来的变量
uniform vec3 uColor;
uniform float uOpacity;
uniform float uRange;
uniform float uSpeed;
uniform float uSge;
uniform float time;
float PI = 3.14159265;
float drawCircle(float index, float range) {
float opacity = 1.0;
if (index >= 1.0 - range) {
opacity = 1.0 - (index - (1.0 - range)) / range;
} else if(index <= range) {
opacity = index / range;
}
return opacity;
}
void main() {
float iTime = -time * uSpeed;
float opacity = 0.0;
float len = distance(vec2(0.5, 0.5), vec2(vUv.x, vUv.y));
float size = 1.0 / uSge;
float rSize = uRange / 2.0;
vec2 range = vec2(0.7 - rSize, 0.7 + rSize);
float index = mod(iTime + len, size);
// 中心圆
vec2 cRadius = vec2(0.06, 0.12);
if (index < size && len <= 0.5) {
float i = sin(index / size * PI);
// 处理边缘锯齿
if (i >= range.x && i <= range.y){
// 归一
float t = (i - range.x) / (range.y - range.x);
// 边缘锯齿范围
float r = 0.3;
opacity = drawCircle(t, r);
}
// 渐变
opacity *= 1.0 - len / 0.5;
};
gl_FragColor = vec4(uColor, uOpacity * opacity);
}`;
构建threejs渲染环境
// 创建three环境
const container = document.getElementById('td-container');
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
camera.position.x = -3;
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
const lightGroup = new THREE.Group();
const ambientLight = new THREE.AmbientLight(0x404040);
lightGroup.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(-10, 6, 20);
lightGroup.add(directionalLight);
scene.add(light)
scene.background = new THREE.Color(0.9, 0.9, 0.9);
着色器使用
着色器是在图元材质中进行使用,分别对应vertexShader和fragmentShader,着色器中定义的变量通过uniforms传入。uniforms中的参数可以直接修改,threejs实例会监听uniforms的变量变化重新渲染图形。
// 相关的配置参数
const config = {
width: 8,
widthSegments: 8,
color: 0xff91c2,
opacity: 1,
range: 0.5,
speed: 0.1,
seg: 6,
};
const geometry = new THREE.PlaneBufferGeometry(
config.width,
config.width,
config.widthSegments,
config.widthSegments,
);
// 使用自定义着色器构建材质
const material = new THREE.ShaderMaterial({
uniforms: {
uColor: { value: new THREE.Color(config.color) },
uOpacity: { value: config.opacity },
uRange: { value: config.range }, // 圆环的大小
uSpeed: { value: config.speed }, // 扩散的速度
uSge: { value: config.seg }, // 圆环个数
uRadius: { value: config.width / 2 },
time: { value: 0 },
},
// opacity:0.5,
transparent: true,
vertexShader: vertexShader, // 顶点着色器
fragmentShader: fragmentShader, // 片元着色器
side: THREE.DoubleSide,
});
// 将材质绘制到一个平面上
const model = new THREE.Mesh(geometry,material);
scene.add(model);
// 执行动画
function animate() {
requestAnimationFrame(animate);
// 控制时间参数变化让圆圈动起来
material.uniforms['time'].value += 0.02;
renderer.render(scene, camera);
}
animate();
说明:在一个plane上,绘制出扩散波纹的材质,控制参数的变化,使得同心环移动起来。可以尝试修改uniforms的参数看图形的变化。 这里其实变换的是每一帧像素的不同颜色达到波纹扩散移动的效果。效果如下: