Elemental Sandbox(元素沙盒)-Three.js 游戏技能特效

「31-Three.js做游戏技能特效」

/~1d293aS7PB~:/

链接:https://pan.quark.cn/s/db04e856cbce


Elemental Sandbox(元素沙盒)

一个基于 Three.js + Vite + 手写 GLSL​ 构建的技能特效(VFX)沙盒。

包含 5 个技能 ​ 和 2 种瞄准方式 。其中 4 个是线型投射 :按下按键武装技能,地面会出现类似《英雄联盟》的箭头并跟随鼠标摆动,点击即可施放。第 5 个是区域投射(Far Cast) :箭头被一个带有刻意加粗边缘的圆圈取代,该圆圈跟随光标移动,并在你确认之前回答地面目标 AoE 必须回答的唯一问题------这东西会占多大的空间


技能概览

按键 技能 类型 描述
Q **Frost Lance(霜冻之枪)**​ 线型 一道碎冰前沿沿直线疾射而出,同时冰晶场从地面后方撕裂隆起------脚下细小密集,远端展开为一面刀刃墙,冲击点周围还会抛起一簇簇冰晶。
E **Storm Lance(风暴之枪)**​ 线型 一道闪电从施法者手中射出,打击前沿后方拖出一束闪电灯丝,维持、闪烁、重新打击,然后爆裂。全程迸射火花,地面留下分支电灼与暗色焦痕,远端形成电离空气壳。
R **Cinder Fall(余烬坠落)**​ 线型 一块燃烧岩石沿弧线被投掷到远处,拖着光线步进(raymarched)的燃烧气体尾迹,全程不断升温:表面的熔岩裂缝随着接近目标而变得越宽越亮。落地后引爆,将碎片抛洒满地,地面撕裂为持续发光的熔岩裂隙网络,直到弹坑燃尽。
F **Nova Beam(新星光束)**​ 线型 施法者双手凝聚光球,从空中吸纳光点,然后沿直线释放一道光柱------白热核心、青色鞘层、螺旋缠绕的金色丝带,以及沿柱体疾驰的冲击光盘。光柱持续驻留,灼烧地面并将溅射物沿光束反向抛回,最后坍缩为一缕细线并消失。**这是沙盒中唯一在命中一秒后仍在持续的效果。**​
V **Voltaic Snare(伏特陷阱)**​ 区域型 一道电流鞭横跨地面甩出,落点处圆环向外弹开超过自身半径后回缩贴合:紫色光柱从中心撕裂升起,触须向外爬向边界,电弧沿边缘游走,整个圆盘燃烧。光柱持续重新打击并将空气吸入其中,最后坍缩为一缕细线。**点击前你量出的圆圈,就是最终生效的精确圆圈。**​

核心特色

你看到的一切都是程序化生成 的。除了角色以外,没有纹理、没有精灵表、没有磁盘上的网格文件

  • 冰晶 → 程序化几何
  • 闪电 → 顶点着色器完全定位的带状条带
  • 陨石 → CPU 上通过断裂平面切割生成的 icosphere
  • 光束 → 以三种半径绘制三次的参数化管状体
  • 陷阱笼 → 同一根带状条带沿 4 条不同参数路径穿绕
  • 箭头、瞄准圈、霜冻、灼痕、熔岩裂隙 → SDF(符号距离场)与噪声着色器
  • 雾气、火花、碎片、闪光 → GPU 粒子

每个参数都是实时滑块------共 938 个------且在模拟暂停时依然保持实时。 ​ 这才是这个项目的核心意义:按 P​ 在喷发、打击或燃烧中途冻结画面,然后对着静止图像重塑轮廓、调色板和时序。


快速开始

复制代码
npm install
npm run dev

