@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,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface textures for the PS1 style: what a template's tagged meshes
|
|
3
|
+
* (`mesh.userData.surface = "ground"`) are textured with. Each role reads a
|
|
4
|
+
* 128 px CC0 texture from the asset library (`style.ps1.surface_<role>`,
|
|
5
|
+
* Screaming Brain Studios' texture packs) and repeats every few metres, so
|
|
6
|
+
* a texel is about a screen block at the templates' camera distance.
|
|
7
|
+
*
|
|
8
|
+
* Until the file arrives (or when the library is not built) the role draws
|
|
9
|
+
* a small procedural texture in its own colours, so the game always draws.
|
|
10
|
+
*/
|
|
11
|
+
import { CanvasTexture, NearestFilter, RepeatWrapping, SRGBColorSpace, TextureLoader } from "three/webgpu";
|
|
12
|
+
/**
|
|
13
|
+
* The roles templates tag. Aliases map the words a template is likely to
|
|
14
|
+
* use onto the same texture (a "floor" in a yard is concrete).
|
|
15
|
+
*/
|
|
16
|
+
export const SURFACES = {
|
|
17
|
+
grass: { id: "surface_grass", metres: 5, base: "#2e4021", speckle: ["#3b5428", "#24331a", "#4a3a26"] },
|
|
18
|
+
dirt: { id: "surface_dirt", metres: 5, base: "#5a3e24", speckle: ["#6e4c2c", "#4a321d", "#7a6448"] },
|
|
19
|
+
stone: { id: "surface_stone", metres: 2.5, base: "#5e5a55", speckle: ["#6d6862", "#4d4944", "#7a746c"] },
|
|
20
|
+
wall: { id: "surface_wall", metres: 2.5, base: "#56524d", speckle: ["#64605a", "#403c38", "#2e2b28"] },
|
|
21
|
+
wood: { id: "surface_wood", metres: 2, base: "#6b4a2b", speckle: ["#7c5733", "#563a21", "#8a6a44"] },
|
|
22
|
+
metal: { id: "surface_metal", metres: 2.5, base: "#4c5560", speckle: ["#5a636e", "#3c444d", "#6c747e"] },
|
|
23
|
+
concrete: { id: "surface_concrete", metres: 4, base: "#5d5f60", speckle: ["#6a6c6d", "#4f5152", "#77797a"] },
|
|
24
|
+
gravel: { id: "surface_gravel", metres: 4, base: "#55534f", speckle: ["#6a6863", "#43413e", "#7c7a74"] },
|
|
25
|
+
plate: { id: "surface_plate", metres: 3, base: "#3c4450", speckle: ["#48515e", "#2f3640", "#56606c"] },
|
|
26
|
+
hazard: { id: "surface_hazard", metres: 1.5, base: "#6b5a1e", speckle: ["#7d6a24", "#2a2a2a", "#8a7630"] },
|
|
27
|
+
};
|
|
28
|
+
const ALIASES = {
|
|
29
|
+
ground: "grass",
|
|
30
|
+
field: "grass",
|
|
31
|
+
floor: "concrete",
|
|
32
|
+
road: "concrete",
|
|
33
|
+
rock: "stone",
|
|
34
|
+
pillar: "stone",
|
|
35
|
+
fence: "metal",
|
|
36
|
+
panel: "plate",
|
|
37
|
+
steel: "metal",
|
|
38
|
+
soil: "dirt",
|
|
39
|
+
sand: "dirt",
|
|
40
|
+
brick: "wall",
|
|
41
|
+
};
|
|
42
|
+
/** The role a tag names, or null. */
|
|
43
|
+
export function surfaceRole(tag) {
|
|
44
|
+
return SURFACES[tag] ?? SURFACES[ALIASES[tag] ?? ""] ?? null;
|
|
45
|
+
}
|
|
46
|
+
function speckled(role, seed) {
|
|
47
|
+
if (typeof document === "undefined")
|
|
48
|
+
return null;
|
|
49
|
+
const c = document.createElement("canvas");
|
|
50
|
+
c.width = c.height = 64;
|
|
51
|
+
const g = c.getContext("2d");
|
|
52
|
+
if (!g)
|
|
53
|
+
return null;
|
|
54
|
+
g.fillStyle = role.base;
|
|
55
|
+
g.fillRect(0, 0, 64, 64);
|
|
56
|
+
let s = seed * 9301 + 49297;
|
|
57
|
+
const rnd = () => ((s = (s * 16807) % 2147483647) / 2147483647);
|
|
58
|
+
for (let i = 0; i < 700; i++) {
|
|
59
|
+
g.fillStyle = role.speckle[Math.floor(rnd() * role.speckle.length)] ?? role.base;
|
|
60
|
+
g.fillRect(Math.floor(rnd() * 64), Math.floor(rnd() * 64), 1 + Math.floor(rnd() * 2), 1 + Math.floor(rnd() * 2));
|
|
61
|
+
}
|
|
62
|
+
return c;
|
|
63
|
+
}
|
|
64
|
+
/** Library textures are muted to this saturation and contrast: the console's palette was dusty, and the actors must stay the loudest thing on screen. */
|
|
65
|
+
const MUTE_SATURATION = 0.55;
|
|
66
|
+
const MUTE_CONTRAST = 0.8;
|
|
67
|
+
/** A muted copy of an image (saturation and contrast pulled in), or null when it cannot be read. */
|
|
68
|
+
export function muted(img) {
|
|
69
|
+
if (typeof document === "undefined" || !img)
|
|
70
|
+
return null;
|
|
71
|
+
const src = img;
|
|
72
|
+
const w = src.width ?? 0;
|
|
73
|
+
const h = src.height ?? 0;
|
|
74
|
+
if (w <= 0 || h <= 0)
|
|
75
|
+
return null;
|
|
76
|
+
const c = document.createElement("canvas");
|
|
77
|
+
c.width = w;
|
|
78
|
+
c.height = h;
|
|
79
|
+
const g = c.getContext("2d", { willReadFrequently: true });
|
|
80
|
+
if (!g)
|
|
81
|
+
return null;
|
|
82
|
+
try {
|
|
83
|
+
g.drawImage(img, 0, 0);
|
|
84
|
+
const im = g.getImageData(0, 0, w, h);
|
|
85
|
+
const d = im.data;
|
|
86
|
+
let mean = 0;
|
|
87
|
+
for (let i = 0; i < d.length; i += 4)
|
|
88
|
+
mean += 0.299 * (d[i] ?? 0) + 0.587 * (d[i + 1] ?? 0) + 0.114 * (d[i + 2] ?? 0);
|
|
89
|
+
mean /= d.length / 4;
|
|
90
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
91
|
+
const r = d[i] ?? 0;
|
|
92
|
+
const gg = d[i + 1] ?? 0;
|
|
93
|
+
const b = d[i + 2] ?? 0;
|
|
94
|
+
const y = 0.299 * r + 0.587 * gg + 0.114 * b;
|
|
95
|
+
const yc = mean + (y - mean) * MUTE_CONTRAST;
|
|
96
|
+
d[i] = yc + (r - y) * MUTE_SATURATION;
|
|
97
|
+
d[i + 1] = yc + (gg - y) * MUTE_SATURATION;
|
|
98
|
+
d[i + 2] = yc + (b - y) * MUTE_SATURATION;
|
|
99
|
+
}
|
|
100
|
+
g.putImageData(im, 0, 0);
|
|
101
|
+
return c;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const toLinear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
108
|
+
/** Mean linear colour of an sRGB image (drawn down to 8x8), or null when it cannot be read. */
|
|
109
|
+
export function imageAverage(img) {
|
|
110
|
+
if (typeof document === "undefined" || !img)
|
|
111
|
+
return null;
|
|
112
|
+
const c = document.createElement("canvas");
|
|
113
|
+
c.width = c.height = 8;
|
|
114
|
+
const g = c.getContext("2d", { willReadFrequently: true });
|
|
115
|
+
if (!g)
|
|
116
|
+
return null;
|
|
117
|
+
try {
|
|
118
|
+
g.imageSmoothingEnabled = true;
|
|
119
|
+
g.drawImage(img, 0, 0, 8, 8);
|
|
120
|
+
const d = g.getImageData(0, 0, 8, 8).data;
|
|
121
|
+
let r = 0;
|
|
122
|
+
let gg = 0;
|
|
123
|
+
let b = 0;
|
|
124
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
125
|
+
r += toLinear((d[i] ?? 0) / 255);
|
|
126
|
+
gg += toLinear((d[i + 1] ?? 0) / 255);
|
|
127
|
+
b += toLinear((d[i + 2] ?? 0) / 255);
|
|
128
|
+
}
|
|
129
|
+
const n = d.length / 4;
|
|
130
|
+
return [r / n, gg / n, b / n];
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The material colour that makes a texture of mean `avg` read like the
|
|
138
|
+
* template's own surface of mean `want`: its brightness, and half its hue
|
|
139
|
+
* (a night field stays a dark blue-green field, not a lit lawn).
|
|
140
|
+
*/
|
|
141
|
+
/** How much of the template's hue the tint carries (0: brightness only, 1: all of it). */
|
|
142
|
+
const HUE = 0.5;
|
|
143
|
+
export function matchTint(want, avg) {
|
|
144
|
+
const lum = (c) => 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
|
|
145
|
+
const l = lum(want) / Math.max(lum(avg), 1e-4);
|
|
146
|
+
const ch = (i) => Math.min(3, Math.max(0, l * (1 - HUE) + (want[i] / Math.max(avg[i], 1e-4)) * HUE));
|
|
147
|
+
return [ch(0), ch(1), ch(2)];
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Loads and caches one texture per role. `resolve` maps a library id to a
|
|
151
|
+
* URL (or null when there is no library), from the template's LibraryClient.
|
|
152
|
+
*/
|
|
153
|
+
export class Ps1Surfaces {
|
|
154
|
+
resolve;
|
|
155
|
+
cache = new Map();
|
|
156
|
+
averages = new Map();
|
|
157
|
+
listeners = new Map();
|
|
158
|
+
loader = new TextureLoader();
|
|
159
|
+
constructor(resolve) {
|
|
160
|
+
this.resolve = resolve;
|
|
161
|
+
}
|
|
162
|
+
get(tag) {
|
|
163
|
+
const role = surfaceRole(tag);
|
|
164
|
+
if (!role)
|
|
165
|
+
return null;
|
|
166
|
+
let t = this.cache.get(role.id);
|
|
167
|
+
if (!t) {
|
|
168
|
+
const canvas = speckled(role, role.id.length * 31 + role.id.charCodeAt(8));
|
|
169
|
+
if (!canvas)
|
|
170
|
+
return null;
|
|
171
|
+
const made = new CanvasTexture(canvas);
|
|
172
|
+
made.wrapS = made.wrapT = RepeatWrapping;
|
|
173
|
+
made.magFilter = made.minFilter = NearestFilter;
|
|
174
|
+
made.generateMipmaps = false;
|
|
175
|
+
made.colorSpace = SRGBColorSpace;
|
|
176
|
+
t = made;
|
|
177
|
+
this.cache.set(role.id, made);
|
|
178
|
+
const first = imageAverage(canvas);
|
|
179
|
+
if (first)
|
|
180
|
+
this.averages.set(role.id, first);
|
|
181
|
+
let url = null;
|
|
182
|
+
try {
|
|
183
|
+
url = this.resolve?.(`style.ps1.${role.id}`) ?? null;
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
url = null;
|
|
187
|
+
}
|
|
188
|
+
if (url) {
|
|
189
|
+
this.loader.loadAsync(url).then((loaded) => {
|
|
190
|
+
// Same texture object: no material or pipeline changes, just new pixels.
|
|
191
|
+
const image = muted(loaded.image) ?? loaded.image;
|
|
192
|
+
const was = made.image;
|
|
193
|
+
const now = image;
|
|
194
|
+
// A new size needs new GPU storage (WebGL cannot write a 128 px image into the 64 px fallback's).
|
|
195
|
+
if (was?.width !== now?.width || was?.height !== now?.height)
|
|
196
|
+
made.dispose();
|
|
197
|
+
made.image = image;
|
|
198
|
+
made.needsUpdate = true;
|
|
199
|
+
const avg = imageAverage(image);
|
|
200
|
+
if (avg) {
|
|
201
|
+
this.averages.set(role.id, avg);
|
|
202
|
+
for (const cb of this.listeners.get(role.id) ?? [])
|
|
203
|
+
cb(avg);
|
|
204
|
+
}
|
|
205
|
+
}, () => undefined);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const id = role.id;
|
|
209
|
+
return {
|
|
210
|
+
texture: t,
|
|
211
|
+
metres: role.metres,
|
|
212
|
+
onAverage: (cb) => {
|
|
213
|
+
const list = this.listeners.get(id) ?? [];
|
|
214
|
+
list.push(cb);
|
|
215
|
+
this.listeners.set(id, list);
|
|
216
|
+
const now = this.averages.get(id);
|
|
217
|
+
if (now)
|
|
218
|
+
cb(now);
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
dispose() {
|
|
223
|
+
for (const t of this.cache.values())
|
|
224
|
+
t.dispose();
|
|
225
|
+
this.cache.clear();
|
|
226
|
+
this.listeners.clear();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { StyleKit } from "../kit.js";
|
|
2
|
+
/** Shared tuning: every toon material, ink pass and particle reads these. */
|
|
3
|
+
export declare const TOON: import("./material.js").ToonUniforms;
|
|
4
|
+
export declare const INK: import("./ink.js").InkUniforms;
|
|
5
|
+
export declare const toonKit: StyleKit;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOON: cel-shaded cartoon rendering for any scene the runtime draws.
|
|
3
|
+
*
|
|
4
|
+
* game({ style: "toon" }) (or a template's ?style=toon) and the render
|
|
5
|
+
* stack draws every frame through this session:
|
|
6
|
+
*
|
|
7
|
+
* - materials: each lit material is drawn through a toon twin (material.ts)
|
|
8
|
+
* at draw time, through the renderer's render-object hook. The scene and
|
|
9
|
+
* its materials are never changed, so templates keep fading, flashing and
|
|
10
|
+
* tinting them, and crowd motion, VAT and skinning carry over;
|
|
11
|
+
* - normals: faceted models are smoothed once, with a crease angle
|
|
12
|
+
* (normals.ts), so the bands cut clean curves;
|
|
13
|
+
* - ink: silhouette and crease lines in one screen pass (ink.ts);
|
|
14
|
+
* - shadows: flat cartoon shadows under every caster (shadows.ts);
|
|
15
|
+
* - effects: stars, streaks and puffs instead of glows (vfx.ts);
|
|
16
|
+
* - look: brighter, saturated presets with cool fill light (look.ts).
|
|
17
|
+
*/
|
|
18
|
+
import { Mesh, MeshBasicMaterial, PlaneGeometry, Vector3 } from "three/webgpu";
|
|
19
|
+
import { inkPost, inkUniforms, NO_NORMAL } from "./ink.js";
|
|
20
|
+
import { toonLook } from "./look.js";
|
|
21
|
+
import { isLit, toonTwin, toonUniforms } from "./material.js";
|
|
22
|
+
import { smoothNormalsInPlace } from "./normals.js";
|
|
23
|
+
import { shadowTwin, shadowUniforms } from "./shadows.js";
|
|
24
|
+
import { toonParticles } from "./vfx.js";
|
|
25
|
+
import { Color } from "three/webgpu";
|
|
26
|
+
/** Shared tuning: every toon material, ink pass and particle reads these. */
|
|
27
|
+
export const TOON = toonUniforms();
|
|
28
|
+
export const INK = inkUniforms();
|
|
29
|
+
/** Stars, streaks and puffs for the GPU particles, inked in INK's colour. */
|
|
30
|
+
const PARTICLES = toonParticles(INK.color);
|
|
31
|
+
const smoothed = new WeakSet();
|
|
32
|
+
/** Smooth a geometry's normals once (the normal attribute is shared by every draw of it). */
|
|
33
|
+
function smooth(geometry) {
|
|
34
|
+
const n = geometry.getAttribute("normal");
|
|
35
|
+
const p = geometry.getAttribute("position");
|
|
36
|
+
if (!n || !p || smoothed.has(n))
|
|
37
|
+
return;
|
|
38
|
+
smoothed.add(n);
|
|
39
|
+
if (n.itemSize !== 3 || p.itemSize !== 3 || !(n.array instanceof Float32Array) || n.count !== p.count)
|
|
40
|
+
return;
|
|
41
|
+
if (p.isInterleavedBufferAttribute || n.isInterleavedBufferAttribute)
|
|
42
|
+
return;
|
|
43
|
+
const index = geometry.getIndex();
|
|
44
|
+
smoothNormalsInPlace(p.array, n.array, index ? index.array : null);
|
|
45
|
+
n.needsUpdate = true;
|
|
46
|
+
}
|
|
47
|
+
function attach(ctx) {
|
|
48
|
+
const { renderer, scene, camera } = ctx;
|
|
49
|
+
const twins = new WeakMap();
|
|
50
|
+
const shadowTwins = new WeakMap();
|
|
51
|
+
const marked = new WeakSet();
|
|
52
|
+
const shadowU = shadowUniforms(new Color(0x2a1c4a));
|
|
53
|
+
let shadowsOn = ctx.tier !== "low";
|
|
54
|
+
// Shadows are drawn after the opaque scene: the first transparent draw (this sentinel goes first) flushes them.
|
|
55
|
+
const sentinel = new Mesh(new PlaneGeometry(0.001, 0.001), new MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false }));
|
|
56
|
+
sentinel.name = "ToonShadowFlush";
|
|
57
|
+
sentinel.frustumCulled = false;
|
|
58
|
+
sentinel.renderOrder = -1e9;
|
|
59
|
+
sentinel.position.set(0, -500, 0);
|
|
60
|
+
scene.add(sentinel);
|
|
61
|
+
const pending = [];
|
|
62
|
+
let sun = null;
|
|
63
|
+
let frames = 0;
|
|
64
|
+
const dir = new Vector3();
|
|
65
|
+
const findSun = () => {
|
|
66
|
+
sun = null;
|
|
67
|
+
scene.traverse((o) => {
|
|
68
|
+
if (!sun && o.isDirectionalLight && o.visible)
|
|
69
|
+
sun = o;
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
const flush = () => {
|
|
73
|
+
for (const p of pending)
|
|
74
|
+
renderer.renderObject(p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7]);
|
|
75
|
+
pending.length = 0;
|
|
76
|
+
};
|
|
77
|
+
const fn = (object, s, cam, geometry, material, group, lightsNode, clippingContext, passId) => {
|
|
78
|
+
if (object === sentinel) {
|
|
79
|
+
flush();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (isLit(material)) {
|
|
83
|
+
smooth(geometry);
|
|
84
|
+
let twin = twins.get(material);
|
|
85
|
+
if (!twin) {
|
|
86
|
+
twin = toonTwin(material, TOON);
|
|
87
|
+
twins.set(material, twin);
|
|
88
|
+
}
|
|
89
|
+
twin.sync(material);
|
|
90
|
+
renderer.renderObject(object, s, cam, geometry, twin, group, lightsNode, clippingContext, passId);
|
|
91
|
+
if (shadowsOn && object.castShadow && !material.transparent && material.visible) {
|
|
92
|
+
let st = shadowTwins.get(material);
|
|
93
|
+
if (!st) {
|
|
94
|
+
st = shadowTwin(material, shadowU);
|
|
95
|
+
shadowTwins.set(material, st);
|
|
96
|
+
}
|
|
97
|
+
pending.push([object, s, cam, geometry, st, group, lightsNode, clippingContext]);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (material.transparent && !marked.has(material)) {
|
|
102
|
+
// Effects leave the ink pass's normal target alone.
|
|
103
|
+
marked.add(material);
|
|
104
|
+
const m = material;
|
|
105
|
+
if (m.mrtNode === null || m.mrtNode === undefined)
|
|
106
|
+
m.mrtNode = NO_NORMAL();
|
|
107
|
+
}
|
|
108
|
+
renderer.renderObject(object, s, cam, geometry, material, group, lightsNode, clippingContext, passId);
|
|
109
|
+
};
|
|
110
|
+
const ink = inkPost(INK, () => true);
|
|
111
|
+
return {
|
|
112
|
+
particles: PARTICLES,
|
|
113
|
+
post: {
|
|
114
|
+
...ink,
|
|
115
|
+
scenePass(pass) {
|
|
116
|
+
ink.scenePass?.(pass);
|
|
117
|
+
// The scene pass renders from inside the post graph, often under another node's
|
|
118
|
+
// reset renderer state (bloom's), so the toon draw hook goes on the pass itself.
|
|
119
|
+
const own = pass.updateBefore.bind(pass);
|
|
120
|
+
pass.updateBefore = (frame) => {
|
|
121
|
+
const prev = renderer.getRenderObjectFunction();
|
|
122
|
+
renderer.setRenderObjectFunction(fn);
|
|
123
|
+
try {
|
|
124
|
+
return own(frame);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
renderer.setRenderObjectFunction(prev);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
render(draw) {
|
|
133
|
+
if (frames++ % 120 === 0 || !sun)
|
|
134
|
+
findSun();
|
|
135
|
+
const s = sun;
|
|
136
|
+
if (s) {
|
|
137
|
+
dir.copy(s.position).sub(s.target.position);
|
|
138
|
+
if (dir.lengthSq() > 1e-8 && dir.y > 0.05)
|
|
139
|
+
shadowU.dir.value.copy(dir.normalize());
|
|
140
|
+
}
|
|
141
|
+
const c = camera;
|
|
142
|
+
if (c.near !== undefined)
|
|
143
|
+
INK.near.value = c.near;
|
|
144
|
+
if (c.far !== undefined)
|
|
145
|
+
INK.far.value = c.far;
|
|
146
|
+
pending.length = 0;
|
|
147
|
+
const prev = renderer.getRenderObjectFunction();
|
|
148
|
+
renderer.setRenderObjectFunction(fn);
|
|
149
|
+
try {
|
|
150
|
+
draw();
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
renderer.setRenderObjectFunction(prev);
|
|
154
|
+
pending.length = 0;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
setSize(width, height) {
|
|
158
|
+
INK.texel.value.set(1 / Math.max(1, width), 1 / Math.max(1, height));
|
|
159
|
+
INK.width.value = Math.min(2.5, Math.max(1, height / 900));
|
|
160
|
+
},
|
|
161
|
+
setQuality(tier) {
|
|
162
|
+
shadowsOn = tier !== "low";
|
|
163
|
+
},
|
|
164
|
+
dispose() {
|
|
165
|
+
sentinel.removeFromParent();
|
|
166
|
+
sentinel.geometry.dispose();
|
|
167
|
+
sentinel.material.dispose();
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export const toonKit = {
|
|
172
|
+
id: "toon",
|
|
173
|
+
look: toonLook,
|
|
174
|
+
needsPost: true,
|
|
175
|
+
// The ink pass draws the lines; the stack's own outline stays off.
|
|
176
|
+
outline: false,
|
|
177
|
+
attach,
|
|
178
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ink lines: one screen-space pass over the scene's depth and normals.
|
|
3
|
+
*
|
|
4
|
+
* Silhouettes come from depth (a second difference of view depth, so flat
|
|
5
|
+
* floors seen at a slant stay clean), creases from normals (an MRT output
|
|
6
|
+
* of the scene pass). The cost is a handful of texture reads per pixel,
|
|
7
|
+
* the same for one hero or two thousand instanced enemies, and it needs no
|
|
8
|
+
* per-mesh work: crowds, VAT bakes, skinned characters and props are all
|
|
9
|
+
* inked the same way. Transparent things (particles, rings, decals) write
|
|
10
|
+
* nothing to the normal target and never write depth, so effects are not
|
|
11
|
+
* outlined.
|
|
12
|
+
*
|
|
13
|
+
* The line colour is a deep ink mixed with a darkened copy of the colour
|
|
14
|
+
* under it, so lines read as painted, not as a black filter.
|
|
15
|
+
*/
|
|
16
|
+
import { Color, Vector2 } from "three/webgpu";
|
|
17
|
+
import type { Node, UniformNode } from "three/webgpu";
|
|
18
|
+
import type { StylePost } from "../kit.js";
|
|
19
|
+
export interface InkUniforms {
|
|
20
|
+
readonly color: UniformNode<"color", Color>;
|
|
21
|
+
/** How much of the colour under a line tints it (0 = flat ink). */
|
|
22
|
+
readonly tint: UniformNode<"float", number>;
|
|
23
|
+
/** Line half-width in device pixels. */
|
|
24
|
+
readonly width: UniformNode<"float", number>;
|
|
25
|
+
/** Relative depth curvature that starts a silhouette line. */
|
|
26
|
+
readonly depthAt: UniformNode<"float", number>;
|
|
27
|
+
/** 1 - N.N' that starts a crease line. */
|
|
28
|
+
readonly creaseAt: UniformNode<"float", number>;
|
|
29
|
+
readonly strength: UniformNode<"float", number>;
|
|
30
|
+
readonly texel: UniformNode<"vec2", Vector2>;
|
|
31
|
+
readonly near: UniformNode<"float", number>;
|
|
32
|
+
readonly far: UniformNode<"float", number>;
|
|
33
|
+
/** 1 shows the normal target, 2 its alpha (tuning). */
|
|
34
|
+
readonly debug: UniformNode<"float", number>;
|
|
35
|
+
}
|
|
36
|
+
export declare function inkUniforms(): InkUniforms;
|
|
37
|
+
/** Normal output for transparent materials: alpha 0 leaves the normal target as it was. */
|
|
38
|
+
export declare const NO_NORMAL: () => Node;
|
|
39
|
+
export declare function inkPost(u: InkUniforms, enabled: () => boolean): StylePost;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ink lines: one screen-space pass over the scene's depth and normals.
|
|
3
|
+
*
|
|
4
|
+
* Silhouettes come from depth (a second difference of view depth, so flat
|
|
5
|
+
* floors seen at a slant stay clean), creases from normals (an MRT output
|
|
6
|
+
* of the scene pass). The cost is a handful of texture reads per pixel,
|
|
7
|
+
* the same for one hero or two thousand instanced enemies, and it needs no
|
|
8
|
+
* per-mesh work: crowds, VAT bakes, skinned characters and props are all
|
|
9
|
+
* inked the same way. Transparent things (particles, rings, decals) write
|
|
10
|
+
* nothing to the normal target and never write depth, so effects are not
|
|
11
|
+
* outlined.
|
|
12
|
+
*
|
|
13
|
+
* The line colour is a deep ink mixed with a darkened copy of the colour
|
|
14
|
+
* under it, so lines read as painted, not as a black filter.
|
|
15
|
+
*/
|
|
16
|
+
import { BlendMode, Color, MaterialBlending, UnsignedByteType, Vector2 } from "three/webgpu";
|
|
17
|
+
import { abs, select, unpackRGBToNormal, packNormalToRGB, dot, float, max, min, mix, mrt, normalView, output, perspectiveDepthToViewZ, screenUV, smoothstep, uniform, vec2, vec3, vec4 } from "three/tsl";
|
|
18
|
+
export function inkUniforms() {
|
|
19
|
+
return {
|
|
20
|
+
color: uniform(new Color(0x170f26)),
|
|
21
|
+
tint: uniform(0.3),
|
|
22
|
+
width: uniform(1),
|
|
23
|
+
depthAt: uniform(0.01),
|
|
24
|
+
creaseAt: uniform(0.32),
|
|
25
|
+
strength: uniform(1),
|
|
26
|
+
texel: uniform(new Vector2(1 / 1280, 1 / 720)),
|
|
27
|
+
near: uniform(0.1),
|
|
28
|
+
far: uniform(500),
|
|
29
|
+
debug: uniform(0),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Normal output for transparent materials: alpha 0 leaves the normal target as it was. */
|
|
33
|
+
export const NO_NORMAL = () => mrt({ normal: vec4(0, 0, 0, 0) });
|
|
34
|
+
export function inkPost(u, enabled) {
|
|
35
|
+
return {
|
|
36
|
+
scenePass(pass) {
|
|
37
|
+
if (!enabled())
|
|
38
|
+
return;
|
|
39
|
+
const m = mrt({ output, normal: vec4(packNormalToRGB(normalView), 1) });
|
|
40
|
+
// Opaque surfaces overwrite normals; transparent ones blend with alpha 0 (see NO_NORMAL).
|
|
41
|
+
m.setBlendMode("normal", new BlendMode(MaterialBlending));
|
|
42
|
+
pass.setMRT(m);
|
|
43
|
+
pass.getTexture("normal").type = UnsignedByteType;
|
|
44
|
+
},
|
|
45
|
+
color(rgb, pass) {
|
|
46
|
+
if (!enabled())
|
|
47
|
+
return rgb;
|
|
48
|
+
const depth = pass.getTextureNode("depth");
|
|
49
|
+
const normal = pass.getTextureNode("normal");
|
|
50
|
+
const step = u.texel.mul(u.width);
|
|
51
|
+
const uvAt = (dx, dy) => screenUV.add(step.mul(vec2(dx, dy)));
|
|
52
|
+
const z = (dx, dy) => perspectiveDepthToViewZ(depth.sample(uvAt(dx, dy)).x, u.near, u.far);
|
|
53
|
+
const z0 = z(0, 0);
|
|
54
|
+
// Second differences: zero on any plane, large across a silhouette.
|
|
55
|
+
const lapX = abs(z(1, 0).add(z(-1, 0)).sub(z0.mul(2)));
|
|
56
|
+
const lapY = abs(z(0, 1).add(z(0, -1)).sub(z0.mul(2)));
|
|
57
|
+
const lapD = abs(z(1, 1).add(z(-1, -1)).sub(z0.mul(2))).add(abs(z(1, -1).add(z(-1, 1)).sub(z0.mul(2)))).mul(0.5);
|
|
58
|
+
const rel = max(max(lapX, lapY), lapD).div(max(abs(z0), float(0.01)));
|
|
59
|
+
const sil = smoothstep(u.depthAt, u.depthAt.mul(2.2), rel);
|
|
60
|
+
// Creases: neighbours whose normals turn sharply away (skip pixels with no surface).
|
|
61
|
+
const n = (dx, dy) => normal.sample(uvAt(dx, dy));
|
|
62
|
+
const c = n(0, 0);
|
|
63
|
+
const n0 = unpackRGBToNormal(c.rgb);
|
|
64
|
+
const turn = (s) => float(1).sub(dot(n0, unpackRGBToNormal(s.rgb))).mul(s.a);
|
|
65
|
+
const crease = max(max(turn(n(1, 0)), turn(n(-1, 0))), max(turn(n(0, 1)), turn(n(0, -1)))).mul(c.a);
|
|
66
|
+
const fold = smoothstep(u.creaseAt, u.creaseAt.mul(1.6), crease);
|
|
67
|
+
const edge = min(max(sil, fold), float(1)).mul(u.strength);
|
|
68
|
+
const ink = mix(vec3(u.color), rgb.mul(0.18), u.tint);
|
|
69
|
+
const out = mix(rgb, ink, edge);
|
|
70
|
+
return select(u.debug.greaterThan(1.5), vec3(c.a), select(u.debug.greaterThan(0.5), c.rgb, out));
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The toon style's look presets. Cartoon lighting is simpler and brighter
|
|
3
|
+
* than the runtime's: one strong key light (it makes the bands), a cool,
|
|
4
|
+
* saturated fill (it colours the shadow side), little fog, a neutral tone
|
|
5
|
+
* curve that keeps colours true, and light bloom only on things that glow.
|
|
6
|
+
* Same preset names as the runtime, so a game's look.preset still means
|
|
7
|
+
* night, dusk, day or studio.
|
|
8
|
+
*/
|
|
9
|
+
import type { Look, LookPreset } from "../../render/presets.js";
|
|
10
|
+
export declare function toonLook(preset: LookPreset): Look;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const LOOKS = {
|
|
2
|
+
// A storybook night: moonlit blues and violets, warm lantern pools, nothing near black.
|
|
3
|
+
night: {
|
|
4
|
+
lighting: {
|
|
5
|
+
sky: 0xa8b8ff,
|
|
6
|
+
ground: 0x3c2c58,
|
|
7
|
+
hemi: 0.7,
|
|
8
|
+
sun: 0xd6e2ff,
|
|
9
|
+
sunIntensity: 4.4,
|
|
10
|
+
sunDir: [-0.42, 0.86, 0.32],
|
|
11
|
+
ambient: 1.9,
|
|
12
|
+
ambientColor: 0x6f62d8,
|
|
13
|
+
shadowStrength: 0.6,
|
|
14
|
+
},
|
|
15
|
+
atmosphere: { fog: 0x2a2a5c, skyTop: 0x141238, mode: "linear", near: 50, far: 150, density: 0.01, heightFog: 0, heightFogTop: 0 },
|
|
16
|
+
grade: {
|
|
17
|
+
exposure: 1.4,
|
|
18
|
+
contrast: 1.04,
|
|
19
|
+
saturation: 1.18,
|
|
20
|
+
shadowTint: [0.92, 0.9, 1.14],
|
|
21
|
+
highlightTint: [1.06, 1.01, 0.92],
|
|
22
|
+
vignette: 0.22,
|
|
23
|
+
bloomStrength: 0.42,
|
|
24
|
+
bloomRadius: 0.35,
|
|
25
|
+
bloomThreshold: 0.95,
|
|
26
|
+
toneMapping: "neutral",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
// Golden hour: orange key light, violet shadows.
|
|
30
|
+
dusk: {
|
|
31
|
+
lighting: {
|
|
32
|
+
sky: 0xffc39a,
|
|
33
|
+
ground: 0x4a3058,
|
|
34
|
+
hemi: 0.6,
|
|
35
|
+
sun: 0xffb478,
|
|
36
|
+
sunIntensity: 3.8,
|
|
37
|
+
sunDir: [0.62, 0.62, 0.42],
|
|
38
|
+
ambient: 1.5,
|
|
39
|
+
ambientColor: 0x8a5ad0,
|
|
40
|
+
shadowStrength: 0.6,
|
|
41
|
+
},
|
|
42
|
+
atmosphere: { fog: 0x8a5a78, skyTop: 0x3a2a60, mode: "linear", near: 55, far: 170, density: 0.008, heightFog: 0, heightFogTop: 0 },
|
|
43
|
+
grade: {
|
|
44
|
+
exposure: 1.1,
|
|
45
|
+
contrast: 1.04,
|
|
46
|
+
saturation: 1.2,
|
|
47
|
+
shadowTint: [0.95, 0.9, 1.1],
|
|
48
|
+
highlightTint: [1.08, 1.0, 0.9],
|
|
49
|
+
vignette: 0.2,
|
|
50
|
+
bloomStrength: 0.38,
|
|
51
|
+
bloomRadius: 0.35,
|
|
52
|
+
bloomThreshold: 0.95,
|
|
53
|
+
toneMapping: "neutral",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
// Saturday-morning daylight: white sun, sky-blue shadows.
|
|
57
|
+
day: {
|
|
58
|
+
lighting: {
|
|
59
|
+
sky: 0xd8ecff,
|
|
60
|
+
ground: 0x6a6040,
|
|
61
|
+
hemi: 0.6,
|
|
62
|
+
sun: 0xfff6e6,
|
|
63
|
+
sunIntensity: 3.6,
|
|
64
|
+
sunDir: [0.38, 0.88, 0.28],
|
|
65
|
+
ambient: 1.7,
|
|
66
|
+
ambientColor: 0x7ca0e8,
|
|
67
|
+
shadowStrength: 0.55,
|
|
68
|
+
},
|
|
69
|
+
atmosphere: { fog: 0xbfe0ff, skyTop: 0x6ab4ff, mode: "linear", near: 60, far: 190, density: 0.006, heightFog: 0, heightFogTop: 0 },
|
|
70
|
+
grade: {
|
|
71
|
+
exposure: 1.0,
|
|
72
|
+
contrast: 1.03,
|
|
73
|
+
saturation: 1.15,
|
|
74
|
+
shadowTint: [0.97, 0.99, 1.06],
|
|
75
|
+
highlightTint: [1.03, 1.01, 0.97],
|
|
76
|
+
vignette: 0.14,
|
|
77
|
+
bloomStrength: 0.25,
|
|
78
|
+
bloomRadius: 0.3,
|
|
79
|
+
bloomThreshold: 1.0,
|
|
80
|
+
toneMapping: "neutral",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
studio: {
|
|
84
|
+
lighting: {
|
|
85
|
+
sky: 0xffffff,
|
|
86
|
+
ground: 0x505060,
|
|
87
|
+
hemi: 0.6,
|
|
88
|
+
sun: 0xffffff,
|
|
89
|
+
sunIntensity: 3.2,
|
|
90
|
+
sunDir: [0.5, 1, 0.6],
|
|
91
|
+
ambient: 1.6,
|
|
92
|
+
ambientColor: 0x9a9ad8,
|
|
93
|
+
shadowStrength: 0.5,
|
|
94
|
+
},
|
|
95
|
+
atmosphere: { fog: 0x2a2838, skyTop: 0x16141f, mode: "linear", near: 70, far: 220, density: 0.005, heightFog: 0, heightFogTop: 0 },
|
|
96
|
+
grade: {
|
|
97
|
+
exposure: 1.0,
|
|
98
|
+
contrast: 1.0,
|
|
99
|
+
saturation: 1.1,
|
|
100
|
+
shadowTint: [1, 1, 1],
|
|
101
|
+
highlightTint: [1, 1, 1],
|
|
102
|
+
vignette: 0.12,
|
|
103
|
+
bloomStrength: 0.25,
|
|
104
|
+
bloomRadius: 0.3,
|
|
105
|
+
bloomThreshold: 1.0,
|
|
106
|
+
toneMapping: "neutral",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
export function toonLook(preset) {
|
|
111
|
+
return LOOKS[preset] ?? LOOKS.studio;
|
|
112
|
+
}
|