【07】基于 Three.js 的 WebGPU 火焰特效代码解析(webgpu_tsl_vfx_flames.html)

在 Web 开发中,使用 Three.js 库结合 WebGPU 来创建炫酷的图形特效是一件非常有趣且富有挑战性的事情。今天我们就来详细解析一段实现火焰特效的代码,让大家深入了解其背后的原理和功能实现。官网示例代码地址:https://github.com/mrdoob/three.js/blob/master/examples/webgpu_tsl_vfx_flames.html

一、代码导入部分

import * as THREE from 'three';

import { PI2, oneMinus, spherizeUV, sin, step, texture, time, Fn, uv, vec2, vec3, vec4, mix, billboarding } from 'three/tsl';

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

这段代码首先导入了 Three.js 的核心库,通过import * as THREE from 'three',我们可以使用 Three.js 提供的各种 3D 相关的类和功能,比如创建场景、相机、渲染器以及各种几何体等。

接着,从three/tsl中导入了一系列实用的函数和数据类型,像PI2(可能是表示2 * PI的常量,用于角度相关计算)、oneMinus(可能用于对某个值取反等操作)、spherizeUV(用于处理 UV 坐标使其呈现球形化效果等)等等,这些函数在后续的材质计算等过程中会发挥重要作用。

最后导入OrbitControls,它能方便地让我们实现用鼠标控制相机旋转、缩放和平移等交互操作,增强场景的可操作性和观赏性。

二、初始化函数init()解析

(一)相机设置

camera = new THREE.PerspectiveCamera( 25, window.innerWidth / window.innerHeight, 0.1, 100 );

camera.position.set( 1, 1, 3 );

这里创建了一个透视相机,第一个参数25表示视角角度,它决定了我们看到场景的宽窄程度;window.innerWidth / window.innerHeight是宽高比,根据当前浏览器窗口大小自适应;0.1和100分别是近裁剪面和远裁剪面的距离,用于控制相机可见的范围。然后设置了相机的初始位置在(1, 1, 3)处,确定了观察场景的起点。

(二)场景创建

scene = new THREE.Scene();

scene.background = new THREE.Color( 0x201919 );

创建了一个 Three.js 的场景对象,它是所有 3D 物体的容器。并且将场景的背景颜色设置为十六进制表示的0x201919,也就是一种偏深色的颜色,作为整个场景的底色。

(三)纹理加载

const textureLoader = new THREE.TextureLoader();

const cellularTexture = textureLoader.load( './textures/noises/voronoi/grayscale-256x256.png' );

const perlinTexture = textureLoader.load( './textures/noises/perlin/rgb-256x256.png' );

首先创建了一个纹理加载器对象textureLoader,然后使用它分别加载了两张纹理图片,一张是基于 Voronoi 算法生成的灰度纹理(用于模拟类似细胞结构的噪声效果),另一张是 Perlin 噪声纹理(常用于生成自然、随机的纹理变化效果),这些纹理在后续火焰材质的构建中会作为噪声源等发挥关键作用。

(四)渐变纹理创建(gradient canvas)

const gradient = {};

gradient.element = document.createElement( 'canvas' );

gradient.element.width = 128;

gradient.element.height = 1;

gradient.context = gradient.element.getContext( '2d' );

gradient.colors = [

'#090033',

'#5f1f93',

'#e02e96',

'#ffbd80',

'#fff0db',

];

gradient.texture = new THREE.CanvasTexture( gradient.element );

gradient.texture.colorSpace = THREE.SRGBColorSpace;

gradient.update = () => {

const fillGradient = gradient.context.createLinearGradient( 0, 0, gradient.element.width, 0 );

for ( let i = 0; i < gradient.colors.length; i ++ ) {

const progress = i / ( gradient.colors.length - 1 );

const color = gradient.colors[ i ];

fillGradient.addColorStop( progress, color );

}

gradient.context.fillStyle = fillGradient;

gradient.context.fillRect( 0, 0, gradient.element.width, gradient.element.height );

gradient.texture.needsUpdate = true;

};

gradient.update();

这部分代码创建了一个自定义的渐变纹理。先通过document.createElement('canvas')创建一个 HTML5 的画布元素,设置其宽度为128像素,高度为1像素,并获取其 2D 绘图上下文。定义了一个颜色数组,包含了从深蓝色到浅黄色等多个颜色值,用于构建渐变

