Three.js 进阶之旅:实现王国之泪神庙能量光环 ?

banner.png

摘要

如封面图所示,本文将基于 Vue3 + Three.js + GLSL 的相关知识,实现游戏《塞尔达传说:王国之泪》中低配版神庙能量光环效果。通过本文的阅读和学习,你将学习到的知识点包括:在 Three.js 中创建螺旋上升曲线的多种方法、如何给模型添加动态渐变色的纹理材质、以及如何给能量光环添加发光效果、如何使用 AI 能力生成卡通风格全景 HDR 等。

bg_0.png

效果

本文实现的效果如下图所示,页面由卡通风格 HDR 全景背景、简易的石头形状的神庙以及具有发光效果和旋转动画的能量光环构成,页面初次加载时镜头由远到近缓动拉近,使用鼠标可以对模型进行缩放和旋转操作。(本文重点实现内容是螺旋上升的发光能量环,至于神庙,为了页面完整我找了一个形状类似的石头模型来代替 ?)。

preview.gif

预览图片使用的是 GIF,为了节约体积对其进行了压缩和减帧,建议通过以下预览链接访问页面,大屏浏览效果更佳啊

本专栏系列代码托管在 Github 仓库【threejs-odessey】后续所有目录也都将在此仓库中更新

? 代码仓库地址:git@github.com:dragonir/threejs-ode…

bg_1.png

原理

在开始实现之前,我们先整理总结下本文中将应用到的知识点,该部分内容主要讨论如何实现神庙光环的外形和纹理材质的相关原理。首先我们来看看在游戏中的神庙截图,可以观察到,神庙光环是一个形态类似轻盈的气流的螺旋上升的圆弧,螺旋的底部圆的半径较大,顶部圆的半径较小,颜色是绿色和蓝色的渐变。

shrine.png

螺旋上升的曲线

对于螺旋上升的圆弧形状,我在实践过程中找到以下几种可行的实现方式。

实现方式 ①:Line2

Three.js 使用 Line 类绘制线条时,无法为线条设置宽度,此时可以使用 Three.js 自带的 Line2 类来实现宽线效果。我们可以像下面这样简单使用 Line2 来创建一个螺旋形状,单独引入需要用到的资源 LineMaterialLineGeometryLine2 来创建用于宽线的材质、几何体和网格模型。首先通过循环调用三角函数创建了一组在空间中螺旋上升的 THREE.Vector3 点数组 points,然后使用该数组,通过 THREE.CatmullRomCurve3 创建曲线,最后使用 Line2 将曲线转换渲染为网格模型并添加到场景中。

import { Line2 } from "three/addons/lines/Line2.js";
import { LineMaterial } from "three/addons/lines/LineMaterial.js";
import { LineGeometry } from "three/addons/lines/LineGeometry.js";

const positions = [];
const colors = [];
const points = [];
for (let i = 0; i < 100; i++) {
  const t = i / 3;
  points.push(new THREE.Vector3(t * Math.sin(2 * t), t, t * Math.cos(2 * t)));
}

const spline = new THREE.CatmullRomCurve3(points);
const divisions = Math.round(3 * points.length);
const point = new THREE.Vector3();
const color = new THREE.Color();
for (let i = 0, l = divisions; i < l; i++) {
  const t = i / l;
  spline.getPoint(t, point);
  positions.push(point.x, point.y, point.z);
  color.setHSL(t, 1.0, 0.5, THREE.SRGBColorSpace);
  colors.push(color.r, color.g, color.b);
}
const lineGeometry = new LineGeometry();
lineGeometry.setPositions(positions);
lineGeometry.setColors(colors);
const line = new Line2(lineGeometry, matLine);
line.computeLineDistances();
line.scale.set(1, 1, 1);
scene.add(line);

line.png

? THREE.CatmullRomCurve3:是 Three.js 中表示三维空间中的 Catmull-Rom 曲线的一个类,它通过给定的一组点来创建曲线。可以通过 getPoints() 方法获取曲线上的一系列点,该方法返回一个包含指定数目点的数组,这些点均匀的分布在曲线上。

该方法可以生成螺旋上升的曲线模型,它也是Three.js 官方宽线示例的用法,大家可以通过以下链接查看源码详细实现。

? 源码地址: github.com/mrdoob/thre…

实现方式 ②:THREE.Curve

