@solidrt/2d 0.0.71 → 0.0.72

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.
@@ -0,0 +1,368 @@
1
+ // Materials: GLSL paired with pipeline state, attached to a LAYER - the
2
+ // layer is the batch (its atlases bound together, N quads in one draw per
3
+ // view), so the material sits on the thing that is one draw entry, as
4
+ // @solidrt/3d's sits on the mesh. Two 2d sprites with different materials
5
+ // are two layers, which is also how you order them (an additive glow
6
+ // layer draws over the scene layer). Deduped hard, the 3d way: one
7
+ // program per material CLASS and atlas list met (the fragment set is
8
+ // generated per atlas list, spriteSource), one render pipeline per
9
+ // instance-layout list met on top (the node layer binds a pose and a
10
+ // sprite buffer, the records and tile layers one record buffer; a
11
+ // pipeline is program + attribute list, so the program never recompiles
12
+ // for another layer kind), created lazily at first use and kept for the
13
+ // class's lifetime. A material INSTANCE is the params and sampler
14
+ // bindings it contributes to every view and chunk target of its layer.
15
+ //
16
+ // Three tiers of custom look, the same three as @solidrt/3d's: a custom
17
+ // vertex stage under the stock fragment (shaderMaterialClass with
18
+ // unlitFragment from ./glsl), a `surface` function inside the stock
19
+ // material (unlit's prelude/surface slot over the Sprite struct), or a
20
+ // fragment of your own over the layer's set (shaderMaterialClass). Per-
21
+ // sprite data for any of them is the material's `instanceBuffers`: an
22
+ // app-owned style record per sprite in any vertex format, written with
23
+ // setInstanceStyle / instanceAttribute / records(layer, stream), the 3d
24
+ // vocabulary unchanged.
25
+ //
26
+ // `blend` is pipeline state and lives here, as in 3d; the deliberate
27
+ // asymmetry is that 3d's `blend` implies `transparent` and the scene's
28
+ // back-to-front sort, while a layer has no depth and draws in record
29
+ // order, so nothing sorts.
30
+ import { compileShader, createBuffer, createRenderPipeline, destroyBuffer, destroyProgram, destroyRenderPipeline, destroyShader, encodeRecord, checkLayout, layoutComponents, layoutKey, linkProgram } from "@solidrt/core/gpu"
31
+ import type { BlendMode, BufferId, ProgramId, RenderPipelineId, ShaderParams, TextureBindings, VertexAttribute, VertexBufferLayout } from "@solidrt/core/gpu"
32
+ import { forwardedName, LAYER_ATTRIBUTE_NAMES, spriteSource, unlitFragment, unlitVertex, VERTEX_UNIFORMS } from "./glsl.ts"
33
+ import type { AtlasShading, UnlitSourceOptions } from "./glsl.ts"
34
+
35
+ /** What a layer draws with once its material met its atlases and its
36
+ * instance layouts: the pipeline and the unit quad every instance
37
+ * reuses (the class's, shared by every layer of the class). */
38
+ export type SpritePipeline = { pipeline: RenderPipelineId; quad: BufferId }
39
+
40
+ export type Material = {
41
+ /**
42
+ * The pipeline this material draws a layer with, for the layer's
43
+ * atlases (the fragment set is generated for them: one sampler per
44
+ * atlas, the distance-field decodes) and its instance layouts (the
45
+ * layer's own records, the material's instance buffers after them) -
46
+ * lazily created, one program per atlas list and one pipeline per
47
+ * layout list met. The layers call it at creation.
48
+ */
49
+ pipeline(atlases: readonly AtlasShading[], layouts: readonly VertexAttribute[][], label: string): SpritePipeline
50
+ /** The uniform values this material seeds on every target of its
51
+ * layer (a view, a tile chunk); the layer's own params apply over them. */
52
+ params: ShaderParams
53
+ /** Sampler bindings beyond the atlases (a palette LUT, a noise
54
+ * texture), bound on every target of the layer. They share the
55
+ * device's sampler budget with the atlases (limits.maxTextureUnits). */
56
+ textures?: TextureBindings
57
+ /** How the layer's sprites blend into its targets: pipeline state,
58
+ * fixed with the material. */
59
+ blend: BlendMode
60
+ /** The instance buffers the pipeline declares after the layer's own
61
+ * (shaderMaterialClass's `instanceBuffers`): one per-sprite style
62
+ * record stream each, app-owned, in any vertex format. */
63
+ instanceBuffers?: VertexBufferLayout[]
64
+ /** The style record a fresh sprite starts with (one value per
65
+ * component of the FIRST instance buffer's attributes, in order);
66
+ * absent = zeros. */
67
+ instanceStyle?: ArrayLike<number>
68
+ /** Present on materials that own their pipeline (shaderMaterial). */
69
+ dispose?(): void
70
+ }
71
+
72
+ export type ShaderMaterialClassOptions = {
73
+ /**
74
+ * Vertex stage GLSL. MUST declare and use `uniform vec4 uCamera`,
75
+ * `uniform vec4 uCameraRot` and `uniform vec2 uViewport` (the view's
76
+ * camera mapping, written per view) - a source mentioning any of them
77
+ * not at all throws right here - and write the varyings the fragment
78
+ * set reads (SPRITE_VARYINGS from `@solidrt/2d/glsl`, SPRITE_VERTEX_BODY
79
+ * writes them). Declare any of the layer's record attributes by name
80
+ * (iPos, iRot, iScale, iUv, iTint, iScreenPx, iAtlas, iOutline - the
81
+ * same names on every layer kind) and your own instance buffers';
82
+ * undeclared ones are skipped. SPRITE_VERTEX is the stock stage.
83
+ */
84
+ vertex: string
85
+ /**
86
+ * Fragment stage GLSL over the layer's SET, which the layer prepends:
87
+ * the varyings, the atlas samplers, `uTint`, the `Sprite` struct,
88
+ * `spriteOf()` and `spriteSample(uv)` (spriteSource in `./glsl`) are
89
+ * declared for you - declare none of them. Start main with `Sprite s
90
+ * = spriteOf();` (the derivatives every tap uses are taken there);
91
+ * `unlitFragment(options)` is the stock one.
92
+ */
93
+ fragment: string
94
+ /**
95
+ * Instance buffers: one layout per per-sprite buffer (`{ attributes
96
+ * }`: any vertex format, tightly packed in list order; stepMode is
97
+ * "instance" by definition here), bound after the layer's own records.
98
+ * The vertex stage reads the attributes as `in` variables beside the
99
+ * record's, and each drawn sprite gets one record from each buffer:
100
+ * the material's STYLE record(s), written with setInstanceStyle (the
101
+ * first buffer), instanceAttribute + updateRecords (any), or
102
+ * records(layer, stream) in bulk. A palette index is a `uint8x4` at
103
+ * four bytes, a dissolve amount a `float16x2`. Names may not reuse the
104
+ * layer's own (LAYER_ATTRIBUTE_NAMES).
105
+ */
106
+ instanceBuffers?: VertexBufferLayout[]
107
+ /** The style record a fresh sprite starts with (one value per
108
+ * component of the first instance buffer's attributes, in order);
109
+ * default zeros. The identity of whatever the stage does with the
110
+ * record - white for a multiplied tint. */
111
+ instanceStyle?: ArrayLike<number>
112
+ /**
113
+ * How the sprites blend into the layer's targets, core gpu's
114
+ * BlendMode; default "alpha" (premultiplied source-over in draw order,
115
+ * the fragment writing premultiplied output). "add" for glows,
116
+ * explosions and additive particles, "multiply" to darken, "none" to
117
+ * overwrite.
118
+ */
119
+ blend?: BlendMode
120
+ label?: string
121
+ }
122
+
123
+ /** The instance half of a shader material: uniform seeds and sampler
124
+ * bindings for one parameterisation of a class's program. */
125
+ export type ShaderMaterialInstanceOptions = {
126
+ /** Uniform seeds beyond the standard set (a prelude's uniforms);
127
+ * update per layer later with setLayerParams. */
128
+ params?: ShaderParams
129
+ /** Sampler bindings beyond the atlases; names may not be an atlas
130
+ * sampler's (uAtlasN). */
131
+ textures?: TextureBindings
132
+ }
133
+
134
+ export type ShaderMaterialOptions = ShaderMaterialClassOptions & ShaderMaterialInstanceOptions
135
+
136
+ /**
137
+ * One program and pipeline, many parameterisations: the class/instance
138
+ * split unlit has internally, for your own GLSL. `instance()` returns a
139
+ * Material sharing the class's pipelines with its own params/textures -
140
+ * the class compiles once per atlas list met, and dispose() is on the
141
+ * class alone (instances hold nothing of their own).
142
+ */
143
+ export type ShaderMaterialClass = {
144
+ instance(opts?: ShaderMaterialInstanceOptions): Material
145
+ /** Destroy the shared programs, pipelines and quad. Layers still
146
+ * drawing with an instance draw nothing valid afterwards. */
147
+ dispose(): void
148
+ }
149
+
150
+ // The atlas sampler names a material texture may not take.
151
+ const ATLAS_UNIFORM = /^uAtlas\d+$/
152
+
153
+ /** The key of an atlas list for the program cache: the shading of each
154
+ * atlas in order (a colour atlas, or a field with its range), which is
155
+ * exactly what spriteSource varies on. */
156
+ function atlasKey(atlases: readonly AtlasShading[]): string {
157
+ return atlases.map(a => (a.sdf === undefined ? "rgb" : "sdf" + a.sdf.range)).join(",")
158
+ }
159
+
160
+ /** The texture bindings' validation, shared by the instance forms:
161
+ * a name an atlas sampler takes is a throw. */
162
+ function checkTextures(site: string, textures: TextureBindings | undefined): void {
163
+ if (textures === undefined) return
164
+ for (let name of Object.keys(textures)) {
165
+ if (ATLAS_UNIFORM.test(name)) throw new Error(`${site}: texture '${name}' is an atlas sampler's name; the layer binds those itself`)
166
+ }
167
+ }
168
+
169
+ /**
170
+ * A material class from your own GLSL: sources without a `#version` line
171
+ * get the standard pipeline preamble (`fragColor`, `iResolution`), the
172
+ * fragment behind the layer's generated set. Two calls with identical
173
+ * sources compile two programs - there is no dedupe by source value (a
174
+ * hidden cache keyed by content is the anti-pattern the GPU layer avoids
175
+ * throughout); the class IS the app-owned split. Create one per program
176
+ * at app scope, `instance()` per look, and `dispose()` the class when
177
+ * the app is done with the look for good.
178
+ */
179
+ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMaterialClass {
180
+ // The camera contract, checked where the mistake is made: a vertex
181
+ // stage that never mentions the mapping uniforms cannot place sprites,
182
+ // and with shared params skipping undeclared names the omission would
183
+ // otherwise surface as a blank view, not an error.
184
+ for (let name of VERTEX_UNIFORMS) {
185
+ if (!new RegExp("\\b" + name + "\\b").test(opts.vertex)) {
186
+ throw new Error(`shaderMaterial vertex stage must declare and use '${name}' (the camera mapping; see SPRITE_VARYINGS in @solidrt/2d/glsl)`)
187
+ }
188
+ }
189
+ // Records are tightly packed like the layer's own; explicit strides and
190
+ // offsets are the engine's, reachable through core's own pipeline API.
191
+ // An unknown format would otherwise make a NaN stride at the layer.
192
+ for (let layout of opts.instanceBuffers ?? []) {
193
+ if (layout.stepMode === "vertex") throw new Error("shaderMaterial instanceBuffers are instance-step by definition; drop stepMode")
194
+ if (layout.arrayStride !== undefined || layout.attributes.some(a => a.offset !== undefined)) {
195
+ throw new Error("shaderMaterial instanceBuffers are tightly packed records; drop arrayStride and offset")
196
+ }
197
+ checkLayout(layout.attributes, "shaderMaterial instanceBuffers")
198
+ for (let attr of layout.attributes) {
199
+ if (LAYER_ATTRIBUTE_NAMES.includes(attr.name)) throw new Error(`shaderMaterial instance attribute '${attr.name}' is one of the layer's own record attributes; pick another name`)
200
+ }
201
+ }
202
+ // An empty list declares nothing - same as absent.
203
+ let instanceBuffers = opts.instanceBuffers?.length ? opts.instanceBuffers.map(b => ({ stepMode: "instance" as const, attributes: b.attributes.map(a => ({ ...a })) })) : undefined
204
+ if (opts.instanceStyle !== undefined) {
205
+ if (instanceBuffers === undefined) throw new Error("shaderMaterial instanceStyle needs an instance buffer to describe")
206
+ // Encoded once here so a wrong length throws at creation, not at the
207
+ // first addSprite.
208
+ encodeRecord(instanceBuffers[0]!.attributes, opts.instanceStyle, "shaderMaterial instanceStyle")
209
+ }
210
+ let blend = opts.blend ?? "alpha"
211
+ let label = opts.label ?? "shader-material"
212
+ let quad: BufferId | undefined
213
+ let programs = new Map<string, ProgramId>()
214
+ let pipelines = new Map<string, RenderPipelineId>()
215
+ let quadFor = (): BufferId => {
216
+ // One unit quad (triangle strip), reused by every instance of every
217
+ // layer of the class.
218
+ if (quad === undefined) quad = createBuffer(new Float32Array([-0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5]), { label: `${label}-quad`, autoFree: false })
219
+ return quad
220
+ }
221
+ let programFor = (atlases: readonly AtlasShading[]): ProgramId => {
222
+ let key = atlasKey(atlases)
223
+ let program = programs.get(key)
224
+ if (program === undefined) {
225
+ let vs = compileShader("vertex", opts.vertex, { header: true })
226
+ let fs = compileShader("fragment", spriteSource(atlases) + opts.fragment, { header: true })
227
+ program = linkProgram(vs, fs, { label })
228
+ destroyShader(vs)
229
+ destroyShader(fs)
230
+ programs.set(key, program)
231
+ }
232
+ return program
233
+ }
234
+ let pipelineFor = (atlases: readonly AtlasShading[], layouts: readonly VertexAttribute[][], layerLabel: string): SpritePipeline => {
235
+ let key = atlasKey(atlases) + "|" + layouts.map(l => layoutKey(l)).join("|")
236
+ let pipeline = pipelines.get(key)
237
+ if (pipeline === undefined) {
238
+ pipeline = createRenderPipeline(programFor(atlases), {
239
+ label: `${label}-${layerLabel}`,
240
+ topology: "triangle-strip",
241
+ buffers: [{ attributes: [{ name: "aPos", format: "float32x2" }] }, ...layouts.map(attributes => ({ stepMode: "instance" as const, attributes })), ...(instanceBuffers ?? [])],
242
+ blend,
243
+ })
244
+ pipelines.set(key, pipeline)
245
+ }
246
+ return { pipeline, quad: quadFor() }
247
+ }
248
+ return {
249
+ instance(inst = {}) {
250
+ checkTextures("shaderMaterial instance", inst.textures)
251
+ return {
252
+ pipeline: pipelineFor,
253
+ params: inst.params ?? {},
254
+ textures: inst.textures,
255
+ blend,
256
+ instanceBuffers,
257
+ instanceStyle: opts.instanceStyle,
258
+ }
259
+ },
260
+ dispose() {
261
+ for (let pipeline of pipelines.values()) destroyRenderPipeline(pipeline)
262
+ pipelines.clear()
263
+ for (let program of programs.values()) destroyProgram(program)
264
+ programs.clear()
265
+ if (quad !== undefined) {
266
+ destroyBuffer(quad)
267
+ quad = undefined
268
+ }
269
+ },
270
+ }
271
+ }
272
+
273
+ /**
274
+ * A material from your own GLSL: the custom-look escape hatch, first-class
275
+ * next to unlit. A class with a single instance - `shaderMaterialClass()`
276
+ * is the form for one program with many parameterisations. The INSTANCE
277
+ * is the pipeline handle: two calls with identical sources compile two
278
+ * programs. Create one per look at app scope, share it across layers,
279
+ * and `dispose()` it if the app is done with the look for good.
280
+ */
281
+ export function shaderMaterial(opts: ShaderMaterialOptions): Material {
282
+ let cls = shaderMaterialClass(opts)
283
+ let material = cls.instance(opts)
284
+ material.dispose = cls.dispose
285
+ return material
286
+ }
287
+
288
+ export type UnlitOptions = Omit<UnlitSourceOptions, "forwards"> & {
289
+ /** See ShaderMaterialClassOptions.blend; default "alpha". */
290
+ blend?: BlendMode
291
+ /** Uniform seeds for the prelude's uniforms (the layer's params apply
292
+ * over them; update later with `layer.setParams`). */
293
+ params?: ShaderParams
294
+ /** Sampler bindings the prelude declares (a palette LUT, a noise
295
+ * texture), beyond the atlases. */
296
+ textures?: TextureBindings
297
+ /**
298
+ * Per-sprite style record(s) the surface reads, see
299
+ * ShaderMaterialClassOptions.instanceBuffers. The stock vertex stage
300
+ * forwards every attribute unchanged as a flat varying - `iPalette`
301
+ * reaches the prelude and the surface as `vPalette` (forwardedName),
302
+ * so the names follow the layer's own `i` + capital convention - and
303
+ * the app writes the records with setInstanceStyle, instanceAttribute
304
+ * or records(layer, stream). A palette index per sprite, a flash
305
+ * amount, a dissolve threshold: the data a look varies on.
306
+ */
307
+ instanceBuffers?: VertexBufferLayout[]
308
+ /** See ShaderMaterialClassOptions.instanceStyle. */
309
+ instanceStyle?: ArrayLike<number>
310
+ label?: string
311
+ }
312
+
313
+ // One shaderMaterialClass per unlit option combination (blend x prelude x
314
+ // surface x instance layouts), cached for the app's lifetime like 3d's;
315
+ // one program per atlas list and one pipeline per layout list inside
316
+ // each.
317
+ let unlitClasses = new Map<string, ShaderMaterialClass>()
318
+
319
+ /**
320
+ * The stock material, the one every layer draws with unless told
321
+ * otherwise: the atlas texel times the sprite's tint (a distance-field
322
+ * atlas decoded to its fill and outline), times the layer tint, blended
323
+ * with `blend`. `prelude`/`surface` is the tier-2 slot: `void
324
+ * surface(inout Sprite s)` called with the struct filled and before the
325
+ * layer tint - a palette lookup, a hit flash, a dissolve, a scrolling
326
+ * `spriteSample` - with the prelude's uniforms as layer params and the
327
+ * `instanceBuffers` records as flat varyings. One program per distinct
328
+ * source, keyed like every option; the name says the shading model,
329
+ * Unity's Sprite-Unlit, which a 2d `lit` would differ on.
330
+ */
331
+ export function unlit(opts: UnlitOptions = {}): Material {
332
+ let blend = opts.blend ?? "alpha"
333
+ let prelude = opts.prelude ?? ""
334
+ let surface = opts.surface ?? ""
335
+ let instanceBuffers = opts.instanceBuffers?.length ? opts.instanceBuffers : undefined
336
+ let forwards = instanceBuffers?.flatMap(b => b.attributes) ?? []
337
+ for (let attr of forwards) forwardedName(attr.name)
338
+ let key = [blend, prelude, surface, forwards.map(a => a.name + ":" + a.format).join(","), opts.instanceStyle === undefined ? "" : Array.from(opts.instanceStyle).join(" ")].join("|")
339
+ let cls = unlitClasses.get(key)
340
+ if (cls === undefined) {
341
+ cls = shaderMaterialClass({
342
+ vertex: unlitVertex(forwards),
343
+ fragment: unlitFragment({ prelude, surface, forwards }),
344
+ instanceBuffers,
345
+ instanceStyle: opts.instanceStyle,
346
+ blend,
347
+ label: opts.label ?? "unlit",
348
+ })
349
+ unlitClasses.set(key, cls)
350
+ }
351
+ return cls.instance({ params: opts.params, textures: opts.textures })
352
+ }
353
+
354
+ /** The sampler count a material binds beyond the atlases: what
355
+ * checkAtlases subtracts from the device's budget. Internal. */
356
+ export function materialSamplers(material: Material): number {
357
+ return material.textures === undefined ? 0 : Object.keys(material.textures).length
358
+ }
359
+
360
+ /** The style record a fresh sprite of `material` starts with in its
361
+ * instance buffer `i`: the material's instanceStyle encoded for the
362
+ * first buffer, zeros for every other. Internal - the layers copy it
363
+ * into every new slot. */
364
+ export function materialBlank(material: Material, i: number): Uint8Array {
365
+ let layout = material.instanceBuffers![i]!.attributes
366
+ let values = i === 0 && material.instanceStyle !== undefined ? material.instanceStyle : new Array<number>(layoutComponents(layout)).fill(0)
367
+ return encodeRecord(layout, values, "instanceStyle")
368
+ }