@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,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The toon material: banded cel lighting in place of the physical model.
|
|
3
|
+
*
|
|
4
|
+
* - The key light (the sun or moon) lights in three flat bands: shadow,
|
|
5
|
+
* mid and full, with crisp anti-aliased edges. The shadow side keeps
|
|
6
|
+
* only the fill, so its colour comes from the cool ambient light: warm
|
|
7
|
+
* light, cool shadows.
|
|
8
|
+
* - Point lights (lanterns, lamps) light in two hard-edged pools.
|
|
9
|
+
* - A crisp specular blob on glossy things, a rim of light on the lit edge.
|
|
10
|
+
* - Albedo is pushed toward a saturated cartoon palette; textures are
|
|
11
|
+
* posterized into flat patches.
|
|
12
|
+
*
|
|
13
|
+
* The style never replaces a scene's materials: it draws each lit material
|
|
14
|
+
* through a toon twin made here (see index.ts), sharing its colours and
|
|
15
|
+
* copying its node graphs, so crowd motion, VAT animation and hit flashes
|
|
16
|
+
* keep working and the template's own material edits still show.
|
|
17
|
+
*/
|
|
18
|
+
import { MeshStandardNodeMaterial } from "three/webgpu";
|
|
19
|
+
import type { Material, NodeBuilder, PhysicalLightingModel, Texture, UniformNode } from "three/webgpu";
|
|
20
|
+
/** One set of uniforms every toon material reads (tuning moves them all). */
|
|
21
|
+
export interface ToonUniforms {
|
|
22
|
+
/** N.L where the lit side begins (the terminator), and where the full band begins. */
|
|
23
|
+
readonly terminator: UniformNode<"float", number>;
|
|
24
|
+
readonly fullAt: UniformNode<"float", number>;
|
|
25
|
+
/** Key-light level in the shadow band and in the mid band (full = 1). */
|
|
26
|
+
readonly shadowLevel: UniformNode<"float", number>;
|
|
27
|
+
readonly midLevel: UniformNode<"float", number>;
|
|
28
|
+
/** Point-light pool thresholds (light luminance at the surface) and the pools' brightness. */
|
|
29
|
+
readonly poolLow: UniformNode<"float", number>;
|
|
30
|
+
readonly poolHigh: UniformNode<"float", number>;
|
|
31
|
+
readonly poolLevel: UniformNode<"float", number>;
|
|
32
|
+
/** Rim: where it starts (1 - |N.V|) and how bright it is (times albedo). */
|
|
33
|
+
readonly rimStart: UniformNode<"float", number>;
|
|
34
|
+
readonly rimStrength: UniformNode<"float", number>;
|
|
35
|
+
/** Specular blob: N.H threshold and brightness for a fully glossy surface. */
|
|
36
|
+
readonly specAt: UniformNode<"float", number>;
|
|
37
|
+
readonly specStrength: UniformNode<"float", number>;
|
|
38
|
+
/** Albedo saturation multiplier, and how many value steps textures are posterized to. */
|
|
39
|
+
readonly saturation: UniformNode<"float", number>;
|
|
40
|
+
readonly posterize: UniformNode<"float", number>;
|
|
41
|
+
}
|
|
42
|
+
export declare function toonUniforms(): ToonUniforms;
|
|
43
|
+
type Classic = Material & {
|
|
44
|
+
color?: import("three/webgpu").Color;
|
|
45
|
+
emissive?: import("three/webgpu").Color;
|
|
46
|
+
emissiveIntensity?: number;
|
|
47
|
+
map?: Texture | null;
|
|
48
|
+
emissiveMap?: Texture | null;
|
|
49
|
+
alphaMap?: Texture | null;
|
|
50
|
+
roughness?: number;
|
|
51
|
+
metalness?: number;
|
|
52
|
+
flatShading?: boolean;
|
|
53
|
+
fog?: boolean;
|
|
54
|
+
wireframe?: boolean;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* A lit material drawn in the toon style. Physical lighting is swapped
|
|
58
|
+
* for ToonLighting; metals keep their colour (no environment to reflect),
|
|
59
|
+
* their gloss feeds the specular blob.
|
|
60
|
+
*/
|
|
61
|
+
export declare class ToonMaterial extends MeshStandardNodeMaterial {
|
|
62
|
+
private readonly u;
|
|
63
|
+
readonly isToonMaterial = true;
|
|
64
|
+
constructor(u: ToonUniforms);
|
|
65
|
+
setupLightingModel(): PhysicalLightingModel;
|
|
66
|
+
setupDiffuseColor(builder: NodeBuilder): void;
|
|
67
|
+
/** Mirror the source's per-frame state (the template fades, flashes and recolours it). */
|
|
68
|
+
sync(src: Classic): void;
|
|
69
|
+
}
|
|
70
|
+
/** Does this material react to lights (and so gets a toon twin)? */
|
|
71
|
+
export declare function isLit(m: Material): boolean;
|
|
72
|
+
/** The toon twin of a lit material: shared colours, copied state and node graphs. */
|
|
73
|
+
export declare function toonTwin(source: Material, u: ToonUniforms): ToonMaterial;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The toon material: banded cel lighting in place of the physical model.
|
|
3
|
+
*
|
|
4
|
+
* - The key light (the sun or moon) lights in three flat bands: shadow,
|
|
5
|
+
* mid and full, with crisp anti-aliased edges. The shadow side keeps
|
|
6
|
+
* only the fill, so its colour comes from the cool ambient light: warm
|
|
7
|
+
* light, cool shadows.
|
|
8
|
+
* - Point lights (lanterns, lamps) light in two hard-edged pools.
|
|
9
|
+
* - A crisp specular blob on glossy things, a rim of light on the lit edge.
|
|
10
|
+
* - Albedo is pushed toward a saturated cartoon palette; textures are
|
|
11
|
+
* posterized into flat patches.
|
|
12
|
+
*
|
|
13
|
+
* The style never replaces a scene's materials: it draws each lit material
|
|
14
|
+
* through a toon twin made here (see index.ts), sharing its colours and
|
|
15
|
+
* copying its node graphs, so crowd motion, VAT animation and hit flashes
|
|
16
|
+
* keep working and the template's own material edits still show.
|
|
17
|
+
*/
|
|
18
|
+
import { DoubleSide, LightingModel, MeshStandardNodeMaterial } from "three/webgpu";
|
|
19
|
+
import { abs, BRDF_Lambert, diffuseColor, dot, float, fwidth, luminance, max, mix, normalize, normalView, positionViewDirection, roughness, smoothstep, sqrt, floor, uniform, vec3, } from "three/tsl";
|
|
20
|
+
import { NO_NORMAL } from "./ink.js";
|
|
21
|
+
export function toonUniforms() {
|
|
22
|
+
return {
|
|
23
|
+
terminator: uniform(0.0),
|
|
24
|
+
fullAt: uniform(0.42),
|
|
25
|
+
shadowLevel: uniform(0.0),
|
|
26
|
+
midLevel: uniform(0.55),
|
|
27
|
+
poolLow: uniform(0.35),
|
|
28
|
+
poolHigh: uniform(1.6),
|
|
29
|
+
poolLevel: uniform(2.4),
|
|
30
|
+
rimStart: uniform(0.68),
|
|
31
|
+
rimStrength: uniform(0.55),
|
|
32
|
+
specAt: uniform(0.965),
|
|
33
|
+
specStrength: uniform(0.9),
|
|
34
|
+
saturation: uniform(1.25),
|
|
35
|
+
posterize: uniform(5),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** A smooth 0..1 step at `edge` whose width is one pixel of `x`'s change (crisp, never aliased). */
|
|
39
|
+
function crisp(edge, x) {
|
|
40
|
+
const w = max(fwidth(x), float(1e-4));
|
|
41
|
+
return smoothstep(edge.sub(w), edge.add(w), x);
|
|
42
|
+
}
|
|
43
|
+
class ToonLighting extends LightingModel {
|
|
44
|
+
u;
|
|
45
|
+
/** The first directional light seen (gates the rim to the lit side). */
|
|
46
|
+
sunDir = null;
|
|
47
|
+
constructor(u) {
|
|
48
|
+
super();
|
|
49
|
+
this.u = u;
|
|
50
|
+
}
|
|
51
|
+
direct({ lightDirection, lightColor, lightNode, reflectedLight }) {
|
|
52
|
+
const u = this.u;
|
|
53
|
+
const dir = lightDirection;
|
|
54
|
+
const color = lightColor;
|
|
55
|
+
const ndl = dot(normalView, dir);
|
|
56
|
+
const albedo = BRDF_Lambert({ diffuseColor: diffuseColor.rgb });
|
|
57
|
+
const light = lightNode.light;
|
|
58
|
+
if (light && light.isDirectionalLight === true) {
|
|
59
|
+
this.sunDir ??= dir;
|
|
60
|
+
const lit = crisp(u.terminator, ndl);
|
|
61
|
+
const full = crisp(u.fullAt, ndl);
|
|
62
|
+
const band = u.shadowLevel.add(u.midLevel.sub(u.shadowLevel).mul(lit)).add(float(1).sub(u.midLevel).mul(full));
|
|
63
|
+
reflectedLight.directDiffuse.addAssign(color.mul(band).mul(albedo));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// Pools: the light's strength at the surface (half-Lambert wrap), cut into two flat steps.
|
|
67
|
+
const strength = luminance(color).mul(ndl.mul(0.5).add(0.5));
|
|
68
|
+
const pool = crisp(u.poolLow, strength).mul(0.3).add(crisp(u.poolHigh, strength).mul(0.7));
|
|
69
|
+
const hue = color.div(max(luminance(color), float(1e-4)));
|
|
70
|
+
reflectedLight.directDiffuse.addAssign(hue.mul(pool).mul(u.poolLevel).mul(albedo));
|
|
71
|
+
}
|
|
72
|
+
// A crisp highlight on glossy surfaces, only on the lit side.
|
|
73
|
+
const h = normalize(dir.add(positionViewDirection));
|
|
74
|
+
const gloss = float(1).sub(roughness).max(0);
|
|
75
|
+
const spec = crisp(u.specAt, dot(normalView, h)).mul(crisp(float(0.05), ndl)).mul(gloss.mul(gloss)).mul(u.specStrength);
|
|
76
|
+
reflectedLight.directSpecular.addAssign(color.mul(spec).mul(0.32));
|
|
77
|
+
}
|
|
78
|
+
indirect(builder) {
|
|
79
|
+
const ctx = builder.context;
|
|
80
|
+
const albedo = BRDF_Lambert({ diffuseColor: diffuseColor.rgb });
|
|
81
|
+
ctx.reflectedLight.indirectDiffuse.addAssign(ctx.irradiance.mul(albedo));
|
|
82
|
+
ctx.reflectedLight.indirectDiffuse.mulAssign(ctx.ambientOcclusion);
|
|
83
|
+
}
|
|
84
|
+
finish(builder) {
|
|
85
|
+
const u = this.u;
|
|
86
|
+
const ctx = builder.context;
|
|
87
|
+
// A thin band of light along the silhouette, on the side the key light reaches.
|
|
88
|
+
const edge = float(1).sub(abs(dot(normalView, positionViewDirection)));
|
|
89
|
+
let rim = crisp(u.rimStart, edge);
|
|
90
|
+
if (this.sunDir)
|
|
91
|
+
rim = rim.mul(smoothstep(-0.15, 0.3, dot(normalView, this.sunDir)));
|
|
92
|
+
ctx.outgoingLight.addAssign(diffuseColor.rgb.mul(rim).mul(u.rimStrength));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A lit material drawn in the toon style. Physical lighting is swapped
|
|
97
|
+
* for ToonLighting; metals keep their colour (no environment to reflect),
|
|
98
|
+
* their gloss feeds the specular blob.
|
|
99
|
+
*/
|
|
100
|
+
export class ToonMaterial extends MeshStandardNodeMaterial {
|
|
101
|
+
u;
|
|
102
|
+
isToonMaterial = true;
|
|
103
|
+
constructor(u) {
|
|
104
|
+
super();
|
|
105
|
+
this.u = u;
|
|
106
|
+
}
|
|
107
|
+
setupLightingModel() {
|
|
108
|
+
return new ToonLighting(this.u);
|
|
109
|
+
}
|
|
110
|
+
setupDiffuseColor(builder) {
|
|
111
|
+
super.setupDiffuseColor(builder);
|
|
112
|
+
const c = diffuseColor.rgb;
|
|
113
|
+
const l = luminance(c);
|
|
114
|
+
let out = mix(vec3(l), c, this.u.saturation);
|
|
115
|
+
if (this.map) {
|
|
116
|
+
// Textures turn into flat patches: value posterized in a perceptual space.
|
|
117
|
+
const v = sqrt(max(l, float(1e-5)));
|
|
118
|
+
const q = floor(v.mul(this.u.posterize).add(0.5)).div(this.u.posterize).max(0.04);
|
|
119
|
+
out = out.mul(q.mul(q).div(max(l, float(1e-5))));
|
|
120
|
+
}
|
|
121
|
+
diffuseColor.rgb.assign(out);
|
|
122
|
+
}
|
|
123
|
+
/** Mirror the source's per-frame state (the template fades, flashes and recolours it). */
|
|
124
|
+
sync(src) {
|
|
125
|
+
if (this.opacity !== src.opacity)
|
|
126
|
+
this.opacity = src.opacity;
|
|
127
|
+
if (src.emissiveIntensity !== undefined && this.emissiveIntensity !== src.emissiveIntensity)
|
|
128
|
+
this.emissiveIntensity = src.emissiveIntensity;
|
|
129
|
+
if (this.visible !== src.visible)
|
|
130
|
+
this.visible = src.visible;
|
|
131
|
+
if (this.transparent !== src.transparent || this.depthWrite !== src.depthWrite || this.side !== src.side || this.alphaTest !== src.alphaTest) {
|
|
132
|
+
this.transparent = src.transparent;
|
|
133
|
+
this.depthWrite = src.depthWrite;
|
|
134
|
+
this.side = src.side;
|
|
135
|
+
this.alphaTest = src.alphaTest;
|
|
136
|
+
// A faded (see-through) surface leaves the ink pass's normals alone, so it is not inked over what it shows.
|
|
137
|
+
const self = this;
|
|
138
|
+
const own = src.mrtNode ?? null;
|
|
139
|
+
self.mrtNode = src.transparent && !own ? NO_NORMAL() : own;
|
|
140
|
+
this.needsUpdate = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Does this material react to lights (and so gets a toon twin)? */
|
|
145
|
+
export function isLit(m) {
|
|
146
|
+
const t = m;
|
|
147
|
+
if (t.isToonMaterial)
|
|
148
|
+
return false;
|
|
149
|
+
if (t.isMeshStandardMaterial || t.isMeshPhongMaterial || t.isMeshLambertMaterial || t.isMeshToonMaterial)
|
|
150
|
+
return true;
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
/** The toon twin of a lit material: shared colours, copied state and node graphs. */
|
|
154
|
+
export function toonTwin(source, u) {
|
|
155
|
+
const src = source;
|
|
156
|
+
const m = new ToonMaterial(u);
|
|
157
|
+
m.name = `toon:${source.name}`;
|
|
158
|
+
// Shared colour objects: the template's own edits (tints, flashes) show through.
|
|
159
|
+
if (src.color)
|
|
160
|
+
m.color = src.color;
|
|
161
|
+
if (src.emissive)
|
|
162
|
+
m.emissive = src.emissive;
|
|
163
|
+
m.emissiveIntensity = src.emissiveIntensity ?? 1;
|
|
164
|
+
m.map = src.map ?? null;
|
|
165
|
+
m.emissiveMap = src.emissiveMap ?? null;
|
|
166
|
+
m.alphaMap = src.alphaMap ?? null;
|
|
167
|
+
m.vertexColors = source.vertexColors;
|
|
168
|
+
m.side = source.side === DoubleSide ? DoubleSide : source.side;
|
|
169
|
+
m.transparent = source.transparent;
|
|
170
|
+
m.opacity = source.opacity;
|
|
171
|
+
m.alphaTest = source.alphaTest;
|
|
172
|
+
m.depthWrite = source.depthWrite;
|
|
173
|
+
m.depthTest = source.depthTest;
|
|
174
|
+
m.blending = source.blending;
|
|
175
|
+
m.polygonOffset = source.polygonOffset;
|
|
176
|
+
m.polygonOffsetFactor = source.polygonOffsetFactor;
|
|
177
|
+
m.polygonOffsetUnits = source.polygonOffsetUnits;
|
|
178
|
+
m.visible = source.visible;
|
|
179
|
+
m.colorWrite = source.colorWrite;
|
|
180
|
+
m.fog = src.fog ?? true;
|
|
181
|
+
m.wireframe = src.wireframe ?? false;
|
|
182
|
+
// Smooth normals band cleanly (index.ts welds them with a crease angle).
|
|
183
|
+
m.flatShading = false;
|
|
184
|
+
// Metals keep their colour as albedo; gloss drives the highlight.
|
|
185
|
+
const metal = src.metalness ?? 0;
|
|
186
|
+
m.metalness = 0;
|
|
187
|
+
m.roughness = Math.min(1, Math.max(0, (src.roughness ?? 0.8) * (1 - metal * 0.6)));
|
|
188
|
+
// Node graphs (crowd motion, VAT, hit flashes) carry over as they are.
|
|
189
|
+
const s = source;
|
|
190
|
+
const d = m;
|
|
191
|
+
if (s.positionNode)
|
|
192
|
+
d.positionNode = s.positionNode;
|
|
193
|
+
if (s.normalNode)
|
|
194
|
+
d.normalNode = s.normalNode;
|
|
195
|
+
if (s.colorNode)
|
|
196
|
+
d.colorNode = s.colorNode;
|
|
197
|
+
if (s.opacityNode)
|
|
198
|
+
d.opacityNode = s.opacityNode;
|
|
199
|
+
if (s.emissiveNode)
|
|
200
|
+
d.emissiveNode = s.emissiveNode;
|
|
201
|
+
if (s.castShadowPositionNode)
|
|
202
|
+
d.castShadowPositionNode = s.castShadowPositionNode;
|
|
203
|
+
if (s.mrtNode)
|
|
204
|
+
d.mrtNode = s.mrtNode;
|
|
205
|
+
else if (source.transparent)
|
|
206
|
+
d.mrtNode = NO_NORMAL();
|
|
207
|
+
return m;
|
|
208
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smooth normals with a crease angle ("auto smooth"), written in place.
|
|
3
|
+
*
|
|
4
|
+
* Low-poly models ship faceted: every triangle has its own normal, so a
|
|
5
|
+
* banded light cuts them into facets and they read as low-poly with an
|
|
6
|
+
* outline. Cel shading wants smooth curves that the bands slice cleanly,
|
|
7
|
+
* with real corners kept sharp. For every vertex, this averages the
|
|
8
|
+
* (area-weighted) normals of the faces that meet at its position and lie
|
|
9
|
+
* within `creaseDegrees` of the vertex's own normal (its face's, for a
|
|
10
|
+
* faceted model). Topology, indices, colours and skin weights are untouched,
|
|
11
|
+
* so skinned characters, instanced crowds and VAT bakes keep working.
|
|
12
|
+
*
|
|
13
|
+
* Pure array math: node-testable.
|
|
14
|
+
*/
|
|
15
|
+
export interface SmoothOptions {
|
|
16
|
+
/** Faces meeting at more than this angle keep a hard edge. Default 62. */
|
|
17
|
+
readonly creaseDegrees?: number;
|
|
18
|
+
/** Positions closer than this are the same point (welds split vertices). Default 1e-4. */
|
|
19
|
+
readonly tolerance?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Recompute `normals` (xyz per vertex) from `positions` and optional
|
|
23
|
+
* triangle `indices`. `normals` is both the reference (each vertex's own
|
|
24
|
+
* normal decides which neighbours it blends with) and the output.
|
|
25
|
+
*/
|
|
26
|
+
export declare function smoothNormalsInPlace(positions: ArrayLike<number>, normals: Float32Array | number[], indices: ArrayLike<number> | null, opts?: SmoothOptions): void;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smooth normals with a crease angle ("auto smooth"), written in place.
|
|
3
|
+
*
|
|
4
|
+
* Low-poly models ship faceted: every triangle has its own normal, so a
|
|
5
|
+
* banded light cuts them into facets and they read as low-poly with an
|
|
6
|
+
* outline. Cel shading wants smooth curves that the bands slice cleanly,
|
|
7
|
+
* with real corners kept sharp. For every vertex, this averages the
|
|
8
|
+
* (area-weighted) normals of the faces that meet at its position and lie
|
|
9
|
+
* within `creaseDegrees` of the vertex's own normal (its face's, for a
|
|
10
|
+
* faceted model). Topology, indices, colours and skin weights are untouched,
|
|
11
|
+
* so skinned characters, instanced crowds and VAT bakes keep working.
|
|
12
|
+
*
|
|
13
|
+
* Pure array math: node-testable.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Recompute `normals` (xyz per vertex) from `positions` and optional
|
|
17
|
+
* triangle `indices`. `normals` is both the reference (each vertex's own
|
|
18
|
+
* normal decides which neighbours it blends with) and the output.
|
|
19
|
+
*/
|
|
20
|
+
export function smoothNormalsInPlace(positions, normals, indices, opts = {}) {
|
|
21
|
+
const vertexCount = Math.floor(positions.length / 3);
|
|
22
|
+
const triCount = Math.floor((indices ? indices.length : vertexCount) / 3);
|
|
23
|
+
if (triCount === 0)
|
|
24
|
+
return;
|
|
25
|
+
const cosCrease = Math.cos(((opts.creaseDegrees ?? 62) * Math.PI) / 180);
|
|
26
|
+
const tol = opts.tolerance ?? 1e-4;
|
|
27
|
+
const at = (t, k) => (indices ? (indices[t * 3 + k] ?? 0) : t * 3 + k);
|
|
28
|
+
// Area-weighted face normals.
|
|
29
|
+
const face = new Float32Array(triCount * 3);
|
|
30
|
+
for (let t = 0; t < triCount; t++) {
|
|
31
|
+
const a = at(t, 0) * 3;
|
|
32
|
+
const b = at(t, 1) * 3;
|
|
33
|
+
const c = at(t, 2) * 3;
|
|
34
|
+
const ux = (positions[b] ?? 0) - (positions[a] ?? 0);
|
|
35
|
+
const uy = (positions[b + 1] ?? 0) - (positions[a + 1] ?? 0);
|
|
36
|
+
const uz = (positions[b + 2] ?? 0) - (positions[a + 2] ?? 0);
|
|
37
|
+
const vx = (positions[c] ?? 0) - (positions[a] ?? 0);
|
|
38
|
+
const vy = (positions[c + 1] ?? 0) - (positions[a + 1] ?? 0);
|
|
39
|
+
const vz = (positions[c + 2] ?? 0) - (positions[a + 2] ?? 0);
|
|
40
|
+
face[t * 3] = uy * vz - uz * vy;
|
|
41
|
+
face[t * 3 + 1] = uz * vx - ux * vz;
|
|
42
|
+
face[t * 3 + 2] = ux * vy - uy * vx;
|
|
43
|
+
}
|
|
44
|
+
// Faces around each welded position.
|
|
45
|
+
const key = (v) => `${Math.round((positions[v * 3] ?? 0) / tol)},${Math.round((positions[v * 3 + 1] ?? 0) / tol)},${Math.round((positions[v * 3 + 2] ?? 0) / tol)}`;
|
|
46
|
+
const facesAt = new Map();
|
|
47
|
+
const keys = new Array(vertexCount);
|
|
48
|
+
for (let v = 0; v < vertexCount; v++)
|
|
49
|
+
keys[v] = key(v);
|
|
50
|
+
for (let t = 0; t < triCount; t++) {
|
|
51
|
+
for (let k = 0; k < 3; k++) {
|
|
52
|
+
const v = at(t, k);
|
|
53
|
+
const kk = keys[v] ?? "";
|
|
54
|
+
let list = facesAt.get(kk);
|
|
55
|
+
if (!list) {
|
|
56
|
+
list = [];
|
|
57
|
+
facesAt.set(kk, list);
|
|
58
|
+
}
|
|
59
|
+
list.push(t);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// The reference normal of each vertex: its own (authored) normal, else the face it belongs to.
|
|
63
|
+
const ref = new Float32Array(vertexCount * 3);
|
|
64
|
+
for (let v = 0; v < vertexCount; v++) {
|
|
65
|
+
let nx = normals[v * 3] ?? 0;
|
|
66
|
+
let ny = normals[v * 3 + 1] ?? 0;
|
|
67
|
+
let nz = normals[v * 3 + 2] ?? 0;
|
|
68
|
+
const l = Math.hypot(nx, ny, nz);
|
|
69
|
+
if (l > 1e-8) {
|
|
70
|
+
ref[v * 3] = nx / l;
|
|
71
|
+
ref[v * 3 + 1] = ny / l;
|
|
72
|
+
ref[v * 3 + 2] = nz / l;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
ref[v * 3] = Number.NaN;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!indices) {
|
|
79
|
+
// Non-indexed: a vertex with no authored normal takes its face's.
|
|
80
|
+
for (let t = 0; t < triCount; t++) {
|
|
81
|
+
const fx = face[t * 3] ?? 0;
|
|
82
|
+
const fy = face[t * 3 + 1] ?? 0;
|
|
83
|
+
const fz = face[t * 3 + 2] ?? 0;
|
|
84
|
+
const l = Math.hypot(fx, fy, fz) || 1;
|
|
85
|
+
for (let k = 0; k < 3; k++) {
|
|
86
|
+
const v = t * 3 + k;
|
|
87
|
+
if (Number.isNaN(ref[v * 3] ?? 0)) {
|
|
88
|
+
ref[v * 3] = fx / l;
|
|
89
|
+
ref[v * 3 + 1] = fy / l;
|
|
90
|
+
ref[v * 3 + 2] = fz / l;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (let v = 0; v < vertexCount; v++) {
|
|
96
|
+
const rx = ref[v * 3] ?? 0;
|
|
97
|
+
const ry = ref[v * 3 + 1] ?? 0;
|
|
98
|
+
const rz = ref[v * 3 + 2] ?? 0;
|
|
99
|
+
if (Number.isNaN(rx))
|
|
100
|
+
continue;
|
|
101
|
+
const list = facesAt.get(keys[v] ?? "");
|
|
102
|
+
if (!list)
|
|
103
|
+
continue;
|
|
104
|
+
let sx = 0;
|
|
105
|
+
let sy = 0;
|
|
106
|
+
let sz = 0;
|
|
107
|
+
for (const t of list) {
|
|
108
|
+
const fx = face[t * 3] ?? 0;
|
|
109
|
+
const fy = face[t * 3 + 1] ?? 0;
|
|
110
|
+
const fz = face[t * 3 + 2] ?? 0;
|
|
111
|
+
const l = Math.hypot(fx, fy, fz);
|
|
112
|
+
if (l < 1e-12)
|
|
113
|
+
continue;
|
|
114
|
+
if ((fx * rx + fy * ry + fz * rz) / l < cosCrease)
|
|
115
|
+
continue;
|
|
116
|
+
sx += fx;
|
|
117
|
+
sy += fy;
|
|
118
|
+
sz += fz;
|
|
119
|
+
}
|
|
120
|
+
const l = Math.hypot(sx, sy, sz);
|
|
121
|
+
if (l < 1e-12)
|
|
122
|
+
continue;
|
|
123
|
+
normals[v * 3] = sx / l;
|
|
124
|
+
normals[v * 3 + 1] = sy / l;
|
|
125
|
+
normals[v * 3 + 2] = sz / l;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cartoon ground shadows: every shadow caster drawn a second time,
|
|
3
|
+
* flattened onto the floor along the key light, in one flat colour.
|
|
4
|
+
*
|
|
5
|
+
* The flattened copy reuses the caster's own vertex stage (crowd motion,
|
|
6
|
+
* VAT frames, skinning, instancing), so 2,000 enemies cast 2,000 matching
|
|
7
|
+
* shadows with no extra CPU work and no shadow map. Overlaps never darken
|
|
8
|
+
* twice: each shadow writes depth just above the floor and tests "less
|
|
9
|
+
* than", so the second layer of the same shadow fails the test. They are
|
|
10
|
+
* drawn after the opaque scene (the floor is already there) and before
|
|
11
|
+
* effects.
|
|
12
|
+
*/
|
|
13
|
+
import { MeshBasicNodeMaterial, Vector3 } from "three/webgpu";
|
|
14
|
+
import type { Color, Material, UniformNode } from "three/webgpu";
|
|
15
|
+
export interface ShadowUniforms {
|
|
16
|
+
/** Direction light travels from (unit, y > 0). */
|
|
17
|
+
readonly dir: UniformNode<"vec3", Vector3>;
|
|
18
|
+
readonly floor: UniformNode<"float", number>;
|
|
19
|
+
readonly color: UniformNode<"color", Color>;
|
|
20
|
+
readonly opacity: UniformNode<"float", number>;
|
|
21
|
+
}
|
|
22
|
+
export declare function shadowUniforms(color: Color): ShadowUniforms;
|
|
23
|
+
/** The flattened twin of a caster's material. */
|
|
24
|
+
export declare function shadowTwin(source: Material, u: ShadowUniforms): MeshBasicNodeMaterial;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cartoon ground shadows: every shadow caster drawn a second time,
|
|
3
|
+
* flattened onto the floor along the key light, in one flat colour.
|
|
4
|
+
*
|
|
5
|
+
* The flattened copy reuses the caster's own vertex stage (crowd motion,
|
|
6
|
+
* VAT frames, skinning, instancing), so 2,000 enemies cast 2,000 matching
|
|
7
|
+
* shadows with no extra CPU work and no shadow map. Overlaps never darken
|
|
8
|
+
* twice: each shadow writes depth just above the floor and tests "less
|
|
9
|
+
* than", so the second layer of the same shadow fails the test. They are
|
|
10
|
+
* drawn after the opaque scene (the floor is already there) and before
|
|
11
|
+
* effects.
|
|
12
|
+
*/
|
|
13
|
+
import { DoubleSide, LessDepth, MeshBasicNodeMaterial, Vector3 } from "three/webgpu";
|
|
14
|
+
import { cameraProjectionMatrix, cameraViewMatrix, float, max, modelWorldMatrix, mrt, positionLocal, uniform, vec3, vec4 } from "three/tsl";
|
|
15
|
+
export function shadowUniforms(color) {
|
|
16
|
+
return { dir: uniform(new Vector3(-0.45, 0.85, 0.3).normalize()), floor: uniform(0), color: uniform(color), opacity: uniform(0.34) };
|
|
17
|
+
}
|
|
18
|
+
/** The flattened twin of a caster's material. */
|
|
19
|
+
export function shadowTwin(source, u) {
|
|
20
|
+
const m = new MeshBasicNodeMaterial();
|
|
21
|
+
m.name = `toon-shadow:${source.name}`;
|
|
22
|
+
const pn = source.positionNode;
|
|
23
|
+
if (pn)
|
|
24
|
+
m.positionNode = pn;
|
|
25
|
+
const world = modelWorldMatrix.mul(vec4(positionLocal, 1)).xyz;
|
|
26
|
+
const h = max(world.y.sub(u.floor), float(0));
|
|
27
|
+
// Steep sun: never stretch a shadow past about two heights.
|
|
28
|
+
const dy = max(u.dir.y, float(0.35));
|
|
29
|
+
const flat = vec3(world.x.sub(u.dir.x.div(dy).mul(h)), u.floor.add(0.012), world.z.sub(u.dir.z.div(dy).mul(h)));
|
|
30
|
+
m.vertexNode = cameraProjectionMatrix.mul(cameraViewMatrix).mul(vec4(flat, 1));
|
|
31
|
+
m.colorNode = vec4(vec3(u.color), 1);
|
|
32
|
+
m.opacityNode = u.opacity;
|
|
33
|
+
m.transparent = true;
|
|
34
|
+
m.depthWrite = true;
|
|
35
|
+
m.depthFunc = LessDepth;
|
|
36
|
+
m.side = DoubleSide;
|
|
37
|
+
m.forceSinglePass = true;
|
|
38
|
+
m.fog = true;
|
|
39
|
+
// Shadows leave the ink pass's normals alone.
|
|
40
|
+
m.mrtNode = mrt({ normal: vec4(0, 0, 0, 0) });
|
|
41
|
+
return m;
|
|
42
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cartoon effects for the GPU particles: flat shapes, not glows.
|
|
3
|
+
*
|
|
4
|
+
* - Sparks, bursts, kills and magic: five-point stars and pointed streaks
|
|
5
|
+
* (a diamond stretched along the velocity reads as a speed line), flat
|
|
6
|
+
* colour with a white-hot core, popping in and snapping out.
|
|
7
|
+
* - Smoke and dust: round puffs with a lumpy edge, a light top and a shaded
|
|
8
|
+
* bottom, an ink rim, that swell and then shrink away.
|
|
9
|
+
*
|
|
10
|
+
* Shapes are drawn in the fragment stage with one-pixel anti-aliasing;
|
|
11
|
+
* there are no textures.
|
|
12
|
+
*/
|
|
13
|
+
import type { Color, UniformNode } from "three/webgpu";
|
|
14
|
+
import type { ParticleShade } from "../../render/particles.js";
|
|
15
|
+
export declare function toonParticles(ink: UniformNode<"color", Color>): ParticleShade;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { abs, atan, cos, dot, float, fwidth, length, max, mix, pow, select, sin, smoothstep, vec2, vec3 } from "three/tsl";
|
|
2
|
+
/** 1 inside (d > 0), 0 outside, with a one-pixel soft edge. */
|
|
3
|
+
function inside(d) {
|
|
4
|
+
const w = max(fwidth(d), float(1e-4));
|
|
5
|
+
return smoothstep(w.negate(), w, d);
|
|
6
|
+
}
|
|
7
|
+
export function toonParticles(ink) {
|
|
8
|
+
const inkRgb = ink;
|
|
9
|
+
return {
|
|
10
|
+
size(t, seed, blend) {
|
|
11
|
+
if (blend === "add") {
|
|
12
|
+
// Pop in past full size, settle, then the preset's own shrink takes it away.
|
|
13
|
+
const pop = smoothstep(0, 0.07, t).mul(0.75).add(0.35);
|
|
14
|
+
const settle = float(1).add(smoothstep(0.05, 0.2, t).oneMinus().mul(smoothstep(0, 0.05, t)).mul(0.35));
|
|
15
|
+
return pop.mul(settle).mul(seed.mul(0.5).add(0.85));
|
|
16
|
+
}
|
|
17
|
+
// Puffs swell out, then shrink away instead of fading.
|
|
18
|
+
return mix(float(0.45), float(1.3), smoothstep(0, 0.3, t)).mul(float(1).sub(smoothstep(0.55, 1, t).mul(0.75)));
|
|
19
|
+
},
|
|
20
|
+
shade({ t, uv, color, seed, blend }) {
|
|
21
|
+
const p = uv.sub(0.5).mul(2);
|
|
22
|
+
const r = length(p);
|
|
23
|
+
const ang = atan(p.y, p.x);
|
|
24
|
+
if (blend === "add") {
|
|
25
|
+
const base = color.div(2.2);
|
|
26
|
+
// A five-point star, turned by the seed.
|
|
27
|
+
const lobes = pow(cos(ang.mul(5).add(seed.mul(6.283))).mul(0.5).add(0.5), float(2.2));
|
|
28
|
+
const star = inside(mix(float(0.42), float(0.98), lobes).sub(r));
|
|
29
|
+
// A pointed streak: a diamond, long along the velocity.
|
|
30
|
+
const streak = inside(float(0.95).sub(abs(p.x).mul(1.7).add(abs(p.y))));
|
|
31
|
+
const shape = select(seed.lessThan(0.32), star, streak);
|
|
32
|
+
const core = smoothstep(0.55, 0.05, r).mul(0.75);
|
|
33
|
+
const c = mix(base.mul(1.35), vec3(1, 0.98, 0.92), core);
|
|
34
|
+
return { color: c, opacity: shape.mul(float(1).sub(smoothstep(0.82, 1, t))), blend: "alpha" };
|
|
35
|
+
}
|
|
36
|
+
// A puff: lumpy round edge, light from the top left, ink rim.
|
|
37
|
+
const edge = float(0.9).add(sin(ang.mul(5).add(seed.mul(6.283))).mul(0.07));
|
|
38
|
+
const body = inside(edge.sub(r));
|
|
39
|
+
const light = mix(color, vec3(0.93, 0.9, 0.96), 0.6);
|
|
40
|
+
const lower = smoothstep(-0.1, 0.55, dot(p, vec2(0.55, -0.75)));
|
|
41
|
+
const shaded = mix(light, light.mul(vec3(0.7, 0.68, 0.82)), lower);
|
|
42
|
+
const rim = smoothstep(edge.sub(0.24), edge.sub(0.1), r);
|
|
43
|
+
const c = mix(shaded, inkRgb, rim.mul(0.8));
|
|
44
|
+
return { color: c, opacity: body.mul(float(1).sub(smoothstep(0.8, 1, t))).mul(0.95) };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An art style for a template that draws without the render stack (its
|
|
3
|
+
* own lights and `renderer.render`): the style's session, post stages and
|
|
4
|
+
* grade on that scene, in one call. Templates on `createRenderStack` pass
|
|
5
|
+
* `style` to it instead.
|
|
6
|
+
*
|
|
7
|
+
* const view = createStyledView(handle, scene, camera, "handdrawn");
|
|
8
|
+
* view?.setSize(w, h);
|
|
9
|
+
* view ? view.render() : renderer.render(scene, camera);
|
|
10
|
+
*/
|
|
11
|
+
import type { Camera, Scene } from "three/webgpu";
|
|
12
|
+
import type { RendererHandle } from "../renderer.js";
|
|
13
|
+
import { PostFX } from "../render/post.js";
|
|
14
|
+
import type { LookPreset } from "../render/presets.js";
|
|
15
|
+
import type { QualityTier } from "../render/quality.js";
|
|
16
|
+
import type { StyleKit, StyleSession } from "./kit.js";
|
|
17
|
+
export interface StyledView {
|
|
18
|
+
readonly session: StyleSession;
|
|
19
|
+
readonly post: PostFX;
|
|
20
|
+
/** Draw a frame (also advances the style's clock). */
|
|
21
|
+
render(nowMs?: number): void;
|
|
22
|
+
/** Canvas size in CSS pixels (after renderer.setSize). */
|
|
23
|
+
setSize(width: number, height: number): void;
|
|
24
|
+
dispose(): void;
|
|
25
|
+
}
|
|
26
|
+
export interface StyledViewOptions {
|
|
27
|
+
/** Which of the style's grades to use (the scene keeps its own lights). Default "night". */
|
|
28
|
+
readonly preset?: LookPreset;
|
|
29
|
+
/** Bloom on (default: the style's choice). */
|
|
30
|
+
readonly bloom?: boolean;
|
|
31
|
+
/** Quality tier the session tunes for. Default "high". */
|
|
32
|
+
readonly tier?: QualityTier;
|
|
33
|
+
}
|
|
34
|
+
/** The style's view of a scene, or null when the style has no session (lowpoly, unknown, or a kit with world builders only). */
|
|
35
|
+
export declare function createStyledView(handle: RendererHandle, scene: Scene, camera: Camera, style: string | StyleKit | null | undefined, opts?: StyledViewOptions): StyledView | null;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { PostFX } from "../render/post.js";
|
|
2
|
+
import { styleKit } from "./index.js";
|
|
3
|
+
/** The style's view of a scene, or null when the style has no session (lowpoly, unknown, or a kit with world builders only). */
|
|
4
|
+
export function createStyledView(handle, scene, camera, style, opts = {}) {
|
|
5
|
+
const kit = typeof style === "string" || style === null || style === undefined ? styleKit(style) : style;
|
|
6
|
+
if (!kit?.attach)
|
|
7
|
+
return null;
|
|
8
|
+
const preset = opts.preset ?? "night";
|
|
9
|
+
const grade = kit.look?.(preset).grade;
|
|
10
|
+
const post = new PostFX(handle.renderer, scene, camera, { preset, bloom: opts.bloom ?? kit.bloom ?? true, ...(grade ? { overrides: grade } : {}) });
|
|
11
|
+
const session = kit.attach({ handle, renderer: handle.renderer, scene, camera, tier: opts.tier ?? "high", preset, post });
|
|
12
|
+
if (session.post)
|
|
13
|
+
post.setStyle(session.post);
|
|
14
|
+
return {
|
|
15
|
+
session,
|
|
16
|
+
post,
|
|
17
|
+
render(nowMs) {
|
|
18
|
+
session.frame?.((nowMs ?? performance.now()) / 1000);
|
|
19
|
+
if (session.render)
|
|
20
|
+
session.render(() => post.render());
|
|
21
|
+
else
|
|
22
|
+
post.render();
|
|
23
|
+
},
|
|
24
|
+
setSize(width, height) {
|
|
25
|
+
post.setSize(width, height);
|
|
26
|
+
const pr = handle.renderer.getPixelRatio();
|
|
27
|
+
session.setSize?.(width * pr, height * pr);
|
|
28
|
+
},
|
|
29
|
+
dispose() {
|
|
30
|
+
session.dispose();
|
|
31
|
+
post.dispose();
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|