另一种实现螺旋上升的曲线的方法是使用 THREE.Curve,它是 Three.js 中表示曲线的一个基类,我们可以像下面这样通过继承 THREE.Curve 来实现用于创建螺旋上升曲线的类,它接收 4 个参数 radiusTop, radiusBottom, height, turns,分别表示螺旋曲线顶部圆的半径、底部圆的半径、高度,螺旋的次数,然后在 getPoint 方法中使用上述参数,通过三角函数构建螺旋上升的曲线形状,并返回组成曲线的点。

class SpiralCurve extends THREE.Curve {
  constructor(radiusTop, radiusBottom, height, turns) {
    super();
    this.radiusTop = radiusTop;
    this.radiusBottom = radiusBottom;
    this.height = height;
    this.turns = turns;
  }
  getPoint(t) {
    const angle = this.turns * 2 * Math.PI * t;
    const radius = (this.radiusTop - this.radiusBottom) * t + this.radiusBottom;
    const x = Math.cos(angle) * radius;
    const y = t * this.height;
    const z = Math.sin(angle) * radius;
    return new THREE.Vector3(x, y, z);
  }
}

本文实例中,就是采用该方法来实现神庙螺旋上升的的能量光环的。

? THREE.Curve:可以创建自定义曲线,除了可以自定义 getPoint() 方法创建不同的曲线效果之外,还可以自定义 getTangent() 等方法来获取每个点的切线向量等来控制曲线的外观和行为。

发光且半透明渐变的纹理

解决能量光环的形状问题后,现在考虑如何实现具有 ?? 蓝绿渐变色且发光和运动的纹理材质。为了实现这样的效果,我搜集了很多资料,最终也仅仅实现了本文实例页面中这种简易的效果 ?

实现方式 ① 光线拖尾轨迹发生器

首先想到的方式是通过建模的方法解决这个问题,通过搜索大量资料发现Blender插件-光线拖尾轨迹发生器Light Trails Generator 1.1正好满足我的需求,它是一款免费Blender 插件,使用它可以非常容易地创建出漂亮的光线效果。But,但是导出的时候模型的材质丢失了,只能导出白模,我试着导出 gltfglbobjfbx 等格式都不行,于是放弃了,打算通过代码实现。(放个钩子 ? 在这里,等待一个精通 Blender 且可以正常导出附带材质的光线模型的大佬)

blender.png

实现方式 ② 着色器

半透明、渐变色、且具有流动动画……通过 Three.js 现有的基础材质是无法满足需求的,只能通过支持加载自定义着色器的 THREE.ShaderMaterial 着色器材质来实现。在片段着色器中,我们可以通过纹理采样、噪声、颜色插值等方式对每个像素的纹理信息和其他参数进行处理,实现最终神庙能量光环动态的光环效果。

void main() {
  vec2 olduv = gl_FragCoord.xy/resolution.xy ;
  vec2 uv = vUv ;
  vec2 imguv = uv;
  float scale = 1.;


  vec3 rgbcolor0 = rgbcol(color0);
  vec3 rgbcolor1 = rgbcol(color1);

  // 设置纹理
  vec2 newUv = vec2(cor.x + time,cor.x+cor.y);
  vec3 noisetex = texture2D(perlinnoise,mod(newUv,1.)).rgb;
  vec3 noisetex2 = texture2D(sparknoise,mod(newUv,1.)).rgb;
  vec3 noisetex3 = texture2D(waterturbulence,mod(newUv,1.)).rgb;

  // 设置纹理色调
  float tone0 =  1. - smoothstep(0.3,0.6,noisetex.r);
  float tone1 =  smoothstep(0.3,0.6,noisetex2.r);
  float tone2 =  smoothstep(0.3,0.6,noisetex3.r);

  // 设置每个色调的不透明度
  float opacity0 = setOpacity(tone0,tone0,tone0,.29);
  float opacity1 = setOpacity(tone1,tone1,tone1,.49);
  float opacity2 = setOpacity(tone2,tone2,tone2,.69);

  // 设置噪声
  float gradienttone = 1. - smoothstep(0.196,0.532,pct);
  vec4 circularnoise = vec4( vec3(gradienttone)*noisetexvUv*1.4, 1.0 );
  float gradopacity = setOpacity(circularnoise.r,circularnoise.g,circularnoise.b,0.19);

  // ...
  gl_FragColor += vec4(108.0)*result*(y*0.02);
  gl_FragColor *= vec4(gradopacity);
}

? 关于 Three.js 中着色器的相关基础知识,可以阅读本专栏的其他文章:《Shader着色器入门》《Shader着色器基础图案》

