@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.
Files changed (244) hide show
  1. package/GAME-DESIGN.md +195 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +30 -0
  4. package/README.md +147 -2
  5. package/dist/api/appearance.d.ts +37 -0
  6. package/dist/api/appearance.js +41 -0
  7. package/dist/api/cards.d.ts +105 -0
  8. package/dist/api/cards.js +133 -0
  9. package/dist/api/collector.d.ts +114 -0
  10. package/dist/api/collector.js +204 -0
  11. package/dist/api/dungeon.d.ts +58 -0
  12. package/dist/api/dungeon.js +216 -0
  13. package/dist/api/harness.d.ts +149 -0
  14. package/dist/api/harness.js +549 -0
  15. package/dist/api/idle.d.ts +71 -0
  16. package/dist/api/idle.js +104 -0
  17. package/dist/api/race-harness.d.ts +3 -0
  18. package/dist/api/race-harness.js +148 -0
  19. package/dist/api/registry.d.ts +241 -0
  20. package/dist/api/registry.js +394 -0
  21. package/dist/api/scheduler.d.ts +56 -0
  22. package/dist/api/scheduler.js +160 -0
  23. package/dist/api/schemas.d.ts +958 -0
  24. package/dist/api/schemas.js +859 -0
  25. package/dist/api/tower.d.ts +127 -0
  26. package/dist/api/tower.js +183 -0
  27. package/dist/game/adapter.d.ts +69 -0
  28. package/dist/game/adapter.js +341 -0
  29. package/dist/game/arena-bot.d.ts +66 -0
  30. package/dist/game/arena-bot.js +188 -0
  31. package/dist/game/bot.d.ts +95 -0
  32. package/dist/game/bot.js +285 -0
  33. package/dist/game/compile.d.ts +97 -0
  34. package/dist/game/compile.js +476 -0
  35. package/dist/game/director.d.ts +42 -0
  36. package/dist/game/director.js +179 -0
  37. package/dist/game/earn-source.d.ts +122 -0
  38. package/dist/game/earn-source.js +386 -0
  39. package/dist/game/earn.d.ts +49 -0
  40. package/dist/game/earn.js +55 -0
  41. package/dist/game/index.d.ts +37 -0
  42. package/dist/game/index.js +25 -0
  43. package/dist/game/platformer.d.ts +156 -0
  44. package/dist/game/platformer.js +619 -0
  45. package/dist/game/profile.d.ts +97 -0
  46. package/dist/game/profile.js +140 -0
  47. package/dist/game/race.d.ts +177 -0
  48. package/dist/game/race.js +386 -0
  49. package/dist/game/racer.d.ts +98 -0
  50. package/dist/game/racer.js +379 -0
  51. package/dist/game/runner.d.ts +286 -0
  52. package/dist/game/runner.js +824 -0
  53. package/dist/game/threats.d.ts +23 -0
  54. package/dist/game/threats.js +49 -0
  55. package/dist/game/view.d.ts +47 -0
  56. package/dist/game/view.js +123 -0
  57. package/dist/index.d.ts +33 -0
  58. package/dist/index.js +24 -0
  59. package/dist/net/bits.d.ts +33 -0
  60. package/dist/net/bits.js +135 -0
  61. package/dist/net/client.d.ts +75 -0
  62. package/dist/net/client.js +151 -0
  63. package/dist/net/index.d.ts +23 -0
  64. package/dist/net/index.js +23 -0
  65. package/dist/net/input.d.ts +49 -0
  66. package/dist/net/input.js +112 -0
  67. package/dist/net/interpolation.d.ts +37 -0
  68. package/dist/net/interpolation.js +97 -0
  69. package/dist/net/netsim.d.ts +22 -0
  70. package/dist/net/netsim.js +52 -0
  71. package/dist/net/prediction.d.ts +31 -0
  72. package/dist/net/prediction.js +101 -0
  73. package/dist/net/profile-client.d.ts +75 -0
  74. package/dist/net/profile-client.js +173 -0
  75. package/dist/net/protocol.d.ts +208 -0
  76. package/dist/net/protocol.js +8 -0
  77. package/dist/net/room-client.d.ts +57 -0
  78. package/dist/net/room-client.js +144 -0
  79. package/dist/net/schema.d.ts +78 -0
  80. package/dist/net/schema.js +121 -0
  81. package/dist/net/sim-adapter.d.ts +134 -0
  82. package/dist/net/sim-adapter.js +22 -0
  83. package/dist/net/snapshot.d.ts +61 -0
  84. package/dist/net/snapshot.js +301 -0
  85. package/dist/rules/adapter.d.ts +53 -0
  86. package/dist/rules/adapter.js +261 -0
  87. package/dist/rules/cards.d.ts +115 -0
  88. package/dist/rules/cards.js +547 -0
  89. package/dist/rules/collector.d.ts +161 -0
  90. package/dist/rules/collector.js +732 -0
  91. package/dist/rules/genres.d.ts +13 -0
  92. package/dist/rules/genres.js +38 -0
  93. package/dist/rules/harness.d.ts +16 -0
  94. package/dist/rules/harness.js +127 -0
  95. package/dist/rules/idle.d.ts +115 -0
  96. package/dist/rules/idle.js +370 -0
  97. package/dist/rules/index.d.ts +16 -0
  98. package/dist/rules/index.js +14 -0
  99. package/dist/rules/kit.d.ts +171 -0
  100. package/dist/rules/kit.js +88 -0
  101. package/dist/rules/tower.d.ts +214 -0
  102. package/dist/rules/tower.js +655 -0
  103. package/dist/runtime/arena.d.ts +86 -0
  104. package/dist/runtime/arena.js +269 -0
  105. package/dist/runtime/assets.d.ts +26 -0
  106. package/dist/runtime/assets.js +35 -0
  107. package/dist/runtime/camera.d.ts +113 -0
  108. package/dist/runtime/camera.js +247 -0
  109. package/dist/runtime/crowd.d.ts +18 -0
  110. package/dist/runtime/crowd.js +62 -0
  111. package/dist/runtime/feel/audio.d.ts +150 -0
  112. package/dist/runtime/feel/audio.js +327 -0
  113. package/dist/runtime/feel/clock.d.ts +52 -0
  114. package/dist/runtime/feel/clock.js +80 -0
  115. package/dist/runtime/feel/flash.d.ts +28 -0
  116. package/dist/runtime/feel/flash.js +60 -0
  117. package/dist/runtime/feel/index.d.ts +27 -0
  118. package/dist/runtime/feel/index.js +16 -0
  119. package/dist/runtime/feel/kit.d.ts +159 -0
  120. package/dist/runtime/feel/kit.js +226 -0
  121. package/dist/runtime/feel/knockback.d.ts +41 -0
  122. package/dist/runtime/feel/knockback.js +70 -0
  123. package/dist/runtime/feel/noise.d.ts +6 -0
  124. package/dist/runtime/feel/noise.js +30 -0
  125. package/dist/runtime/feel/numbers.d.ts +91 -0
  126. package/dist/runtime/feel/numbers.js +172 -0
  127. package/dist/runtime/feel/rumble.d.ts +40 -0
  128. package/dist/runtime/feel/rumble.js +58 -0
  129. package/dist/runtime/feel/sound.d.ts +96 -0
  130. package/dist/runtime/feel/sound.js +176 -0
  131. package/dist/runtime/feel/spring.d.ts +42 -0
  132. package/dist/runtime/feel/spring.js +95 -0
  133. package/dist/runtime/feel/trauma.d.ts +48 -0
  134. package/dist/runtime/feel/trauma.js +76 -0
  135. package/dist/runtime/feel/tween.d.ts +66 -0
  136. package/dist/runtime/feel/tween.js +180 -0
  137. package/dist/runtime/feel/types.d.ts +30 -0
  138. package/dist/runtime/feel/types.js +5 -0
  139. package/dist/runtime/index.d.ts +28 -0
  140. package/dist/runtime/index.js +26 -0
  141. package/dist/runtime/input.d.ts +38 -0
  142. package/dist/runtime/input.js +159 -0
  143. package/dist/runtime/interpolate.d.ts +6 -0
  144. package/dist/runtime/interpolate.js +13 -0
  145. package/dist/runtime/render/animation.d.ts +63 -0
  146. package/dist/runtime/render/animation.js +128 -0
  147. package/dist/runtime/render/atmosphere.d.ts +32 -0
  148. package/dist/runtime/render/atmosphere.js +59 -0
  149. package/dist/runtime/render/crowd-motion.d.ts +68 -0
  150. package/dist/runtime/render/crowd-motion.js +163 -0
  151. package/dist/runtime/render/decals.d.ts +64 -0
  152. package/dist/runtime/render/decals.js +222 -0
  153. package/dist/runtime/render/highlight.d.ts +29 -0
  154. package/dist/runtime/render/highlight.js +110 -0
  155. package/dist/runtime/render/index.d.ts +35 -0
  156. package/dist/runtime/render/index.js +22 -0
  157. package/dist/runtime/render/lighting.d.ts +50 -0
  158. package/dist/runtime/render/lighting.js +142 -0
  159. package/dist/runtime/render/particles.d.ts +117 -0
  160. package/dist/runtime/render/particles.js +289 -0
  161. package/dist/runtime/render/post.d.ts +92 -0
  162. package/dist/runtime/render/post.js +191 -0
  163. package/dist/runtime/render/presets.d.ts +57 -0
  164. package/dist/runtime/render/presets.js +116 -0
  165. package/dist/runtime/render/quality.d.ts +113 -0
  166. package/dist/runtime/render/quality.js +180 -0
  167. package/dist/runtime/render/rig.d.ts +29 -0
  168. package/dist/runtime/render/rig.js +107 -0
  169. package/dist/runtime/render/sprite.d.ts +34 -0
  170. package/dist/runtime/render/sprite.js +58 -0
  171. package/dist/runtime/render/stack.d.ts +74 -0
  172. package/dist/runtime/render/stack.js +132 -0
  173. package/dist/runtime/render/vat.d.ts +78 -0
  174. package/dist/runtime/render/vat.js +219 -0
  175. package/dist/runtime/renderer.d.ts +19 -0
  176. package/dist/runtime/renderer.js +18 -0
  177. package/dist/runtime/stats.d.ts +11 -0
  178. package/dist/runtime/stats.js +29 -0
  179. package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
  180. package/dist/runtime/styles/handdrawn/ground.js +282 -0
  181. package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
  182. package/dist/runtime/styles/handdrawn/index.js +94 -0
  183. package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
  184. package/dist/runtime/styles/handdrawn/ink.js +68 -0
  185. package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
  186. package/dist/runtime/styles/handdrawn/looks.js +42 -0
  187. package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
  188. package/dist/runtime/styles/handdrawn/paint.js +129 -0
  189. package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
  190. package/dist/runtime/styles/handdrawn/palette.js +70 -0
  191. package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
  192. package/dist/runtime/styles/handdrawn/textures.js +150 -0
  193. package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
  194. package/dist/runtime/styles/handdrawn/vfx.js +73 -0
  195. package/dist/runtime/styles/index.d.ts +9 -0
  196. package/dist/runtime/styles/index.js +32 -0
  197. package/dist/runtime/styles/kit.d.ts +193 -0
  198. package/dist/runtime/styles/kit.js +1 -0
  199. package/dist/runtime/styles/pixel/index.d.ts +46 -0
  200. package/dist/runtime/styles/pixel/index.js +167 -0
  201. package/dist/runtime/styles/ps1/index.d.ts +29 -0
  202. package/dist/runtime/styles/ps1/index.js +41 -0
  203. package/dist/runtime/styles/ps1/lights.d.ts +47 -0
  204. package/dist/runtime/styles/ps1/lights.js +146 -0
  205. package/dist/runtime/styles/ps1/materials.d.ts +62 -0
  206. package/dist/runtime/styles/ps1/materials.js +285 -0
  207. package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
  208. package/dist/runtime/styles/ps1/pipeline.js +111 -0
  209. package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
  210. package/dist/runtime/styles/ps1/surfaces.js +228 -0
  211. package/dist/runtime/styles/toon/index.d.ts +5 -0
  212. package/dist/runtime/styles/toon/index.js +178 -0
  213. package/dist/runtime/styles/toon/ink.d.ts +39 -0
  214. package/dist/runtime/styles/toon/ink.js +73 -0
  215. package/dist/runtime/styles/toon/look.d.ts +10 -0
  216. package/dist/runtime/styles/toon/look.js +112 -0
  217. package/dist/runtime/styles/toon/material.d.ts +74 -0
  218. package/dist/runtime/styles/toon/material.js +208 -0
  219. package/dist/runtime/styles/toon/normals.d.ts +26 -0
  220. package/dist/runtime/styles/toon/normals.js +127 -0
  221. package/dist/runtime/styles/toon/shadows.d.ts +24 -0
  222. package/dist/runtime/styles/toon/shadows.js +42 -0
  223. package/dist/runtime/styles/toon/vfx.d.ts +15 -0
  224. package/dist/runtime/styles/toon/vfx.js +47 -0
  225. package/dist/runtime/styles/view.d.ts +35 -0
  226. package/dist/runtime/styles/view.js +34 -0
  227. package/dist/runtime/styles/voxel/core.d.ts +114 -0
  228. package/dist/runtime/styles/voxel/core.js +577 -0
  229. package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
  230. package/dist/runtime/styles/voxel/geometry.js +40 -0
  231. package/dist/runtime/styles/voxel/index.d.ts +21 -0
  232. package/dist/runtime/styles/voxel/index.js +23 -0
  233. package/dist/runtime/styles/voxel/kit.d.ts +6 -0
  234. package/dist/runtime/styles/voxel/kit.js +11 -0
  235. package/dist/runtime/styles/voxel/look.d.ts +10 -0
  236. package/dist/runtime/styles/voxel/look.js +16 -0
  237. package/dist/runtime/styles/voxel/props.d.ts +9 -0
  238. package/dist/runtime/styles/voxel/props.js +52 -0
  239. package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
  240. package/dist/runtime/styles/voxel/shapes.js +83 -0
  241. package/dist/runtime/styles/voxel/world.d.ts +49 -0
  242. package/dist/runtime/styles/voxel/world.js +422 -0
  243. package/llms.txt +183 -0
  244. package/package.json +82 -3
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The pixel art style. The scene renders into a
3
+ * low-resolution target, one pixel of it per sprite texel at the camera's
4
+ * focus, and every pixel goes to the screen as a hard square block: no
5
+ * filtering, no blur, one pixel scale for the whole picture. The colour is
6
+ * then snapped to the style's 16-colour palette (the library build snaps
7
+ * the sprites to the same one) with a 4x4 ordered dither, so lit 3D ground,
8
+ * walls and effects read as one piece of pixel art with the sprites.
9
+ *
10
+ * Sprites (render/sprite.ts) are resized by `spriteScale` here each frame,
11
+ * so a sprite texel is exactly one low-resolution pixel.
12
+ */
13
+ import { RenderPipeline, Vector2, Vector3 } from "three/webgpu";
14
+ import { dot, float, floor, fract, length, mix, mrt, output, pass, renderOutput, screenUV, smoothstep, uniform, vec2, vec3, vec4 } from "three/tsl";
15
+ import { SPRITE_TEXEL, spriteScale } from "../../render/sprite.js";
16
+ /**
17
+ * The pixel style's palette (DawnBringer 16). The asset library's
18
+ * manifest/pixel.json carries the same list; its test keeps them equal.
19
+ */
20
+ export const PIXEL_PALETTE = [
21
+ "#140c1c",
22
+ "#442434",
23
+ "#30346d",
24
+ "#4e4a4e",
25
+ "#854c30",
26
+ "#346524",
27
+ "#d04648",
28
+ "#757161",
29
+ "#597dce",
30
+ "#d27d2c",
31
+ "#8595a1",
32
+ "#6daa2c",
33
+ "#d2aa99",
34
+ "#6dc2ca",
35
+ "#dad45e",
36
+ "#deeed6",
37
+ ];
38
+ /**
39
+ * The integer block size (screen pixels per low-res pixel) that puts
40
+ * `texelsPerMetre` low-res rows on each metre of a view `viewMetres` tall.
41
+ */
42
+ export function pixelBlock(bufferHeight, viewMetres, texelsPerMetre, minRows = 120) {
43
+ const rows = Math.max(minRows, viewMetres * texelsPerMetre);
44
+ return Math.max(1, Math.round(bufferHeight / rows));
45
+ }
46
+ /** Height in metres the camera sees at the ground point it looks at (perspective cameras; orthographic: its frustum). */
47
+ export function viewMetres(camera, forward, position) {
48
+ const cam = camera;
49
+ if (cam.isOrthographicCamera)
50
+ return (cam.top - cam.bottom) / (cam.zoom || 1);
51
+ const down = Math.max(0.15, -forward.y);
52
+ const dist = Math.max(1, position.y / down);
53
+ return 2 * dist * Math.tan(((cam.fov || 50) * Math.PI) / 360);
54
+ }
55
+ function hex(c) {
56
+ const n = parseInt(c.slice(1), 16);
57
+ return new Vector3(((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255);
58
+ }
59
+ /** 4x4 Bayer threshold, 0..1, for an integer pixel cell. */
60
+ function bayer4(cell) {
61
+ const b2 = (a) => {
62
+ const f = floor(a);
63
+ return fract(dot(f, vec2(0.5, f.y.mul(0.75))));
64
+ };
65
+ return b2(cell.mul(0.5)).mul(0.25).add(b2(cell));
66
+ }
67
+ /** Nearest palette colour (weighted RGB), by a chain of compares. */
68
+ function nearest(c, palette) {
69
+ const w = vec3(0.3, 0.59, 0.11);
70
+ let best = vec3(palette[0] ?? new Vector3());
71
+ let bestD = float(1e9);
72
+ for (const p of palette) {
73
+ const d = c.sub(vec3(p));
74
+ const dist = dot(d.mul(d), w);
75
+ const closer = dist.lessThan(bestD);
76
+ best = closer.select(vec3(p), best);
77
+ bestD = closer.select(dist, bestD);
78
+ }
79
+ return best;
80
+ }
81
+ class PixelSession {
82
+ ctx;
83
+ opts;
84
+ pipeline;
85
+ scenePass;
86
+ low = uniform(new Vector2(320, 180));
87
+ buffer = new Vector2();
88
+ forward = new Vector3();
89
+ position = new Vector3();
90
+ block = 0;
91
+ constructor(ctx, opts) {
92
+ this.ctx = ctx;
93
+ this.opts = opts;
94
+ this.pipeline = new RenderPipeline(ctx.renderer);
95
+ this.pipeline.outputColorTransform = false;
96
+ this.scenePass = pass(ctx.scene, ctx.camera);
97
+ // Sprites tag themselves (render/sprite.ts SPRITE_MRT); everything else is the 3D world.
98
+ this.scenePass.setMRT(mrt({ output, sprite: float(0) }));
99
+ const color = this.scenePass.getTextureNode("output");
100
+ const spriteTag = this.scenePass.getTextureNode("sprite");
101
+ // One sample at the centre of the low-res pixel under this screen pixel: hard square blocks.
102
+ const cell = floor(screenUV.mul(this.low));
103
+ const at = cell.add(0.5).div(this.low);
104
+ let c = color.sample(at).rgb;
105
+ // The stack's hurt pulse, in low-res pixel steps (no vignette: it would ring the field).
106
+ const post = ctx.post;
107
+ if (post) {
108
+ const d = length(at.sub(0.5)).mul(1.414);
109
+ const hurtEdge = smoothstep(0.45, 1.15, d).mul(post.hurt).mul(0.85);
110
+ c = mix(c, c.mul(vec3(1.25, 0.35, 0.3)).add(vec3(0.22, 0, 0)), hurtEdge);
111
+ }
112
+ const toned = renderOutput(vec4(c, 1)).rgb;
113
+ const luma = dot(toned, vec3(0.299, 0.587, 0.114));
114
+ // Sprites: their own palette colours, a little more saturated, no dither.
115
+ const vivid = mix(vec3(luma), toned, this.opts.saturation);
116
+ const sprite = nearest(vivid, PIXEL_PALETTE.map(hex));
117
+ // The world: a few flat light bands. The dark field is two calm, mossy tones; light pools step up
118
+ // in at most three warm bands; the band edges get a light ordered dither.
119
+ const chroma = toned.div(luma.max(0.02));
120
+ const l = luma.add(bayer4(cell).sub(0.5).mul(this.opts.dither));
121
+ const bands = this.opts.bands;
122
+ let level = float(bands[0] ?? 0.08);
123
+ for (let i = 1; i < bands.length; i++) {
124
+ const lo = bands[i - 1] ?? 0;
125
+ const hi = bands[i] ?? 1;
126
+ level = l.greaterThan((lo + hi) / 2).select(float(hi), level);
127
+ }
128
+ const lit = level.greaterThan(bands[1] ?? 0.16);
129
+ const fieldTint = vec3(0.86, 1, 0.9);
130
+ const hue = lit.select(mix(vec3(1), chroma, 1.15), mix(fieldTint, chroma, 0.25));
131
+ const world = hue.mul(level).min(vec3(1));
132
+ this.pipeline.outputNode = vec4(spriteTag.sample(at).r.greaterThan(0.5).select(sprite, world), 1);
133
+ }
134
+ /** Fit the block size to the canvas and the camera's view, and resize the sprites to match. */
135
+ fit() {
136
+ const { renderer, camera } = this.ctx;
137
+ renderer.getDrawingBufferSize(this.buffer);
138
+ camera.getWorldDirection(this.forward);
139
+ camera.getWorldPosition(this.position);
140
+ const metres = viewMetres(camera, this.forward, this.position);
141
+ const block = pixelBlock(this.buffer.y, metres, this.opts.texelsPerMetre, this.opts.minRows);
142
+ if (block !== this.block) {
143
+ this.block = block;
144
+ this.scenePass.setResolutionScale(1 / block);
145
+ }
146
+ // The same float steps PassNode takes, so the sampled cells match its target exactly.
147
+ const w = Math.max(1, Math.floor(this.buffer.x * (1 / block)));
148
+ const h = Math.max(1, Math.floor(this.buffer.y * (1 / block)));
149
+ this.low.value.set(w, h);
150
+ // A sprite texel is one low-res pixel at the focus.
151
+ spriteScale.value = metres / h / SPRITE_TEXEL;
152
+ }
153
+ render() {
154
+ this.fit();
155
+ this.pipeline.render();
156
+ }
157
+ dispose() {
158
+ spriteScale.value = 1;
159
+ this.pipeline.dispose();
160
+ }
161
+ }
162
+ /** The pixel style with its options. */
163
+ export function createPixelStyle(o = {}) {
164
+ const opts = { texelsPerMetre: o.texelsPerMetre ?? 1 / SPRITE_TEXEL, dither: o.dither ?? 0.025, saturation: o.saturation ?? 1.25, bands: o.bands ?? [0.08, 0.14, 0.3, 0.52, 0.82], minRows: o.minRows ?? 120 };
165
+ return { id: "pixel", attach: (ctx) => new PixelSession(ctx, opts) };
166
+ }
167
+ export const pixelKit = createPixelStyle();
@@ -0,0 +1,29 @@
1
+ /**
2
+ * PS1-retro: the 1996-1999 console look, through the normal runtime.
3
+ *
4
+ * - The scene draws at about 240 lines (crisp blocks, whole device pixels
5
+ * each), no anti-aliasing, and leaves the screen as 15-bit colour
6
+ * through the console's 4x4 ordered dither (pipeline.ts).
7
+ * - Vertices snap to that pixel grid, textures map affinely and sample
8
+ * nearest at console sizes, lit materials light per vertex with smooth
9
+ * Gouraud normals and no specular (materials.ts, lights.ts).
10
+ * - Tagged template surfaces get CC0 128 px textures from the library
11
+ * (surfaces.ts); fog comes in closer (a short draw distance).
12
+ *
13
+ * A template opts in by naming the style in createRenderStack (or calling
14
+ * `styleKit("ps1").attach(...)` when it draws with renderer.render).
15
+ */
16
+ import type { StyleKit, StyleSession } from "../kit.js";
17
+ import { VertexLights } from "./lights.js";
18
+ import { Ps1Patcher } from "./materials.js";
19
+ import { Ps1Pipeline } from "./pipeline.js";
20
+ export { VertexLights, MAX_POINT_LIGHTS } from "./lights.js";
21
+ export { Ps1Patcher, tessellate } from "./materials.js";
22
+ export { Ps1Pipeline, blockGrid } from "./pipeline.js";
23
+ export { Ps1Surfaces, SURFACES, surfaceRole } from "./surfaces.js";
24
+ export interface Ps1Session extends StyleSession {
25
+ readonly pipeline: Ps1Pipeline;
26
+ readonly patcher: Ps1Patcher;
27
+ readonly lights: VertexLights;
28
+ }
29
+ export declare const ps1Kit: StyleKit;
@@ -0,0 +1,41 @@
1
+ import { VertexLights } from "./lights.js";
2
+ import { Ps1Patcher } from "./materials.js";
3
+ import { Ps1Pipeline } from "./pipeline.js";
4
+ import { Ps1Surfaces } from "./surfaces.js";
5
+ export { VertexLights, MAX_POINT_LIGHTS } from "./lights.js";
6
+ export { Ps1Patcher, tessellate } from "./materials.js";
7
+ export { Ps1Pipeline, blockGrid } from "./pipeline.js";
8
+ export { Ps1Surfaces, SURFACES, surfaceRole } from "./surfaces.js";
9
+ /** Fog pulls in to this share of the template's distances (a short draw distance). */
10
+ const FOG_PULL = 0.6;
11
+ export const ps1Kit = {
12
+ id: "ps1",
13
+ attach(ctx) {
14
+ const { handle, scene, camera } = ctx;
15
+ const lights = new VertexLights();
16
+ const pipeline = new Ps1Pipeline(handle.renderer, scene, camera, ctx.post);
17
+ const surfaces = new Ps1Surfaces(ctx.resolve);
18
+ const patcher = new Ps1Patcher(lights, pipeline.resolution, (role) => surfaces.get(role));
19
+ if (ctx.atmosphere) {
20
+ ctx.atmosphere.near.value *= FOG_PULL;
21
+ ctx.atmosphere.far.value *= FOG_PULL;
22
+ }
23
+ const r = {
24
+ pipeline,
25
+ patcher,
26
+ lights,
27
+ // The pipeline draws the whole frame (the stack's own post only lends its grade).
28
+ render() {
29
+ patcher.scan(scene);
30
+ lights.update(scene, camera, performance.now());
31
+ pipeline.render();
32
+ },
33
+ // No setSize: the pipeline fits itself to the drawing buffer each frame.
34
+ dispose() {
35
+ pipeline.dispose();
36
+ surfaces.dispose();
37
+ },
38
+ };
39
+ return r;
40
+ },
41
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Per-vertex (Gouraud) lighting for the PS1 style. The scene's own lights
3
+ * (ambient, hemisphere, directional and point lights the templates and the
4
+ * LightingRig add) are gathered once a frame into a few shared uniforms, and
5
+ * every styled material lights its vertices from them in the vertex stage.
6
+ * Big triangles therefore light like a 1997 console: blotchy lantern pools
7
+ * on a coarse ground grid, smooth ramps across a character, no specular.
8
+ *
9
+ * The maths follows three.js's physical Lambert (irradiance x albedo / pi,
10
+ * same point-light falloff), so a scene keeps its brightness when it
11
+ * switches style.
12
+ */
13
+ import { Vector3, Vector4 } from "three/webgpu";
14
+ import type { Camera, Node, Scene } from "three/webgpu";
15
+ /** Point lights each vertex sums (the nearest to the camera). */
16
+ export declare const MAX_POINT_LIGHTS = 8;
17
+ /** Directional lights (sun, moon, rim). */
18
+ export declare const MAX_DIR_LIGHTS = 2;
19
+ export declare class VertexLights {
20
+ /** Ambient irradiance (colour x intensity, summed). */
21
+ readonly ambient: import("three/webgpu").UniformNode<"vec3", Vector3>;
22
+ readonly hemiSky: import("three/webgpu").UniformNode<"vec3", Vector3>;
23
+ readonly hemiGround: import("three/webgpu").UniformNode<"vec3", Vector3>;
24
+ /** xyz: direction toward the light (world). */
25
+ readonly dirDir: import("three/webgpu").UniformNode<"vec4", Vector4>[];
26
+ /** rgb: colour x intensity. */
27
+ readonly dirColor: import("three/webgpu").UniformNode<"vec4", Vector4>[];
28
+ /** xyz: position (world), w: cutoff distance (0 = none). */
29
+ readonly pointPos: import("three/webgpu").UniformNode<"vec4", Vector4>[];
30
+ /** rgb: colour x intensity, w: decay exponent. */
31
+ readonly pointColor: import("three/webgpu").UniformNode<"vec4", Vector4>[];
32
+ /** Overall gain on the vertex light (1 = three.js's physical Lambert). */
33
+ readonly gain: import("three/webgpu").UniformNode<"float", number>;
34
+ /** Vertex light is clamped here (the console modulated textures up to 2x). */
35
+ readonly ceiling: import("three/webgpu").UniformNode<"float", number>;
36
+ private readonly points;
37
+ private nextScan;
38
+ private lights;
39
+ private node;
40
+ /**
41
+ * The lit colour multiplier for the current vertex: built once, shared by
42
+ * every styled material (wrapped in `varying()` so it runs per vertex).
43
+ */
44
+ irradiance(): Node<"vec3">;
45
+ /** Gather the scene's lights into the uniforms. Rescans the scene graph twice a second. */
46
+ update(scene: Scene, camera: Camera, nowMs: number): void;
47
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Per-vertex (Gouraud) lighting for the PS1 style. The scene's own lights
3
+ * (ambient, hemisphere, directional and point lights the templates and the
4
+ * LightingRig add) are gathered once a frame into a few shared uniforms, and
5
+ * every styled material lights its vertices from them in the vertex stage.
6
+ * Big triangles therefore light like a 1997 console: blotchy lantern pools
7
+ * on a coarse ground grid, smooth ramps across a character, no specular.
8
+ *
9
+ * The maths follows three.js's physical Lambert (irradiance x albedo / pi,
10
+ * same point-light falloff), so a scene keeps its brightness when it
11
+ * switches style.
12
+ */
13
+ import { Vector3, Vector4 } from "three/webgpu";
14
+ import { dot, float, max, mix, normalize, normalWorld, positionWorld, pow, saturate, uniform, vec3 } from "three/tsl";
15
+ /** Point lights each vertex sums (the nearest to the camera). */
16
+ export const MAX_POINT_LIGHTS = 8;
17
+ /** Directional lights (sun, moon, rim). */
18
+ export const MAX_DIR_LIGHTS = 2;
19
+ const tmpA = new Vector3();
20
+ const tmpB = new Vector3();
21
+ const slots = (n, w = 0) => Array.from({ length: n }, () => uniform(new Vector4(0, 0, 0, w)));
22
+ export class VertexLights {
23
+ /** Ambient irradiance (colour x intensity, summed). */
24
+ ambient = uniform(new Vector3());
25
+ hemiSky = uniform(new Vector3());
26
+ hemiGround = uniform(new Vector3());
27
+ /** xyz: direction toward the light (world). */
28
+ dirDir = slots(MAX_DIR_LIGHTS);
29
+ /** rgb: colour x intensity. */
30
+ dirColor = slots(MAX_DIR_LIGHTS);
31
+ /** xyz: position (world), w: cutoff distance (0 = none). */
32
+ pointPos = slots(MAX_POINT_LIGHTS);
33
+ /** rgb: colour x intensity, w: decay exponent. */
34
+ pointColor = slots(MAX_POINT_LIGHTS, 2);
35
+ /** Overall gain on the vertex light (1 = three.js's physical Lambert). */
36
+ gain = uniform(1);
37
+ /** Vertex light is clamped here (the console modulated textures up to 2x). */
38
+ ceiling = uniform(2.2);
39
+ points = [];
40
+ nextScan = 0;
41
+ lights = [];
42
+ node = null;
43
+ /**
44
+ * The lit colour multiplier for the current vertex: built once, shared by
45
+ * every styled material (wrapped in `varying()` so it runs per vertex).
46
+ */
47
+ irradiance() {
48
+ if (this.node)
49
+ return this.node;
50
+ const n = normalize(normalWorld);
51
+ const p = positionWorld;
52
+ let irr = vec3(this.ambient).add(mix(vec3(this.hemiGround), vec3(this.hemiSky), n.y.mul(0.5).add(0.5)));
53
+ for (let i = 0; i < MAX_DIR_LIGHTS; i++) {
54
+ const d = this.dirDir[i];
55
+ const c = this.dirColor[i];
56
+ if (!d || !c)
57
+ continue;
58
+ irr = irr.add(c.xyz.mul(max(dot(n, d.xyz), 0)));
59
+ }
60
+ for (let i = 0; i < MAX_POINT_LIGHTS; i++) {
61
+ const lp = this.pointPos[i];
62
+ const lc = this.pointColor[i];
63
+ if (!lp || !lc)
64
+ continue;
65
+ const toL = lp.xyz.sub(p);
66
+ const dist = max(toL.length(), float(1e-4));
67
+ const ndl = max(dot(n, toL.div(dist)), 0);
68
+ // three.js getDistanceAttenuation: 1 / d^decay, softened to zero at the cutoff.
69
+ const fall = float(1).div(max(pow(dist, lc.w), float(0.01)));
70
+ const edge = pow(saturate(float(1).sub(pow(dist.div(max(lp.w, float(1e-4))), 4))), 2);
71
+ const window = lp.w.greaterThan(0).select(edge, float(1));
72
+ irr = irr.add(lc.xyz.mul(ndl.mul(fall).mul(window)));
73
+ }
74
+ // Lambert: irradiance x albedo / pi (the material applies the albedo).
75
+ this.node = irr.mul(1 / Math.PI).mul(this.gain).min(vec3(this.ceiling));
76
+ return this.node;
77
+ }
78
+ /** Gather the scene's lights into the uniforms. Rescans the scene graph twice a second. */
79
+ update(scene, camera, nowMs) {
80
+ if (nowMs >= this.nextScan) {
81
+ this.nextScan = nowMs + 500;
82
+ this.lights = [];
83
+ scene.traverseVisible((o) => {
84
+ if (o.isLight)
85
+ this.lights.push(o);
86
+ });
87
+ }
88
+ const amb = this.ambient.value.set(0, 0, 0);
89
+ const sky = this.hemiSky.value.set(0, 0, 0);
90
+ const gnd = this.hemiGround.value.set(0, 0, 0);
91
+ let dirs = 0;
92
+ this.points.length = 0;
93
+ for (const l of this.lights) {
94
+ if (!l.visible || l.intensity <= 0)
95
+ continue;
96
+ const k = l.intensity;
97
+ if (l.isAmbientLight) {
98
+ amb.x += l.color.r * k;
99
+ amb.y += l.color.g * k;
100
+ amb.z += l.color.b * k;
101
+ }
102
+ else if (l.isHemisphereLight) {
103
+ sky.x += l.color.r * k;
104
+ sky.y += l.color.g * k;
105
+ sky.z += l.color.b * k;
106
+ const g = l.groundColor ?? l.color;
107
+ gnd.x += g.r * k;
108
+ gnd.y += g.g * k;
109
+ gnd.z += g.b * k;
110
+ }
111
+ else if (l.isDirectionalLight && dirs < MAX_DIR_LIGHTS) {
112
+ l.getWorldPosition(tmpA);
113
+ if (l.target)
114
+ l.target.getWorldPosition(tmpB);
115
+ else
116
+ tmpB.set(0, 0, 0);
117
+ tmpA.sub(tmpB).normalize();
118
+ this.dirDir[dirs]?.value.set(tmpA.x, tmpA.y, tmpA.z, 0);
119
+ this.dirColor[dirs]?.value.set(l.color.r * k, l.color.g * k, l.color.b * k, 0);
120
+ dirs++;
121
+ }
122
+ else if (l.isPointLight)
123
+ this.points.push(l);
124
+ }
125
+ for (let i = dirs; i < MAX_DIR_LIGHTS; i++)
126
+ this.dirColor[i]?.value.set(0, 0, 0, 0);
127
+ // The nearest point lights to the camera (a top-down camera: the ones on screen).
128
+ camera.getWorldPosition(tmpB);
129
+ if (this.points.length > MAX_POINT_LIGHTS) {
130
+ const d = new Map();
131
+ for (const l of this.points)
132
+ d.set(l, l.getWorldPosition(tmpA).distanceToSquared(tmpB));
133
+ this.points.sort((a, b) => (d.get(a) ?? 0) - (d.get(b) ?? 0));
134
+ }
135
+ for (let i = 0; i < MAX_POINT_LIGHTS; i++) {
136
+ const l = this.points[i];
137
+ if (!l) {
138
+ this.pointColor[i]?.value.set(0, 0, 0, 2);
139
+ continue;
140
+ }
141
+ l.getWorldPosition(tmpA);
142
+ this.pointPos[i]?.value.set(tmpA.x, tmpA.y, tmpA.z, l.distance ?? 0);
143
+ this.pointColor[i]?.value.set(l.color.r * l.intensity, l.color.g * l.intensity, l.color.b * l.intensity, l.decay ?? 2);
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Turns whatever a template put in the scene into PS1 geometry and
3
+ * materials, without the template knowing: every mesh is found once, and
4
+ * - its vertices snap to the low-res pixel grid (the wobble),
5
+ * - its textures map affinely (no perspective correction: textures swim
6
+ * on big polygons), sample nearest, and stay at console sizes,
7
+ * - lit materials light per vertex from the shared VertexLights (Gouraud:
8
+ * smooth below a crease angle, no specular, no shadow maps),
9
+ * - big planes and boxes are tessellated (as console games did, to keep
10
+ * the warping and the vertex lighting in check),
11
+ * - meshes tagged `userData.surface = "<role>"` get the style's texture for
12
+ * that role in world-space box UVs (ground, walls, pillars).
13
+ *
14
+ * Patching overrides three methods on the material instance itself
15
+ * (vertex output, lighting, cache key), so materials the template keeps
16
+ * and mutates (opacity fades, hit flashes, crowd motion) keep working.
17
+ */
18
+ import { Vector3 } from "three/webgpu";
19
+ import type { BufferGeometry, Object3D, UniformNode, Vector2 } from "three/webgpu";
20
+ import type { VertexLights } from "./lights.js";
21
+ import type { SurfaceTexture } from "./surfaces.js";
22
+ /** A texture the patcher can resolve for a surface role (ground, wall...). */
23
+ export type SurfaceSource = (role: string) => SurfaceTexture | null;
24
+ export declare class Ps1Patcher {
25
+ private readonly lights;
26
+ private readonly resolution;
27
+ private readonly surfaces;
28
+ /** 0 = perspective-correct textures, 1 = full affine (the console). */
29
+ readonly affine: UniformNode<"float", number>;
30
+ /** Vertex snap grid as a fraction of the low-res pixel grid (1 = one pixel). */
31
+ readonly snap: UniformNode<"float", number>;
32
+ private readonly seenMaterials;
33
+ private readonly seenGeometry;
34
+ private readonly seenTextures;
35
+ private readonly seenMeshes;
36
+ private irradianceVarying;
37
+ /** Meshes and materials patched so far (tests and stats). */
38
+ readonly counts: {
39
+ meshes: number;
40
+ materials: number;
41
+ geometries: number;
42
+ surfaces: number;
43
+ textures: number;
44
+ };
45
+ constructor(lights: VertexLights, resolution: UniformNode<"vec2", Vector2>, surfaces: SurfaceSource);
46
+ /** Snap a clip-space position to the low-res pixel grid. */
47
+ private snapClip;
48
+ /** Screen-linear (affine) UVs: interpolate uv*w and w, divide per pixel. */
49
+ private affineUV;
50
+ private light;
51
+ /** Walk the scene and patch what is new. Cheap after the first pass (weak sets). */
52
+ scan(root: Object3D): void;
53
+ private patchMesh;
54
+ /** Smooth normals below the crease angle, in place (Gouraud on organic shapes, hard edges on boxes). */
55
+ private smooth;
56
+ /** A surface: tessellate big flat cells, world-space box UVs every `metres`, the style's texture. */
57
+ private surface;
58
+ private patchTexture;
59
+ private patchMaterial;
60
+ }
61
+ /** Tessellate a PlaneGeometry or BoxGeometry whose cells are larger than MAX_CELL world metres. */
62
+ export declare function tessellate(g: BufferGeometry, worldScale: Vector3): BufferGeometry;