源码:
源码作者:jam3
下载地址:https://github.com/Jam3/jam3-lesson-webgl-shader-threejs
这是一个很好的如何在threejs中使用自定义shader的入门教程。
若github下载太慢,可可关注公众号"实用图形学游玩指南及源码",回复“tsh1”使用国内网盘下载。
下面的内容翻译自readme.md
WebGL 课程 — ThreeJS Shaders
在这堂课里,我们将学习如何在THREEJS中使用片段和顶点着色器。请确保你对着色器和有基本的了解。
为什么要用 ThreeJS?
使用原生WebGL,那么就要写很多样板文件,增加了很大工作量。而使用ThreeJS提供了便利的封装,让我们更加专注于自己的事情。
如果你对ThreeJS和WebGL有基本的了解,那么学习起来将轻松很多。
Setup
你可以先直接下载ThreeJS,但在这里我们将直接使用npm和budo来更快投入开发。
💡 npm和budo是前端的模组,在这里你并不需要知道有关的太多信息。
第一步,你应该首先克隆这个项目到本地并且安装相关依赖。
git clone https://github.com/Jam3/jam3-lesson-webgl-shader-threejs.git
# move into directory
cd jam3-lesson-webgl-shader-threejs
# install dependencies
npm install
# start dev server
npm run start
你应该看到如下画面,这生成一个供index.html使用的bundle.js
现在来浏览器中打开http://localhost:9966/你就能看到一个白色的bunny,你可用鼠标拖动摄像机来从不同角度观察这个3D的bunny。
这个项目通过babelify使用了Babel,也使用了brfs。
代码概览
代码被放在不同的文件里。
-
[./lib/index.js](https://link.zhihu.com/?target=https%3A//github.com/Jam3/jam3-lesson-webgl-shader-threejs/blob/master/lib/index.js)
这里是我们demo的核心部分,创建了我们的应用,几何,网格和渲染循环。我们确保THREE能正常工作,我们通过require命令从其他文件引入。
global.THREE = require('three');
-
[./lib/createApp.js](https://link.zhihu.com/?target=https%3A//github.com/Jam3/jam3-lesson-webgl-shader-threejs/blob/master/lib/createApp.js)
这个样本程序使用orbit-controls,这样你就能用鼠标控制摄像机了。这不是这节课的重点。 -
[./lib/createBunnyGeometry.js](https://link.zhihu.com/?target=https%3A//github.com/Jam3/jam3-lesson-webgl-shader-threejs/blob/master/lib/createBunnyGeometry.js)
这个文件从网格多边形(mesh primitive)中创建了ThreeJS几何体,也就是一个小兔子 -
[./lib/shader.frag](https://link.zhihu.com/?target=https%3A//github.com/Jam3/jam3-lesson-webgl-shader-threejs/blob/master/lib/shader.frag)
这是你将在本节课使用的片段着色器。 -
[./lib/shader.vert](https://link.zhihu.com/?target=https%3A//github.com/Jam3/jam3-lesson-webgl-shader-threejs/blob/master/lib/shader.vert)
这是你在本节课使用的顶点着色器。 -
[./lib/reference/](https://link.zhihu.com/?target=https%3A//github.com/Jam3/jam3-lesson-webgl-shader-threejs/blob/master/lib/reference)
这里你可以看到一些写好的着色器示例。
Mesh & Shader
在 ThreeJS,Mesh
是最基本的渲染3D形状的材料。它由Geometry
和Material
组成,例如下面这段代码在常见中添加一个正方体。
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 'red' });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
在./lib/index.js, 我们使用类似的代码创建3D的bunny的几何模型。
// Get a nicely prepared geometry
const geometry = createBunnyGeometry({ flat: true });
现在要为我们的bunny创建材质。
我们使用RawShaderMaterial
, 这是一种可以使用vertexShader
和fragmentShader
的特殊材质。我们使用brfs, 一种 source transform,这样我们就能在开发时在单独的文件里编写shader,并在打包时将它们包括进去。
const path = require('path');
const fs = require('fs');
// Create our vertex/fragment shaders
const material = new THREE.RawShaderMaterial({
vertexShader: fs.readFileSync(path.join(__dirname, 'shader.vert'), 'utf8'),
fragmentShader: fs.readFileSync(path.join(__dirname, 'shader.frag'), 'utf8')
});
💡 也许glslify比brfs
更合你意。
就像创建box的mesh一样,我们创建了 bunny的mesh,
// Setup our mesh
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
步骤一:你的第一个shader
这个简单的Shader使用白色的纹理覆盖了bunny。在你挑战复杂的事情,先把简单的事情做好总时没错的。
顶点着色器
这个代码位于[./lib/shader.vert](https://link.zhihu.com/?target=https%3A//github.com/Jam3/jam3-lesson-webgl-shader-threejs/blob/master/lib/shader.vert)
attribute vec3 position;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
void main () {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
片段着色器和顶点着色器需要 一个main()
函数,WebGL将让它被每一个像素或每一个顶点调用。
顶点着色器引进了两个新概念:attributes和uniforms.
Attributes
顶点着色器将在几何体的每个顶点上运行,包含一些_attributes,比如_Position (XYZ), UV coordinates (XY), Normal (XYZ)。需要在JavaScript程序中预先给attributes赋值,然后传到顶点着色器中。
例如,一个三角形有3个顶点,每个顶点有一个_attribute_保存了XYZ左边。对每个三角形来说,GPU将执行三次顶点着色器程序。
我们的 bunny 顶点着色器只有一个 attribute 。
attribute vec3 position;
Uniforms
顶点和片段着色器都能使用_uniforms,_这是一个所有片段使用的常量,在JavaScript中赋值好,然后再着色器中读取。
我们我们使用一个 uniform来为我们的mesh规定一个常量RGB颜色。
ThreeJS提供一些给着色器的内建uniforms,例如如下的四个矩阵,都是mat4的
-
projectionMatrix
— 将3D世界坐标系转换为2D屏幕坐标系 -
viewMatrix
—PerspectiveCamera
世界矩阵的逆 -
modelMatrix
—Mesh
的局部坐标矩阵 -
modelViewMatrix
— view 和 model 矩阵的联合
你需要如下定义它们才能使用。
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
顶点投影
顶点着色器的目标是将我们的3D数据转换为能被WebGL光栅化投影到2D屏幕上的数据。
为了做到这点,我们使用如下代码。
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
💡 我们现在不必关心1.0
为什么要作为位置里的w
分量。但你也可以看看here.
我们也可以写的不同,用vec4定义attribute,WebGL将用 w= 1.0默认扩展每个向量,然后分别乘上三个矩阵
attribute vec4 position;
uniform mat4 projectionMatrix;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
void main () {
gl_Position = projectionMatrix * viewMatrix * modelMatrix * position;
}
片段着色器
如果你打开了[./lib/shader.frag](https://link.zhihu.com/?target=https%3A//github.com/Jam3/jam3-lesson-webgl-shader-threejs/blob/master/lib/shader.frag)
, 你将看到如下代码
precision highp float;
void main () {
gl_FragColor = vec4(1.0);
}
第一行precision
, 定义了GPU使用的浮点数的精度。当使用RawShaderMaterial
, 你就得在片段着色器中第一行这样定义。你可以使用lowp
,mediump
, 或highp
, 但一般来说将使用highp
。
💡 顶点着色浮点数据默认精度是highp
, 所以之前我们不需要定义它。
然后,我们使用gl_FragColor
, 这是一个vec4
作为颜色输出,你应该将这个向量的四个分量都写上。这里我们将它写为纯白:(1.0, 1.0, 1.0, 1.0)
。与ShaderToy中的fragColor
一样。
步骤2:与中心的距离
这里,我们将更加每个像素离中心(0, 0, 0)
的距离XYZ来决定它们的颜色。
顶点着色器:
这一步,顶点着色器类似于这样:
attribute vec4 position;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
varying float distFromCenter;
void main () {
distFromCenter = length(position.xyz);
gl_Position = projectionMatrix * modelViewMatrix * position;
}
这个着色器使用了新概念:varyings. 这个值只能在顶点着色器中写入,并被传递到片段着色器中。 这里,我们使用position
向量的长度,即这个顶点于vec3(0.0)
的距离。
💡 你也可以使用内置的distance
,例如:
distFromCenter = distance(position.xyz, vec3(0.0));
片段着色器
precision highp float;
varying float distFromCenter;
void main () {
gl_FragColor = vec4(vec3(distFromCenter), 1.0);
}
在片段着色器中,varyings是只读的,来自于顶点着色器。片段着色器将用顶点间的插值着色。比如一个顶点的值是0.0,另一个顶点的值是1.0,这两个顶点中间的像素的值将是介于这两个值之间的值。
这里,像素的颜色将有distFromCenter
来决定 ,也就是说黑色(0.0,0.0,0.0)代表靠近中心点,白色(1.0,1.0,1.0)代表远离中心点。
步骤三:可视化法向量
这里,我们将在mesh里可视化法向量。这是一个新的attribute,已经在createBunnyGeometry.js
定义好。法向量是用来定义三角形朝向的, RGB对应着XYZ,也就是说你看到的蓝色地方的三角形对着Z轴,其他同理。
顶点着色器
attribute vec4 position;
attribute vec3 normal;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
varying vec3 vNormal;
void main () {
vNormal = normal;
gl_Position = projectionMatrix * modelViewMatrix * position;
}
这个着色器里,我们使用了一个新的attribute,normal
, 并且使用varying 类型的vNormal
传递给片段着色器。
片段着色器
precision highp float;
varying vec3 vNormal;
void main () {
gl_FragColor = vec4(vNormal, 1.0);
}
Here, we simply render the passedvNormal
for each fragment. It ends up looking nice, even though some values will be clamped because they are less than 0.0.
这里,我们知识简单的用法向量当成颜色来渲染,看起来效果很不错,虽然小于0的值会被转换为0。
💡index.js
中创建的几何体使用了{ flat: true }
, 参数,意味着法向量是独立的。你也可以改变这个参数看看联合的法向量是什么效果。
步骤4:爆炸三角形
然后,我们将沿着表面法向量推动每个三角形来实现爆炸效果!
顶点着色器
attribute vec4 position;
attribute vec3 normal;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
varying vec3 vNormal;
void main () {
vNormal = normal;
vec4 offset = position;
float dist = 0.25;
offset.xyz += normal * dist;
gl_Position = projectionMatrix * modelViewMatrix * offset;
}
这个着色器里,我们将使用一个向量来调节每个顶点的位置。这里我们直接使用normal
来作为位移的方向,dist
作为位移的程度。
片段着色器无需改变。
步骤5:动画
最后,我们将实现爆炸动画,我们只需要给着色器添加一个uniform
我们需要在js中设置好,我们打开index.js
并且定义一个uniforms
:
// Create our vertex/fragment shaders
const material = new THREE.RawShaderMaterial({
vertexShader: fs.readFileSync(path.join(__dirname, 'shader.vert'), 'utf8'),
fragmentShader: fs.readFileSync(path.join(__dirname, 'shader.frag'), 'utf8'),
uniforms: {
time: { type: 'f', value: 0 }
}
});
我们定义了time
uniform (着色器中将使用相同的名字), 数据类型是浮点型 (ThreeJS 使用'f'
来表示 — 参见here), 并且提供默认值。
然后,我们的渲染循环将在每帧增加这个值。
// Time since beginning
let time = 0;
// Start our render loop
createLoop((dt) => {
// update time
time += dt / 1000;
// set value
material.uniforms.time.value = time;
// render
...
}).start();
顶点着色器
attribute vec4 position;
attribute vec3 normal;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform float time;
varying vec3 vNormal;
void main () {
vNormal = normal;
vec4 offset = position;
// Animate between 0 and 1
// sin(x) returns a value in [-1...1] range
float dist = sin(time) * 0.5 + 0.5;
offset.xyz += normal * dist;
gl_Position = projectionMatrix * modelViewMatrix * offset;
}
在这个着色器中,我们将在main函数签名定义我们的uniform。由于这是一个uniform,这将在渲染时对所有顶点保持一样的值。
uniform float time;
我们使用GLSL内建的sin函数(与JS中的Math.sin一样),将返回-1.0到1.0的值。我们将它归一化到0到1,这样我们的mesh就只向外面爆炸。
float dist = sin(time) * 0.5 + 0.5;
Voilà! 我们有了个爆炸效果的 bunny! 🐰
附录: ShaderMaterial vs RawShaderMaterial
你可能很疑惑为什么ThreeJS 同时拥有 ShaderMaterial 和 RawShaderMaterial。我建议使用RawShaderMaterial,因为不容易出错,但你得多花一些时间来调节各种参数。
你也可以使用ShaderMaterial
这样就可以跳过一些设置,直接使用 ThreeJS's的内建attributes, uniforms。
void main () {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position.xyz, 1.0);
}
下一步
如果github下载太慢,可关注公众号,回复“tsh1”获得源码国内网盘下载地址,以及持续跟踪最新消息!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。