原创 何贤 2025-03-09 09:00 重庆
点击关注公众号,“技术干货” 及时达!
引言
哈喽大家好!距离我上次发文已经过去半个月了,差点又变回了那只熟悉的“老鸽子”。不行,我不能堕落!我还没有将 Web3D
推广到普罗大众,还没有让更多人感受到三维图形的魅力 (其实是还没有捞着米)。怀着这样的心情,我决定重新振作,继续为大家带来更多关于 Three.js
和 Shader
的进阶内容。
前置条件
欢迎阅读本篇文章!在深入探讨 Three.js
和 Shader (GLSL)
的进阶内容之前,确保您已经具备以下基础知识:
「Three.js 基础」:您需要熟悉 Three.js
的基本概念和使用方法,包括场景(Scene
)、相机(Camera
)、渲染器(Renderer
)、几何体(Geometry
)、材质(Material
)和网格(Mesh
)等核心组件。如果您还不熟悉这些内容,建议先学习 Three.js
的入门教程。
「Shader 语法」:本文涉及 GLSL
(OpenGL Shading Language)的编写,因此您需要了解 GLSL
的基本语法,包括顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)的编写,以及如何在 Three.js
中使用自定义着色器。
Hero Section 概览
❝Hero Section 是网页设计中的一个术语,通常指页面顶部的一个大型横幅区域。但对于开发人员而言,这个概念可以更直观地理解为「用户在访问网站的瞬间所感受到的视觉冲击,或者促使用户停留在该网站的关键原因因素」。
❞
话说这天老何接到了一个私活
起始钱不钱的无所谓!主要是想挑战一下自己(不是)。最后的成品如图所示 (互动方式为鼠标滑动
+ 鼠标点击
GIF 压缩太多了内容了,实际要好看很多)。
PC端在线预览地址: https://fluid-light.vercel.app
Debug调试界面: https://fluid-light.vercel.app/#debug
源码地址: https://github.com/hexianWeb/fluid-light
基础场景搭建
首先我来为你解读一下这个场景里面有什么,他非常简单。只有一个「可以接受光照影响的平面几何体」以及「数个点光源」构成,仅此而已。
让我去掉后处理以及一些页面文本元素展示给你看
构建这样的一个基础场景不难。
构建平面几何体
让我们先来解决平面几何体
值得注意的是,为了让显示效果更好,我使用了正交相机并让平面覆盖整个视窗大小
this.geometry = new THREE.PlaneGeometry(2 * 屏幕宽高比, 2);
然后构建相应的物理材质,可以去 polyhaven 下载一些自己喜欢的texture
并下载下来。
根据右边的分类选择纹理大类细分,随后选择想要下载的纹理点击下载。
因为我们本质是需要 Displacement Texture
置换贴图 & Normal Texture
法线贴图
所以不需要太在意这个纹理是作用在什么物件上面的
随后将纹理导入后赋予材质相应的属性,并对部分参数进行调整。通常直接赋予displacementMap
后 Threejs
中显示平面的凹凸会特别明显。所以记得通过
displacementScale
来调整相应的大小。
this.material = new THREE.MeshPhysicalMaterial({
color: '#121423',
metalness: 0.59,
roughness: 0.41,
displacementMap: 下载的纹理贴图,
displacementScale: 0.1,
normalMap: 下载的法线贴图,
normalScale: new THREE.Vector2(0.68, 0.75),
side: THREE.FrontSide
});
最后将物体加入场景即可
this.mesh = new THREE.Mesh(this.geometry, this.material);
scene.add(this.mesh);
(tips:「MeshStandardMaterial」 和 「MeshPhysicalMaterial」 适合需要真实感光照和复杂物理特性的场景,但性能消耗较高。如果您的电脑出现卡顿可以选择消耗较少性能的物理材质)
灯光加入战场
在本案例中,高级感的来源之一就是灯光的变换。如果您足够细心,可能会注意到一些更微妙的细节:场景中的灯光并不是简单地从 A Color
切换到 B Color
,而是同时存在多个光源,并且它们的强度也在动态变化。这种设计使得场景的光影效果更加丰富和立体。
如果场景中只有一个光源,效果可能会显得单调。而本案例中,灯光的变化呈现出一种层次感:「中间是白色,周围还有一层类似年轮的光圈,最后逐渐扩散为纯色背景」。这种效果的关键在于「同一时间场景中存在多个点光源」。虽然多个光源会显著增加性能消耗,但为了实现唯美的视觉效果,这是值得的。
让我们逐步分析灯光是如何实现的。
1. 封装创建点光源的函数
为了简化代码并提高复用性,我们可以先封装一个创建点光源的函数。这个函数会返回一个包含光源对象和目标颜色的对象。
createPointLight(intensity) {
const light = new THREE.PointLight(
0xff_ff_ff,
intensity,
100,
Math.random() * 10
);
light.position.copy(this.lightPosition); //所有的光源都同步在一个位置
return {
object: light,
targetColor: new THREE.Color()
};
}
2. 生成多个点光源
接下来,我们可以调用这个函数生成多个点光源,并将它们添加到场景中。
this.colors = [
new THREE.Color('orange'),
new THREE.Color('red'),
new THREE.Color('red'),
new THREE.Color('orange'),
new THREE.Color('lightblue'),
new THREE.Color('green'),
new THREE.Color('blue'),
new THREE.Color('blue')
];
this.lights = [
this.createPointLight(2),
this.createPointLight(3),
this.createPointLight(2.5),
this.createPointLight(10),
this.createPointLight(2),
this.createPointLight(3),
];
// 初始化灯光颜色
const numberLights = this.lights.length;
for (let index = 0; index < numberLights; index++) {
const colorIndex = Math.min(index, this.colors.length - 1);
this.lights[index].object.color.copy(this.colors[colorIndex]);
}
for (const light of this.lights) this.scene.add(light.object);
3. 动态调整光源强度
在场景中,所有光源同时存在,但它们的强度会有所不同。「每次由光照强度为 10 的光源担任场景的主色」。当用户点击场景时,灯光会像上楼梯或者传送带一样逐步切换,即由新的点光源担任场景主色。
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
8 8"b, "Ya
8 8 "b, "Ya
8 aa=D光源=a8, "b, "Ya
8 8"b, "Ya "8""""""8
8 8 "b, "Ya 8 8
8 a=C光源=8, "b, "Ya8 8
8 8"b, "Ya "8""""""" 8
8 8 "b, "Ya 8 8
8 a=B光源=8, "b, "Ya8 8
8 8"b, "Ya "8""""""" 8
8 8 "b, "Ya 8 8
8=A光源=, "b, "Ya8 8
8"b, "Ya "8""""""" 8
8 "b, "Ya 8 8
8, "b, "Ya8 8
"Ya "8""""""" 8
"Ya 8 8
"Ya8 8
"""""""""""""""""""""""""""""""""""""
让我们看看代码是如何实现的吧
window.addEventListener('click', () => {
// 打乱颜色数组(看个人喜好)
this.colors = [...this.colors.sort(() => Math.random() - 0.5)];
// 标记开始颜色过渡
this.colorTransition = true;
// 为每个灯光设置目标颜色
const numberLights = this.lights.length;
for (let index = 0; index < numberLights; index++) {
const colorIndex = Math.min(index, this.colors.length - 1);
this.lights[index].targetColor = this.colors[colorIndex].clone();
}
});
然后再Render函数中以easeing
方式更新颜色
update() {
// 只在需要时更新颜色
if (this.colorTransition) {
const numberLights = this.lights.length;
const baseSmooth = 0.25;
const smoothIncrement = 0.05;
let allTransitioned = true; // 检查所有颜色是否已完成过渡
for (let index = 0; index < numberLights; index++) {
const smoothTime = baseSmooth + index * smoothIncrement;
// 使用目标颜色进行平滑过渡
const currentColor = this.lights[index].object.color;
const targetColor = this.lights[index].targetColor;
this.dampC(currentColor, targetColor, smoothTime, delta);
// 检查是否还在过渡
if (!this.isColorClose(currentColor, targetColor)) {
allTransitioned = false;
}
}
// 如果所有颜色都已完成过渡,停止更新
if (allTransitioned) {
this.colorTransition = false;
}
}
}
后处理完善场景
在完成了场景的基本构建之后,我们已经实现了大约 80% 的内容。即使现在加上 UI,效果也不会太差。不过,为了让场景更具视觉冲击力和艺术感,我们可以通过后处理(Post Processing)技术来进一步提升质感。
使用 UnrealBloomPass
和 FilmPass
在本文中,我们将使用 UnrealBloomPass
(辉光效果)和 FilmPass
(电影滤镜)来增强场景的视觉效果。以下是具体的实现步骤:
「引入后处理库」:首先,我们需要引入 Three.js
的后处理库 EffectComposer
以及相关的 Pass
类。
「创建 EffectComposer
」:EffectComposer
是后处理的核心类,用于管理和执行各种后处理效果。
「添加 RenderPass
」:RenderPass
用于将场景渲染到后处理管道中。
「添加 UnrealBloomPass
」:UnrealBloomPass
用于实现辉光效果,可以使场景中的亮部区域产生光晕。
「添加 FilmPass
」:FilmPass
用于模拟电影胶片的效果,增加颗粒感和复古风格。
这里的具体参数需要看个人品味进行调试。同款参数可以从这里看我的源码。具体路径位于src\js\world\effect.js
this.composer = new EffectComposer(this.renderer);
this.composer.addPass(this.renderPass);
this.composer.addPass(this.bloomPass);
this.composer.addPass(this.filmPass);
此时页面的质感是不是一下就上来了呢?
最后我们需要添加最关键的一部,就是画面扭曲。
这里我们需要用到 Threejs
的 ShaderPass
,让我们来创建一个初始的ShaderPass
,仅将 EffectComposer 的读取缓冲区的图像内容复制到其写入缓冲区,而不应用任何效果。
具体内容你可以从 Threejs 后处理中了解到更多
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
const BaseShader = {
name: 'BaseShader',
uniforms: {
'tDiffuse': { value: null },
'opacity': { value: 1.0 }
},
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: /* glsl */`
uniform float opacity;
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
vec4 texel = texture2D( tDiffuse, vUv );
gl_FragColor = opacity * texel;
}`
};
const BasePass = new ShaderPass( BaseShader );
此时画面不会有任何变化
让我们对uv
进行简单操纵,让其读取tDiffuse
时可以发生扭曲
vec2 uv = vUv;
uv.y += sin(uv.x * frequency + offset) * amplitude;
gl_FragColor = texture2D(tDiffuse, uv);
最后得到效果
最后一些话
随着 AI 技术的快速发展,各类技术的门槛正在大幅降低,以往被视为高门槛的 3D
技术也不例外。与此同时,过去困扰开发者的数字资产构建成本问题,也正在被最新的 3D generation
技术所攻克。这意味着,在不久的将来,前端开发将迎来一次技术迁移,开发者需要掌握更新颖的交互方式和更出色的视觉效果。
「为什么选择 Three.js
?」
Three.js
作为最流行的 WebGL
库之一,不仅简化了三维图形的开发流程,还提供了丰富的功能和强大的扩展性。无论是创建复杂的 3D 场景,还是实现炫酷的视觉效果,Three.js
都能帮助开发者快速实现目标。
「本专栏的愿景」
本专栏的愿景是通过分享 Three.js
的中高级应用和实战技巧,帮助开发者更好地将 3D
技术应用到实际项目中,打造令人印象深刻的 Hero Section
。我们希望通过本专栏的内容,能够激发开发者的创造力,推动 Web3D
技术的普及和应用。
下期预告
「未来科技?机器人概念官网来袭 !!!」
点击关注公众号,“技术干货” 及时达!