@netforge/engine 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GAME-DESIGN.md +195 -0
- package/LICENSE +21 -0
- package/NOTICE +30 -0
- package/README.md +147 -2
- package/dist/api/appearance.d.ts +37 -0
- package/dist/api/appearance.js +41 -0
- package/dist/api/cards.d.ts +105 -0
- package/dist/api/cards.js +133 -0
- package/dist/api/collector.d.ts +114 -0
- package/dist/api/collector.js +204 -0
- package/dist/api/dungeon.d.ts +58 -0
- package/dist/api/dungeon.js +216 -0
- package/dist/api/harness.d.ts +149 -0
- package/dist/api/harness.js +549 -0
- package/dist/api/idle.d.ts +71 -0
- package/dist/api/idle.js +104 -0
- package/dist/api/race-harness.d.ts +3 -0
- package/dist/api/race-harness.js +148 -0
- package/dist/api/registry.d.ts +241 -0
- package/dist/api/registry.js +394 -0
- package/dist/api/scheduler.d.ts +56 -0
- package/dist/api/scheduler.js +160 -0
- package/dist/api/schemas.d.ts +958 -0
- package/dist/api/schemas.js +859 -0
- package/dist/api/tower.d.ts +127 -0
- package/dist/api/tower.js +183 -0
- package/dist/game/adapter.d.ts +69 -0
- package/dist/game/adapter.js +341 -0
- package/dist/game/arena-bot.d.ts +66 -0
- package/dist/game/arena-bot.js +188 -0
- package/dist/game/bot.d.ts +95 -0
- package/dist/game/bot.js +285 -0
- package/dist/game/compile.d.ts +97 -0
- package/dist/game/compile.js +476 -0
- package/dist/game/director.d.ts +42 -0
- package/dist/game/director.js +179 -0
- package/dist/game/earn-source.d.ts +122 -0
- package/dist/game/earn-source.js +386 -0
- package/dist/game/earn.d.ts +49 -0
- package/dist/game/earn.js +55 -0
- package/dist/game/index.d.ts +37 -0
- package/dist/game/index.js +25 -0
- package/dist/game/platformer.d.ts +156 -0
- package/dist/game/platformer.js +619 -0
- package/dist/game/profile.d.ts +97 -0
- package/dist/game/profile.js +140 -0
- package/dist/game/race.d.ts +177 -0
- package/dist/game/race.js +386 -0
- package/dist/game/racer.d.ts +98 -0
- package/dist/game/racer.js +379 -0
- package/dist/game/runner.d.ts +286 -0
- package/dist/game/runner.js +824 -0
- package/dist/game/threats.d.ts +23 -0
- package/dist/game/threats.js +49 -0
- package/dist/game/view.d.ts +47 -0
- package/dist/game/view.js +123 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +24 -0
- package/dist/net/bits.d.ts +33 -0
- package/dist/net/bits.js +135 -0
- package/dist/net/client.d.ts +75 -0
- package/dist/net/client.js +151 -0
- package/dist/net/index.d.ts +23 -0
- package/dist/net/index.js +23 -0
- package/dist/net/input.d.ts +49 -0
- package/dist/net/input.js +112 -0
- package/dist/net/interpolation.d.ts +37 -0
- package/dist/net/interpolation.js +97 -0
- package/dist/net/netsim.d.ts +22 -0
- package/dist/net/netsim.js +52 -0
- package/dist/net/prediction.d.ts +31 -0
- package/dist/net/prediction.js +101 -0
- package/dist/net/profile-client.d.ts +75 -0
- package/dist/net/profile-client.js +173 -0
- package/dist/net/protocol.d.ts +208 -0
- package/dist/net/protocol.js +8 -0
- package/dist/net/room-client.d.ts +57 -0
- package/dist/net/room-client.js +144 -0
- package/dist/net/schema.d.ts +78 -0
- package/dist/net/schema.js +121 -0
- package/dist/net/sim-adapter.d.ts +134 -0
- package/dist/net/sim-adapter.js +22 -0
- package/dist/net/snapshot.d.ts +61 -0
- package/dist/net/snapshot.js +301 -0
- package/dist/rules/adapter.d.ts +53 -0
- package/dist/rules/adapter.js +261 -0
- package/dist/rules/cards.d.ts +115 -0
- package/dist/rules/cards.js +547 -0
- package/dist/rules/collector.d.ts +161 -0
- package/dist/rules/collector.js +732 -0
- package/dist/rules/genres.d.ts +13 -0
- package/dist/rules/genres.js +38 -0
- package/dist/rules/harness.d.ts +16 -0
- package/dist/rules/harness.js +127 -0
- package/dist/rules/idle.d.ts +115 -0
- package/dist/rules/idle.js +370 -0
- package/dist/rules/index.d.ts +16 -0
- package/dist/rules/index.js +14 -0
- package/dist/rules/kit.d.ts +171 -0
- package/dist/rules/kit.js +88 -0
- package/dist/rules/tower.d.ts +214 -0
- package/dist/rules/tower.js +655 -0
- package/dist/runtime/arena.d.ts +86 -0
- package/dist/runtime/arena.js +269 -0
- package/dist/runtime/assets.d.ts +26 -0
- package/dist/runtime/assets.js +35 -0
- package/dist/runtime/camera.d.ts +113 -0
- package/dist/runtime/camera.js +247 -0
- package/dist/runtime/crowd.d.ts +18 -0
- package/dist/runtime/crowd.js +62 -0
- package/dist/runtime/feel/audio.d.ts +150 -0
- package/dist/runtime/feel/audio.js +327 -0
- package/dist/runtime/feel/clock.d.ts +52 -0
- package/dist/runtime/feel/clock.js +80 -0
- package/dist/runtime/feel/flash.d.ts +28 -0
- package/dist/runtime/feel/flash.js +60 -0
- package/dist/runtime/feel/index.d.ts +27 -0
- package/dist/runtime/feel/index.js +16 -0
- package/dist/runtime/feel/kit.d.ts +159 -0
- package/dist/runtime/feel/kit.js +226 -0
- package/dist/runtime/feel/knockback.d.ts +41 -0
- package/dist/runtime/feel/knockback.js +70 -0
- package/dist/runtime/feel/noise.d.ts +6 -0
- package/dist/runtime/feel/noise.js +30 -0
- package/dist/runtime/feel/numbers.d.ts +91 -0
- package/dist/runtime/feel/numbers.js +172 -0
- package/dist/runtime/feel/rumble.d.ts +40 -0
- package/dist/runtime/feel/rumble.js +58 -0
- package/dist/runtime/feel/sound.d.ts +96 -0
- package/dist/runtime/feel/sound.js +176 -0
- package/dist/runtime/feel/spring.d.ts +42 -0
- package/dist/runtime/feel/spring.js +95 -0
- package/dist/runtime/feel/trauma.d.ts +48 -0
- package/dist/runtime/feel/trauma.js +76 -0
- package/dist/runtime/feel/tween.d.ts +66 -0
- package/dist/runtime/feel/tween.js +180 -0
- package/dist/runtime/feel/types.d.ts +30 -0
- package/dist/runtime/feel/types.js +5 -0
- package/dist/runtime/index.d.ts +28 -0
- package/dist/runtime/index.js +26 -0
- package/dist/runtime/input.d.ts +38 -0
- package/dist/runtime/input.js +159 -0
- package/dist/runtime/interpolate.d.ts +6 -0
- package/dist/runtime/interpolate.js +13 -0
- package/dist/runtime/render/animation.d.ts +63 -0
- package/dist/runtime/render/animation.js +128 -0
- package/dist/runtime/render/atmosphere.d.ts +32 -0
- package/dist/runtime/render/atmosphere.js +59 -0
- package/dist/runtime/render/crowd-motion.d.ts +68 -0
- package/dist/runtime/render/crowd-motion.js +163 -0
- package/dist/runtime/render/decals.d.ts +64 -0
- package/dist/runtime/render/decals.js +222 -0
- package/dist/runtime/render/highlight.d.ts +29 -0
- package/dist/runtime/render/highlight.js +110 -0
- package/dist/runtime/render/index.d.ts +35 -0
- package/dist/runtime/render/index.js +22 -0
- package/dist/runtime/render/lighting.d.ts +50 -0
- package/dist/runtime/render/lighting.js +142 -0
- package/dist/runtime/render/particles.d.ts +117 -0
- package/dist/runtime/render/particles.js +289 -0
- package/dist/runtime/render/post.d.ts +92 -0
- package/dist/runtime/render/post.js +191 -0
- package/dist/runtime/render/presets.d.ts +57 -0
- package/dist/runtime/render/presets.js +116 -0
- package/dist/runtime/render/quality.d.ts +113 -0
- package/dist/runtime/render/quality.js +180 -0
- package/dist/runtime/render/rig.d.ts +29 -0
- package/dist/runtime/render/rig.js +107 -0
- package/dist/runtime/render/sprite.d.ts +34 -0
- package/dist/runtime/render/sprite.js +58 -0
- package/dist/runtime/render/stack.d.ts +74 -0
- package/dist/runtime/render/stack.js +132 -0
- package/dist/runtime/render/vat.d.ts +78 -0
- package/dist/runtime/render/vat.js +219 -0
- package/dist/runtime/renderer.d.ts +19 -0
- package/dist/runtime/renderer.js +18 -0
- package/dist/runtime/stats.d.ts +11 -0
- package/dist/runtime/stats.js +29 -0
- package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
- package/dist/runtime/styles/handdrawn/ground.js +282 -0
- package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
- package/dist/runtime/styles/handdrawn/index.js +94 -0
- package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
- package/dist/runtime/styles/handdrawn/ink.js +68 -0
- package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
- package/dist/runtime/styles/handdrawn/looks.js +42 -0
- package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
- package/dist/runtime/styles/handdrawn/paint.js +129 -0
- package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
- package/dist/runtime/styles/handdrawn/palette.js +70 -0
- package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
- package/dist/runtime/styles/handdrawn/textures.js +150 -0
- package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
- package/dist/runtime/styles/handdrawn/vfx.js +73 -0
- package/dist/runtime/styles/index.d.ts +9 -0
- package/dist/runtime/styles/index.js +32 -0
- package/dist/runtime/styles/kit.d.ts +193 -0
- package/dist/runtime/styles/kit.js +1 -0
- package/dist/runtime/styles/pixel/index.d.ts +46 -0
- package/dist/runtime/styles/pixel/index.js +167 -0
- package/dist/runtime/styles/ps1/index.d.ts +29 -0
- package/dist/runtime/styles/ps1/index.js +41 -0
- package/dist/runtime/styles/ps1/lights.d.ts +47 -0
- package/dist/runtime/styles/ps1/lights.js +146 -0
- package/dist/runtime/styles/ps1/materials.d.ts +62 -0
- package/dist/runtime/styles/ps1/materials.js +285 -0
- package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
- package/dist/runtime/styles/ps1/pipeline.js +111 -0
- package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
- package/dist/runtime/styles/ps1/surfaces.js +228 -0
- package/dist/runtime/styles/toon/index.d.ts +5 -0
- package/dist/runtime/styles/toon/index.js +178 -0
- package/dist/runtime/styles/toon/ink.d.ts +39 -0
- package/dist/runtime/styles/toon/ink.js +73 -0
- package/dist/runtime/styles/toon/look.d.ts +10 -0
- package/dist/runtime/styles/toon/look.js +112 -0
- package/dist/runtime/styles/toon/material.d.ts +74 -0
- package/dist/runtime/styles/toon/material.js +208 -0
- package/dist/runtime/styles/toon/normals.d.ts +26 -0
- package/dist/runtime/styles/toon/normals.js +127 -0
- package/dist/runtime/styles/toon/shadows.d.ts +24 -0
- package/dist/runtime/styles/toon/shadows.js +42 -0
- package/dist/runtime/styles/toon/vfx.d.ts +15 -0
- package/dist/runtime/styles/toon/vfx.js +47 -0
- package/dist/runtime/styles/view.d.ts +35 -0
- package/dist/runtime/styles/view.js +34 -0
- package/dist/runtime/styles/voxel/core.d.ts +114 -0
- package/dist/runtime/styles/voxel/core.js +577 -0
- package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
- package/dist/runtime/styles/voxel/geometry.js +40 -0
- package/dist/runtime/styles/voxel/index.d.ts +21 -0
- package/dist/runtime/styles/voxel/index.js +23 -0
- package/dist/runtime/styles/voxel/kit.d.ts +6 -0
- package/dist/runtime/styles/voxel/kit.js +11 -0
- package/dist/runtime/styles/voxel/look.d.ts +10 -0
- package/dist/runtime/styles/voxel/look.js +16 -0
- package/dist/runtime/styles/voxel/props.d.ts +9 -0
- package/dist/runtime/styles/voxel/props.js +52 -0
- package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
- package/dist/runtime/styles/voxel/shapes.js +83 -0
- package/dist/runtime/styles/voxel/world.d.ts +49 -0
- package/dist/runtime/styles/voxel/world.js +422 -0
- package/llms.txt +183 -0
- package/package.json +82 -3
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Mesh } from "three/webgpu";
|
|
2
|
+
import type { Node, Object3D, UniformNode } from "three/webgpu";
|
|
3
|
+
import type { ParticleBurstOptions as FeelBurstOptions, ParticleSink } from "../feel/types.js";
|
|
4
|
+
export type { ParticleSink };
|
|
5
|
+
/** The feel kit's burst options plus the renderer's extras (all optional). */
|
|
6
|
+
export interface ParticleBurstOptions extends FeelBurstOptions {
|
|
7
|
+
readonly dirY?: number;
|
|
8
|
+
/** Mean speed, m/s. */
|
|
9
|
+
readonly speed?: number;
|
|
10
|
+
/** Cone spread 0..1 (1 = sphere). */
|
|
11
|
+
readonly spread?: number;
|
|
12
|
+
/** Birth size, m (overrides `scale`). */
|
|
13
|
+
readonly size?: number;
|
|
14
|
+
}
|
|
15
|
+
/** What a particle look (an art style's) reads, per fragment. */
|
|
16
|
+
export interface ParticleShadeInput {
|
|
17
|
+
/** Age over life, 0..1. */
|
|
18
|
+
readonly t: Node<"float">;
|
|
19
|
+
/** Quad uv, 0..1 (v runs along the velocity stretch). */
|
|
20
|
+
readonly uv: Node<"vec2">;
|
|
21
|
+
/** The burst colour (linear; the additive pool carries a bloom boost of 2.2). */
|
|
22
|
+
readonly color: Node<"vec3">;
|
|
23
|
+
/** A stable 0..1 per particle. */
|
|
24
|
+
readonly seed: Node<"float">;
|
|
25
|
+
/** The pool: "add" (sparks, bursts, embers, magic) or "alpha" (smoke, dust). */
|
|
26
|
+
readonly blend: "add" | "alpha";
|
|
27
|
+
}
|
|
28
|
+
export interface ParticleShadeOutput {
|
|
29
|
+
readonly color: Node<"vec3">;
|
|
30
|
+
readonly opacity: Node<"float">;
|
|
31
|
+
/** Blend the pool this way instead of its own ("alpha" draws flat, opaque shapes). */
|
|
32
|
+
readonly blend?: "add" | "alpha";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A particle look: every particle's shape and colour, plus an optional
|
|
36
|
+
* size curve over its life (a pop, a shrink). The default is soft round
|
|
37
|
+
* glows; an art style brings its own (the toon style draws stars, streaks
|
|
38
|
+
* and puffs).
|
|
39
|
+
*/
|
|
40
|
+
export interface ParticleShade {
|
|
41
|
+
shade(p: ParticleShadeInput): ParticleShadeOutput;
|
|
42
|
+
/** Size multiplier over life (vertex stage). */
|
|
43
|
+
size?(t: Node<"float">, seed: Node<"float">, blend: "add" | "alpha"): Node<"float">;
|
|
44
|
+
}
|
|
45
|
+
export type ParticlePresetName = "sparks" | "burst" | "embers" | "smoke" | "hitSpark" | "dust" | "magic" | "kill" | "bigKill";
|
|
46
|
+
export interface ParticlePreset {
|
|
47
|
+
readonly blend: "add" | "alpha";
|
|
48
|
+
readonly count: number;
|
|
49
|
+
/** Speed range, m/s. */
|
|
50
|
+
readonly speed: readonly [number, number];
|
|
51
|
+
/** Cone half-angle around the direction, 0..1 (1 = full sphere). */
|
|
52
|
+
readonly spread: number;
|
|
53
|
+
/** Default direction (unit-ish); the sphere when spread is 1. */
|
|
54
|
+
readonly dir: readonly [number, number, number];
|
|
55
|
+
/** Lifetime range, s. */
|
|
56
|
+
readonly life: readonly [number, number];
|
|
57
|
+
/** Billboard size at birth and death, m. */
|
|
58
|
+
readonly size: readonly [number, number];
|
|
59
|
+
/** Gravity, m/s^2 (negative floats up). */
|
|
60
|
+
readonly gravity: number;
|
|
61
|
+
/** Linear drag, 1/s. */
|
|
62
|
+
readonly drag: number;
|
|
63
|
+
readonly color: number;
|
|
64
|
+
/** Stretch along velocity (0 = round dot). */
|
|
65
|
+
readonly stretch: number;
|
|
66
|
+
/** Spawn position jitter radius, m. */
|
|
67
|
+
readonly jitter: number;
|
|
68
|
+
}
|
|
69
|
+
export declare const PARTICLE_PRESETS: Readonly<Record<ParticlePresetName, ParticlePreset>>;
|
|
70
|
+
/**
|
|
71
|
+
* A ring of `capacity` particle slots. Allocation never iterates live
|
|
72
|
+
* particles: the oldest slot is simply overwritten. Records which slot
|
|
73
|
+
* ranges changed since the last flush so only those bytes are uploaded.
|
|
74
|
+
*/
|
|
75
|
+
export declare class ParticleRing {
|
|
76
|
+
readonly capacity: number;
|
|
77
|
+
head: number;
|
|
78
|
+
private dirty;
|
|
79
|
+
/** Slots written so far (stats). */
|
|
80
|
+
written: number;
|
|
81
|
+
constructor(capacity: number);
|
|
82
|
+
/** Take `n` slots (n is clamped to capacity). Returns the slot indices in write order. */
|
|
83
|
+
alloc(n: number): number[];
|
|
84
|
+
private mark;
|
|
85
|
+
/** Merged dirty slot ranges [start, count], sorted, and clears them. */
|
|
86
|
+
flush(): [number, number][];
|
|
87
|
+
}
|
|
88
|
+
export interface GpuParticlesOptions {
|
|
89
|
+
/** Slots per blend pool (additive and alpha). Default 4096. */
|
|
90
|
+
readonly capacity?: number;
|
|
91
|
+
/** Random source (tests pass a seeded one). */
|
|
92
|
+
readonly random?: () => number;
|
|
93
|
+
/** The particle look (an art style's); default soft round glows. */
|
|
94
|
+
readonly look?: ParticleShade;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Pooled GPU particles: one draw for every additive particle on screen
|
|
98
|
+
* (and one for alpha-blended smoke and dust). The CPU writes only the
|
|
99
|
+
* spawn slots of a burst; the vertex stage computes motion (velocity, drag,
|
|
100
|
+
* gravity, a floor clamp), billboarding, velocity stretch, size and fade.
|
|
101
|
+
*/
|
|
102
|
+
export declare class GpuParticles implements ParticleSink {
|
|
103
|
+
readonly time: UniformNode<"float", number>;
|
|
104
|
+
private readonly pools;
|
|
105
|
+
private readonly rand;
|
|
106
|
+
/** Bursts requested so far (playtest checks). */
|
|
107
|
+
bursts: number;
|
|
108
|
+
constructor(parent: Object3D, opts?: GpuParticlesOptions);
|
|
109
|
+
get meshes(): Mesh[];
|
|
110
|
+
burst(preset: string, x: number, y: number, z: number, opts?: ParticleBurstOptions): void;
|
|
111
|
+
/** Continuous emission helper: call per frame with a rate (particles/s) and dt. */
|
|
112
|
+
emit(preset: string, x: number, y: number, z: number, rate: number, dt: number, opts?: ParticleBurstOptions): void;
|
|
113
|
+
private carry;
|
|
114
|
+
/** Advance the particle clock (seconds) and upload this frame's spawn slots. */
|
|
115
|
+
update(timeSeconds: number): void;
|
|
116
|
+
dispose(): void;
|
|
117
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { AdditiveBlending, DynamicDrawUsage, InstancedBufferAttribute, InstancedBufferGeometry, Mesh, MeshBasicNodeMaterial, NormalBlending, PlaneGeometry, } from "three/webgpu";
|
|
2
|
+
import { cameraProjectionMatrix, clamp, exp, float, hash, instanceIndex, instancedBufferAttribute, length, max, mix, modelViewMatrix, normalize, positionGeometry, pow, smoothstep, step, uniform, uv, varying, vec2, vec3, vec4, } from "three/tsl";
|
|
3
|
+
export const PARTICLE_PRESETS = {
|
|
4
|
+
sparks: { blend: "add", count: 14, speed: [5, 11], spread: 0.55, dir: [0, 1, 0], life: [0.25, 0.5], size: [0.12, 0.02], gravity: 14, drag: 2, color: 0xffc070, stretch: 1.6, jitter: 0.05 },
|
|
5
|
+
burst: { blend: "add", count: 18, speed: [2, 6], spread: 1, dir: [0, 1, 0], life: [0.35, 0.7], size: [0.26, 0.04], gravity: 9, drag: 2.5, color: 0xb59cff, stretch: 0.4, jitter: 0.15 },
|
|
6
|
+
embers: { blend: "add", count: 6, speed: [0.3, 1.2], spread: 0.35, dir: [0, 1, 0], life: [1.2, 2.4], size: [0.1, 0.0], gravity: -0.8, drag: 0.6, color: 0xff9a3c, stretch: 0, jitter: 0.4 },
|
|
7
|
+
smoke: { blend: "alpha", count: 6, speed: [0.4, 1.4], spread: 0.6, dir: [0, 1, 0], life: [0.8, 1.5], size: [0.5, 1.6], gravity: -1.2, drag: 1.5, color: 0x3a3a44, stretch: 0, jitter: 0.25 },
|
|
8
|
+
hitSpark: { blend: "add", count: 8, speed: [6, 12], spread: 0.35, dir: [0, 0.3, 1], life: [0.1, 0.22], size: [0.14, 0.03], gravity: 6, drag: 5, color: 0xfff0c0, stretch: 2.4, jitter: 0.02 },
|
|
9
|
+
dust: { blend: "alpha", count: 8, speed: [0.6, 2], spread: 0.25, dir: [0, 0.15, 0], life: [0.4, 0.8], size: [0.25, 0.7], gravity: 1, drag: 3, color: 0x6b6458, stretch: 0, jitter: 0.2 },
|
|
10
|
+
kill: { blend: "add", count: 10, speed: [2.5, 6], spread: 0.8, dir: [0, 1, 0], life: [0.35, 0.65], size: [0.22, 0.03], gravity: 12, drag: 2, color: 0xb59cff, stretch: 0.6, jitter: 0.15 },
|
|
11
|
+
bigKill: { blend: "add", count: 34, speed: [3, 9], spread: 1, dir: [0, 1, 0], life: [0.5, 1.0], size: [0.38, 0.05], gravity: 9, drag: 1.8, color: 0xffc070, stretch: 0.5, jitter: 0.4 },
|
|
12
|
+
magic: { blend: "add", count: 16, speed: [0.5, 2.5], spread: 1, dir: [0, 1, 0], life: [0.6, 1.2], size: [0.16, 0.02], gravity: -2.5, drag: 1.2, color: 0x7ad7ff, stretch: 0.2, jitter: 0.3 },
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A ring of `capacity` particle slots. Allocation never iterates live
|
|
16
|
+
* particles: the oldest slot is simply overwritten. Records which slot
|
|
17
|
+
* ranges changed since the last flush so only those bytes are uploaded.
|
|
18
|
+
*/
|
|
19
|
+
export class ParticleRing {
|
|
20
|
+
capacity;
|
|
21
|
+
head = 0;
|
|
22
|
+
dirty = [];
|
|
23
|
+
/** Slots written so far (stats). */
|
|
24
|
+
written = 0;
|
|
25
|
+
constructor(capacity) {
|
|
26
|
+
this.capacity = capacity;
|
|
27
|
+
if (!Number.isInteger(capacity) || capacity < 1)
|
|
28
|
+
throw new RangeError("ParticleRing: capacity must be a positive integer");
|
|
29
|
+
}
|
|
30
|
+
/** Take `n` slots (n is clamped to capacity). Returns the slot indices in write order. */
|
|
31
|
+
alloc(n) {
|
|
32
|
+
const count = Math.min(Math.max(0, Math.floor(n)), this.capacity);
|
|
33
|
+
const out = [];
|
|
34
|
+
if (count === 0)
|
|
35
|
+
return out;
|
|
36
|
+
const start = this.head;
|
|
37
|
+
for (let i = 0; i < count; i++)
|
|
38
|
+
out.push((start + i) % this.capacity);
|
|
39
|
+
this.head = (start + count) % this.capacity;
|
|
40
|
+
this.written += count;
|
|
41
|
+
const end = start + count;
|
|
42
|
+
if (end <= this.capacity)
|
|
43
|
+
this.mark(start, count);
|
|
44
|
+
else {
|
|
45
|
+
this.mark(start, this.capacity - start);
|
|
46
|
+
this.mark(0, end - this.capacity);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
mark(start, count) {
|
|
51
|
+
const last = this.dirty[this.dirty.length - 1];
|
|
52
|
+
if (last && last[0] + last[1] === start)
|
|
53
|
+
last[1] += count;
|
|
54
|
+
else
|
|
55
|
+
this.dirty.push([start, count]);
|
|
56
|
+
}
|
|
57
|
+
/** Merged dirty slot ranges [start, count], sorted, and clears them. */
|
|
58
|
+
flush() {
|
|
59
|
+
if (this.dirty.length === 0)
|
|
60
|
+
return [];
|
|
61
|
+
const r = this.dirty.slice().sort((a, b) => a[0] - b[0]);
|
|
62
|
+
const merged = [];
|
|
63
|
+
for (const [s, c] of r) {
|
|
64
|
+
const m = merged[merged.length - 1];
|
|
65
|
+
if (m && s <= m[0] + m[1])
|
|
66
|
+
m[1] = Math.max(m[1], s + c - m[0]);
|
|
67
|
+
else
|
|
68
|
+
merged.push([s, c]);
|
|
69
|
+
}
|
|
70
|
+
this.dirty = [];
|
|
71
|
+
// Overlap cap: never more than the whole buffer.
|
|
72
|
+
return merged.map(([s, c]) => [s, Math.min(c, this.capacity - s)]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
class Pool {
|
|
76
|
+
ring;
|
|
77
|
+
mesh;
|
|
78
|
+
spawn;
|
|
79
|
+
vel;
|
|
80
|
+
params;
|
|
81
|
+
color;
|
|
82
|
+
constructor(capacity, blend, time, look) {
|
|
83
|
+
this.ring = new ParticleRing(capacity);
|
|
84
|
+
const quad = new PlaneGeometry(1, 1);
|
|
85
|
+
const g = new InstancedBufferGeometry();
|
|
86
|
+
g.index = quad.index;
|
|
87
|
+
g.setAttribute("position", quad.getAttribute("position"));
|
|
88
|
+
g.setAttribute("uv", quad.getAttribute("uv"));
|
|
89
|
+
g.instanceCount = capacity;
|
|
90
|
+
const mk = () => {
|
|
91
|
+
const a = new InstancedBufferAttribute(new Float32Array(capacity * 4), 4);
|
|
92
|
+
a.setUsage(DynamicDrawUsage);
|
|
93
|
+
return a;
|
|
94
|
+
};
|
|
95
|
+
this.spawn = mk();
|
|
96
|
+
this.vel = mk();
|
|
97
|
+
this.params = mk();
|
|
98
|
+
this.color = mk();
|
|
99
|
+
// Every slot starts dead: born far in the past with zero life.
|
|
100
|
+
for (let i = 0; i < capacity; i++)
|
|
101
|
+
this.spawn.array[i * 4 + 3] = -1e6;
|
|
102
|
+
const sp = instancedBufferAttribute(this.spawn, "vec4");
|
|
103
|
+
const ve = instancedBufferAttribute(this.vel, "vec4");
|
|
104
|
+
const pa = instancedBufferAttribute(this.params, "vec4");
|
|
105
|
+
const co = instancedBufferAttribute(this.color, "vec4");
|
|
106
|
+
const age = time.sub(sp.w);
|
|
107
|
+
const life = max(ve.w, float(1e-4));
|
|
108
|
+
const t = clamp(age.div(life), 0, 1);
|
|
109
|
+
const alive = step(0, age).mul(step(age, life));
|
|
110
|
+
const k = max(pa.w, float(1e-3));
|
|
111
|
+
const decay = exp(k.negate().mul(age));
|
|
112
|
+
const f = float(1).sub(decay).div(k);
|
|
113
|
+
const g2 = pa.z;
|
|
114
|
+
const p = sp.xyz.add(ve.xyz.mul(f)).add(vec3(0, g2.mul(age).mul(age).mul(-0.5), 0));
|
|
115
|
+
const pc = vec3(p.x, max(p.y, float(0.02)), p.z);
|
|
116
|
+
const seed = hash(instanceIndex);
|
|
117
|
+
const curve = look?.size?.(t, seed, blend);
|
|
118
|
+
const size = (curve ? mix(pa.x, pa.y, t).mul(curve) : mix(pa.x, pa.y, t)).mul(alive);
|
|
119
|
+
const view = modelViewMatrix.mul(vec4(pc, 1));
|
|
120
|
+
const velNow = ve.xyz.mul(decay).add(vec3(0, g2.mul(age).negate(), 0));
|
|
121
|
+
const viewAhead = modelViewMatrix.mul(vec4(pc.add(velNow.mul(0.05)), 1));
|
|
122
|
+
const vv = viewAhead.xy.sub(view.xy).mul(20);
|
|
123
|
+
const vl = length(vv);
|
|
124
|
+
const along = normalize(vv.add(vec2(1e-5, 1e-4)));
|
|
125
|
+
// Right-handed with `along` so the quad keeps front-facing winding (a mirrored basis is back-face culled).
|
|
126
|
+
const perp = vec2(along.y, along.x.negate());
|
|
127
|
+
const stretch = float(1).add(co.w.mul(vl).mul(0.06));
|
|
128
|
+
const corner = positionGeometry.xy;
|
|
129
|
+
const offset = along.mul(corner.y.mul(size).mul(stretch)).add(perp.mul(corner.x.mul(size)));
|
|
130
|
+
const mat = new MeshBasicNodeMaterial();
|
|
131
|
+
mat.vertexNode = cameraProjectionMatrix.mul(vec4(view.xy.add(offset), view.z, 1));
|
|
132
|
+
const vt = varying(t);
|
|
133
|
+
const valive = varying(alive);
|
|
134
|
+
let drawBlend = blend;
|
|
135
|
+
if (look) {
|
|
136
|
+
const out = look.shade({ t: vt, uv: uv(), color: varying(co.xyz), seed: varying(seed), blend });
|
|
137
|
+
mat.colorNode = vec4(out.color, 1);
|
|
138
|
+
mat.opacityNode = out.opacity.mul(valive);
|
|
139
|
+
drawBlend = out.blend ?? blend;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
const d = length(uv().sub(0.5)).mul(2);
|
|
143
|
+
const soft = blend === "add" ? pow(smoothstep(1, 0, d), float(1.6)) : smoothstep(1, 0.2, d);
|
|
144
|
+
const fade = blend === "add" ? float(1).sub(vt.mul(vt)) : float(1).sub(vt).mul(smoothstep(0, 0.15, vt));
|
|
145
|
+
const alpha = soft.mul(fade).mul(valive);
|
|
146
|
+
mat.colorNode = vec4(co.xyz.mul(float(1).sub(vt.mul(0.45))), 1);
|
|
147
|
+
mat.opacityNode = blend === "add" ? alpha : alpha.mul(0.6);
|
|
148
|
+
}
|
|
149
|
+
mat.transparent = true;
|
|
150
|
+
mat.depthWrite = false;
|
|
151
|
+
mat.blending = drawBlend === "add" ? AdditiveBlending : NormalBlending;
|
|
152
|
+
mat.fog = false;
|
|
153
|
+
this.mesh = new Mesh(g, mat);
|
|
154
|
+
this.mesh.frustumCulled = false;
|
|
155
|
+
this.mesh.renderOrder = blend === "add" ? 20 : 10;
|
|
156
|
+
this.mesh.name = `GpuParticles.${blend}`;
|
|
157
|
+
}
|
|
158
|
+
upload() {
|
|
159
|
+
const ranges = this.ring.flush();
|
|
160
|
+
if (ranges.length === 0)
|
|
161
|
+
return;
|
|
162
|
+
for (const a of [this.spawn, this.vel, this.params, this.color]) {
|
|
163
|
+
a.clearUpdateRanges();
|
|
164
|
+
for (const [s, c] of ranges)
|
|
165
|
+
a.addUpdateRange(s * 4, c * 4);
|
|
166
|
+
a.needsUpdate = true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Pooled GPU particles: one draw for every additive particle on screen
|
|
172
|
+
* (and one for alpha-blended smoke and dust). The CPU writes only the
|
|
173
|
+
* spawn slots of a burst; the vertex stage computes motion (velocity, drag,
|
|
174
|
+
* gravity, a floor clamp), billboarding, velocity stretch, size and fade.
|
|
175
|
+
*/
|
|
176
|
+
export class GpuParticles {
|
|
177
|
+
time = uniform(0);
|
|
178
|
+
pools;
|
|
179
|
+
rand;
|
|
180
|
+
/** Bursts requested so far (playtest checks). */
|
|
181
|
+
bursts = 0;
|
|
182
|
+
constructor(parent, opts = {}) {
|
|
183
|
+
const cap = opts.capacity ?? 4096;
|
|
184
|
+
this.rand = opts.random ?? Math.random;
|
|
185
|
+
this.pools = { add: new Pool(cap, "add", this.time, opts.look), alpha: new Pool(Math.max(64, cap >> 2), "alpha", this.time, opts.look) };
|
|
186
|
+
parent.add(this.pools.add.mesh, this.pools.alpha.mesh);
|
|
187
|
+
}
|
|
188
|
+
get meshes() {
|
|
189
|
+
return [this.pools.add.mesh, this.pools.alpha.mesh];
|
|
190
|
+
}
|
|
191
|
+
burst(preset, x, y, z, opts = {}) {
|
|
192
|
+
const p = PARTICLE_PRESETS[preset] ?? PARTICLE_PRESETS.burst;
|
|
193
|
+
this.bursts++;
|
|
194
|
+
// A big kill is a large burst plus a ring of sparks and a puff of smoke.
|
|
195
|
+
if (preset === "bigKill") {
|
|
196
|
+
this.burst("sparks", x, y + 0.3, z, { count: 16, spread: 0.9, ...(opts.scale !== undefined ? { scale: opts.scale } : {}) });
|
|
197
|
+
this.burst("smoke", x, y + 0.2, z, { count: 4 });
|
|
198
|
+
this.bursts -= 2;
|
|
199
|
+
}
|
|
200
|
+
const pool = this.pools[p.blend];
|
|
201
|
+
const slots = pool.ring.alloc(opts.count ?? p.count);
|
|
202
|
+
const r = this.rand;
|
|
203
|
+
let dx = opts.dirX ?? p.dir[0];
|
|
204
|
+
let dy = opts.dirY ?? p.dir[1];
|
|
205
|
+
let dz = opts.dirZ ?? p.dir[2];
|
|
206
|
+
const dl = Math.hypot(dx, dy, dz);
|
|
207
|
+
if (dl > 1e-6) {
|
|
208
|
+
dx /= dl;
|
|
209
|
+
dy /= dl;
|
|
210
|
+
dz /= dl;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
dy = 1;
|
|
214
|
+
}
|
|
215
|
+
const spread = Math.min(1, Math.max(0, opts.spread ?? p.spread));
|
|
216
|
+
const sMul = opts.speed !== undefined ? opts.speed / ((p.speed[0] + p.speed[1]) / 2) : 1;
|
|
217
|
+
const zMul = opts.size !== undefined ? opts.size / p.size[0] : (opts.scale ?? 1);
|
|
218
|
+
const hex = opts.color ?? p.color;
|
|
219
|
+
const cr = ((hex >> 16) & 255) / 255;
|
|
220
|
+
const cg = ((hex >> 8) & 255) / 255;
|
|
221
|
+
const cb = (hex & 255) / 255;
|
|
222
|
+
// Linear-ish color for HDR blending: square approximates sRGB -> linear; additive gets a bloom boost.
|
|
223
|
+
const boost = p.blend === "add" ? 2.2 : 1;
|
|
224
|
+
const t0 = this.time.value;
|
|
225
|
+
const sp = pool.spawn.array;
|
|
226
|
+
const ve = pool.vel.array;
|
|
227
|
+
const pa = pool.params.array;
|
|
228
|
+
const co = pool.color.array;
|
|
229
|
+
for (const s of slots) {
|
|
230
|
+
// A direction inside the cone: random unit vector blended toward dir by (1 - spread).
|
|
231
|
+
let ux = r() * 2 - 1;
|
|
232
|
+
let uy = r() * 2 - 1;
|
|
233
|
+
let uz = r() * 2 - 1;
|
|
234
|
+
const ul = Math.hypot(ux, uy, uz) || 1;
|
|
235
|
+
ux = ux / ul;
|
|
236
|
+
uy = uy / ul;
|
|
237
|
+
uz = uz / ul;
|
|
238
|
+
let vx = dx + (ux - dx) * spread;
|
|
239
|
+
let vy = dy + (uy - dy) * spread;
|
|
240
|
+
let vz = dz + (uz - dz) * spread;
|
|
241
|
+
const vl = Math.hypot(vx, vy, vz) || 1;
|
|
242
|
+
const speed = (p.speed[0] + (p.speed[1] - p.speed[0]) * r()) * sMul;
|
|
243
|
+
vx = (vx / vl) * speed;
|
|
244
|
+
vy = (vy / vl) * speed;
|
|
245
|
+
vz = (vz / vl) * speed;
|
|
246
|
+
const j = p.jitter;
|
|
247
|
+
const o = s * 4;
|
|
248
|
+
sp[o] = x + (r() * 2 - 1) * j;
|
|
249
|
+
sp[o + 1] = y + (r() * 2 - 1) * j * 0.5;
|
|
250
|
+
sp[o + 2] = z + (r() * 2 - 1) * j;
|
|
251
|
+
sp[o + 3] = t0;
|
|
252
|
+
ve[o] = vx;
|
|
253
|
+
ve[o + 1] = vy;
|
|
254
|
+
ve[o + 2] = vz;
|
|
255
|
+
ve[o + 3] = p.life[0] + (p.life[1] - p.life[0]) * r();
|
|
256
|
+
pa[o] = p.size[0] * zMul;
|
|
257
|
+
pa[o + 1] = p.size[1] * zMul;
|
|
258
|
+
pa[o + 2] = p.gravity;
|
|
259
|
+
pa[o + 3] = p.drag;
|
|
260
|
+
const shade = 0.85 + 0.3 * r();
|
|
261
|
+
co[o] = cr * cr * boost * shade;
|
|
262
|
+
co[o + 1] = cg * cg * boost * shade;
|
|
263
|
+
co[o + 2] = cb * cb * boost * shade;
|
|
264
|
+
co[o + 3] = p.stretch;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** Continuous emission helper: call per frame with a rate (particles/s) and dt. */
|
|
268
|
+
emit(preset, x, y, z, rate, dt, opts = {}) {
|
|
269
|
+
const want = rate * dt + this.carry;
|
|
270
|
+
const n = Math.floor(want);
|
|
271
|
+
this.carry = want - n;
|
|
272
|
+
if (n > 0)
|
|
273
|
+
this.burst(preset, x, y, z, { ...opts, count: n });
|
|
274
|
+
}
|
|
275
|
+
carry = 0;
|
|
276
|
+
/** Advance the particle clock (seconds) and upload this frame's spawn slots. */
|
|
277
|
+
update(timeSeconds) {
|
|
278
|
+
this.time.value = timeSeconds;
|
|
279
|
+
this.pools.add.upload();
|
|
280
|
+
this.pools.alpha.upload();
|
|
281
|
+
}
|
|
282
|
+
dispose() {
|
|
283
|
+
for (const m of this.meshes) {
|
|
284
|
+
m.removeFromParent();
|
|
285
|
+
m.geometry.dispose();
|
|
286
|
+
m.material.dispose();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Color, RenderPipeline, Vector3 } from "three/webgpu";
|
|
2
|
+
import type { Camera, Scene, WebGPURenderer } from "three/webgpu";
|
|
3
|
+
import type BloomNode from "three/addons/tsl/display/BloomNode.js";
|
|
4
|
+
import type { GradePreset, LookPreset } from "./presets.js";
|
|
5
|
+
import type { QualityTier } from "./quality.js";
|
|
6
|
+
import type { StylePost } from "../styles/kit.js";
|
|
7
|
+
export interface PostFXOptions {
|
|
8
|
+
readonly preset?: LookPreset;
|
|
9
|
+
readonly overrides?: Partial<GradePreset>;
|
|
10
|
+
/** Bloom on (default true). */
|
|
11
|
+
readonly bloom?: boolean;
|
|
12
|
+
/** Toon outline from depth edges (default false). */
|
|
13
|
+
readonly outline?: boolean;
|
|
14
|
+
/** FXAA on the final image (default true; turn off when the scene pass is multisampled). */
|
|
15
|
+
readonly fxaa?: boolean;
|
|
16
|
+
/** Post-processing at all. Off renders straight to the canvas (still tone mapped once by the renderer). Default true. */
|
|
17
|
+
readonly enabled?: boolean;
|
|
18
|
+
/** Bloom render-target scale. Default 0.5. */
|
|
19
|
+
readonly bloomScale?: number;
|
|
20
|
+
/** An art style's post stages (runtime/styles): scene-pass setup, an HDR stage before bloom, a colour stage before the grade, a display stage after tone mapping. */
|
|
21
|
+
readonly style?: StylePost;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The post stack: scene pass (HDR) -> bloom -> optional depth outline ->
|
|
25
|
+
* color grade (exposure, contrast, saturation, split tone, vignette, hurt
|
|
26
|
+
* pulse) -> tone mapping and sRGB (once, via renderOutput) -> FXAA.
|
|
27
|
+
*
|
|
28
|
+
* Tone mapping is the renderer's (`renderer.toneMapping`), so the same
|
|
29
|
+
* curve applies whether post is on (here, in renderOutput) or off
|
|
30
|
+
* (renderer.render to the canvas). Every knob is a uniform.
|
|
31
|
+
*/
|
|
32
|
+
export declare class PostFX {
|
|
33
|
+
private readonly renderer;
|
|
34
|
+
private readonly scene;
|
|
35
|
+
private readonly camera;
|
|
36
|
+
readonly pipeline: RenderPipeline;
|
|
37
|
+
/** Extra exposure multiplier inside the grade (the preset's exposure is renderer.toneMappingExposure). */
|
|
38
|
+
readonly exposure: import("three/webgpu").UniformNode<"float", number>;
|
|
39
|
+
readonly contrast: import("three/webgpu").UniformNode<"float", number>;
|
|
40
|
+
readonly saturation: import("three/webgpu").UniformNode<"float", number>;
|
|
41
|
+
readonly shadowTint: import("three/webgpu").UniformNode<"vec3", Vector3>;
|
|
42
|
+
readonly highlightTint: import("three/webgpu").UniformNode<"vec3", Vector3>;
|
|
43
|
+
readonly vignette: import("three/webgpu").UniformNode<"float", number>;
|
|
44
|
+
/** 0..1: the game drives this when the player is hurt (red edge pulse). */
|
|
45
|
+
readonly hurt: import("three/webgpu").UniformNode<"float", number>;
|
|
46
|
+
readonly outlineColor: import("three/webgpu").UniformNode<"color", Color>;
|
|
47
|
+
/** Outline thickness in pixels. */
|
|
48
|
+
readonly outlineThickness: import("three/webgpu").UniformNode<"float", number>;
|
|
49
|
+
/** Relative depth jump that counts as an edge. */
|
|
50
|
+
readonly outlineThreshold: import("three/webgpu").UniformNode<"float", number>;
|
|
51
|
+
readonly outlineStrength: import("three/webgpu").UniformNode<"float", number>;
|
|
52
|
+
private readonly texel;
|
|
53
|
+
private readonly near;
|
|
54
|
+
private readonly far;
|
|
55
|
+
private bloomNode;
|
|
56
|
+
private bloomOn;
|
|
57
|
+
private outlineOn;
|
|
58
|
+
private fxaaOn;
|
|
59
|
+
private bloomScale;
|
|
60
|
+
private grade;
|
|
61
|
+
private style;
|
|
62
|
+
enabled: boolean;
|
|
63
|
+
constructor(renderer: WebGPURenderer, scene: Scene, camera: Camera, opts?: PostFXOptions);
|
|
64
|
+
/** Bloom strength, radius and threshold uniforms (null while bloom is off). */
|
|
65
|
+
get bloom(): BloomNode | null;
|
|
66
|
+
setPreset(preset: LookPreset, overrides?: Partial<GradePreset>): void;
|
|
67
|
+
private applyGrade;
|
|
68
|
+
/** Toggle stages; rebuilds the node graph only when something changed. */
|
|
69
|
+
configure(o: {
|
|
70
|
+
bloom?: boolean;
|
|
71
|
+
outline?: boolean;
|
|
72
|
+
fxaa?: boolean;
|
|
73
|
+
bloomScale?: number;
|
|
74
|
+
enabled?: boolean;
|
|
75
|
+
}): void;
|
|
76
|
+
/** Apply a quality tier: low turns post off; medium drops bloom resolution; outline needs medium+. */
|
|
77
|
+
setQuality(tier: QualityTier, outlineWanted?: boolean): void;
|
|
78
|
+
get stages(): {
|
|
79
|
+
enabled: boolean;
|
|
80
|
+
bloom: boolean;
|
|
81
|
+
outline: boolean;
|
|
82
|
+
fxaa: boolean;
|
|
83
|
+
};
|
|
84
|
+
/** Swap the art style's post stages (runtime/styles) and rebuild the graph. */
|
|
85
|
+
setStyle(style: StylePost | null): void;
|
|
86
|
+
private build;
|
|
87
|
+
/** Call when the canvas size changes (CSS pixels times pixel ratio handled inside). */
|
|
88
|
+
setSize(width: number, height: number): void;
|
|
89
|
+
/** Draw the frame: through the stack, or straight to the canvas when disabled. */
|
|
90
|
+
render(): void;
|
|
91
|
+
dispose(): void;
|
|
92
|
+
}
|