接着创建一个线性渐变对象fillGradient,并通过循环在不同的进度位置添加对应的颜色停止点,最后将这个渐变设置为画布的填充样式并填充整个画布区域。通过将这个画布转换为CanvasTexture并设置其颜色空间等属性,创建出了一个可以在材质中使用的渐变纹理,并且调用update方法初始化这个渐变纹理的绘制内容。

(五)火焰 1 材质创建(flame1Material)

const flame1Material = new THREE.SpriteNodeMaterial( { transparent: true, side: THREE.DoubleSide } );

flame1Material.colorNode = Fn( () => {

// main UV

const mainUv = uv().toVar();

mainUv.assign( spherizeUV( mainUv, 10 ).mul( 0.6 ).add( 0.2 ) ); // spherize

mainUv.assign( mainUv.pow( vec2( 1, 2 ) ) ); // stretch

mainUv.assign( mainUv.mul( 2, 1 ).sub( vec2( 0.5, 0 ) ) ); // scale

// gradients

const gradient1 = sin( time.mul( 10 ).sub( mainUv.y.mul( PI2 ).mul( 2 ) ) ).toVar();

const gradient2 = mainUv.y.smoothstep( 0, 1 ).toVar();

mainUv.x.addAssign( gradient1.mul( gradient2 ).mul( 0.2 ) );

// cellular noise

const cellularUv = mainUv.mul( 0.5 ).add( vec2( 0, time.negate().mul( 0.5 ) ) ).mod( 1 );

const cellularNoise = texture( cellularTexture, cellularUv, 0 ).r.oneMinus().smoothstep( 0, 0.5 ).oneMinus();

cellularNoise.mulAssign( gradient2 );

// shape

const shape = mainUv.sub( 0.5 ).mul( vec2( 3, 2 ) ).length().oneMinus().toVar();

shape.assign( shape.sub( cellularNoise ) );

// gradient color

const gradientColor = texture( gradient.texture, vec2( shape.remap( 0, 1, 0, 1 ), 0 ) );

// output

const color = mix( gradientColor, vec3( 1 ), shape.step( 0.8 ).oneMinus() );

const alpha = shape.smoothstep( 0, 0.3 );

return vec4( color.rgb, alpha );

} )();

创建了一个SpriteNodeMaterial材质用于火焰 1,设置其为透明且双面渲染(方便从不同角度都能正确显示)。

  • UV 坐标处理

首先获取主 UV 坐标mainUv,然后对其进行一系列变换,比如先通过spherizeUV函数进行球形化处理(参数10可能控制球形化的程度等),接着进行拉伸(通过pow函数改变 UV 坐标的幂次方来实现拉伸效果)和缩放(通过乘法和减法运算调整坐标范围)等操作,这些操作都是为了让纹理在后续应用时呈现出特定的形状和分布。

  • 梯度计算

计算两个梯度相关的值gradient1和gradient2,gradient1利用三角函数结合时间和 UV 坐标的y分量来生成随时间变化且基于 UV 位置的波动效果,gradient2则是通过对mainUv.y进行平滑过渡计算得到一个渐变值,然后将gradient1和gradient2结合起来对mainUv.x进行调整,实现颜色等属性在水平方向上基于梯度的变化。

  • 细胞噪声应用

根据mainUv计算出cellularUv用于采样细胞纹理,获取细胞噪声值cellularNoise,并对其进行一系列范围调整和与gradient2的乘法操作,使得细胞噪声能够与之前的梯度效果相结合,影响火焰的形状和颜色变化等。

  • 形状计算

通过对mainUv进行位移、缩放等操作后计算其长度的反向值得到一个基础形状值shape,再减去细胞噪声值进一步调整形状,这个形状值后续会用于决定火焰的轮廓等特征。

  • 最终颜色和透明度计算

根据形状值从渐变纹理中采样得到gradientColor,然后通过mix函数将其与白色(vec3(1))按照一定条件混合得到最终颜色color,同时根据形状值计算出透明度alpha,最后返回一个包含颜色和透明度信息的vec4类型的值作为材质的颜色节点输出,决定了火焰 1 的外观表现。

