• 作者:老汪软件技巧
  • 发表时间:2024-12-18 04:02
  • 浏览量:

图片

现实中南方下雪还挺碰运气的,就算下雪了,想要在雪地上 “书写” 文字、画画什么的,这种想法更是不切实际;那只能想办法在 “赛博空间” 中满足个人雪地写字的愿望了~

先看一下实现后的效果.

下雪效果:

雪地写字效果:

行走脚印效果:

虽然美观度上有些欠缺,不过无论如何是满足了 “雪地写字” 的关键诉求,我甚至还额外贴心地加了 “飘雪” 的氛围感,你就说走不走心吧…

图片

1. 源起

事情的起因是上周看到了这个开源案例源码演示 “雪地实时地形重塑” 的技术( 说人话就是 “雪地脚印”这种特效):

这种雪地脚印在游戏中很常见,比如在黑神话悟空里就有:

图片

不过网上使用 Threejs 来实现这种雪地脚印效果的教程或者源码寥寥无几,因此我看到这个就挺稀罕。

抽空拜读了作者源码,看完源码后我就寻思着能否自己在这上面改造改造。为了额外达成“雪地写字”的效果,我在作者源码的基础上简单做了几点改造:

支持通过鼠标在雪地上写字:这个是我最想要的效果,起初我是想要让人物在雪地上走动写字的,试了一下,这难度还不如让我去开飞机新增下雪效果:主要是为了突出氛围感,没有下雪光走路多没意思啊其他的一些交互细节:比如写字的时候就不要让相机运动等等,自己接管OrbitControls控件事件等

其实就是在作者源码的基础上做 “加法”,对作者的关键源码基本没有改动。改造后,就可以像文章开头展示的那样可以自由写字了:

对于没有美术功底的我,在功能层面基本上已经达到我个人的预期,进一步场景润色我也有心无力….(论美术审美修养的重要性)。

文章到这里,对于不想继续看源码解读部分的读者,可以拖到底部获取我提供的个人改造的代码版本(附带简单的操作说明),源码是 MIT 协议,拿去随便改吧,想怎么改就怎么改~~

为了完整性,文章接下来的内容还是会从技术角度上解释一下如何实现。

毕竟很久没输出技术文章了, 手痒痒,接下来即将进入稍显枯燥的源码解读阶段,虽枯燥但是我相信对技术提升还是有一些帮助的。

图片

友情提示:阅读源码需要 Three.js 、React 基础知识,本文默认读者已经有这两方面的基础,不然这文章要写得没完没了

2. 源码导读

这里放一下原作者的仓库和文章,顺带表达一下对作者的感谢,从中学到了很多实用的技术:

源码仓库地址:/oguzhantufe…

作者对应的文章:《Creating Dynamic Terrain Deformation with React Three Fiber》 /codrops/202…

本文尽可能简单导读一下代码结构,不在这里大段贴源代码占用篇幅,旨在给读者对源码有个概览。

先从简单的开始

2.1FPSLimiter帧率限定器

图片

作者短短几行代码就实现了3D 渲染的帧率(FPS) 限定器,允许开发者指定具体的目标帧率(默认为 60fps)

主要作用:在不同设备上统一帧率体验,避免性能差异带来的不一致。

一般使用场景为:

这个组件实用方便,直接可以 copy 过来放在其他项目中使用

体会到阅读优秀源码的好处了,抄作业都很方便

2.2lerpAngle线性角度插值

图片

lerpAngle(线性角度插值)函数的源码如下:

export const lerpAngle = (a, b, t) => {
  const difference = b - a;
  const shortestAngle =
    ((((difference + Math.PI) % (Math.PI2)) + Math.PI2) % (Math.PI2)) -
    Math.PI;
  return a + shortestAngle * t;
};

其实就是简单的线性插值函数,这种技法在 3D 动画场景挺常用,源码简单解释如下:

difference = b - a: 计算两个角度的差值shortestAngle的计算过程确保选择最短的旋转路径:最后使用线性插值公式:a + shortestAngle * t

这个函数就是让你的动画 “如德芙般丝滑”,提供良好的用户体验,常用在:

2.3 雾特效

图片

它使用着色器(shader)来实现一个基于距离的渐变透明效果,代码没几行:

