一、着色器材质ShaderMaterial的基本使用
废话不多讲先来看案例
console.log('着色器入门')
// 引入three.js
import * as THREE from 'three'
// 引入OrbitControls控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
// 初始化场景
const scene = new THREE.Scene()
//创建透视相机
const camera = new THREE.PerspectiveCamera(
90,
window.innerWidth / window.innerHeight,
0.1,
1000
)
// 设置相机位置
camera.position.set(0, 0, 2)
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix()
// 添加相机到场景
scene.add(camera)
// 创建辅助轴
const axesHelper = new THREE.AxesHelper(5)
// 添加辅助轴到场景
scene.add(axesHelper)
// 创建着色器材质
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: `
void main(){
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 ) ;
}
`,
fragmentShader: `
void main(){
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`,
})
// 利用着色器材质创建一个平面
const plane = new THREE.Mesh(
new THREE.PlaneBufferGeometry(1, 1, 64, 64),
shaderMaterial
)
// 添加平面到场景
scene.add(plane)
// 初始化渲染器
const renderer = new THREE.WebGLRenderer()
// 设置渲染器的大小
renderer.setSize(window.innerWidth, window.innerHeight)
// 监听屏幕大小改变的变化,设置渲染的尺寸
window.addEventListener("resize", () => {
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比例
renderer.setPixelRatio(window.devicePixelRatio);
});
// 添加渲染器到dom
document.body.appendChild(renderer.domElement)
// 初始化控制器
const controls = new OrbitControls(camera, renderer.domElement)
// 设置控制器阻尼
controls.enableDamping = true
// 渲染函数
function render() {
// 更新控制器
controls.update()
// 渲染
renderer.render(scene, camera)
// 动画
requestAnimationFrame(render)
}
// 调用渲染函数
render()
运行效果如下图所示:
这个案例就是使用着色器材质创建一个红色的平面。
核心其实就是这一段代码
// 创建着色器材质
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: `
void main(){
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 ) ;
}
`,
fragmentShader: `
void main(){
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`,
})
// 利用着色器材质创建一个平面
const plane = new THREE.Mesh(
new THREE.PlaneBufferGeometry(1, 1, 64, 64),
shaderMaterial
)
// 添加平面到场景
scene.add(plane)
这里来详细讲讲这一段代码的作用:
上面的代码创建了一个着色器材质(ShaderMaterial
),这是three.js
中用于创建自定义着色的高级功能。着色器是直接在GPU上运行的小程序,这可以极大地提高渲染效率。这个例子使用了两种类型的着色器:顶点着色器和片元着色器。
vertexShader
: 这是顶点着色器的代码。顶点着色器的主要任务是计算顶点的最终位置(3D坐标转变为屏幕的2D坐标
)。在这个例子中,顶点着色器仅仅是执行了一个标准的模型-视图-投影变换。函数void main()
是着色器的入口点。
`gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0 );`
这行代码是进行了一个坐标变换,将模型空间中的顶点位置变换到裁剪空间中。
position
是原始的顶点位置modelMatrix
将其变换到世界空间viewMatrix
将世界空间变换到视图空间projectionMatrix
将视图空间变换到裁剪空间
fragmentShader
: 这是片元着色器的代码。片元着色器的任务是计算每个像素的最终颜色。在这个例子中,片元着色器直接设置了所有的像素为红色。
`gl_FragColor = vec4(1.0,0.0,0.0,1.0);`
这行代码将片元的颜色设置为红色。vec4
代表一个四维的向量,前三个元素分别代表了红色,绿色和蓝色的分量,每个值的范围为0.0~1.0,最后一个元素代表了透明度。
总的来说,该代码创建了一个红色效果的自定义着色器材质。每个物体在转移到屏幕坐标后都会被染成红色。
二、顶点着色器(vertex shader)和片元着色器(fragment shader)的数据交互
在Three.js
中,顶点着色器(vertex shader
)和片元着色器(fragment shader
)是通过统一着色语言(GLSL
)进行数据交互的。
在着色器程序中,顶点着色器首先处理每个顶点数据,这些数据可以是顶点的位置、颜色、纹素坐标等。计算结果会存储在特殊的变量中,例如 gl_Position
和 gl_PointSize
。这些结果可以通过"varying
"变量被传递到片元着色器中。
"varying
"变量是顶点着色器和片元着色器之间唯一的通信方式。你可以将它视为从顶点着色器传递给片元着色器的一种“桥梁”。"varying
"变量在顶点着色器中被写入,在片元着色器中被读取。
顶点着色器计算的结果被插值 (undersampling
) 到每个片元上。其过程称为光栅化 (rasterization
)。在光栅化过程中,"varying
"变量的数据会在每个像素上都被插值(这被称为传递)。之后,片元着色器会使用这些数据来计算每个像素的最终颜色值。
注意,"varying
"变量的数量和大小受到硬件限制,过度使用可能会导致性能下降。在需要传递大量数据时,可以尝试使用纹理或缓冲区对象。
可能介绍了概念大家还是云里雾里,我们通过一个案例来分析说明。
还是开始那个基本的案例,为了能更明显的感觉到varying
传递数据的作用,我们将着色器材质中设置顶点着色器材质和片元着色器的代码抽离出来,新建两个文件
- vertex.glsl
void main(){
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 ) ;
}
- fragment.glsl
void main(){
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
然后在基础案例中引入这两个glsl,使用到着色器材质的顶点着色器和片元着色器上
完整代码如下:
// 引入three.js
import * as THREE from 'three'
// 引入OrbitControls控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
// 引入顶点着色器
import vertexShader from '../shader/myshader/vertex.glsl'
// 引入片元着色器
import fragmentShader from '../shader/myshader/fragment.glsl'
// 初始化场景
const scene = new THREE.Scene()
//创建透视相机
const camera = new THREE.PerspectiveCamera(
90,
window.innerWidth / window.innerHeight,
0.1,
1000
)
// 设置相机位置
camera.position.set(0, 0, 2)
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix()
// 添加相机到场景
scene.add(camera)
// 创建辅助轴
const axesHelper = new THREE.AxesHelper(5)
// 添加辅助轴到场景
scene.add(axesHelper)
// 创建着色器材质
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
})
// 利用着色器材质创建一个平面
const plane = new THREE.Mesh(
new THREE.PlaneBufferGeometry(1, 1, 64, 64),
shaderMaterial
)
// 添加平面到场景
scene.add(plane)
// 初始化渲染器
const renderer = new THREE.WebGLRenderer()
// 设置渲染器的大小
renderer.setSize(window.innerWidth, window.innerHeight)
// 监听屏幕大小改变的变化,设置渲染的尺寸
window.addEventListener("resize", () => {
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比例
renderer.setPixelRatio(window.devicePixelRatio);
});
// 添加渲染器到dom
document.body.appendChild(renderer.domElement)
// 初始化控制器
const controls = new OrbitControls(camera, renderer.domElement)
// 设置控制器阻尼
controls.enableDamping = true
// 渲染函数
function render() {
// 更新控制器
controls.update()
// 渲染
renderer.render(scene, camera)
// 动画
requestAnimationFrame(render)
}
// 调用渲染函数
render()
改动其实也就这一点:
// 引入
import vertexShader from '../shader/myshader/vertex.glsl'
import fragmentShader from '../shader/myshader/fragment.glsl'
// 使用
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
})
当然效果还是一样的,如图:
接下来我们尝试在顶点着色器中传入一个变量到片元着色器中来修改平面的颜色。
在顶点着色器中
- vertex.glsl
varying float vColor;
void main(){
vColor = 1.0;
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 ) ;
}
- fragment.glsl
varying float vColor;
void main(){
gl_FragColor = vec4(1.0, vColor, 0.0, 1.0);
}
我们在顶点着色器中定义了一个float
的vColor
变量,并赋值为1.0
,然后通过varying
传递出去,
在片元着色器中同样通过varying
接收这个vColor
变量并使用
此时我们就成功将平面的颜色改为了黄色,效果如图:
三、统一着色语言(GLSL)
看了上面的两个案例,可能不了解glsl的小伙伴会有疑问,vertex.glsl
和fragment.glsl
两个文件中的float是什么?vec4
是什么?gl_Position
、gl_FragColor
又是什么?好家伙还有个void main()
???
跟我这搞c
语言呢?
那么接下来就介绍一下glsl语言的基础语法。
1. 数据类型
GLSL语言有多种数据类型,包括:
数据类型 | 描述 | 示例 |
---|---|---|
bool | 布尔类型,只能是 true 或 false | bool a = true; |
int | 整型 | int a = 10; |
float | 浮点型 | float a = 1.0; |
double | 双浮点型,比 float 更高精度 | double a = 1.0; |
vec2 | 二维向量,包含两个 float 组件 | vec2 a = vec2(1.0, 2.0); |
vec3 | 三维向量,包含三个 float 组件 | vec3 a = vec3 (1.0, 2.0, 3.0); |
vec4 | 四维向量,包含四个 float 组件 | vec4 a = vec4 (1.0, 2.0, 3.0, 4.0); |
ivec2 | 二维整型向量 | ivec2 a = ivec2(1, 2); |
ivec3 | 三维整型向量 | ivec3 a = ivec3(1, 2, 3); |
ivec4 | 四维整型向量 | ivec4 a = ivec4(1, 2, 3, 4); |
mat2 | 二维矩阵 | mat2 a = mat2(1.0, 2.0, 3.0, 4.0); |
mat3 | 三维矩阵 | mat3 a = mat3(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0); |
mat4 | 四维矩阵 | mat4 a = mat4(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0); |
sampler2D | 用于访问纹理 | uniform sampler2D texture; |
samplerCube | 用于存储和查询立方体纹理 | uniform samplerCube texture; |
请注意,GLSL中的变量必须在开始位置声明,这和许多其他编程语言不同。
2. 控制语句
GLSL中的控制语句具有和C语言相似的语法。以下是一些示例:
- if-else 语句:
if (condition) {
// Code to execute if condition is true
} else {
// Code to execute if condition is false
}
例如:
if (color.r > 0.5) {
color.r = 1.0;
} else {
color.r = 0.0;
}
- for 语句:
for (initialization; condition; post-loop expression) {
// code to execute for each loop iteration
}
例如:
for (int i = 0; i < 3; i++) {
color.rgb[i] = 1.0;
}
- while 语句:
while (condition) {
// code to execute while condition is true
}
例如:
int i = 0;
while (i < 3) {
color.rgb[i++] = 1.0;
}
- do-while 语句:
do {
// Code to execute
} while (condition)
例如:
int i = 0;
do {
color.rgb[i++] = 1.0;
} while (i < 3);
- switch-case 语句:
switch (expression) {
case constant1:
// code to execute if expression equals constant1
break;
case constant2:
// code to execute if expression equals constant2
break;
default:
// code to execute if none of the above conditions are met
}
例如:
int i = getValue();
switch (i) {
case 0:
color.r = 1.0;
break;
case 1:
color.g = 1.0;
break;
default:
color.b = 1.0;
}
3. 函数
在GLSL
(OpenGL
着色语言)中,函数的声明和使用方式与C语言相似。简单来说,首先需要声明函数的返回类型,接着声明函数名和括号中的参数列表,然后在大括号中定义函数的具体操作。下面举例说明。
例如,我们声明一个将向量颜色分量都乘以2的函数:
vec3 doubleColor(vec3 color) {
return 2.0 * color;
}
这里的vec3
是函数返回类型,表示一个三维向量。函数名是doubleColor
,参数是一个三维向量color
。函数体内部的2.0 * color
表示将颜色的每一个分量都乘以2。
然后这个函数可以在着色器程序的任何地方被使用,例如:
void main() {
vec3 color = vec3(1.0, 0.5, 0.3);
vec3 newColor = doubleColor(color);
gl_FragColor = vec4(newColor, 1.0);
}
在这个main
函数中,首先定义了一个原始颜色color
,然后用我们定义的doubleColor
函数将原始颜色的每个分量都乘以2得到新的颜色newColor
。最后将包含newColor
的颜色设置为片元颜色。
需要注意的是,所有的GLSL函数必须在调用之前就已经声明了,这与JavaScript等其他语言不同。
4. 内置attributes 和 uniforms
在WebGL
和GLSL
中,attributes
和 uniforms
是两种预定义的类型,都是用来在着色器中存储和传递数据的。它们的主要区别在于,attributes
存储的是每个顶点独有的数据,如位置,颜色,纹理坐标等;而uniforms
则用于存储在一个渲染调用中对所有顶点都相同的数据,比如变换矩阵,光照参数等。
在threejs
中,这些attributes
和uniforms
都是通过JavaScript
在CPU
端设置,然后在GPU
端的着色器中获取其值进行计算。
下面是一些常见的attributes
和uniforms
:
-
attributes:
- position:顶点的位置。
- normal:顶点的法向量。
- uv:顶点的纹理坐标。
-
uniforms:
- modelViewMatrix:模型视图矩阵。
- projectionMatrix:投影矩阵。
- normalMatrix:法向量矩阵。
- time:用于动画等需要时间参数的场景。
例如,下面是一个顶点着色器的例子,其中使用了position attribute和modelViewMatrix、projectionMatrix两个uniform:
attribute vec3 position; // 顶点位置
uniform mat4 modelViewMatrix; // 模型视图矩阵
uniform mat4 projectionMatrix; // 投影矩阵
void main() {
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); // 计算顶点在视图空间中的位置
gl_Position = projectionMatrix * mvPosition; // 投影到屏幕坐标
}
然后在JavaScript中设定这些值:
var geometry = new THREE.BufferGeometry();
var vertices = new Float32Array([
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, -1.0, 1.0
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
var material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 1.0 },
resolution: { value: new THREE.Vector2() }
},
vertexShader: document.getElementById('vertexShader').textContent,
fragmentShader: document.getElementById('fragmentShader').textContent
});
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
这样,我们就可以在着色器中使用这些attributes
和uniforms
进行渲染计算了。
四、通过uv渲染彩色平面
通过上面的介绍,我们知道uv是内置的属性,他刚好是二维的,那么如果我们将他从顶点着色器传递到片元着色器,进行渲染平面会有什么结果呢?
一起看看吧
- vertex.glsl
varying vec2 vUv;
void main(){
vUv = uv;
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 ) ;
}
- fragment.glsl
varying float vColor;
varying vec2 vUv;
void main(){
gl_FragColor = vec4(vUv, 0.0, 1.0);
}
完整示例代码
// 引入three.js
import * as THREE from 'three'
// 引入OrbitControls控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
// 引入顶点着色器
import vertexShader from '../shader/myshader/vertex.glsl'
// 引入片元着色器
import fragmentShader from '../shader/myshader/fragment.glsl'
// 初始化场景
const scene = new THREE.Scene()
//创建透视相机
const camera = new THREE.PerspectiveCamera(
90,
window.innerWidth / window.innerHeight,
0.1,
1000
)
// 设置相机位置
camera.position.set(0, 0, 2)
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix()
// 添加相机到场景
scene.add(camera)
// 创建辅助轴
const axesHelper = new THREE.AxesHelper(5)
// 添加辅助轴到场景
scene.add(axesHelper)
// 创建着色器材质
const shaderMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
})
// 利用着色器材质创建一个平面
const plane = new THREE.Mesh(
new THREE.PlaneBufferGeometry(1, 1, 64, 64),
shaderMaterial
)
// 添加平面到场景
scene.add(plane)
// 初始化渲染器
const renderer = new THREE.WebGLRenderer()
// 设置渲染器的大小
renderer.setSize(window.innerWidth, window.innerHeight)
// 监听屏幕大小改变的变化,设置渲染的尺寸
window.addEventListener("resize", () => {
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比例
renderer.setPixelRatio(window.devicePixelRatio);
});
// 添加渲染器到dom
document.body.appendChild(renderer.domElement)
// 初始化控制器
const controls = new OrbitControls(camera, renderer.domElement)
// 设置控制器阻尼
controls.enableDamping = true
// 渲染函数
function render() {
// 更新控制器
controls.update()
// 渲染
renderer.render(scene, camera)
// 动画
requestAnimationFrame(render)
}
// 调用渲染函数
render()
效果如下:
吼吼!利用uv渲染的平面成彩色了是不是很神奇。
为什么呢?
这是因为vUv参数对应的是一个二维向量(vec2
),这个向量用于确定像素对应的纹理坐标。它通常是在顶点着色器(Vertex Shader
)中计算出来的,然后传递到片元着色器(Fragment Shader
)进行使用。
在gl_FragColor = vec4(vUv, 0.0, 1.0)
;这行代码中,我们创建了一个颜色向量,它的前两个分量(R和G)是提供的vUv值,第三个分量(B)是0.0,第四个分量(alpha,表示透明度)是1.0。
简单来说,也就是将纹理位置的坐标值直接转化为了颜色值,因此会得到彩色的效果。
例如
- 纹理坐标(0, 0)对应的是黑色(R=0, G=0, B=0)
- 纹理坐标(1, 0)或者(0, 1)对应的是红色或者绿色(R=1, G=0, B=0或者R=0, G=1, B=0)
- 纹理坐标(1, 1)对应的是黄色(R=1, G=1, B=0)
五、总结
好啦,到这里我们详细介绍了three.js库中的着色器ShaderMaterial和统一着色语言GLSL的基本语法。
首先介绍了ShaderMaterial在three.js中的地位以及功用。它是一种特殊的材质书写方式,让你可以编写自定义着色器。ShaderMaterial给予了我们底层的、未过滤的访问权,让我们可以编写自己的顶点(vertex)和片段(fragment)着色器程序。
接着深入解析了统一着色语言GLSL(OpenGL Shading Language)的语法基础。GLSL是C语言的一种方言,是Open GL的一部分,用于编写短程图形渲染框架。文章讲解了其基本语言构架、数据类型和运算符、流程控制以及函数和过程等核心知识点。
最后给出了如何将这两者结合在一起,如何在three.js中使用GLSL着色器进行渲染的示例,包括如何创建着色器、如何编写着色器代码、以及如何将其应用到three.js场景中。