(六)火焰 2 材质创建(flame2Material)

其创建过程和火焰 1 材质类似,但在具体的噪声应用、坐标变换等细节上有所不同,比如使用了 Perlin 噪声进行更多复杂的干扰效果生成,这里就不再赘述重复逻辑,其整体目的也是通过各种计算来生成一个随时间变化、有着独特外观的火焰材质效果。

(七) Billboarding 设置


flame1Material.vertexNode = billboarding();

flame2Material.vertexNode = billboarding();

这里调用billboarding函数为火焰 1 和火焰 2 的材质设置 Billboarding 效果,也就是让火焰始终面向相机(这里只在水平方向跟随相机旋转),保证火焰在场景中看起来是正对着观察者的,增强视觉效果的真实感。

(八)创建精灵对象并添加到场景

const flame1 = new THREE.Sprite( flame1Material );

flame1.center.set( 0.5, 0 );

flame1.scale.x = 0.5; // optional

flame1.position.x = - 0.5;

scene.add( flame1 );

const flame2 = new THREE.Sprite( flame2Material );

flame2.center.set( 0.5, 0 );

flame2.position.x = 0.5;

scene.add( flame2 );

分别创建了两个基于之前创建的材质的精灵对象(Sprite),它们本质上是一种可以在场景中方便地展示 2D 纹理效果的对象,常用于粒子、特效等的呈现。设置了它们的中心位置、缩放大小以及在场景中的x坐标位置,并将它们添加到场景中,这样它们就能在渲染时显示出来了。

(九)渲染器创建和相关设置

renderer = new THREE.WebGPURenderer( { antialias: true } );

renderer.setPixelRatio( window.devicePixelRatio );

renderer.setSize( window.innerWidth, window.innerHeight );

renderer.setAnimationLoop( animate );

document.body.appendChild( renderer.domElement );

controls = new OrbitControls( camera, renderer.domElement );

controls.enableDamping = true;

controls.minDistance = 0.1;

controls.maxDistance = 50;

window.addEventListener( 'resize', onWindowResize );

创建了一个 WebGPU 渲染器,开启抗锯齿效果,设置其像素比与设备像素比匹配,调整渲染器的尺寸为浏览器窗口大小,并指定了动画循环函数为animate,然后将渲染器的 DOM 元素添加到页面的body标签内,使其能够在页面上显示渲染的内容。

接着创建了OrbitControls实例,关联相机和渲染器的 DOM 元素,开启阻尼效果让相机操作更平滑,同时设置了相机距离的最小和最大值限制,方便用户交互操作相机。最后添加了窗口大小改变的监听器,当窗口大小变化时调用onWindowResize函数来相应地调整相机和渲染器的相关参数,保证场景始终能正确地显示在窗口内。

三、窗口大小改变处理函数onWindowResize()

function onWindowResize() {

camera.aspect = window.innerWidth / window.innerHeight;

camera.updateProjectionMatrix();

renderer.setSize( window.innerWidth, window.innerHeight );

}

当浏览器窗口大小发生改变时,这个函数会被触发。它首先更新相机的宽高比,使其与新的窗口宽高比一致,然后调用updateProjectionMatrix方法让相机的投影矩阵更新,保证视角等渲染效果正确。同时也调整渲染器的尺寸为新的窗口大小,确保场景能完整且正确地渲染在改变后的窗口区域内。

四、动画循环函数animate()

async function animate() {

controls.update();

renderer.render( scene, camera );

}

这是整个程序的动画循环核心函数,在每一帧渲染时被调用。首先调用controls.update()来更新相机的控制状态(比如根据鼠标操作等实时更新相机位置、角度等),然后通过renderer.render(scene, camera)使用渲染器将场景中的内容(包含我们创建的火焰等物体)根据相机的视角渲染到页面上,不断循环这个过程就实现了动画效果,让火焰等特效能够动态地展示在页面中。

通过对这段代码的详细解析,我们可以看到如何利用 Three.js 结合 WebGPU 以及各种数学计算、纹理操作等手段来创建出逼真且炫酷的火焰特效,希望大家能从中学习到相关的知识,并运用到自己的 Web 3D 开发项目中去。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jiaberrr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值