three.js 着色器学习笔记
着色器顾名思义就是给物体增加颜色的工具。我们也知道,创建物体时可以设置该物体的材质,材质决定了物体的外观。
其实使用着色器,就是给物体设置一种特殊的材质,这个材质是由着色器来决定显示效果。
下面以实际代码来展示不同材质的效果。
基本材质
function init () {
// 创建渲染器
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('main')
})
const width = window.innerWidth
const height = window.innerHeight
renderer.setClearColor(0xffffff)
renderer.setSize(width, height)
// 创建场景
const scene = new THREE.Scene()
// 原点
const origin = new THREE.Vector3(0, 0, 0)
// 创建照相机
const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000)
camera.position.set(0, 10, 10)
camera.lookAt(origin)
scene.add(camera)
// 创建光源
const light = new THREE.DirectionalLight(0xffffff, 0.8)
light.position.set(0, 10, 10)
scene.add(light)
// 创建基本材质的球体
const baseSphere = new THREE.Mesh(new THREE.SphereGeometry(1, 20, 20),
new THREE.MeshBasicMaterial({
color: 0xff0000
})
)
scene.add(baseSphere)
renderer.render(scene, camera)
}
init()
代码很简单,渲染出了一个基本材质MeshBasicMaterial
的球体。
Lambert材质
同样增加一个球体,其他参数相同仅仅是材质不同。
// 创建 Lambert 材质球体
const lambertSphere = new THREE.Mesh(new THREE.SphereGeometry(1, 20, 20),
new THREE.MeshLambertMaterial({
color: 0xff0000
})
)
// 将该球体放置在基本材质球体的左边
lambertSphere.position.set(-3, 0, 0)
scene.add(lambertSphere)
在基本材质的左边可以看到一个新的小球,这就是 Lambert 材质的小球。
Phong材质
// 创建 Phong材质 材质球体
const phongSphere = new THREE.Mesh(new THREE.SphereGeometry(1, 20, 20),
new THREE.MeshPhongMaterial({
color: 0xff0000
})
)
phongSphere.position.set(3, 0, 0)
scene.add(phongSphere)
除了上面三种外,还可以贴图。下面就开始进入正题,着色器,或者说着色器材质?
着色器
着色器分为顶点着色器与片元着色器,这里也不具体介绍,因为导师的书籍和博客都已经介绍得非常清楚了。
重点在这两句话:
- 顶点着色器就是每个顶点调用一次的程序。在顶点着色器中,可以访问到顶点的三维位置、颜色、法向量等信息。可以通过修改这些值,或者将其传递到片元着色器中,实现特定的渲染效果。
- 片元着色器就是每个片元调用一次的程序。在片元着色器中,可以访问到片元在二维屏幕上的坐标、深度信息、颜色等信息。通过改变这些值,可以实现特定的渲染效果。
OK,话不多说,再新建一个小球,这次先不设置材质,看看会怎么样。
// 没有材质的球体
const shaderSphere = new THREE.Mesh(new THREE.SphereGeometry(1, 20, 20))
shaderSphere.position.set(0, 3, 0)
scene.add(shaderSphere)
而且刷新会有一个随机的颜色,因为似乎默认使用了基本材质?
使用基本材质(BasicMaterial)的物体,渲染后物体的颜色始终为该材质的颜色,而不会由于光照产生明暗、阴影效果。如果没有指定材质的颜色,则颜色是随机的。
接下来就来添加材质。
// 新建着色器材质
var material = new THREE.ShaderMaterial({
vertexShader: `
uniform vec3 color;
uniform vec3 light;
varying vec3 vColor;
varying vec3 vNormal;
varying vec3 vLight;
void main()
{
// pass to fs
vColor = color;
vNormal = normalize(normalMatrix * normal);
vec4 viewLight = viewMatrix * vec4(light, 1.0);
vLight = viewLight.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`,
fragmentShader: `
varying vec3 vColor;
varying vec3 vNormal;
varying vec3 vLight;
void main() {
float diffuse = dot(normalize(vLight), vNormal);
if (diffuse > 0.8) {
diffuse = 1.0;
}
else if (diffuse > 0.5) {
diffuse = 0.6;
}
else if (diffuse > 0.2) {
diffuse = 0.4;
}
else {
diffuse = 0.2;
}
gl_FragColor = vec4(vColor * diffuse, 1.0);
}`,
uniforms: {
color: {
type: 'v3', // 指定变量类型为三维向量
value: new THREE.Color('#ff0000') // 要传递给着色器的颜色值
},
light: {
type: 'v3',
value: light.position // 光源位置
}
}
})
这样就新建了一个材质,并且该材质的显示效果由vertexShader
和fragmentShader
两个着色器决定。
将该材质用在我们没有材质的小球上。
shaderSphere.material = material
再看看浏览器,发现和之前完全不同了。
可以看到上面的球体颜色有很明显的变化,上面颜色浅下面深,因为光是从顶上照射下来,所以有这种效果,如果改变光照方向,效果也不一样。
实现了需要的效果没有太大意义,更需要的是理解为什么会有这种效果。
顶点着色器
uniform vec3 color;
uniform vec3 light;
varying vec3 vColor;
varying vec3 vNormal;
varying vec3 vLight;
void main() {
// pass to fs
vColor = color;
vNormal = normalize(normalMatrix * normal);
vec4 viewLight = viewMatrix * vec4(light, 1.0);
vLight = viewLight.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
一开始看的时候也完全不明白写的是什么,以学习另一种语言的心理再来看就慢慢能够理解了。
uniform
和varying
就是和 js 中的 var、let一样,用来声明变量的。所以这段代码一开始就声明了五个变量
- color
- light
- vColor
- vNormal
- vLight
使用uniform
声明的变量将会从我们的 js 代码中传递过来,所以color
就是#ff0000
,light就是光源位置。
然后是main()
函数,表示会执行这里面的代码,第一行vColor = color;
很简单就是一个赋值操作。
接下来就复杂了。。。
vNormal = normalize(normalMatrix * normal);
虽然能知道是给vNormal
变量赋值,但是normalize
、normalMatrix
和normal
都是哪里来的?
回头看看之前说的重点,顶点着色器,也就是这段代码能够访问到顶点的三维位置、颜色、法向量等信息。normalMatrix
和normal
就是顶点信息,而normalize
则是全局方法。
OK,现在知道变量是怎么来的,但是为啥要这么做呢?。。。。
竟无语凝噎,还是看导师的博客吧。
- 算法理解
总之,就是得到了 vNormal
、vColor
和vLight
这三个变量并传递给片元着色器。
片元着色器
varying vec3 vColor;
varying vec3 vNormal;
varying vec3 vLight;
void main() {
float diffuse = dot(normalize(vLight), vNormal);
if (diffuse > 0.8) {
diffuse = 1.0;
}
else if (diffuse > 0.5) {
diffuse = 0.6;
}
else if (diffuse > 0.2) {
diffuse = 0.4;
}
else {
diffuse = 0.2;
}
gl_FragColor = vec4(vColor * diffuse, 1.0);
}
最为核心的代码就是main()
函数中的第一句:
float diffuse = dot(normalize(vLight), vNormal);
调用normalize
方法得到的结果,与vNormal
作为参数再传入dot方法,最后得到的结果是片元的亮度值。
并且,将亮度值固定为1.0
、0.6
、0.4
和0.2
这四个值,所以,最终渲染出来的结果肯定只有四种颜色。
如果不将亮度值规定为四个值,而是直接使用,又是什么效果呢?
结果和预想的一样,diffuse是一个0 ~ 1的值,并且是逐渐变化的。
总结
物体的外观是由材质决定的,three.js 中有提供现成的材质,也可以自己使用着色器来自定义材质。
着色器分为顶点着色器与片元着色器,这两种着色器其实本质上就是以另一种语言实现的函数方法。由于是另一种语言所以也是有自己的变量声明规则以及全局方法,同时 three.js 也能够传递变量给着色器。
重点在于着色器中能够直接取到关于物体的顶点信息与片元信息,正是借助这些信息才能够定制显示效果。
参考
本文仅作webgl学习技术参考,点击阅读原文。