@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,219 @@
1
+ import { AnimationMixer, BufferGeometry, DataTexture, DynamicDrawUsage, Float32BufferAttribute, FloatType, InstancedBufferAttribute, InstancedBufferGeometry, Mesh, NearestFilter, RGBAFormat, Vector3, } from "three/webgpu";
2
+ import { cameraViewMatrix, cos, float, floor, fract, instancedBufferAttribute, int, materialEmissive, ivec2, mix, mod, normalize, sin, textureLoad, uniform, varying, vec3, vec4, vertexIndex, } from "three/tsl";
3
+ /**
4
+ * Bake one or more clips of a skinned mesh into a vertex animation texture
5
+ * (CPU, at load). Frames are laid out clip after clip; a crowd then plays
6
+ * any clip on thousands of instances in one draw (`VatCrowd`).
7
+ */
8
+ export function bakeVertexAnimation(mesh, clips, opts = {}) {
9
+ const list = Array.isArray(clips) ? clips : [clips];
10
+ const fps = opts.fps ?? 30;
11
+ const geometry = mesh.geometry;
12
+ const count = geometry.getAttribute("position").count;
13
+ const width = Math.min(count, opts.maxWidth ?? 2048);
14
+ const rowsPerFrame = Math.ceil(count / width);
15
+ const plan = [];
16
+ let total = 0;
17
+ for (const c of list) {
18
+ const frames = Math.max(1, opts.frames ?? Math.ceil(c.duration * fps));
19
+ plan.push({ name: c.name, start: total, frames });
20
+ total += frames;
21
+ }
22
+ const height = rowsPerFrame * total;
23
+ const pos = new Float32Array(width * height * 4);
24
+ const wantNormals = opts.normals ?? true;
25
+ const nor = wantNormals ? new Float32Array(width * height * 4) : null;
26
+ const scratch = wantNormals ? new BufferGeometry() : null;
27
+ if (scratch) {
28
+ if (geometry.index)
29
+ scratch.setIndex(geometry.index);
30
+ scratch.setAttribute("position", new Float32BufferAttribute(new Float32Array(count * 3), 3));
31
+ }
32
+ const mixer = new AnimationMixer(mesh);
33
+ const v = new Vector3();
34
+ list.forEach((clip, ci) => {
35
+ const p = plan[ci];
36
+ mixer.stopAllAction();
37
+ const action = mixer.clipAction(clip);
38
+ action.reset().play();
39
+ for (let f = 0; f < p.frames; f++) {
40
+ mixer.setTime(p.frames > 1 ? (f / p.frames) * clip.duration : 0);
41
+ mesh.updateMatrixWorld(true);
42
+ mesh.skeleton.update();
43
+ const sp = scratch?.getAttribute("position").array;
44
+ for (let i = 0; i < count; i++) {
45
+ mesh.getVertexPosition(i, v);
46
+ const row = (p.start + f) * rowsPerFrame + Math.floor(i / width);
47
+ const o = (row * width + (i % width)) * 4;
48
+ pos[o] = v.x;
49
+ pos[o + 1] = v.y;
50
+ pos[o + 2] = v.z;
51
+ pos[o + 3] = 1;
52
+ if (sp) {
53
+ sp[i * 3] = v.x;
54
+ sp[i * 3 + 1] = v.y;
55
+ sp[i * 3 + 2] = v.z;
56
+ }
57
+ }
58
+ if (scratch && nor) {
59
+ scratch.getAttribute("position").needsUpdate = true;
60
+ scratch.computeVertexNormals();
61
+ const n = scratch.getAttribute("normal");
62
+ for (let i = 0; i < count; i++) {
63
+ const row = (p.start + f) * rowsPerFrame + Math.floor(i / width);
64
+ const o = (row * width + (i % width)) * 4;
65
+ nor[o] = n.getX(i);
66
+ nor[o + 1] = n.getY(i);
67
+ nor[o + 2] = n.getZ(i);
68
+ nor[o + 3] = 0;
69
+ }
70
+ }
71
+ }
72
+ action.stop();
73
+ });
74
+ mixer.uncacheRoot(mesh);
75
+ const tex = (data) => {
76
+ const t = new DataTexture(data, width, height, RGBAFormat, FloatType);
77
+ t.minFilter = NearestFilter;
78
+ t.magFilter = NearestFilter;
79
+ t.generateMipmaps = false;
80
+ t.needsUpdate = true;
81
+ return t;
82
+ };
83
+ return {
84
+ positions: tex(pos),
85
+ normals: nor ? tex(nor) : null,
86
+ vertexCount: count,
87
+ width,
88
+ rowsPerFrame,
89
+ totalFrames: total,
90
+ fps,
91
+ clips: plan,
92
+ geometry,
93
+ };
94
+ }
95
+ /**
96
+ * Thousands of animated characters in one draw: every instance samples the
97
+ * VAT at its own clip, phase and rate, then is rotated, scaled and placed
98
+ * from per-instance attributes. CPU work per instance is one 12-float write.
99
+ */
100
+ export class VatCrowd {
101
+ vat;
102
+ capacity;
103
+ mesh;
104
+ time = uniform(0);
105
+ count = 0;
106
+ xform;
107
+ anim;
108
+ clip;
109
+ clipIndex = new Map();
110
+ constructor(vat, material, capacity, opts = {}) {
111
+ this.vat = vat;
112
+ this.capacity = capacity;
113
+ for (const c of vat.clips)
114
+ this.clipIndex.set(c.name, c);
115
+ const g = new InstancedBufferGeometry();
116
+ const src = vat.geometry;
117
+ if (src.index)
118
+ g.setIndex(src.index);
119
+ for (const name of ["position", "normal", "uv"]) {
120
+ const a = src.getAttribute(name);
121
+ if (a)
122
+ g.setAttribute(name, a);
123
+ }
124
+ g.instanceCount = 0;
125
+ const mk = () => {
126
+ const a = new InstancedBufferAttribute(new Float32Array(capacity * 4), 4);
127
+ a.setUsage(DynamicDrawUsage);
128
+ return a;
129
+ };
130
+ this.xform = mk(); // x, y, z, yaw
131
+ this.anim = mk(); // phase s, rate, scale, flash
132
+ this.clip = mk(); // start frame, frames
133
+ const X = instancedBufferAttribute(this.xform, "vec4");
134
+ const A = instancedBufferAttribute(this.anim, "vec4");
135
+ const C = instancedBufferAttribute(this.clip, "vec4");
136
+ const tt = this.time.mul(A.y).add(A.x).mul(vat.fps);
137
+ const frames = C.y.max(1);
138
+ const f0 = mod(floor(tt), frames);
139
+ const f1 = mod(f0.add(1), frames);
140
+ const a = fract(tt);
141
+ const vi = int(vertexIndex);
142
+ const w = int(vat.width);
143
+ const col = vi.mod(w);
144
+ const row = vi.div(w);
145
+ const rpf = int(vat.rowsPerFrame);
146
+ const at = (f) => ivec2(col, int(C.x.add(f)).mul(rpf).add(row));
147
+ const p0 = textureLoad(vat.positions, at(f0)).xyz;
148
+ const p1 = textureLoad(vat.positions, at(f1)).xyz;
149
+ const p = mix(p0, p1, a).mul(A.z);
150
+ const cy = cos(X.w);
151
+ const sy = sin(X.w);
152
+ const rot = (v) => {
153
+ const n = v;
154
+ return vec3(n.x.mul(cy).add(n.z.mul(sy)), n.y, n.x.mul(sy).negate().add(n.z.mul(cy)));
155
+ };
156
+ material.positionNode = rot(p).add(X.xyz);
157
+ const useNormals = (opts.bakedNormals ?? true) && vat.normals !== null && material.flatShading !== true;
158
+ if (useNormals && vat.normals) {
159
+ const n = mix(textureLoad(vat.normals, at(f0)).xyz, textureLoad(vat.normals, at(f1)).xyz, a);
160
+ const nw = varying(rot(n));
161
+ material.normalNode = normalize(cameraViewMatrix.mul(vec4(nw, 0)).xyz);
162
+ }
163
+ if ("emissiveNode" in material) {
164
+ material.emissiveNode = materialEmissive.add(vec3(varying(A.w)).mul(float(2.5)));
165
+ }
166
+ this.mesh = new Mesh(g, material);
167
+ this.mesh.frustumCulled = false;
168
+ this.mesh.castShadow = true;
169
+ this.mesh.name = "VatCrowd";
170
+ }
171
+ clipNamed(name) {
172
+ return this.clipIndex.get(name);
173
+ }
174
+ begin() {
175
+ this.count = 0;
176
+ }
177
+ /** Add one instance this frame. `phase` offsets the clip in seconds; `rate` scales playback. */
178
+ push(x, y, z, yaw, opts = {}) {
179
+ if (this.count >= this.capacity)
180
+ return;
181
+ const o = this.count * 4;
182
+ const X = this.xform.array;
183
+ const A = this.anim.array;
184
+ const C = this.clip.array;
185
+ X[o] = x;
186
+ X[o + 1] = y;
187
+ X[o + 2] = z;
188
+ X[o + 3] = yaw;
189
+ A[o] = opts.phase ?? 0;
190
+ A[o + 1] = opts.rate ?? 1;
191
+ A[o + 2] = opts.scale ?? 1;
192
+ A[o + 3] = Math.min(1, Math.max(0, opts.flash ?? 0));
193
+ const c = (opts.clip !== undefined ? this.clipIndex.get(opts.clip) : undefined) ?? this.vat.clips[0];
194
+ C[o] = c?.start ?? 0;
195
+ C[o + 1] = c?.frames ?? 1;
196
+ this.count++;
197
+ }
198
+ /** Upload the instances written since `begin` and set the time (seconds). */
199
+ end(timeSeconds) {
200
+ this.time.value = timeSeconds;
201
+ this.mesh.geometry.instanceCount = this.count;
202
+ for (const a of [this.xform, this.anim, this.clip]) {
203
+ a.clearUpdateRanges();
204
+ if (this.count > 0)
205
+ a.addUpdateRange(0, this.count * 4);
206
+ a.needsUpdate = true;
207
+ }
208
+ }
209
+ addTo(parent) {
210
+ parent.add(this.mesh);
211
+ return this;
212
+ }
213
+ dispose() {
214
+ this.mesh.removeFromParent();
215
+ this.mesh.geometry.dispose();
216
+ this.vat.positions.dispose();
217
+ this.vat.normals?.dispose();
218
+ }
219
+ }
@@ -0,0 +1,19 @@
1
+ import { WebGPURenderer } from "three/webgpu";
2
+ export type Backend = "webgpu" | "webgl2";
3
+ export interface RendererOptions {
4
+ /** Skip WebGPU even where the browser has it (testing, or a known-bad driver). */
5
+ readonly forceWebGL?: boolean;
6
+ readonly antialias?: boolean;
7
+ /** Cap on devicePixelRatio; the quality tier lowers it. Default 2. */
8
+ readonly maxPixelRatio?: number;
9
+ }
10
+ export interface RendererHandle {
11
+ readonly renderer: WebGPURenderer;
12
+ readonly backend: Backend;
13
+ }
14
+ /**
15
+ * three.js WebGPURenderer. It targets WebGPU where `navigator.gpu` yields an
16
+ * adapter and falls back to its WebGL2 backend otherwise, so one code path
17
+ * (node materials, instancing) covers every browser (spec §5).
18
+ */
19
+ export declare function createRenderer(canvas: HTMLCanvasElement, opts?: RendererOptions): Promise<RendererHandle>;
@@ -0,0 +1,18 @@
1
+ import { WebGPURenderer } from "three/webgpu";
2
+ /**
3
+ * three.js WebGPURenderer. It targets WebGPU where `navigator.gpu` yields an
4
+ * adapter and falls back to its WebGL2 backend otherwise, so one code path
5
+ * (node materials, instancing) covers every browser (spec §5).
6
+ */
7
+ export async function createRenderer(canvas, opts = {}) {
8
+ const renderer = new WebGPURenderer({
9
+ canvas,
10
+ antialias: opts.antialias ?? true,
11
+ forceWebGL: opts.forceWebGL ?? false,
12
+ powerPreference: "high-performance",
13
+ });
14
+ renderer.setPixelRatio(Math.min(globalThis.devicePixelRatio || 1, opts.maxPixelRatio ?? 2));
15
+ await renderer.init();
16
+ const backend = renderer.backend.isWebGPUBackend ? "webgpu" : "webgl2";
17
+ return { renderer, backend };
18
+ }
@@ -0,0 +1,11 @@
1
+ /** Rolling frame-time statistics (ms) over the last `size` frames. */
2
+ export declare class FrameStats {
3
+ private readonly buf;
4
+ private i;
5
+ private n;
6
+ constructor(size?: number);
7
+ push(ms: number): void;
8
+ get count(): number;
9
+ mean(): number;
10
+ percentile(q: number): number;
11
+ }
@@ -0,0 +1,29 @@
1
+ /** Rolling frame-time statistics (ms) over the last `size` frames. */
2
+ export class FrameStats {
3
+ buf;
4
+ i = 0;
5
+ n = 0;
6
+ constructor(size = 240) {
7
+ this.buf = new Float64Array(size);
8
+ }
9
+ push(ms) {
10
+ this.buf[this.i] = ms;
11
+ this.i = (this.i + 1) % this.buf.length;
12
+ this.n = Math.min(this.n + 1, this.buf.length);
13
+ }
14
+ get count() {
15
+ return this.n;
16
+ }
17
+ mean() {
18
+ let s = 0;
19
+ for (let k = 0; k < this.n; k++)
20
+ s += this.buf[k] ?? 0;
21
+ return this.n ? s / this.n : 0;
22
+ }
23
+ percentile(q) {
24
+ if (!this.n)
25
+ return 0;
26
+ const a = Array.from(this.buf.subarray(0, this.n)).sort((x, y) => x - y);
27
+ return a[Math.min(a.length - 1, Math.floor(q * (a.length - 1)))] ?? 0;
28
+ }
29
+ }
@@ -0,0 +1,11 @@
1
+ import type { Texture } from "three/webgpu";
2
+ /** A meadow at the scale of one tile: washes, pen grass, pebbles and flowers. */
3
+ export declare function paintField(seed?: number, size?: number): Texture | null;
4
+ /**
5
+ * A pool of light as a painter leaves it: a soft wash with a ragged edge
6
+ * and a faint tide line, white with alpha (the template tints it). Not
7
+ * tiled: one pool per texture.
8
+ */
9
+ export declare function paintGlow(seed?: number, size?: number): Texture | null;
10
+ /** A paved yard tile: four slabs ruled in pencil, with stains and cracks. */
11
+ export declare function paintPaving(seed?: number, size?: number): Texture | null;
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Painted surfaces for template-built ground: a field (loose washes of
3
+ * green and ochre, pen grass, pebbles, a few flowers) and a paved yard
4
+ * (slabs ruled in pencil, stains, cracks). Painted once on a canvas with
5
+ * the style's pigments, tileable, deterministic. Original work.
6
+ */
7
+ import { CanvasTexture, RepeatWrapping, SRGBColorSpace } from "three/webgpu";
8
+ import { PIGMENTS } from "./palette.js";
9
+ import { seeded } from "./textures.js";
10
+ const css = (hex, a = 1) => `rgba(${(hex >> 16) & 255},${(hex >> 8) & 255},${hex & 255},${a})`;
11
+ function canvas(size) {
12
+ if (typeof OffscreenCanvas !== "undefined") {
13
+ const c = new OffscreenCanvas(size, size);
14
+ const g = c.getContext("2d");
15
+ return g ? { c, g } : null;
16
+ }
17
+ if (typeof document === "undefined")
18
+ return null;
19
+ const c = document.createElement("canvas");
20
+ c.width = c.height = size;
21
+ const g = c.getContext("2d");
22
+ return g ? { c, g } : null;
23
+ }
24
+ /** Draw at (x, y) and at its wrapped copies near the edges, so the tile repeats seamlessly. */
25
+ function tiled(size, x, y, reach, draw) {
26
+ for (const dx of [-size, 0, size]) {
27
+ for (const dy of [-size, 0, size]) {
28
+ const px = x + dx;
29
+ const py = y + dy;
30
+ if (px < -reach || py < -reach || px > size + reach || py > size + reach)
31
+ continue;
32
+ draw(px, py);
33
+ }
34
+ }
35
+ }
36
+ /** An irregular closed blob path (a wash's outline). */
37
+ function blob(g, x, y, r, rnd, lumps = 9) {
38
+ const k = Array.from({ length: lumps }, () => 0.7 + rnd() * 0.6);
39
+ g.beginPath();
40
+ for (let i = 0; i <= 48; i++) {
41
+ const a = (i / 48) * Math.PI * 2;
42
+ const f = a / (Math.PI * 2) * lumps;
43
+ const i0 = Math.floor(f) % lumps;
44
+ const i1 = (i0 + 1) % lumps;
45
+ const t = f - Math.floor(f);
46
+ const s = t * t * (3 - 2 * t);
47
+ const rr = r * ((k[i0] ?? 1) * (1 - s) + (k[i1] ?? 1) * s);
48
+ const px = x + Math.cos(a) * rr;
49
+ const py = y + Math.sin(a) * rr * 0.8;
50
+ if (i === 0)
51
+ g.moveTo(px, py);
52
+ else
53
+ g.lineTo(px, py);
54
+ }
55
+ g.closePath();
56
+ }
57
+ /** A watercolour wash: a pale fill and a darker tide line where the water dried. */
58
+ function wash(g, x, y, r, color, alpha, rnd) {
59
+ blob(g, x, y, r, rnd);
60
+ g.fillStyle = css(color, alpha);
61
+ g.fill();
62
+ g.strokeStyle = css(color, Math.min(1, alpha * 1.6));
63
+ g.lineWidth = 1.5 + rnd() * 1.5;
64
+ g.stroke();
65
+ }
66
+ /** A wobbly pen line from (x0, y0) to (x1, y1). */
67
+ function penLine(g, x0, y0, x1, y1, rnd, wobble = 1.2) {
68
+ const n = Math.max(2, Math.ceil(Math.hypot(x1 - x0, y1 - y0) / 14));
69
+ g.beginPath();
70
+ g.moveTo(x0 + (rnd() - 0.5) * wobble, y0 + (rnd() - 0.5) * wobble);
71
+ for (let i = 1; i <= n; i++) {
72
+ const t = i / n;
73
+ g.lineTo(x0 + (x1 - x0) * t + (rnd() - 0.5) * wobble, y0 + (y1 - y0) * t + (rnd() - 0.5) * wobble);
74
+ }
75
+ g.stroke();
76
+ }
77
+ function texture(c) {
78
+ const t = new CanvasTexture(c);
79
+ t.wrapS = t.wrapT = RepeatWrapping;
80
+ t.colorSpace = SRGBColorSpace;
81
+ t.anisotropy = 4;
82
+ t.needsUpdate = true;
83
+ return t;
84
+ }
85
+ /** A meadow at the scale of one tile: washes, pen grass, pebbles and flowers. */
86
+ export function paintField(seed = 3, size = 1024) {
87
+ const cv = canvas(size);
88
+ if (!cv)
89
+ return null;
90
+ const { c, g } = cv;
91
+ const rnd = seeded(seed);
92
+ // One pale base wash over the whole sheet, so the field reads as one place.
93
+ g.fillStyle = css(0xdcd8b0);
94
+ g.fillRect(0, 0, size, size);
95
+ // Wet-in-wet: many soft spots of green and ochre glazed over each other, no edges.
96
+ g.globalCompositeOperation = "multiply";
97
+ const soft = [PIGMENTS.leaf, PIGMENTS.sap, PIGMENTS.leaf, PIGMENTS.olive, PIGMENTS.naples, PIGMENTS.ochre];
98
+ for (let i = 0; i < 220; i++) {
99
+ const x = rnd() * size;
100
+ const y = rnd() * size;
101
+ const r = 40 + rnd() * 110;
102
+ const col = soft[i % soft.length] ?? PIGMENTS.leaf;
103
+ const a = 0.05 + rnd() * 0.06;
104
+ tiled(size, x, y, r, (px, py) => {
105
+ const grad = g.createRadialGradient(px, py, 0, px, py, r);
106
+ grad.addColorStop(0, css(col, a));
107
+ grad.addColorStop(0.6, css(col, a * 0.7));
108
+ grad.addColorStop(1, css(col, 0));
109
+ g.fillStyle = grad;
110
+ g.fillRect(px - r, py - r, r * 2, r * 2);
111
+ });
112
+ }
113
+ // A few washes that dried with a tide line: the hand of the painter.
114
+ const hard = [PIGMENTS.sap, PIGMENTS.olive, PIGMENTS.ochre, PIGMENTS.leaf];
115
+ for (let i = 0; i < 9; i++) {
116
+ const x = rnd() * size;
117
+ const y = rnd() * size;
118
+ const r = 40 + rnd() * 90;
119
+ const col = hard[i % hard.length] ?? PIGMENTS.sap;
120
+ tiled(size, x, y, r * 1.4, (px, py) => wash(g, px, py, r, col, 0.06 + rnd() * 0.04, seeded(seed * 100 + i)));
121
+ }
122
+ g.globalCompositeOperation = "source-over";
123
+ // Pen grass: little fans of strokes, in clumps.
124
+ g.lineCap = "round";
125
+ for (let k = 0; k < 70; k++) {
126
+ const cx = rnd() * size;
127
+ const cy = rnd() * size;
128
+ const n = 4 + Math.floor(rnd() * 8);
129
+ for (let i = 0; i < n; i++) {
130
+ const x = cx + (rnd() - 0.5) * 70;
131
+ const y = cy + (rnd() - 0.5) * 50;
132
+ const blades = 3 + Math.floor(rnd() * 3);
133
+ const h = 7 + rnd() * 9;
134
+ const tone = rnd() < 0.5 ? PIGMENTS.sepia : PIGMENTS.viridian;
135
+ tiled(size, x, y, 20, (px, py) => {
136
+ g.strokeStyle = css(tone, 0.55 + rnd() * 0.25);
137
+ g.lineWidth = 1 + rnd() * 0.7;
138
+ for (let b = 0; b < blades; b++) {
139
+ const a = -Math.PI / 2 + (b / (blades - 1) - 0.5) * 1.1 + (rnd() - 0.5) * 0.3;
140
+ g.beginPath();
141
+ g.moveTo(px + (b - blades / 2) * 1.6, py);
142
+ g.quadraticCurveTo(px + Math.cos(a) * h * 0.5, py + Math.sin(a) * h * 0.6, px + Math.cos(a) * h, py + Math.sin(a) * h);
143
+ g.stroke();
144
+ }
145
+ });
146
+ }
147
+ }
148
+ // Pebbles: a pale fill and an inked outline, a shadow stroke underneath.
149
+ for (let i = 0; i < 70; i++) {
150
+ const x = rnd() * size;
151
+ const y = rnd() * size;
152
+ const rx = 3 + rnd() * 7;
153
+ const ry = rx * (0.55 + rnd() * 0.3);
154
+ const rot = rnd() * Math.PI;
155
+ tiled(size, x, y, 16, (px, py) => {
156
+ g.fillStyle = css(PIGMENTS.stone, 0.9);
157
+ g.strokeStyle = css(PIGMENTS.ink, 0.8);
158
+ g.lineWidth = 1.2;
159
+ g.beginPath();
160
+ g.ellipse(px, py, rx, ry, rot, 0, Math.PI * 2);
161
+ g.fill();
162
+ g.stroke();
163
+ g.strokeStyle = css(PIGMENTS.umber, 0.5);
164
+ g.beginPath();
165
+ g.ellipse(px + 0.8, py + 1.2, rx, ry, rot, 0.2, Math.PI - 0.2);
166
+ g.stroke();
167
+ });
168
+ }
169
+ // Flowers: dots of colour with an inked ring, here and there.
170
+ const petals = [PIGMENTS.vermilion, PIGMENTS.ochre, PIGMENTS.rose, PIGMENTS.white];
171
+ for (let i = 0; i < 46; i++) {
172
+ const x = rnd() * size;
173
+ const y = rnd() * size;
174
+ const col = petals[i % petals.length] ?? PIGMENTS.ochre;
175
+ tiled(size, x, y, 10, (px, py) => {
176
+ g.fillStyle = css(col, 0.85);
177
+ g.beginPath();
178
+ g.arc(px, py, 2.4, 0, Math.PI * 2);
179
+ g.fill();
180
+ g.strokeStyle = css(PIGMENTS.ink, 0.6);
181
+ g.lineWidth = 0.9;
182
+ g.stroke();
183
+ });
184
+ }
185
+ return texture(c);
186
+ }
187
+ /**
188
+ * A pool of light as a painter leaves it: a soft wash with a ragged edge
189
+ * and a faint tide line, white with alpha (the template tints it). Not
190
+ * tiled: one pool per texture.
191
+ */
192
+ export function paintGlow(seed = 13, size = 256) {
193
+ const cv = canvas(size);
194
+ if (!cv)
195
+ return null;
196
+ const { c, g } = cv;
197
+ const rnd = seeded(seed);
198
+ const m = size / 2;
199
+ for (let k = 0; k < 5; k++) {
200
+ const r = m * (0.5 + k * 0.1);
201
+ blob(g, m + (rnd() - 0.5) * 6, m + (rnd() - 0.5) * 6, r, rnd, 11);
202
+ g.fillStyle = `rgba(255,255,255,${0.14 - k * 0.018})`;
203
+ g.fill();
204
+ }
205
+ blob(g, m, m, m * 0.86, rnd, 13);
206
+ g.strokeStyle = "rgba(255,255,255,0.32)";
207
+ g.lineWidth = 2.5;
208
+ g.stroke();
209
+ const t = new CanvasTexture(c);
210
+ t.colorSpace = SRGBColorSpace;
211
+ t.needsUpdate = true;
212
+ return t;
213
+ }
214
+ /** A paved yard tile: four slabs ruled in pencil, with stains and cracks. */
215
+ export function paintPaving(seed = 7, size = 1024) {
216
+ const cv = canvas(size);
217
+ if (!cv)
218
+ return null;
219
+ const { c, g } = cv;
220
+ const rnd = seeded(seed);
221
+ g.fillStyle = css(0xe6dfcf);
222
+ g.fillRect(0, 0, size, size);
223
+ const slabs = 4;
224
+ const s = size / slabs;
225
+ const tones = [PIGMENTS.slate, PIGMENTS.stone, PIGMENTS.cerulean, PIGMENTS.stone];
226
+ for (let i = 0; i < slabs; i++) {
227
+ for (let j = 0; j < slabs; j++) {
228
+ const col = tones[Math.floor(rnd() * tones.length)] ?? PIGMENTS.stone;
229
+ g.fillStyle = css(col, 0.14 + rnd() * 0.12);
230
+ g.fillRect(i * s + 3, j * s + 3, s - 6, s - 6);
231
+ }
232
+ }
233
+ for (let i = 0; i < 14; i++) {
234
+ const x = rnd() * size;
235
+ const y = rnd() * size;
236
+ const r = 20 + rnd() * 60;
237
+ tiled(size, x, y, r * 1.4, (px, py) => wash(g, px, py, r, i % 2 ? PIGMENTS.umber : PIGMENTS.paynes, 0.08 + rnd() * 0.06, seeded(seed * 50 + i)));
238
+ }
239
+ // Seams: two pencil passes each, slightly off one another.
240
+ g.strokeStyle = css(PIGMENTS.paynes, 0.55);
241
+ g.lineCap = "round";
242
+ for (let pass = 0; pass < 2; pass++) {
243
+ g.lineWidth = pass === 0 ? 2.2 : 1.1;
244
+ for (let i = 0; i <= slabs; i++) {
245
+ const p = i * s + (pass ? 1.5 : 0);
246
+ penLine(g, p, 0, p, size, rnd, 1.6);
247
+ penLine(g, 0, p, size, p, rnd, 1.6);
248
+ }
249
+ }
250
+ // Cracks: short zig-zags in ink.
251
+ g.strokeStyle = css(PIGMENTS.ink, 0.6);
252
+ g.lineWidth = 1;
253
+ for (let i = 0; i < 18; i++) {
254
+ let x = rnd() * size;
255
+ let y = rnd() * size;
256
+ g.beginPath();
257
+ g.moveTo(x, y);
258
+ const n = 3 + Math.floor(rnd() * 4);
259
+ let a = rnd() * Math.PI * 2;
260
+ for (let k = 0; k < n; k++) {
261
+ a += (rnd() - 0.5) * 1.4;
262
+ x += Math.cos(a) * (6 + rnd() * 12);
263
+ y += Math.sin(a) * (6 + rnd() * 12);
264
+ g.lineTo(x, y);
265
+ }
266
+ g.stroke();
267
+ }
268
+ // Hatched corners where the slabs meet, a draughtsman's habit.
269
+ g.strokeStyle = css(PIGMENTS.paynes, 0.35);
270
+ g.lineWidth = 1;
271
+ for (let i = 0; i < slabs; i++) {
272
+ for (let j = 0; j < slabs; j++) {
273
+ if (rnd() < 0.5)
274
+ continue;
275
+ const x = i * s + 6;
276
+ const y = j * s + 6;
277
+ for (let k = 0; k < 5; k++)
278
+ penLine(g, x + k * 5, y, x, y + k * 5, rnd, 0.6);
279
+ }
280
+ }
281
+ return texture(c);
282
+ }
@@ -0,0 +1,4 @@
1
+ import type { StyleKit } from "../kit.js";
2
+ /** Lines, hatching and wash borders are redrawn this many times a second (the boil). */
3
+ export declare const BOIL_FPS = 8;
4
+ export declare const handdrawnKit: StyleKit;