然后打开 Vite 输出的 URL(默认 http://127.0.0.1:5173)。

复制代码
npm run build
npm run preview

资源文件

public/ 目录下共有 6 个二进制资源,启动时自动加载:

文件 用途
public/models/Idle.fbx 绑定骨骼的角色及其待机动画片段
public/models/diffuse.png 角色的颜色贴图
public/models/cast1.fbx 施法动画
public/models/cast2.fbx 施法动画
public/models/cast3.fbx 施法动画 ------ Frost Lance、Root Snare 和 Glacier Crown 的默认动画
public/hdri/spruit_sunrise.hdr 用于基于图像的照明(IBL)和冰晶反射的 HDR 环境探针

所有 4 个 FBX 文件都是同一骨骼的 Mixamo 导出,每个都包含一个蒙皮网格和一个动画栈。角色网格来自 idle 文件;cast 文件仅加载其动画片段,附带导入的重复骨骼在提取 AnimationClip 后立即释放。片段通过骨骼名称绑定到骨骼,这就是另一个文件中的动画无需重定向即可在此播放的原因。

FBX 本身不携带材质,因此 diffuse.png 在导入时作为颜色贴图分配给 PBR 材质------如果 FBX 内嵌了纹理,则保留其自有贴图,因为该贴图是针对其自身 UV 制作的。

每个技能在其设置块中选择要播放的动画片段(castAnim),位于编辑器文件夹中 The cast 下的下拉菜单。默认情况下,槽位 1、5、6(Frost Lance、Root Snare、Glacier Crown)使用 cast3,其余三个使用 cast1。动画以单次播放覆盖循环待机,过渡由 character.castBlendIn / castBlendOut 控制。

HDR 用于基于图像的照明和冰晶反射源------从不作为可见天空显示,舞台始终保持平坦的暗色背景。


操作控制

技能武装与施放

输入 动作
Q (或 1 武装 Frost Lance --- 再次按下收起
E (或 2 武装 Storm Lance --- 再次按下收起
R (或 3 武装 Cinder Fall --- 再次按下收起
F (或 4 武装 Nova Beam --- 再次按下收起
V (或 5 武装 Voltaic Snare(区域型,用圆圈瞄准)
鼠标移动 摆动瞄准箭头,或移动区域圆圈
左键点击 沿箭头施放,或在圆圈当前位置落点
Esc / 右键 取消已武装的技能
右键 + 拖拽 轨道旋转摄像机
滚轮 缩放

沙盒控制

按键 动作
G 显示/隐藏 VFX 编辑器面板
P 暂停 / 恢复 ------ 编辑器在暂停时仍持续生效
C 清除所有活跃特效
H 隐藏控制面板

提示:rangeminRange 是每个技能独立的,因此指示器的射程会随当前选中的槽位变化。瞄准距离小于当前技能的 minRange 时指示器会染红并拒绝施放;如果你希望允许向脚下施放(Snare 默认允许------无法对自己脚下释放的陷阱会失去一半用途),将 minRange 设为 0 即可。冷却时间也是每个技能独立的,使用一个槽位不会锁定其他槽位。


项目结构

复制代码
src/
  abilities/      技能基类(行进前沿)、IceAbility、ThunderAbility、
                   MeteorAbility、BeamAbility、SnareAbility、对象池管理器
  animation/       FBX 角色加载、AnimationMixer、各技能施法动画片段、
                   程序化施法前冲
  assets/         程序化冰晶和陨石几何、闪电条带、光束管体及其冲击光盘
  config/         settings.js ------ 每个参数的唯一数据源
  core/           App、Renderer、CameraRig、Time、Layers、共享帧 uniform
  effects/        瞄准箭头、区域圆圈、地面贴花、裂隙、爆发、光源池、
                   镜头震动、全屏闪烁
  input/          InputManager(事件)和 AimController(两种瞄准形状)
  loaders/        带共享 LoadingManager 的 AssetLoader
  materials/      IceMaterial、LightningMaterial、MeteorMaterial、
                   VolumetricFireMaterial、BeamMaterial、SnareMaterial
  particles/      GPU 粒子系统 + 引擎和速率发射器
  postprocessing/ Composer 管线、调色着色器、扭曲着色器
  shaders/lib/    共享 GLSL:噪声库、通用辅助函数
  ui/             HUD、lil-gui 编辑器、预设管理器、样式
  utils/          数学工具、颜色缓存、对象池、资源释放、着色器补丁
  world/          Environment(场景光照)、地板、灰尘、接触阴影
  archive/        已退役的四元素沙盒 ------ 见 archive/README.md

架构设计

Settings 就是 API

src/config/settings.js 保存所有可调整的值。没有任何其他模块拥有这个状态:着色器、粒子系统、光源和后处理通道每帧都读取这些对象。这就是编辑器无需重建即可工作的原因------拖动滑块会同时改变已经存在的冰场、下一次施放、环境和后处理堆栈。预设加载通过深合并写入同一对象,因此所有实时绑定始终有效。

复制代码
import { settings } from './config/settings.js';
settings.ice.height = 7;          // 下一帧立即可见,即使正在施放中
settings.thunder.jitter = 1.2;    // 重新扭曲已经在空中的闪电
settings.global.timeScale = 0.1;  // 将整个施放过程放慢到爬行

技能块在 ELEMENTS 中按 id 索引,需要知道"玩家正拿着哪个技能"的共享系统(瞄准控制器、冷却、HUD)都通过 settings[element] 查找。它们依赖的四个必填字段是:rangeminRangespeedcooldown;区域型技能额外需要第五个字段 zoneRadius。块中的其他所有内容都是该技能自己的业务。

"暂停时编辑"的运作规则

IceAbility 中的 spike 记录只存储骰子决定的结果 :沿直线的位置比例、有符号的横向比例,以及少量无量纲抖动值。在施放开始时不会捕获任何米、弧度或秒。 ​ 所有维度都在 update 循环中根据 settings.ice 解析------该循环在零长度帧上也会运行。

因此,拖动 height 会重新拉伸已经存在的冰场;拖动 lean 会重新倾斜它;拖动 clumping 会重新将其向中心线聚集。记录唯一捕获的值是时间戳------即其自身喷发被触发的时刻。那些是事件,不是维度。

四个形状控制参数(facets、taper、roughness、bend)无法表示为逐实例变换,因此被烘焙到几何体中 ------一个六面晶体只需 60 个三角形,完全重建比在顶点着色器中近似要便宜得多。IceAbility#_syncGeometry 对这四个值进行哈希,当哈希变化时重建三个晶体网格,这就是它们能作为实时滑块而非需要重启的常量的原因。

瞄准系统

AimController 每帧(而不仅是在鼠标移动时)将指针射线投射到地平面,因此用已武装技能轨道旋转摄像机时,指示器会在静止光标下摆动。它将距离限制在 [minRange, range] 内,跟踪 0..1 的揭示包络,并发出携带原点、单位方向和距离的单一施放事件------这正是 Ability#spawn 所需的签名。它不决定施放的任何具体内容。

它基于真实时间而非缩放后的模拟增量运行,因此即使沙盒暂停,指示器也会继续动画。

系统中有两种指示器但只有一个控制器 。绘制哪一种由 ELEMENT_META[element].cast 决定------CastShape.LINECastShape.ZONE------这也是两者唯一的区别。武装、限制、验证、揭示和发射都是共享的,最终都产生相同的三参数施放事件,因为从瞄准侧来看,区域型施放只是你只关心远端的线型施放。这就是为什么区域瞄准不需要对 AbilityAbilityManagerApp 做任何修改:SnareAbility 将其中心读取为 pointAt(1) 并从那里向外展开。

区域型圆圈(Far-Cast Circle)

ZoneIndicator 是箭头的对应物,由同样的两种理念构建:米制,无纹理

它的足迹是一个四边形,其片元着色器将 UV 重映射为距目标的米数,因此无论圆圈直径是 2 米还是 8 米,边界都保持 0.34 米 ​ 的厚度。这个带是屏幕上最重的标记------它就是全部信息------并且它通过 boundaryBias 在标称半径两侧不对称分布,而不是居中,这样外边缘能如实反映效果的实际范围。内部有边缘加权的渐变、向外扩散的等高线环、扭曲的细丝,以及一个下方向臂更长的标线(因为四边形携带了施法者的偏航角,该臂即代表朝向)。

施法者处的射程环是将闪电的条带弯曲成圆形:输入 (t, side),输出世界坐标。一个足以容纳 20 米射程的四边形将是 40 米见方,会为一条细线着色一整屏被丢弃的片段。

圆圈在武装时会向外弹开超过其半径然后回缩就位,陷阱落地时也会做同样的动作。线性增长的圆圈读起来像 UI 元素;过冲回弹的圆圈读起来像施法者做出的动作。

箭头:一个 SDF

AimIndicator 是一个单一的地面四边形。其片元着色器将 UV 重映射为从施法者开始的米数,因此 settings.aim 中的每个控件都是真实的测量值------无论施放是 3 米还是 15 米长,箭杆始终保持 0.42 米​ 宽。

轮廓是矩形(箭杆)和 iq 精确三角形 SDF(箭头)的圆角并集;廉价的半平面相交在这个浅楔形上会留下可见的角伪影。从这个单一距离场中,着色器派生出轮廓、边缘加权的内部渐变、V 形箭头(通过 |x| 偏斜相位,将平带变成指向施放方向的箭头)、霜冻噪声和 Voronoi 板块、施法者脚下的环、射程上限弧、固定在冲击点的六瓣霜冻玫瑰,以及武装技能时的扫出动画。

冰晶(Ice)

materials/IceMaterial.js 是对 MeshStandardMaterial 的补丁而非替换,因此晶体投射并接收场景的真实阴影,并拾取 HDR 探针。风格化效果在此基础上注入:

  • 厚度着色(Thickness tint) --- 正面观察的面穿过晶体的路径最长,因此向 colorDeep 变暗;掠射边缘保持浅色。这一项使冰场读起来像可以看穿的实体,而不是蓝色塑料。
  • 内部断裂 --- 在世界空间中采样的脊状噪声,因此裂纹平面保持固定的物理尺寸,无论尖刺是脚踝高还是三米高,相邻晶体看起来都像从同一块中开采出来的。
  • 羽霜和冰霜(Feather frost & rime) --- 在局部空间(沿晶体 0..1)采样的 fbm,因此乳白色纹理和从底部爬升的霜冻跟随每个尖刺自身的轴,无论其如何缩放或倾斜。
  • 闪光(Glint) --- 在世界空间中滚动的高频场,经硬阈值处理,偏向掠射角,这是真实冰晶捕捉光线的位置。
  • 出生闪光(Birth flash) --- 技能从 1 到 0 在 birthFade 期间驱动的逐实例属性,因此晶体在喷发瞬间从内部被点亮。

三个 InstancedMesh 共享一个材质。之所以是三个而不是一个,是因为刻面不同,而不仅仅是比例不同------逐实例缩放无法买到这种轮廓多样性,而三个绘制调用是非常廉价的代价。

闪电(Lightning)

ThunderAbility 将"CPU 上无维度"的规则贯彻得比冰更彻底:根本没有路径对象 。闪电是一个 InstancedBufferGeometry------参数空间中的扁平四边形梯,每个顶点仅携带 (t, side):沿闪电的距离和条带的哪一侧。一个实例就是一根灯丝。materials/LightningMaterial.js 每帧将该对转换为世界坐标,因此单个条带可以服务于任何长度、任何形状和任何宽度的闪电。

三种东西叠加形成形状:

  1. 轴(axis) --- 从手到冲击点的直线,受 sag 弯曲。唯一知道施放方向的部分。
  2. 扇形(fan) --- 在垂直于轴的平面中每根灯丝的恒定偏移,从手部的 spreadNear 展开到目标的 spread,并随 twist 绕轴旋转。这就是将一根灯丝与下一根分开的原因。
  3. 扭结(kinks) --- 线性插值的值噪声八度。刻意使用线性:smoothstep 会磨圆角落,而角落正是它读起来像闪电而不是像摆动管子的原因。

条带通过局部切线与视图向量叉乘来面向摄像机,这就是为什么闪电从任何角度都保持表观厚度,而从未成为屏幕空间线条。它被绘制两次------底部是宽而柔和的晕,顶部是热核心------因为将辉光绘制为真实条带而不是留给泛光,才能让它附着在每一个扭结上。

两个时钟驱动闪烁:restrike 每秒 N 次将每根灯丝捕捉到新形状,crawl 在两次捕捉之间连续滑动扭结;两者结合防止持留的闪电看起来像静态条带。一次施放只捕获一个数字------种子 ,因此两次施放不会画出相同的闪电------并且每帧根据 settings.thunder 解析每个米、弧度和秒。这就是为什么拖动 jitter 会重新扭曲已经在空中的闪电。

值得注意的坑(Ground burns): ​ 第一个版本在 atan(y, x) 上采样灯丝场,这会沿给定方位角给每个半径赋予相同的值,从而从中心画出笔直的辐条------像烟花,不像灼痕。在平面中采样相同的噪声并扭曲查找,才能让灯丝蜿蜒分叉。

光束(Nova Beam)

Nova Beam 共享闪电的规则------CPU 上无维度------但达到了相反的外观。闪电的魅力在于其噪声是分段线性的并保留了角落,而光束中的每个噪声项都是平滑的,沿流动方向强烈拉伸并向下游爬行。扭结的光束就是闪电。

它是真实的管状体而非面向摄像机的条带,因为这么粗的柱子必须有横截面:轮廓在轨道旋转时必须正确弯曲,远壁必须透过近壁叠加,冲击光盘必须贴合它。createBeamTubeGeometry 是丰富了一个维度的条带------每个顶点携带 (t, a),即沿管体的距离和环绕角度------materials/BeamMaterial.js 每帧将该对转换为世界坐标。

这个管体被绘制三次,诀窍在于三者的权重分配:

描述
**Halo(光晕)**​ 最宽,只有边缘项。光束推开的大气。
**Sheath(鞘层)**​ 边缘加权,读起来是空心的,轮廓边缘是最亮的部分。
**Core(核心)**​ 窄,加权方式相反:当视线沿管体方向时最亮,穿过管体的路径最长。

外部边缘加权,内部轴向加权,两个面叠加:这就是一个体积积分,而且很廉价。反转正是中间读起来像实心光柱而不是被照亮的管子的原因。增大 coreWidth 或推高 coreFill,三层就会坍缩为一个白管------青色鞘层和金色线圈之所以清晰可辨,正是因为核心给它们留了空间。

还有两个实例化通道为其添加结构。线圈是弯曲成螺旋的闪电条带,面向摄像机且刻意偏暖------颜色分离是防止它们融入鞘层的原因。冲击光盘是一个实例化环面,其相位为 fract(index / count + time × speed),因此序列是时钟的纯函数,CPU 上没有任何队列。两者都根据管体使用的同一 beamRadius() 定位,这就是为什么在拖动轮廓时所有五部分都保持焊接在一起。

光束也是唯一有第四拍 的技能。其他三个的运行顺序是 行进 → 冲击 → 消退;这个在前面加了一个蓄力 ,而且它不需要基类的任何修改:advance() 只是拒绝让前沿离开手,直到法球充能完毕,因此 IMPACT 变成了燃烧阶段,相位机保持不变。远端因此有一个持续发生的冲击------沿管线反向抛回的溅射物、从燃烧点脱落的压力壳、推过地板的灰尘和冲击波环,所有这些都通过粒子使用的同一分数速率发射器进行速率限制,因此每个速率都是实时滑块。

陷阱(Voltaic Snare)

Voltaic Snare 是第一个围绕 而非线 构建的技能,将所有东西凝聚在一起的关键是:zoneRadius 在每个消费者的代码中只在一个地方读取,且从未被复制:指示器量出它、触须终结于它、边缘电弧沿它运行、场燃烧它、柱子的喉部和展开都是它的分数。拖动它,所有五个部分会一起移动,即使在施放中途、时钟停止时也是如此。

整个笼子------种植它的鞭子、柱子、触须和边缘电弧------是一个实例化条带,与闪电和光束线圈使用的相同。灯丝的角色在顶点着色器中通过将其实例索引与四个实时计数进行比较来决定,角色选择它沿哪条参数路径穿行:

角色 路径
**Leash(锁链)**​ 从手到行进尖端的下垂线,落在地板上。
**Column(柱子)**​ 扭曲的爬升,半径从 throat 展开到 columnSpread
**Tendril(触须)**​ 向外蜿蜒,其 veer 是每根灯丝的常数而非噪声,因此它会像已选定方向的放电那样弯曲。
**Rim(边缘)**​ 沿边界运行的弧,在跨度中点跳过边界。

然后每个偏移都存在于通过有限差分从路径获取的帧中,这就是让同一个扭结函数服务于垂直柱子和在地板上平爬的灯丝的原因。两个贴地的角色抑制了该偏移的垂直分量并钳制在地板以上------自由 y 的扭结会将每个触须的一半埋入地下,效果会读作断裂的虚线。将某个计数设为 0 会直接退役该角色,这就是锁链在圆环接管的帧消失的方式。两个绘制调用覆盖所有四个角色,无论空中有多少灯丝。

场是一个四边形而不是池化贴花,原因只有一个:贴花在生成时捕获其半径,而这个圆圈必须在站立时根据 zoneRadius 重新缩放。它的纹理在平面中采样并进行域扭曲------与闪电的地面灼痕相同的教训,原因也相同。

值得为下一个区域型技能借鉴的唯一东西是弹开(snap) :圆环在 Easing.outCubic 上打开,乘以一个在后期达到峰值并在恰好 1 时归零的凸起,因此它超过半径后回弹贴合,柱子以慢 1.7 倍的时钟沿同一节奏爬升。地面先行,然后空气在其上击穿。


添加新技能

  1. config/settings.js 中添加设置块,并在 ELEMENTS / ELEMENT_META 中添加条目。
  2. 继承 Ability 基类并实现 createShaderscreateParticlesonTravelonImpactonFade
  3. abilities/AbilityManager.js 中注册该类。
  4. ui/Editor.js 中添加编辑器文件夹,在 ui/glyphs.js 中添加符号。
  5. input/InputManager.js 中绑定按键------它发出带有从 0 开始的槽位索引的 ability 事件,App 通过 ELEMENTS 映射它。

要使其成为区域型而非线型,只需添加两样东西 :在其 ELEMENT_META 条目中添加 cast: CastShape.ZONE,并在其设置块中添加 zoneRadius。圆圈指示器、射程环、弹出动画和整个瞄准循环都是免费的,技能将其中心读取为 pointAt(1)

其他一切------对象池、行进前沿、局部坐标系、光源、阶段、逐技能冷却、瞄准射程和摄像机取景------都是继承的或由 ELEMENTS 驱动。HUD 从该数组构建其槽位,因此新技能会自动出现在栏中。


粒子系统

particles/ParticleSystem.js 是一个 GPU 模拟的实例化四边形系统 。运动(速度、重力、解析阻力、旋度湍流、涡旋)、生命期大小、颜色梯度和 alpha 消退都在着色器中从逐实例属性求值;CPU 只写入生成数据,且只上传更改的槽位。粒子生活在环形缓冲区中,因此连续施放会回收槽位而不是分配新内存。轮廓(柔和、烟雾、条纹、叶片、碎片、圆环)都是程序化的------项目中没有任何精灵纹理。

  • Frost Lance 使用三个系统:雾(非叠加,因此雾真正遮挡并赋予冰场深度)、碎片(重力下的发光芯片)和闪光(叠加,负重力------参考帧标志性的上升羽流)。
  • Storm Lance 使用四个:火花(重力下速度拉伸的条纹)、光点(螺栓周围的慢速电离漂移)、烟雾(焦地板上的非叠加雾霾)和碎片(发光芯片)。其火花每帧从闪电上的多个点发射,而不是一个点:光束沿整个长度脱落,单一原点会使每批读起来像星爆。
  • Nova Beam 使用四个,其中一个用了两次:其光点在蓄力时螺旋进入法球,发射后成为从柱子脱落的漂移------相同的辉光,方向相反。其火花从管体径向抛出,然后被 sparkForward 沿下游拖拽,这就是"压力"的读法;而闪电的火花是下落,这一个差异在区分两个技能方面做了大量工作。

渲染管线

每帧流程:

  1. 深度预渲染(Depth prepass) --- 将不透明世界渲染到半分辨率打包深度缓冲区。每个 VFX 着色器都对其进行采样以实现软交叉,因此没有任何东西会在地面上切出硬线。晶体位于 LAYER.WORLD,因此雾和闪光在它们上面柔和消退。
  2. 扭曲通道(Distortion pass) --- 扭曲层上的网格将屏幕空间 UV 偏移写入第二个半分辨率缓冲区。当前构建中没有任何东西写入它;保留该通道是因为它是折射效果的钩子。
  3. Composer --- 场景 → 折射扭曲 → 泛光 → 色调映射(ACES) → 调色。

调色通道将色差、lift/gain/contrast/saturation/temperature、晕影、胶片颗粒和冲击闪烁折叠为一次重采样。

阴影来自单个方向光,其正交阴影摄像机每帧重新居中于角色,并适配 52 米方框,分辨率 4096²(约 1.3 厘米/纹素)。曾尝试使用 three/addons 的 CSM 模块并移除:它会全局替换 three 的 lights_fragment_begin 块,因此任何未明确向其注册的材质都会静默失去所有方向光照明。

接触阴影是真实的渲染:角色深度从下方捕获到 256² 目标中,模糊两次并投影到地面上。


编辑器与预设

G​ 打开面板。文件夹包括:预设、全局、瞄准指示器、区域圆圈、Frost Lance、Storm Lance、Cinder Fall、Nova Beam、Voltaic Snare、环境、后处理、摄像机、角色。每个文件夹默认折叠------这里的控件足够多,打开一个部分会把其余部分推出屏幕。

  • 全局乘数同时缩放一切(速度、辉光、噪声、粒子、光源、冲击强度、镜头震动、时间缩放......)。
  • 瞄准指示器 --- 箭头的轮廓(米)、轮廓和填充、V 形箭头和霜冻、环和玫瑰。
  • 区域圆圈(40 个控件) --- 边界带、内部、刻度、扫出和标线、射程环和弹出。由所有区域型技能共享,因此归档在瞄准下而不是任何单个技能下。
  • Frost Lance(113 个控件,其中 25 个是颜色) --- 施放、足迹、轮廓、晶体本身、喷发时序、冰材质、地面霜冻、雾/碎片/闪光、冲击和动态光源。
  • Storm Lance(123 个控件,其中 34 个是颜色) --- 施放、闪电出手位置、束、单根灯丝、条带、闪烁和重击、闪电颜色、地面灼痕、火花/光点/烟雾/碎片、枪口和冲击、动态光源。
  • Nova Beam(176 个控件) --- 施放、出手位置、柱子、核心/鞘/光晕堆叠、表面和流动、光束颜色、线圈、冲击光盘、蓄力和吸入、地面效果、火花/光点/蒸汽/碎片、释放/冲击/燃烧、两个动态光源。
  • Voltaic Snare(174 个控件,其中 33 个是颜色) --- 施放和足迹、锁链、柱子、触须、边缘电弧、共享灯丝形状和闪烁、条带及其颜色、地面场、灼痕、火花/上升气流/烟雾/碎片、投掷/弹出/持续、动态光源。

预设保存到 localStorage,可以复制、删除、导出为 JSON、从 JSON 导入或重置为出厂默认。

每个技能都暴露其绘制的每种颜色,且没有任何颜色派生自另一种:晶体调色板、闪电调色板、光束的四层及其线圈和光盘、地面标记、冲击壳、冲击波环、屏幕闪烁,以及每个粒子系统的四站生命期渐变(出生 → 早期 → 晚期 → 死亡)。不改变冰色只给雾着色,或将火花冷却为橙色而灯丝保持蓝色,只需点一下取色器。

预设是 settings 树的纯快照,因此导出的文件可读且可手动编辑。

值得了解的旋钮

这些旋钮对其技能的重塑作用最大:

技能 参数 效果
ice.heightCurve 斜坡爬升的延迟程度;提高后冰场保持低位直到在目标处爆发
ice.frontBias < 1 将晶体向冲击点聚集
闪电 thunder.jitter & jitterScale 闪电扭结的剧烈程度和频率
闪电 thunder.strands & spread 束的宽度感
闪电 thunder.restrike 频闪强度
光束 beam.radius & beam.flare 柱子的厚重感和落地展开程度
光束 beam.charge & beam.lifetime 蓄力和持续,这是让该技能感觉不同于其他三个的原因
光束 beam.coreWidth / beam.coreFill 决定各层是否可分离,还是爆成白色
陷阱 snare.zoneRadius 整个区域型技能的基础数字,实时联动所有部分
陷阱 snare.snapTime & snare.height 打开瞬间的感觉
陷阱 snare.tendrils / snare.rimArcs / snare.strands 足迹中实际被点亮的部分
区域 zone.boundary & zone.snap 圆圈边缘的厚度和弹出强度

性能说明

  • 技能、贴花、爆发和粒子都按类型池化。连续施放 12 次最多构建 4 个技能实例,然后停止分配。
  • 整个冰场无论晶体数量多少都是 3 个绘制调用;上限为 288 个晶体。
  • 整条闪电无论灯丝数量多少都是 2 个绘制调用 ;上限为 24 根灯丝,每根 72 个采样。路径不触及 CPU,strands 几乎免费。
  • 整个陷阱(锁链、柱子、触须、边缘电弧)是 2 个绘制调用 + 场 1 个,无论有多少灯丝;四个角色的上限为 56。与闪电相同,形状不触及 CPU,因此增加触须或边缘电弧几乎免费。其瞄准圆圈额外 2 个:一个四边形和一个环形条带。
  • 整条光束是 6 个绘制调用 ,无论有多少线圈和光盘------三个管体通道共享一个几何,加上线圈、光盘和蓄力球各一个实例化绘制。与闪电相同,形状不触及 CPU,因此线圈和环几乎免费。它占用六个动态光源中的两个(柱子和施法者双手),因此四个并发光束会耗尽池;LightPool.acquire() 返回 null,每次使用句柄都有防护。
  • 六个动态点光源在启动时创建并以零强度停放,而不是添加和移除------更改光源数量会强制每个材质重新编译。
  • 阴影贴图每帧只更新一次,即使场景被渲染多次。
  • 启动时运行 renderer.compileAsync(),因此第一次施放永远不会因着色器编译而卡顿。
  • 像素比上限为 1.75;深度和扭曲缓冲区为半分辨率。

测量结果(默认施放):空闲 32 个绘制调用,完整冰场约 69 个,空中闪电约 49 个,约 1150 个活跃粒子。一个陷阱站立时(笼子、场、边缘灼痕)约 45 个绘制调用和约 480 个活跃粒子,武装其圆圈花费 2 个。四个并发施放(池的上限,无论来自哪些槽位)峰值约 186 个绘制调用和五个动态光源中的五个。

实时计数器(FPS、活跃粒子、实例、绘制调用)位于 HUD 的右上角。


归档

src/archive/ 保存了该项目的前一个版本:一个四元素弯曲沙盒(火、水、土、气),沿自由绘制样条线施放,外加一个行走模式让角色沿同一笔划移动。活跃应用没有导入其中任何内容,因此 Vite 永远不会打包它。

它被退役是因为这个构建用线性技能射击替换了路径绘制,这移除了所有这些系统所基于的输入。特别是光线步进的火焰和水面效果值得挖掘。请参阅 src/archive/README.md 了解其中的内容和如何恢复某个部分。


已知问题

  • 晶体以 transparent: truedepthWrite: true 绘制。这对于近乎不透明的冰是正确的权衡,可以防止冰场自身排序错误,但在低 ice.opacity 时,重叠尖刺之间的排序伪影会变得可见。
  • 喷发前沿是平坦地板上的直线。这两个假设都是硬编码的------地面是 y = 0 的单平面,瞄准射线投射以该平面为目标。
  • 扭曲通道运行时没有任何东西写入它。它每帧花费一次半分辨率清除的成本。
  • 冲击簇被径向放置在端点周围,因此在非常短的施放距离下,它可能比应有的更多地与后面的带重叠。
  • 区域型技能在两个方面继承了平坦地板假设:圆圈绘制在 y = 0 的单个四边形上,陷阱的触须和边缘电弧也相对于该平面放置。两者都不会覆盖台阶。
  • 瞄准圆圈和陷阱的场都是叠加的,因此足迹会提亮地板而不是遮蔽它。在浅色地板上,边界需要在其下方添加非叠加通道才能保持可读。

许可证

代码按"原样"提供,仅用于本项目目的。捆绑的 HDR 探针和角色 FBX 保留其原始许可证。

相关推荐
小小龙学IT1 小时前
Dear ImGui 开源即时模式 GUI 库深度解析
c++·ui·开源
用户69371750013841 小时前
前阵子刷屏的 Ox-Alpha 真身揭晓!GLM-5.3-Flash 上线,这价格真的杀疯了
前端·后端·github
四千岁1 小时前
抓包LangChain-了解LangChain的原理
前端·javascript·后端
南雨北斗1 小时前
TS方式构建Vue3 toast组件(一)
前端
IT小白杨1 小时前
2026游戏工作室环境隔离指南:从设备指纹到移动IP的部署实践
前端·经验分享·网络协议·tcp/ip·游戏·安全架构·指纹浏览器
用户298698530141 小时前
使用 JavaScript 在 React 中实现 Word 转 PDF
前端·javascript·react.js
网安小学生(兼顾数据库版)1 小时前
Mend SCA 实操指南:使用 Mend CLI 进行开源组件安全扫描
安全·开源
hunterandroid1 小时前
[鸿蒙从零到一] @Observed 与 @ObjectLink 深层响应陷阱与最佳实践
前端
hunterandroid1 小时前
[鸿蒙从零到一] ArkTS 装饰器原理与自定义装饰器实践
前端