前言
你好啊,这里是西伯利亚小火龙
书接上回,我们完成了花朵特效、落叶特效、还有后处理特效。
但目前我们还缺少背景。边框还有鼠标轨迹特效,接下来就让我们一起完成它吧!
添加背景
initWrold(){
...
this.foregroundLeavesCreate = new ForegroundLeavesCreate(this);
this.bckgroundCreate = new BackgroundCreate(this);
...
}
BackgroundCreate
类是用于生成一个全屏背景平面网格,使用自定义 ShaderMaterial 渲染出带有纹理扰动和颜色混合效果的背景,可用于 WebGL 场景中作为视觉底图
- 创建几何体
const geometry = new THREE.PlaneGeometry(20, 20, 2, 2)
- Shader 材质定义
const material = new THREE.ShaderMaterial({
uniformsGroups: [this.base.UBO],
uniforms: {
uColor1: {value: new THREE.Color("#ffec95")},
uColor2: {value: new THREE.Color("#ecc168")},
tNoise: {value: this.base.noiseSimplexLayeredTexture}
},
vertexShader: ...,
fragmentShader: ...,
depthTest: !1,
})
-
uColor1/uColor2
: 背景颜色区间 -
tNoise
: 用于扰动的噪声贴图
顶点着色器(vertexShader)
uniform Global { vec2 resolution; float time; float dtRatio; };
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
-
传递
uv
到片元着色器 -
顶点不做任何变形(全屏背景)
片元着色器(fragmentShader)
layout(location = 1) out highp vec4 gInfo; // 用于 G-buffer 输出,留空
uniform Global { vec2 resolution; float time; float dtRatio; };
uniform sampler2D tNoise;
uniform vec3 uColor1, uColor2;
varying vec2 vUv;
mat2 rotateAngle(float a) {
...
}
动画噪声背景逻辑:
vec2 screenUv = gl_FragCoord.xy / resolution.xy;
float aspect = resolution.x / resolution.y;
screenUv.x *= aspect;
// 动画时间步长
float steppedTime = floor(time * 3.0) * 3.14159 * 1.53;
screenUv = rotateAngle(steppedTime) * screenUv;
// 三重采样 + 扰动
float n1 = texture2D(tNoise, screenUv * 4.31).r;
float n2 = texture2D(tNoise, -screenUv * 1.814).r;
float n3 = texture2D(tNoise, screenUv * 5.714).r;
float noise = n1 * n2 * n3;
使用三个方向上的 noise 幅度乘积来产生变化丰富的动态扰动
抖动边缘采样 + 颜色混合:
noise = aastep(0.00015, noise);
vec3 color = mix(uColor2, uColor1, noise);
gl_FragColor = vec4(color, 0.0);
gInfo = vec4(0.0);
-
aastep()
用于抗锯齿 -
在
uColor1
和uColor2
之间做颜色插值 -
不输出 alpha,背景是透明的
-
gInfo
设置为 0,表明这个材质不影响 G-buffer 数据写入(可能配合 deferred shading 使用)
其他:
this.mesh.renderOrder = 0
this.mesh.matrixAutoUpdate = false
this.base.scene.add(this.mesh)
-
禁用矩阵自动更新,提高性能
-
设定 renderOrder 为 0,确保它在最底层渲染(在其他对象前)
-
添加mesh到场景
阶段展示
添加边框
async initLoader() {
...
for (let i = 0; i < geometryArray.length; i++) {
...
if (geometryArray[i].key === 'border') {
this[geometryArray[i].key].setAttribute('inset', new THREE.BufferAttribute(new Float32Array([
0.0002442598342895508,
0.0002442598342895508,
0,
0.0002442598342895508,
0.0002442598342895508,
0,
0.0002442598342895508,
0.0002442598342895508,
0,
0.0002442598342895508,
0.0002442598342895508,
0,
-1,
-1,
0,
-1,
1,
0,
-1,
1,
0,
1,
1,
0,
1,
-1,
0,
-1,
1,
0,
0.0002442598342895508,
0.0002442598342895508,
0,
0.0002442598342895508,
0.0002442598342895508,
0,
0.0002442598342895508,
0.0002442598342895508,
0,
1,
-1,
0,
0.0002442598342895508,
0.0002442598342895508,
0
]), 3))
this[geometryArray[i].key].setAttribute('notch', new THREE.BufferAttribute(new Float32Array([
0,
0,
0,
0,
1,
0,
-1,
0,
0,
-1,
1,
0,
0,
0,
0,
0,
1,
0,
-1,
1,
0,
0,
0,
0,
0,
0,
0,
-1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
]), 3))
}
total += 1;
。。。
}
}
border.drc 本身是带有 inset
和 notch
数据, 但是dracoLoader
加载后会丢失所以需要我们手动配置, 后面会在着色器中使用。
initWrold(){
...
this.bckgroundCreate = new BackgroundCreate(this);
this.borderCreate = new BorderCreate(this);
...
}
class BorderCreate {
constructor(base, t = {}) {
this.base = base; // 引用全局系统(含 renderer, scene, screen 等)
this.options = {...t}; // 接收配置项(目前未使用,但为扩展保留)
this.init(); // 初始化边框
}
init() {
// 从 base 中获取边框几何体
const geometry = this.base.border;
// 将几何体中的 position 顶点坐标进行四舍五入(对齐像素?)
const t = this.base.border.attributes.position.array;
for (let s = 0; s < t.length; s++) {
t[s] = Math.round(t[s]);
}
// 创建 ShaderMaterial,用于渲染边框
const material = new THREE.ShaderMaterial({
uniformsGroups: [this.base.UBO], // 使用共享的 Uniform Buffer Object(含 time, resolution 等)
uniforms: {
uBorderSizePixels: { value: 64 }, // 边框宽度(像素)
uNotchSizePixels: { value: new THREE.Vector2(384, 103) }, // 缺口尺寸(像素)
uColor1: { value: new THREE.Color("#ecc168") }, // 主颜色
uColor2: { value: new THREE.Color("#ffec95") }, // 次颜色(暂未使用)
tNoise: { value: this.base.noiseSimplexLayeredTexture }, // 噪声贴图
uRes: { value: new THREE.Vector2(this.base.screen.width, this.base.screen.height) } // 屏幕尺寸
},
vertexShader: `
// 顶点着色器:用于计算每个顶点的偏移位置
uniform Global{ vec2 resolution; float time; float dtRatio; };
attribute vec3 inset; // 顶点属性,表示边框方向(单位向量)
attribute vec3 notch; // 顶点属性,表示缺口方向(单位向量)
uniform float uBorderSizePixels;
uniform vec2 uNotchSizePixels;
uniform vec2 uRes;
varying vec2 vUv;
void main() {
vUv = uv; // 将 UV 传递给片元着色器
vec3 pos = position;
// 将方向向量从像素坐标系转换为 NDC 坐标系([-1, 1])
vec2 borderDir = inset.xy / uRes;
vec2 notchDir = notch.xy / uRes;
// 应用边框宽度(以像素为单位)
pos.xy += borderDir * uBorderSizePixels;
// 应用 notch 缺口尺寸(以像素为单位)
pos.xy += notchDir * uNotchSizePixels;
// 输出最终顶点位置(NDC 空间)
gl_Position = vec4(pos, 1.0);
}
`,
fragmentShader: `
// 片元着色器:负责绘制边框外观
layout(location = 1) out highp vec4 gInfo; // 输出 G-buffer,用于延迟渲染管线(伪 ID)
uniform Global{ vec2 resolution; float time; float dtRatio; };
float aastep(float threshold, float value) {
// AA 平滑函数,避免锯齿
float afwidth = length(vec2(dFdx(value), dFdy(value))) * 0.7071;
return smoothstep(threshold - afwidth, threshold + afwidth, value);
}
uniform sampler2D tNoise;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform float uThickness;
varying vec2 vUv;
mat2 rotateAngle(float a) {
// 旋转矩阵
float s = sin(a);
float c = cos(a);
return mat2(c, s, -s, c);
}
void main() {
// 将屏幕坐标转换为归一化的 UV(带宽高比)
vec2 screenUv = gl_FragCoord.xy / resolution.xy;
float aspect = resolution.x / resolution.y;
screenUv.x *= aspect;
vec2 uv = screenUv;
vec2 noiseUv = screenUv;
// 时间步长控制旋转动画扰动(每约 0.33 秒更新一次角度)
float steppedTime = floor(time * 3.0) * 3.14159 * 0.2;
noiseUv = rotateAngle(steppedTime) * noiseUv;
// 使用噪声扰动边框
float n0 = texture2D(tNoise, noiseUv).r;
// 计算一个从左到右的渐变(+ 噪声扰动)
float gradient = smoothstep(0.0, 0.2, vUv.x + n0 * 0.1);
// 丢弃部分片元,使边框有断裂/不规则边缘效果
if (gradient < 0.5) {
discard;
}
// 颜色设置
vec3 color = uColor1;
gl_FragColor = vec4(color, 0.87946); // 带透明度
// 写入 G-buffer:蓝色编码 1.0(可能用于识别 border)
gInfo = vec4(1.0, vec3(0.0, 0.0, 1.0));
}
`,
depthTest: false
});
// 创建并配置 Mesh
this.mesh = new THREE.Mesh(geometry, material);
this.mesh.name = "border";
this.mesh.renderOrder = 2; // 渲染顺序早于其他元素(可用于 UI 层)
this.mesh.updateMatrixWorld();
this.mesh.matrixAutoUpdate = false; // 禁用自动更新,提高性能
// 初次调整分辨率
this.resize({ w: this.base.screen.width, h: this.base.screen.height });
// 监听窗口大小变化
this.base.eventManage.on("resize", this.resize.bind(this));
// 添加到场景中
this.base.scene.add(this.mesh);
}
// 处理窗口尺寸变化时更新 uniform 分辨率
resize({ w, h }) {
this.mesh.material.uniforms.uRes.value.set(w, h);
}
}
阶段展示
添加缺口图标
initWrold(){
...
this.borderCreate = new BorderCreate(this);
this.notchCreate = new NotchCreate(this);
...
}
class NotchCreate {
constructor(base, t = {}) {
this.base = base; // 全局控制器,包含 scene、camera、uniforms、纹理等
this.imageAspect = 1; // 记录贴图宽高比(用于正确缩放 UI)
this.options = {...t}; // 可选项,暂未使用
this.init(); // 初始化 Notch
}
init() {
// 创建一个单位大小的平面几何体,并将其原点移至左上角(便于屏幕对齐)
const geometry = new THREE.PlaneGeometry();
geometry.translate(-0.5, 0.5, 0);
// 创建 Shader 材质,包含颜色、贴图、噪声扰动等
const material = new THREE.ShaderMaterial({
uniformsGroups: [this.base.UBO], // 引用统一全局 uniform buffer(resolution、time等)
uniforms: {
uColor1: {value: new THREE.Color("#ecc168")}, // 渐变起始色
uColor2: {value: new THREE.Color("#9f4a16")}, // 渐变目标色
tNoise: {value: this.base.noiseSimplexLayeredTexture}, // 噪声扰动贴图
tMap: {value: this.base.emailTexture} // 显示的文字或图案贴图
},
depthTest: false, // UI 元素一般不需要深度测试
vertexShader: `
uniform Global { vec2 resolution; float time; float dtRatio; };
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
layout(location = 1) out highp vec4 gInfo;
uniform Global{ vec2 resolution; float time; float dtRatio; };
// 自定义抗锯齿函数
float aastep(float threshold, float value){
float afwidth = length(vec2(dFdx(value), dFdy(value))) * 0.7071;
return smoothstep(threshold - afwidth, threshold + afwidth, value);
}
uniform sampler2D tNoise; // 噪声贴图
uniform sampler2D tMap; // 显示内容贴图
uniform vec3 uColor1; // 起始颜色
uniform vec3 uColor2; // 目标颜色
varying vec2 vUv;
// 旋转矩阵函数,用于扭曲 UV
mat2 rotateAngle(float a) {
float s = sin(a);
float c = cos(a);
return mat2(c, s, -s, c);
}
void main() {
vec2 uv = vUv;
// 使用噪声扰动 uv,实现动态抖动视觉
vec2 noiseUv = uv * 0.5;
float steppedTime = floor(time * 3.0) * 3.14159 * 0.2;
noiseUv = rotateAngle(steppedTime) * noiseUv;
float n0 = texture2D(tNoise, noiseUv).r;
uv += n0 * 0.006;
// 缩放 UV 坐标以放大中心内容
uv -= 0.5;
uv *= 1.2;
uv += 0.5;
// 采样图案贴图,获取灰度值
float text = texture2D(tMap, uv).r;
// 使用 aastep 对文字边缘做平滑处理
text = aastep(0.7, text);
// 根据文字灰度混合颜色
vec3 color = mix(uColor1, uColor2, text);
// 输出颜色
gl_FragColor = vec4(color, 0.87946);
// 输出 G-buffer 信息(用于后处理/点击判定)
gInfo = vec4(1.0, vec3(0.0, 0.0, 1.0));
}
`
});
// 创建并添加 UI Mesh 到场景
this.mesh = new THREE.Mesh(geometry, material);
this.mesh.renderOrder = 3; // 提高渲染优先级,保证 UI 在最上层
this.base.scene.add(this.mesh);
// 创建交互器,启用点击 + 悬停监听
this.interaction = new MeshInteractor(this, {
camera: this.base.scene.camera,
meshes: [this.mesh],
hoverCursor: true, // 悬停时变鼠标样式
onHover: () => {},
onClick: () => {
// 点击跳转邮箱
window.location.href = "mailto:hi@abeto.co";
}
});
// 计算贴图的宽高比
this.imageAspect = material.uniforms.tMap.value.image.width /
material.uniforms.tMap.value.image.height;
// 每帧调整位置(响应相机或屏幕变化)
this.base.scene.beforeRenderCbs.push(this.positionMesh.bind(this));
// 启用交互
this.interaction.enable();
}
/**
* 将一个平面 UI 精确放置到相机视野中指定位置
*/
positionUI({
camera = null,
mesh = null,
x = 0,
y = 0,
width = 1,
height = 1,
distance = null,
billboardCamera = true
} = {}) {
const Bm = new THREE.Vector3(); // 缓存向量
const $r = new THREE.Vector2(); // 缓存视野大小
// 计算 UI 距离相机的距离
const l = distance || Bm.subVectors(camera.position, camera.target).length();
// 获取当前距离下相机视野范围(世界单位)
camera.getViewSize(l, $r);
// 缩放 UI 到屏幕等比例大小
const scaleFactor = $r.y / this.base.screen.height;
mesh.scale.set(width * scaleFactor, height * scaleFactor, 1);
// 将屏幕像素坐标 (x, y) 映射到相机空间位置
const nx = x / this.base.screen.width;
const ny = y / this.base.screen.height;
mesh.position.copy(camera.position).add(
Bm.set(
$r.x * -0.5 + $r.x * nx,
$r.y * 0.5 - $r.y * ny,
-l
).applyQuaternion(camera.quaternion)
);
// 面向摄像机(billboard UI)
if (billboardCamera) {
mesh.quaternion.copy(camera.quaternion);
}
mesh.updateMatrixWorld();
}
/**
* 每帧调用,将 Notch 放置在屏幕右上角
*/
positionMesh() {
const height = 200 / this.imageAspect; // 根据图片比例自动算高
const paddingX = 11; // 距离右侧边距
const paddingY = 15; // 距离顶部边距
this.positionUI({
camera: this.base.scene.camera,
mesh: this.mesh,
x: this.base.screen.width - paddingX,
y: this.base.screen.height - paddingY,
width: 200,
height: height
});
}
}
功能 | 实现方式 |
---|---|
✅ UI 创建 | 使用 PlaneGeometry 创建纯 GLSL 驱动的 2D 元素 |
🎨 动态扰动 | 使用噪声贴图动态扰动 UV |
✍️ 内容显示 | tMap 贴图可以显示 logo/文字 |
🖱️ 点击交互 | MeshInteractor 启用点击事件 |
🎯 精准定位 | 使用 getViewSize + 自定义坐标实现屏幕定位 |
📐 自动缩放 | UI 会随贴图宽高比自动适配大小 |
阶段展示
到了这一步,我们基本上完成了目标网站的复刻,现在只差鼠标轨迹了,让我们继续吧!
添加鼠标轨迹
initWrold(){
...
this.notchCreate = new NotchCreate(this);
this.lineCreate = new LineCreate(this);
...
}
LineCreate
类
1. 构造函数部分
constructor(base, t = {}) {
this.base = base;
this.options = { length: 0.5, ...t };
this.count = 1; // 支持多个实例(目前只用 1 条线)
this.points = 16; // 一条线由 16 个顶点构成(16 个 position 点)
this.init(); // 初始化系统
}
2. 初始化核心 init()
构造 Polyline 几何体
this.geometry = this.createPolylineGeometry({
count: this.count,
points: this.points,
closed: false
});
-
创建支持宽度的“线段面片”几何体
-
每段线有两个三角形构成,可以在 shader 中偏移实现厚度
创建 Shader 材质(用于渲染线)
this.material = new THREE.ShaderMaterial({...});
顶点着色器:
变量 | 功能 |
---|---|
position | 储存的是 [currIndex, prevIndex, nextIndex] |
uvy | 表示该顶点使用的数据纹理 Y 坐标(其实恒为 0) |
tTexture1 | 存储点位数据的纹理,每个像素一个 vec3 |
lineWidth | 动态线条宽度,实时变化 |
关键步骤:
- 采样当前位置 + 前后点
- 投影到屏幕空间,计算方向
- 求朝向 → 得到法线 → 位移当前点实现宽度
- 支持多种宽度变化曲线(如
parabola
,pcurve
)
片元着色器:
功能:
- 用
tNoise
噪声贴图扰动 → 使线条“流动” - 使用
(1 - vUv.x)
让头部更不透明、尾部淡出 - 如果噪声太低就
discard
(制造断裂效果)
创建计算材质(用于更新点位)
this.textureData = {
textures: 1,
material: new THREE.ShaderMaterial({...})
}
这是一个在 GPU 中运行的模拟器,逻辑:
- 从上一帧
tTexture1
中读取点位 - 第一个点直接设为
uMousePos
- 其余点使用
mix()
向前一个点靠近 - 可通过
uSnap
强制所有点同步为当前位置
结果写入一个新的纹理,实现“尾巴”拖动效果。
生成 Mesh 并添加到场景中
this.mesh = new CustomLineMesh(this);
this.mesh.name = "line"
...
this.base.scene.add(this.mesh)
-
CustomLineMesh
是对材质 + 几何的包装,支持带宽度 polyline -
关闭
matrixAutoUpdate
是为了提升性能(你手动更新位置)
初始化用户交互 & 动态控制
const e = new THREE.Vector3();
const t = new THREE.Vector3();
let n = 0;
let s = false;
this.base.eventManage.once("touch_move", () => { s = true });
每帧更新逻辑(beforeRender)
this.base.scene.beforeRenderCbs.push(() => { ... })
每帧执行:
- 获取当前触点位置(投影到世界)
- 插值更新
uMousePos
- 计算速度
n
,用于控制线宽变化 - 设置
lineWidth
:
lineWidth = 9 / screenHeight * fit(n, .01, .001, 1, 0)
移动越快越粗,越慢越细
- 判断是否需要 snap 所有点
3.createPolylineGeometry
方法
createPolylineGeometry({ points, count, closed })
核心输出:
position
: 每个顶点存储[当前索引, 前索引, 后索引]
uv
: 标准uv
坐标uvy
: 存储实例编号(这里只为0
)index
: 构建三角 strip 面片
顶点重复两次是为了什么?
每个点上下边界都需要,才能做“宽度”。
4.顶点着色器(vertexShader)
attribute float uvy;
uniform Global { vec2 resolution; float time; float dtRatio; };
uniform sampler2D tTexture1;
uniform float lineWidth;
varying vec2 vUv;
varying vec2 vHighPrecisionZW;
float parabola(float x, float k) {
return pow(4.0 * x * (1.0 - x), k);
}
float pcurve(float x, float a, float b) {
float k = pow(a + b, a + b) / (pow(a, a) * pow(b, b));
return k * pow(x, a) * pow(1.0 - x, b);
}
float when_eq(float x, float y) {
return 1.0 - abs(sign(x - y));
}
void main() {
vUv = uv;
// 从纹理中读取当前/前一个/后一个控制点位置
vec3 current = texelFetch(tTexture1, ivec2(position.x, uvy), 0).xyz;
vec3 previous = texelFetch(tTexture1, ivec2(position.y, uvy), 0).xyz;
vec3 next = texelFetch(tTexture1, ivec2(position.z, uvy), 0).xyz;
// 投影到裁剪空间
mat4 projViewModel = projectionMatrix * modelViewMatrix;
vec4 currentProjected = projViewModel * vec4(current, 1.0);
vec4 previousProjected = projViewModel * vec4(previous, 1.0);
vec4 nextProjected = projViewModel * vec4(next, 1.0);
// 考虑屏幕比例变形
vec2 aspectVec = vec2(resolution.x / resolution.y, 1.0);
// 转换为 NDC 坐标(考虑 aspect 比)
vec2 currentScreen = currentProjected.xy / currentProjected.w * aspectVec;
vec2 previousScreen = previousProjected.xy / previousProjected.w * aspectVec;
vec2 nextScreen = nextProjected.xy / nextProjected.w * aspectVec;
// 求方向向量(朝向),双向平均
vec2 dir1 = normalize(currentScreen - previousScreen);
vec2 dir2 = normalize(nextScreen - currentScreen);
vec2 dir = normalize(dir1 + dir2);
// 边缘兼容处理(若当前点等于 next 或 prev)
dir = mix(dir, dir1, when_eq(position.x, position.z));
dir = mix(dir, dir2, when_eq(position.x, position.y));
// 求法线方向(旋转 90°)
vec2 normal = vec2(-dir.y, dir.x);
normal.x /= aspectVec.x; // 纠正横向拉伸
float w = lineWidth;
// 不同线条形状(可通过 #define SHAPE 控制)
#if SHAPE == 1
w *= uv.x;
#elif SHAPE == 2
w *= 1.0 - uv.x;
#elif SHAPE == 3
w *= parabola(uv.x, 1.0);
#endif
// 偏移位置:上边界 +w,下边界 -w(由 uv.y 控制)
normal *= w;
currentProjected.xy += normal * mix(1.0, -1.0, step(0.5, uv.y));
gl_Position = currentProjected;
// 用于后处理中的高精度深度计算
vHighPrecisionZW = gl_Position.zw;
}
核心逻辑:
- 每个顶点是
[当前, 上一个, 下一个]
的 index - 每帧从数据纹理读取控制点位置
- 计算线的方向 → 得到法线 → 加偏移 → 实现线宽
- 支持不同形状(平头、锥形、抛物线收尾等)
4.片元着色器(fragmentShader)
layout(location = 1) out highp vec4 gInfo;
uniform Global { vec2 resolution; float time; float dtRatio; };
uniform vec3 uColor;
uniform sampler2D tNoise;
varying vec2 vUv;
varying vec2 vHighPrecisionZW;
void main() {
vec3 color = uColor;
// 使用时间控制动画扰动(整数时间步)
float steppedTime = floor(time * 2.0);
// 屏幕坐标 → UV
vec2 screenUV = gl_FragCoord.xy / resolution.xy;
// 采样噪声贴图(扰动加动画)
float noise = texture2D(tNoise, screenUV * 2.0 + steppedTime * 0.02).r;
// 尾部位置更不受干扰,线头更容易被遮断
noise *= (1.0 - vUv.x);
// 噪声太低则丢弃片元,形成“破损、闪烁”效果
if (noise < 0.125) discard;
gl_FragColor = vec4(color, 0.87946); // 最终颜色带透明度
gInfo = vec4(1.0, vec2(0.0), 0.0); // 输出给 gBuffer 的附加数据(非必需)
}
核心逻辑:
- 使用
tNoise
纹理加动画扰动 - 尾部更完整,头部有噪声“撕裂感”
discard
实现断裂的视觉美术效果
CustomLineMesh
类
是一个自定义的三维网格类,专门用于处理具有特殊渲染目标和计算的线段(MeshLines)。这个类的设计目的是为了解决线段渲染中常见的一些问题,如使用多个渲染目标(Multiple Render Targets, MRT)和通过计算材质处理渲染操作。
1. 构造函数
constructor(base) {
super(base.geometry, base.material);
this.base = base;
this.isMeshLine = !0;
this.linesCount = this.base.count;
this.name = this.linesCount > 1 ? "Meshlines" : "Meshline";
this.frustumCulled = !1;
-
super(base.geometry, base.material)
:调用父类THREE.Mesh
的构造函数,传入几何体和材质。 -
this.base = base
:保存传入的base
对象。 -
this.isMeshLine = !0
:表示当前对象是一个 "MeshLine"(线段网格)。 -
this.linesCount = this.base.count
:获取线段的数量。 -
this.name = this.linesCount > 1 ? "Meshlines" : "Meshline"
:根据线段数量设置名称,单一线段时为 "Meshline",多条线段时为 "Meshlines"。 -
this.frustumCulled = !1
:禁用视锥剔除,确保对象始终渲染。
2. WebGL 渲染目标设置
const renderTargetType = this.base.base.renderer.webgl.capabilities.floatRenderTarget ? THREE.FloatType : THREE.HalfFloatType;
const size = this.base.base.utils.ceilPowerOfTwo(Math.max(2, this.base.points));
this.rt1 = new THREE.WebGLMultipleRenderTargets(size, size, this.base.textureData.textures || 1, {
wrapS: THREE.ClampToEdgeWrapping,
wrapT: THREE.ClampToEdgeWrapping,
minFilter: THREE.NearestFilter,
magFilter: THREE.NearestFilter,
format: THREE.RGBAFormat,
type: renderTargetType,
depthBuffer: !1
});
this.rt2 = this.rt1.clone();
this.rtCurrent = 0;
-
判断 WebGL 是否支持浮点渲染目标(float render target),并选择合适的类型(
THREE.FloatType
或THREE.HalfFloatType
)。 -
计算渲染目标的大小,保证它是 2 的幂。
-
使用
THREE.WebGLMultipleRenderTargets
创建渲染目标rt1
和rt2
,这两个渲染目标用于交替存储渲染数据。
3. FullScreenQuad
渲染设置
this.fsQuad = new FullScreenQuad(null);
this.computationMaterial = this.base.textureData.material;
this.fsQuad.material = this.computationMaterial;
-
创建一个全屏四边形 (
FullScreenQuad
) 用于渲染计算操作。 -
computationMaterial
是用于执行计算的材质,这个材质通过this.base.textureData.material
获取。
4. compute
方法
compute(renderer, scene, camera) {
const t = this.base.base.renderer.webgl;
const r = this.computationMaterial.uniforms.uModelMatrix;
const a = this.computationMaterial.uniforms.uViewMatrix;
const o = this.computationMaterial.uniforms.uProjMatrix;
r && r.value.copy(this.matrixWorld);
a && a.value.copy(camera.matrixWorldInverse);
o && o.value.copy(camera.projectionMatrix);
-
compute
方法执行渲染计算,将当前对象的模型矩阵、视图矩阵和投影矩阵传递给计算材质。 -
r
,a
,o
分别是模型矩阵、视图矩阵和投影矩阵的 uniform。
const l = this.rtCurrent === 0 ? this.rt1 : this.rt2;
const c = this.rtCurrent === 0 ? this.rt2 : this.rt1;
this.rtCurrent = (this.rtCurrent + 1) % 2;
- 判断当前使用哪个渲染目标
rt1
或rt2
,并在每次渲染后交替使用它们。
for (let f = 0; f < c.texture.length; f++) {
const A = this.computationMaterial.uniforms[`tTexture${f + 1}`];
A && (A.value = c.texture[f]);
}
- 将前一个渲染目标的纹理传递给计算材质的 uniforms。
const h = t.autoClear;
t.autoClear = !1;
const u = t.getRenderTarget();
t.setRenderTarget(l);
t.getClearColor(new THREE.Color());
const d = t.getClearAlpha();
t.setClearColor(new THREE.Color("#000000"), 0);
t.clear(!0, !1, !1);
this.fsQuad.render(t);
t.autoClear = h;
t.setRenderTarget(u);
t.setClearColor(new THREE.Color(), d);
- 禁用自动清除,保存当前的渲染目标,设置新的渲染目标并清空它,最后通过
fsQuad.render
渲染计算。
for (let f = 0; f < l.texture.length; f++) {
const A = this.material.uniforms[`tTexture${f + 1}`];
A && (A.value = l.texture[f]);
const g = this.material.uniforms[`tTexture${f + 1}Prev`];
g && (g.value = c.texture[f]);
}
- 将计算后的纹理传递给材质的 uniforms,用于后续渲染。
this.afterCompute && this.afterCompute(renderer, scene, camera);
- 如果定义了
afterCompute
方法,则在计算完成后执行它。
5. dispose
方法
dispose() {
var t;
this.fsQuad.dispose();
this.computationMaterial.dispose();
this.rt1.dispose();
this.rt2.dispose();
(t = super.dispose) == null || t.call(this);
}
释放资源,清理 FullScreenQuad
、计算材质、渲染目标 (rt1
, rt2
) 以及调用父类的 dispose
方法进行其他清理。
PlaneProjector
类
PlaneProjector
类是一个实现了将点投影到平面上的系统,并且支持多种不同的平面定义和交互方式。
最终展示
总结
源代码:code.juejin.cn/pen/7494477…
好了,到这里我们就结束了,希望你能获得些什么,这也是我所快乐的。
感谢你的阅读!
我是乌鲁木齐大海龟,也是西伯利亚小火龙,我们下次再见!