AI生成塞尔达风格全景HDR ✨

本文示例页面的塞尔达风格全景背景图,是使用 ? AI绘图工具 生成的,skybox.blockadelabs.com 网站可以免费生成卡通风格的全景图,我们只需在页面底部的输入框中输入描述正向关键字反向关键字即可根据文案生成对应的全景图片,比如在本实例中,我输入的正向关键字是 breath of the wild, zelda, link, tree, flower, forest, river,AI就生成了如下所示的全景 HDR 图片,我们还可以选在左侧下拉框中的的绘制模型风格,比如有电子艺术、迪士尼画风、水彩等多种风格可以选择。生成图片后还可以在以当前图片为基础,在上面添加或修改。

skybox.png

关于 ? AI 生成卡通风格 HDR 全景图,大家感兴趣的话,自己动手试试吧,以下是我生成的比较好看的几张。

pano.png

bg_2.png

实现

资源引入

引入开发本案例页面所有需要的资源,OrbitControls 是镜头轨道控制器,可以使用鼠标拖动旋转视角;EffectComposerUnrealBloomPassRenderPass 是后期效果渲染器和后期通道,用于给神庙能量光环添加发光效果;GLTFLoader 用于加载 glb/gltf 格式的模型文件;TWEENAnimations 方法用于给初始化镜头添加一些丝滑的动画效果;portalVertexShaderportalFragmentShader 分别是用于生成神庙能量光环材质的顶点着色器和片段着色器。

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { TWEEN } from "three/examples/jsm/libs/tween.module.min.js";
import { Animations } from '@/utils';
import portalVertexShader from '@/shaders/power/vertex.glsl';
import portalFragmentShader from '@/shaders/power/fragment.glsl';

场景初始化

初始化场景、相机、控制器等常规方法,可以通过 renderer.toneMapping 等属性来微调页面整体的渲染效果。

// 定义渲染尺寸
const sizes = {
  width: window.innerWidth,
  height: window.innerHeight
}


