【Threejs进阶教程-着色器篇】9.顶点着色器入门
【Threejs进阶教程-着色器篇】9.顶点着色器入门
- 本系列教程第一篇地址,建议按顺序学习
- 认识顶点着色器
- varying介绍
- 顶点着色器与片元着色器分别的作用
- Threejs在Shader中的内置变量
- 各种矩阵
- gl_Position
- 尝试使用顶点着色器
- 增加分段数增强效果
- 制作平面鼓包效果
- 鼓包效果分析
- 路障效果
- 让路障效果变得圆滑
- uniform 控制鼓包效果
- 完整源码
本系列教程第一篇地址,建议按顺序学习
本系列目前已累计第九篇,这里直接省略了2到8篇,可以通过上方专栏来查阅前面的教程
【Threejs进阶教程-着色器篇】1. Shader入门(ShadertoyShader和ThreejsShader入门)
本篇使用到的模板代码,从这里自取一个shader模板代码即可
【模板代码】用于编写Threejs Demo的模板代码
认识顶点着色器
首先我们把着色器部分的代码拎出来逐一分析
<script type="x-shader/x-vertex" id="vertexShader">
varying vec2 vUv;
void main(){
vUv = vec2(uv.x,uv.y);
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * mvPosition;
}
</script>
<script type="x-shader/x-fragment" id="fragmentShader">
varying vec2 vUv;
void main(){
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
</script>
varying介绍
有人发现顶点着色器的第一行与片元着色器的第一行,完全一致,那么这个varying用来做什么的呢?
这里我们直接借用webgl编程指南211页的介绍
varying主要用于 顶点着色器到片元着色器传输数据,是个全局变量
现阶段,我们只需要认识到,varying声明的变量,要在顶点着色器和片元着色器都要存在,且初始值需要在顶点着色器中设置即可
了解了varying之后,我们发现,其实uv的值是从顶点着色器中传递给片元着色器的
顶点着色器与片元着色器分别的作用
现阶段我这里不打算讲渲染原理和管线那些,太繁琐,这里先简单的总结一句
后续在讲到后处理的时候,这部分内容会再细讲
顶点着色器决定外型
片元着色器决定色彩
Threejs在Shader中的内置变量
顶点着色器中使用到了几个threejs的内置变量,在下面的官方文档中有说明
Threejs内置变量-WebGLProgram
一般来说我们最需要关注的几个:
position,模型的顶点信息会传递到这里,常用于计算模型的外观和最终渲染的外型
uv,模型的uv信息会传递到这里,常用于传递给顶点着色器用于计算颜色
normal,模型的法线信息会传递到这里,常用于计算光照等高级计算
如果看完了前面的BufferGeometry教程,有没有发现这里很熟悉?
【ThreeJS基础教程-高级几何体篇】2.6 BufferGeometry与BufferAttribute
你们想的没错,这些就是threejs向shader系统传递的数据,如果这里不懂BufferGeometry的,要继续下去学习Shader,就需要去前面补一下BufferGeometry的相关知识了
各种矩阵
在顶点着色器模板代码中的第四行第五行,分别出现了modelViewMatrix和projectionMatrix这两个矩阵,现阶段先不用管,只需要记住顶点着色器最终计算是这样即可
现阶段顶点着色器的代码,在模板代码的最后两行,除了**vec4(position,1.0);**会稍作改变,其他时间不会发生大的变动
gl_Position
一般来说,顶点着色器也需要有个固定输出,gl_Position就是顶点着色器的最终输出结果,最终结果也是一个vec4类型的对象
这样,顶点着色器的代码我们就介绍完毕了,接下来我们要尝试修改一下顶点着色器,感受一下顶点着色器带来的效果
尝试使用顶点着色器
我们用个最简单的方式来操作顶点
<script type="x-shader/x-vertex" id="vertexShader">
varying vec2 vUv;
void main(){
vUv = vec2(uv.x,uv.y);
vec3 aPosition = position;
//这里我们直接操作顶点的z轴,偏移的激进一点
aPosition.z = sin(aPosition.x * aPosition.y) * 10.0;
vec4 mvPosition = modelViewMatrix * vec4( aPosition , 1.0 );
gl_Position = projectionMatrix * mvPosition;
}
</script>
然后,我们运行起来之后,发现,我们的平面,扭曲了,这是因为我们的z轴发生了改变
增加分段数增强效果
这里我们修改一下addMesh()
function addMesh() {
//增加到100分段
let geometry = new THREE.PlaneGeometry(10,10,3,3);
let material = new THREE.ShaderMaterial({
uniforms,
vertexShader:document.getElementById('vertexShader').textContent,
fragmentShader:document.getElementById('fragmentShader').textContent,
transparent:true
})
let mesh = new THREE.Mesh(geometry,material);
scene.add(mesh);
}
我们可以发现,增加了分段和顶点之后,我们的这个平面变化巨大,已经不再是最初的PlaneGeometry了
这样,我们就完成了一次顶点着色器的尝试
制作平面鼓包效果
首先我们把平面横过来,然后需要做一个数据变换,分段数增加到100,然后设定材质的线框模式开,不然我们等一下不好看到效果
然后修改回最初的顶点着色器代码
addMesh
function addMesh() {
//注意这里必须旋转几何体,旋转了几何体,我们的数据才是正确的
//mesh.rotation是在矩阵层面修改了旋转方向,最终会传递到modelViewMatrix中
//顶点着色器的所有教程,除非特殊说明,否则全部使用旋转几何体
let geometry = new THREE.PlaneGeometry(10,10,100,100).rotateX(-Math.PI/2);
let material = new THREE.ShaderMaterial({
uniforms,
vertexShader:document.getElementById('vertexShader').textContent,
fragmentShader:document.getElementById('fragmentShader').textContent,
transparent:true,
wireframe:true
})
let mesh = new THREE.Mesh(geometry,material);
scene.add(mesh);
}
顶点着色器
<script type="x-shader/x-vertex" id="vertexShader">
varying vec2 vUv;
void main(){
vUv = vec2(uv.x,uv.y);
vec3 aPosition = position;
vec4 mvPosition = modelViewMatrix * vec4( aPosition , 1.0 );
gl_Position = projectionMatrix * mvPosition;
}
</script>
鼓包效果分析
既然要制作鼓包效果,那么,我们需要一个鼓包顶点,然后鼓包顶点处的高度最高,然后依次递减,所以我们这里直接从顶点着色器来定义这个鼓包点
<script type="x-shader/x-vertex" id="vertexShader">
varying vec2 vUv;
void main(){
vUv = vec2(uv.x,uv.y);
vec3 aPosition = position;
//0 为int类型,0.0为float类型, 如果写0,threejs会报错
// 可以写成 .0 来替代0.0以及任何 0.X 的数字, 但是个人不是很喜欢这种写法,看着太混乱
vec3 swelling = vec3(0.0);
//计算鼓包点到顶点的距离
float dis = distance(swelling,aPosition);
dis = clamp(dis,0.0,5.0);
aPosition.y = 5.0 - dis;
vec4 mvPosition = modelViewMatrix * vec4( aPosition , 1.0 );
gl_Position = projectionMatrix * mvPosition;
}
</script>
路障效果
这里,我们先声明了鼓包点在中心点,然后,我们计算鼓包点到四周的距离,但是要做一下限制,如果大于5.0的值,则直接赋值为5.0,紧接着直接把这个计算出来的dis值丢给aPosition.y,我们得到了一个漏斗型
既然我们计算的dis的最大值为5,那么,我们把大小做一下交换即可,用5.0 - dis,即可把漏斗形改成路障型
让路障效果变得圆滑
这里我们使用指数函数来优化
我们现在知道了最高点是5,最低点为0,那么,我们就可以计算它的高度比例,然后把线性的比例换成指数型比例
保持最高点和最低点不变,然后我们直接带入图像上面的数学公式,即可得到我们的鼓包效果
<script type="x-shader/x-vertex" id="vertexShader">
varying vec2 vUv;
void main(){
vUv = vec2(uv.x,uv.y);
vec3 aPosition = position;
//0 为int类型,0.0为float类型, 如果写0,threejs会报错
// 可以写成 .0 来替代0.0以及任何 0.X 的数字, 但是个人不是很喜欢这种写法,看着太混乱
vec3 swelling = vec3(0.0);
//计算鼓包点到顶点的距离
float dis = distance(swelling,aPosition);
dis = clamp(dis,0.0,5.0);
dis = pow( dis / 5.0, 2.0 ) * 5.0;
aPosition.y = 5.0 - dis;
vec4 mvPosition = modelViewMatrix * vec4( aPosition , 1.0 );
gl_Position = projectionMatrix * mvPosition;
}
</script>
uniform 控制鼓包效果
uniform亦可用于顶点着色器
我们在代码中多次使用到5.0,这个实际上是鼓包的最大高度,这里我们抽出来这个常数作为鼓包最大高度,指数函数用的2次幂,这个参数可以抽出一个参数为鼓包圆滑率,我们写到uniform和lil.gui来调试
当然,我们的鼓包中心点,也可以单独拎出来放到uniform中
顶点着色器中编写uniform与片元着色器基本一致
修改后的顶点着色器
<script type="x-shader/x-vertex" id="vertexShader">
varying vec2 vUv;
uniform float maxSwelling;
uniform vec3 swellingCenter;
uniform float swellingPower;
void main(){
vUv = vec2(uv.x,uv.y);
vec3 aPosition = position;
//计算鼓包点到顶点的距离
float dis = distance(swellingCenter,aPosition);
dis = clamp(dis,0.0,maxSwelling);
dis = pow( dis / maxSwelling, swellingPower ) * maxSwelling;
aPosition.y = maxSwelling - dis;
vec4 mvPosition = modelViewMatrix * vec4( aPosition , 1.0 );
gl_Position = projectionMatrix * mvPosition;
}
</script>
修改后的addMesh()和uniforms
let uniforms = {
maxSwelling:{value:5.0},
swellingCenter:{value:new THREE.Vector3()},
swellingPower:{value:2.0}
}
function addMesh() {
//注意这里必须旋转几何体,旋转了几何体,我们的数据才是正确的
//mesh.rotation是在矩阵层面修改了旋转方向,最终会传递到modelViewMatrix中
//顶点着色器的所有教程,除非特殊说明,否则全部使用旋转几何体
let geometry = new THREE.PlaneGeometry(10,10,100,100).rotateX(-Math.PI/2);
let material = new THREE.ShaderMaterial({
uniforms,
vertexShader:document.getElementById('vertexShader').textContent,
fragmentShader:document.getElementById('fragmentShader').textContent,
transparent:true,
wireframe:true
})
let mesh = new THREE.Mesh(geometry,material);
scene.add(mesh);
//注意自己引入lil.gui
let gui = new GUI();
gui.add(uniforms.maxSwelling,'value',0,10).step(0.01).name('最大鼓包高度');
gui.add(uniforms.swellingPower,'value',0,10).step(0.01).name('鼓包曲线');
let folder = gui.addFolder('鼓包中心');
folder.add(uniforms.swellingCenter.value,'x',-5,5);
folder.add(uniforms.swellingCenter.value,'y',-5,5);
folder.add(uniforms.swellingCenter.value,'z',-5,5);
}
完整源码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
body{
width:100vw;
height: 100vh;
overflow: hidden;
margin: 0;
padding: 0;
border: 0;
}
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "../three/build/three.module.js",
"three/addons/": "../three/examples/jsm/"
}
}
</script>
<script type="x-shader/x-vertex" id="vertexShader">
varying vec2 vUv;
uniform float maxSwelling;
uniform vec3 swellingCenter;
uniform float swellingPower;
void main(){
vUv = vec2(uv.x,uv.y);
vec3 aPosition = position;
//计算鼓包点到顶点的距离
float dis = distance(swellingCenter,aPosition);
dis = clamp(dis,0.0,maxSwelling);
dis = pow( dis / maxSwelling, swellingPower ) * maxSwelling;
aPosition.y = maxSwelling - dis;
vec4 mvPosition = modelViewMatrix * vec4( aPosition , 1.0 );
gl_Position = projectionMatrix * mvPosition;
}
</script>
<script type="x-shader/x-fragment" id="fragmentShader">
varying vec2 vUv;
void main(){
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
</script>
<script type="module">
import * as THREE from "../three/build/three.module.js";
import {OrbitControls} from "../three/examples/jsm/controls/OrbitControls.js";
import {GUI} from "../three/examples/jsm/libs/lil-gui.module.min.js";
window.addEventListener('load',e=>{
init();
addMesh();
render();
})
let scene,renderer,camera;
let orbit;
function init(){
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer({
alpha:true,
antialias:true
});
renderer.setSize(window.innerWidth,window.innerHeight);
document.body.appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,0.1,2000);
camera.add(new THREE.PointLight());
camera.position.set(15,15,15);
scene.add(camera);
orbit = new OrbitControls(camera,renderer.domElement);
orbit.enableDamping = true;
scene.add(new THREE.GridHelper(10,10));
}
let uniforms = {
maxSwelling:{value:5.0},
swellingCenter:{value:new THREE.Vector3()},
swellingPower:{value:2.0}
}
function addMesh() {
//注意这里必须旋转几何体,旋转了几何体,我们的数据才是正确的
//mesh.rotation是在矩阵层面修改了旋转方向,最终会传递到modelViewMatrix中
//顶点着色器的所有教程,除非特殊说明,否则全部使用旋转几何体
let geometry = new THREE.PlaneGeometry(10,10,100,100).rotateX(-Math.PI/2);
let material = new THREE.ShaderMaterial({
uniforms,
vertexShader:document.getElementById('vertexShader').textContent,
fragmentShader:document.getElementById('fragmentShader').textContent,
transparent:true,
wireframe:true
})
let mesh = new THREE.Mesh(geometry,material);
scene.add(mesh);
let gui = new GUI();
gui.add(uniforms.maxSwelling,'value',0,10).step(0.01).name('最大鼓包高度');
gui.add(uniforms.swellingPower,'value',0,10).step(0.01).name('鼓包曲线');
let folder = gui.addFolder('鼓包中心');
folder.add(uniforms.swellingCenter.value,'x',-5,5);
folder.add(uniforms.swellingCenter.value,'y',-5,5);
folder.add(uniforms.swellingCenter.value,'z',-5,5);
}
function render() {
renderer.render(scene,camera);
orbit.update();
requestAnimationFrame(render);
}
</script>
</body>
</html>