Shader
之所以叫着色器,是因为它有shading
着色这个概念。
shading
着色,指的是在三维模型,甚至是插画中通过不同的亮度表现深度的手法,这种方法可以使得3D
场景显得更加逼真和生动。
所以说,我们要怎么做,才可以计算得到物体表面的亮度呢?图形学,给了我们答案,那就是用光照模型
所以本章节的主题就是在Shader实现基本的光照模型
啦
开始模板
首先我们来建立一个新的 HTML 文件吧:
<style>
body {
margin: 0;
background: black;
}
</style>
<div id="sketch"></div>
<script src="https://unpkg.com/kokomi.js@1.9.78/build/kokomi.umd.js"></script>
<script src="https://unpkg.com/three@0.154.0/build/three.min.js"></script>
<script>
class Sketch extends kokomi.Base {
create() {
this.camera.position.set(0, 0, 5);
new kokomi.OrbitControls(this);
const geometry = new THREE.SphereGeometry(2, 64, 64);
// const geometry = new THREE.PlaneGeometry(4, 4);
const material = new THREE.ShaderMaterial({
vertexShader: /* glsl */ `
uniform float iTime;
uniform vec3 iResolution;
uniform vec4 iMouse;
varying vec2 vUv;
void main(){
vec3 p=position;
gl_Position=projectionMatrix*modelViewMatrix*vec4(p,1.);
vUv=uv;
}
`,
fragmentShader: /* glsl */ `
uniform float iTime;
uniform vec3 iResolution;
uniform vec4 iMouse;
varying vec2 vUv;
void main(){
vec2 uv=vUv;
gl_FragColor=vec4(uv,0.,1.);
}
`,
});
const mesh = new THREE.Mesh(geometry, material);
this.scene.add(mesh);
const uj = new kokomi.UniformInjector(this);
material.uniforms = {
...material.uniforms,
...uj.shadertoyUniforms,
};
this.update(() => {
uj.injectShadertoyUniforms(material.uniforms);
});
}
}
const sketch = new Sketch("#sketch");
sketch.create();
</script>
然后我们以此为基础开始光照模型的学习。
冯氏光照模型
20 世纪中后期,一位叫做裴祥风的图形学学者提出了“冯氏光照模型”的理念,后来得到了 CG 以及图形学界的广泛应用。
这个模型主要由三种光照组成:环境光
、漫反射
、镜面反射
。
让我们关注片元着色器,在main
中定义好几个变量,结果颜色col
设置为黑色(因为还没有光照),物体的基础颜色objectColor
设置为白色,光照颜色lightColor
为亮红色。
void main(){
vec2 uv=vUv;
vec3 col = vec3(0.);
vec3 objectColor = vec3(1.);
vec3 lightColor = vec3(.875, .286, .333);
gl_FragColor=vec4(col,1.);
}
环境光
指的是物体表面收到来自周围环境光源的光照,这种光照均匀的分布在物体的表面上。
实现起来也是非常的简单,我们只需要给出光的颜色,然后乘上光的强度就可以得出结果了。
// 光照强度0.2
float ambIntensity = .2;
// 光照颜色 * 强度 = 环境光
vec3 ambient = lightColor * ambIntensity;
// 输出颜色 加上 光照颜色 乘以 物体颜色就是结果了
col += ambient*objectColor;
gl_FragColor=vec4(col,1.);
可以看出来,物体整体是偏暗的,因为环境光的作用显示出了淡淡的暗红色。
漫反射
漫反射说的是光照照射到物体表面之后,光线被物体表面反射,反射光线会向所有方向散射的现象。
大概手绘了一个示意图:
也就是说,当光线照射到物体表面的时候,会和法向量N
形成一个夹角θ
。
所谓的法向量就是与物体垂直的向量。
θ
这个夹角就叫做入射角。入射角越大,就说明光照强度越小;反之,如果入射角越小,就说明光照强度越大;当入射角为 0 时,光线和法向量方向会重叠,此时光照垂直照射物体的时候,强度是最大的。
要获得漫反射的光,需要的步骤有如下:
- 获取法向量
N
- 获取光线的方向向量
- 计算漫反射因子。
- 计算漫反射光。
那我们一步步来处理吧:
获取法向量
先说法向量N
,这个变量其实在顶点着色器里已经有了,叫normal
,把它传递给片元着色器就要用到varying
变量。
在顶点着色器上声明normal
。
varying vec3 vNormal;
在主函数main
中,将normal
赋值给vNormal
。
void main() {
...
vNormal = normal;
}
在片元着色器的上方也声明一下:
varying vec3 vNormal;
如此以来,我们就成功的获取了法向量N
。
获取光线的方向向量
在计算光线的方向向量之前,我们要算一下光线照射到物体的那一部分片元的位置,而这里的位置是世界空间坐标系的,也就是从物体坐标系通过模型矩阵变换之后得出的结果。
咱们在顶点着色器的顶部,声明varying
变量vWorldPosition
,表示的是片元的位置,然后接下来在main
函数中计算它:
varying vec3 vWorldPosition;
...
// vWorldPosition=vec3(modelMatrix*vec4(p,1.));
vWorldPosition=(modelMatrix*vec4(p,1.)).xyz;
这里有两种写法,xyz 打点调用和 vec3 转换都是可以的,看个人习惯了!
随后在片元着色器上也声明一下:
varying vec3 vWorldPosition;
如此一来,我们就在片元着色器上获取了片段(光线照射到物体的那一部分片元)的位置,接下来就可以计算光线的方向向量:
vec3 lightPos = vec3(0., 0., 5.);
vec3 lightDir = normalize(lightPos - vWorldPosition);
来解释一下这些变量都是啥:
- lightPos:光照位置,这里我随便设置了一下
- lightDir:光线方向向量,其值就是光照位置和片元位置也就是片段的差,我们需要用
normalize
来归一化一下结果。
计算漫反射因子
漫反射因子就是漫反射强度,这个强度跟入射角θ
有关系,并且他们是呈现反比例的关系的,既然是反比例关系,那我们完全可以用入射角的余弦值来作为漫反射因子。在数学领域,求两个向量的点积就可以得到夹角的余弦值。在GLSL
中有一个方法叫做dot
,它就可以直接计算两个数的点积,我们用它来计算法向量与光线方向向量的余弦值。
float diff = dot(vNormal, lightDir);
但是,余弦函数在角度>90 度的时候会出现负数的情况,为了避免出现负数,我们可以用max
函数来保证最后的结果不小于 0。
所以就是:
float diff = max(dot(vNormal, lightDir), 0.);
计算漫反射光
好了,我们得到了法向量
、光线方向向量
、漫反射因子
,集齐了三个必要条件,可以开始计算漫反射光了。
vec3 diffuse=lightColor*diff;
col+=diffuse*objectColor;
我们只需要把光照颜色和漫反射因子相乘,就可以得到漫反射光照,再把漫反射光照和物体的颜色相乘,加到输出颜色上,就能得到被光照影响之后的物体。
球体上产生了明显的明暗区分,这就是漫反射光产生的效果了。
镜面反射
镜面反射指的是光源照射到物体表面之后,光线被物体表面反射,反射光线呈现出明亮的高光点的现象
我画了一个差不多的原理图:
就跟漫反射相似,镜面反射也是跟法向量和光照方向有关系的,只不过多了一个眼睛的方向。与此同时,镜面反射也取决于物体自身的属性,如果是镜子,那它的表面就很光滑,可以让光线同时往一个方向聚焦。
要获得镜面反射的光,同样需要以下的步骤:
- 获取反射光的方向向量
- 获取眼睛的方向向量
- 计算镜面高光因子
- 计算镜面反射光
让我们开始吧:
反射光的方向向量
GLSL
中,reflect
函数将算出反射光的向量,接受两个参数:入射光方向 & 法向量
vec3 reflectDir = reflect(-lightDir, vNormal);
第一个参数要求的方向是光照指向片元,我们之前算出来的那个光照方向lightDir
是片元指向光照的,所以加个负号给翻过来就好。
眼睛的方向向量
眼睛的方向取名为viewDir
,等于眼睛位置cameraPosition
减去片元的位置vWorldPosition
,结果也需要归一化。
vec3 viewDir = normalize(cameraPosition-vWorldPosition);
镜面高光因子
跟之前计算漫反射因子差不多,要计算反射光的方向向量 & 眼睛的方向向量的点积,同时要确保值不能是负数。
float spec = max(dot(viewDir, reflectDir), 0.);
然而,这样是不够的,因为上面提到反射还跟物体本身的属性有关系,所以考虑到这一点,我们引入一个额外的变量——“反光度”,我们取名叫shininess
,反光度越大,光线就越是聚焦,这里把反光度设置为 30 试试。
float shininess = 30.;
spec = pow(max(dot(viewDir, reflectDir),0.), shininess);
计算镜面反射光
这下要素集齐了,要召唤最终效果了!我们只要把光照颜色与镜面高光因子相乘,就可以得到镜面反射的最终光照了,光照和物体相乘,加上最终的颜色输出,就是我们的被光照影响之后的物体的最后样貌啦!
vec3 specular = lightColor * spec;
col += specular * objectColor;
真是让人激动,一个比较成功的 3D 模型就这样手搓出来了!!
文章末尾贴上全部代码:
<style>
body {
margin: 0;
background: black;
}
</style>
<div id="sketch"></div>
<script src="https://unpkg.com/kokomi.js@1.9.78/build/kokomi.umd.js"></script>
<script src="https://unpkg.com/three@0.154.0/build/three.min.js"></script>
<script>
class Sketch extends kokomi.Base {
create() {
this.camera.position.set(0, 0, 5);
new kokomi.OrbitControls(this);
const geometry = new THREE.SphereGeometry(2, 64, 64);
const material = new THREE.ShaderMaterial({
vertexShader: /* glsl */ `
uniform float iTime;
uniform vec3 iResolution;
uniform vec4 iMouse;
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vWorldPosition;
void main(){
vec3 p=position;
gl_Position=projectionMatrix*modelViewMatrix*vec4(p,1.);
vUv=uv;
vNormal=normal;
vWorldPosition=(modelMatrix*vec4(p,1)).xyz;
}
`,
fragmentShader: /* glsl */ `
uniform float iTime;
uniform vec3 iResolution;
uniform vec4 iMouse;
varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vWorldPosition;
void main(){
vec2 uv=vUv;
vec3 col=vec3(0.);
vec3 objectColor=vec3(1.);
vec3 lightColor=vec3(.875,.286,.333);
// 环境光
float ambIntensity=.2;
vec3 ambient=lightColor*ambIntensity;
col+=ambient*objectColor;
// 漫反射
vec3 lightPos=vec3(0., 0., 5.);
vec3 lightDir=normalize(lightPos-vWorldPosition);
float diff=dot(vNormal,lightDir);
diff=max(diff,0.);
vec3 diffuse=lightColor*diff;
col+=diffuse*objectColor;
// 镜面反射
vec3 reflectDir=reflect(-lightDir,vNormal);
vec3 viewDir=normalize(cameraPosition-vWorldPosition);
float spec=dot(viewDir,reflectDir);
spec=max(spec,0.);
float shininess=32.;
spec=pow(spec,shininess);
vec3 specular=lightColor*spec;
col+=specular*objectColor;
gl_FragColor=vec4(col,1.);
}
`,
});
const mesh = new THREE.Mesh(geometry, material);
this.scene.add(mesh);
const uj = new kokomi.UniformInjector(this);
material.uniforms = {
...material.uniforms,
...uj.shadertoyUniforms,
};
this.update(() => {
uj.injectShadertoyUniforms(material.uniforms);
});
}
}
const sketch = new Sketch("#sketch");
sketch.create();
</script>