// 初始化渲染器
const canvas = document.querySelector('canvas.webgl');
const renderer = new THREE.WebGLRenderer({
  canvas: canvas,
  antialias: true,
  alpha: true,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.autoClear = false;
renderer.setClearAlpha(0);
renderer.useLegacyLights = true;
renderer.toneMapping = THREE.CineonToneMapping;
renderer.toneMappingExposure = 2;

// 初始化场景
const scene = new THREE.Scene();

  // 初始化相机
const camera = new THREE.PerspectiveCamera(55, sizes.width / sizes.height, 1, 1000)
scene.add(camera);
camera.position.set(0, 100, 200);

// 光源
const light = new THREE.AmbientLight(0xffffff, 1.5);
scene.add(light);

// 控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.enablePan = false;

// 页面缩放事件监听
window.addEventListener('resize', () => {
  sizes.width = window.innerWidth;
  sizes.height = window.innerHeight;
  // 更新渲染
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  // 更新相机
  camera.aspect = sizes.width / sizes.height;
  camera.updateProjectionMatrix();
});

创建全景背景

在这里,我们使用 AI 生成的全景图片作为场景的背景,可使用该开源工具HDRI-to-CubeMap将生成的全景图片裁切成 6 张,然后通过以下方法,就可以为场景添加全景效果背景了。

const cubeTextureLoader = new THREE.CubeTextureLoader();
const environmentMap = cubeTextureLoader.load([
  new URL('@/assets/environment/px.jpg', import.meta.url).href,
  new URL('@/assets/environment/nx.jpg', import.meta.url).href,
  new URL('@/assets/environment/py.jpg', import.meta.url).href,
  new URL('@/assets/environment/ny.jpg', import.meta.url).href,
  new URL('@/assets/environment/pz.jpg', import.meta.url).href,
  new URL('@/assets/environment/nz.jpg', import.meta.url).href,
]);
environmentMap.encoding = THREE.sRGBEncoding;
scene.background = environmentMap;
scene.environment = environmentMap;

step_0.gif

创建能量光环模型

接着,我们使用原理中介绍的方法,创建出一条螺旋上升的的曲线形状,然后使用 THREE.TubeGeometry 创建出管状的几何体,给它添加一个基础材质,就能实现如下图所示的效果。

const spiralCurve = new SpiralCurve(5, 1, 10, 5);
const tubePath = new THREE.CurvePath();
tubePath.add(spiralCurve);
const tubeGeometry = new THREE.TubeGeometry(tubePath, 256, 0.25, 64, false);
const power = new THREE.Mesh(tubeGeometry, new THREE.MeshBasicMaterial({ color: 0xffffff }));
scene.add(power);

? THREE.TubeGeometry

Three.js 图形库用于创建具有管状形状的几何体的类,可以生成沿着路径的管状几何体。

THREE.TubeGeometry(path, tubularSegments, radius, radialSegments, closed)
  • path:是 THREE.Curve 类型的路径对象,可以是 THREE.CatmullRomCurve3 或其他类似的曲线对象,用于确定管状几何体的形状。
  • tubularSegments:管状几何体的段数,即沿路径分割的部分数目。
  • radius:管状几何体的半径大小。
  • radialSegments:每个管状几何体段中圆周的分割数。
  • closed:布尔值,表示管状几何体是否闭合。

step_1.png

添加着色器材质

然后,我们使用原理中介绍的着色器来给模型创建着色器材质,在着色器材质中我们设置需要动态修改的同一变量如 timecolor 等。让后将该材质添加到上面步骤创建的螺旋几何体上,就可以实现如下图所示的效果。

const shaderMaterial = new THREE.ShaderMaterial({
  uniforms: {
    time: {
      type: 'f',
      value: 0.0,
    },
    perlinnoise: {
      type: 't',
      value: textureLoader.load(new URL('@/assets/images/perlinnoise.png', import.meta.url).href),
    },
    waterturbulence: {
      type: 't',
      value: textureLoader.load(new URL('@/assets/images/waterturbulence.png', import.meta.url).href),
    },
    color1: {
      value: new THREE.Vector3(2, 20, 2),
    },
    color0: {
      value: new THREE.Vector3(0, 242, 22),
    },
    resolution: {
      value: new THREE.Vector2(sizes.width, sizes.height)
    }
  },
  fragmentShader: portalFragmentShader,
  vertexShader: portalVertexShader,
  side: THREE.DoubleSide,
  transparent: true,
});

step_2.png

添加发光后期效果

发光效果使用了 Three.js 自带的 UnrealBloomPass 辉光后期通道,可以像下面这样创建一个 EffectComposer,接着添加一个 RenderPass 作为第一个通道,它会将我们的场景 scene 和相机 camera 渲染到第一个渲染目标。最后添加一个 BloomPass,它将对输入结果的表面进行模糊处理,从而使得场景产生辉光效果。

const bloomComposer = new EffectComposer(renderer);
const renderScene = new RenderPass(scene, camera);
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, .4, .85);
bloomComposer.renderToScreen = true;
bloomComposer.addPass(renderScene);
bloomComposer.addPass(bloomPass);

const tick = deltaTime => {
  bloomComposer.render();
  window.requestAnimationFrame(tick);
}

tick();

step_3.png

? 关于 Three.js 后期处理原理介绍和示例,可以查阅本专栏的另一篇文章《Three.js 进阶之旅:后期处理》

添加神庙模型

最后,我们完善一下场景,由于建模能力有限,无法还原出游戏中的神庙模型,本案例中我们使用 GLTFLoader 加载器添加一个 glb 格式的类似神庙形状石头模型,将其放在能量光环下面,调整好两者的位置。当模型加载完成时我们可以使用 TWEEN.js 添加一个镜头由远到近的动画,使页面更加丝滑。

const loadingManager = new THREE.LoadingManager();
loadingManager.onLoad = () => {
  Animations.animateCamera(camera, controls, { x: 0, y: 1, z: 15 }, { x: 0, y: 0, z: 0 }, 4000, () => {});
}
const loader = new GLTFLoader(loadingManager);
loader.load(new URL('@/assets/models/shrine.glb', import.meta.url).href, mesh => {
  if (mesh.scene) {
    mesh.scene.scale.set(.1, .1, .1);
    mesh.scene.position.y = -7;
    scene.add(mesh.scene);
  }
});

step_4.png

? 源码地址: github.com/dragonir/th…

bg_3.png

总结

本文中主要包含的知识点包括:

  • 创建自定义路径的曲线的多种方式
  • 创建渐变纹理材质的方法
  • 给场景模型添加后期发光效果
  • 使用AI能力生成图片

想了解其他前端知识或其他未在本文中详细描述的Web 3D开发技术相关知识,可阅读我往期的文章。如果有疑问可以在评论中留言,如果觉得文章对你有帮助,不要忘了一键三连哦 ?

bg_4.png

附录

bg_5.png

参考

footer.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYvkQmxd' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片