const FogMaterial = shaderMaterial(
  // 统一变量(uniforms)
  {
    uCenternew THREE.Vector2(0.50.5),  // 雾效果的中心点
    uRadius0.031,                        // 雾效果的半径
    uColornew THREE.Color(0xffffff),     // 雾的颜色
  },
  // 顶点着色器
  `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  // 片段着色器
  `
    uniform vec2 uCenter;
    uniform float uRadius;
    uniform vec3 uColor;
    varying vec2 vUv;
    void main() {
      float dist = distance(vUv, uCenter);
      float alpha = smoothstep(uRadius - 0.01, uRadius + 0.01, dist);
      gl_FragColor = vec4(uColor, alpha);
    }
  `
);

我们将源码中颜色改成红色的,就能看出这个 shader 的外观样子了:

图片

作者这里也采用了取巧的方式,将颜色设置成白色,由于背景也是白色的,恰好就营造出 “白雪茫茫的一片” 的视觉效果。

在当前场景比较适合,毕竟背景、前景都是白茫茫一片,所以作者简单处理了。其他场景并不太实用,因此客观来说,该处理方法没有普适性

但不能说作者这么实现不好,离开具体场景判断一个技术的好坏是有失偏颇的。“如无必要,勿增实体”,如果简单方法就能满足需求,就不要无所谓地叠加技能点~像作者这样简单处理,既满足了视觉效果,又不造成性能问题,性价比很高。

2.3InfiniteSnowGround无限雪地

图片

这个InfiniteSnowWorld组件的主要内容和作用,是整个 Demo 演示的核心组件。这个文件其实整体结构是很清晰的,就是代码量有 700 多行,放在一起还是略微有点多,阅读起来稍微还是有些吃力。

代码就不贴了,我总结一下整体框架(基本自己制作小游戏也是这么个套路,写过的读者应该不会陌生):

无限雪地世界生成

2. 角色控制系统

3. 雪地变形效果

4. 相机系统

5. 音效系统

这里简单穿插讲解一下如何在 VSCode 编辑器中如何预览 3D 模型文件,估计有些读者比较感兴趣。当前应用所需要的模型、材质都放在 public 目录下了:

图片

其中这个explorer.glb文件就是人物的 3D 模型,只不过是压缩后的,我们来看一下如何在 VSCode 中预览它。

首先我们在 VSCode 中安装流行的 glTF Tools 插件:

右键选择 explorer.glb 文件后,选择glTF: import from GLB进行转换

在弹出的文件框里选择 explorer.glb 文件点击确定,就会转换成可阅读的 GLTF 格式文件了:

图片

之后使用快捷键Ctrl + Shift + P呼出 Preview 3D Model 功能,就可以预览该模型了

通过预览插件,我们选择 Three.js 模式,看到这个模型就是人物模型:

图片

同时你还会看到它额外还携带 3 个动画(从命名上可以猜测出是基于流行的 mixamo 制作的):第一个名为Armature.001||Layer0动画名,是人物原地活动的动画(懒散抖腿的效果):

第二个是名为Armature||Layer0的步行动画效果:

_下雪了写字图片_在雪地上写字

另外其中关于无限雪地的技术细节,推荐查看原作者写的文章《Creating Dynamic Terrain Deformation with React Three Fiber》,文中作者详细介绍了如何使用 React Three Fiber 创建动态地形变形效果。

我这里也简单提及一下,核心是这个deformMesh函数,它用于实现雪地的动态变形效果。主要功能创建角色脚步踩踏雪地时的变形效果,包括下陷和波纹效果。

源码详细解析(大概看一下有个印象就行,真需要了再细读即可…):

const deformMesh = useCallback((mesh, point) => {
  // 1. 基础检查
  if (!mesh) return;
  // 2. 获取受影响的相邻区块
  const neighboringChunks = getNeighboringChunks(point, chunksRef);
  const tempVertex = new THREE.Vector3();
  const geometriesToUpdate = [];
  // 3. 处理每个相邻区块
  neighboringChunks.forEach((chunk) => {
    const geometry = chunk.geometry;
    if (!geometry?.attributes?.positionreturn;
    const positionAttribute = geometry.attributes.position;
    const vertices = positionAttribute.array;
    let hasDeformation = false;
    // 4. 处理每个顶点
    for (let i = 0; i < positionAttribute.count; i++) {
      // 获取顶点位置
      tempVertex.fromArray(vertices, i * 3);
      chunk.localToWorld(tempVertex);
      // 计算到撞击点的距离
      const distance = tempVertex.distanceTo(point);
      // 5. 在影响半径内进行变形
      if (distance < DEFORM_RADIUS) {
        // 计算影响强度
        const influence = Math.pow(
          (DEFORM_RADIUS - distance) / DEFORM_RADIUS,
          3
        );
        // 计算下陷效果
        const yOffset = influence * 10;
        tempVertex.y -= yOffset * Math.sin((distance / DEFORM_RADIUS) * Math.PI);
        // 添加波纹效果
        **tempVertex.y += WAVE_AMPLITUDEMath.sin(WAVE_FREQUENCY * distance);**
        // 更新顶点位置
        chunk.worldToLocal(tempVertex);
        tempVertex.toArray(vertices, i * 3);
        hasDeformation = true;
      }
    }
    // 6. 更新变形后的几何体
    if (hasDeformation) {
      positionAttribute.needsUpdatetrue;
      geometriesToUpdate.push(geometry);
      saveChunkDeformation(chunk);
    }
  });
  // 7. 重新计算法线
  if (geometriesToUpdate.length0) {
    geometriesToUpdate.forEach((geometry) => geometry.computeVertexNormals());
  }
}, [getNeighboringChunks, chunksRef, saveChunkDeformation]);

整体逻辑大致分成以下几点:

确定受影响的区块遍历每个区块的顶点计算顶点到撞击点的距离根据距离计算变形程度应用下陷和波纹效果更新几何体和法线保存变形状态

关键概念解释

变形计算

const influence = Math.pow((DEFORM_RADIUS - distance) / DEFORM_RADIUS3);

下陷效果

const yOffset = influence * 10;
tempVertex.y -= yOffset * Math.sin((distance / DEFORM_RADIUS) * Math.PI);

波纹效果

tempVertex.y += WAVE_AMPLITUDEMath.sin(WAVE_FREQUENCY * distance);

从这里可以看出这个函数是实现雪地动态变形效果的核心,通过精确的数学计算创造出逼真的雪地交互效果。

好了,原作者的源码解读基本就到这儿了,你会发现代码实现 “短小精悍” ,麻雀虽小、五脏俱全,调理清晰,整体阅读起来难度不大,很多代码甚至直接拷贝放在其他 R3F 项目中使用。

3. 源码改造

看完源码后我就寻思着能否自己在这上面改造改造。为了达成“雪地”的效果,我在作者源码的基础上简单做了几点改造:

支持通过鼠标在雪地上写字: 开启“自定义绘制模式”后,用户就可以在雪地上写字了新增了SnowEffect组件: 制造下雪氛围其他的一些交互细节:新增了 leva 控制面板,方便用户选择性开启一些功能

其实就是在作者源码的基础上做 “加法”,对作者的关键源码基本没有改动。

3.1 实现雪地写字

这个功能的关键点有两个:

获取鼠标在 3D 平面上的坐标轨迹然后调用作者提供的deformMesh方法实行变形效果即可。

关键的代码已经标注到下面的截图中,从这里可以看出这个雪地写字功能 “虽然看上去很复杂,其实也就 just so so”:

图片

3.2 下雪氛围

下雪的氛围我单独抽成一个SnowEffect组件,所以也是很方便 copy 到其他项目中使用的:

图片

该下雪效果的的实现我参考了《23a How to make falling snow three.js》:… 这个视频,原文是 three.js 写的,我将其改造成 React 组件。

该组件的功能作用:

主要功能: 创建一个3D雪花效果,包含15000个雪花粒子,这些雪花会在一个固定空间内持续飘落。组件的主体工作流程图如下:

图片

这里关键的就是addSnowFlakes方法,源码解释如下:

// 雪花生成函数
function addSnowFlakes() {
    // 定义雪花数量和活动范围
    const numberSnowflakes = 15000;
    const maxRange = 1000;
    // 为每个雪花设置随机位置和速度
    for (let i = 0; i < numberSnowflakes; i++) {
        // 位置设置
        const x = Math.floor(Math.random() * maxRange - minRange);
        // ...
        // 速度设置
        velocities.push(/*...*/);
    }
}


上一条查看详情 +linux中创建系统服务
下一条 查看详情 +没有了