原创 何贤 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, "Ya8 8 "b, "Ya8 aa=D光源=a8, "b, "Ya8 8"b, "Ya "8""""""88 8 "b, "Ya 8 88 a=C光源=8, "b, "Ya8 88 8"b, "Ya "8""""""" 88 8 "b, "Ya 8 88 a=B光源=8, "b, "Ya8 88 8"b, "Ya "8""""""" 88 8 "b, "Ya 8 88=A光源=, "b, "Ya8 88"b, "Ya "8""""""" 88 "b, "Ya 8 88, "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 技术的普及和应用。
下期预告
「未来科技?机器人概念官网来袭 !!!」
点击关注公众号,“技术干货” 及时达!
