@solidrt/2d 0.0.56 → 0.0.58
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/AGENTS.md +560 -267
- package/README.md +3 -3
- package/examples/README.md +38 -13
- package/examples/anim.tsx +6 -8
- package/examples/camera-probe.tsx +16 -14
- package/examples/camera.tsx +21 -15
- package/examples/parity.tsx +11 -9
- package/examples/pick.tsx +43 -7
- package/examples/split-screen.tsx +111 -0
- package/examples/springs.tsx +6 -9
- package/examples/sprites.tsx +9 -12
- package/examples/tiles.tsx +24 -9
- package/examples/views.tsx +177 -0
- package/package.json +2 -2
- package/src/atlas.ts +35 -33
- package/src/camera.ts +26 -1
- package/src/camera2d.ts +692 -119
- package/src/components/auto-oversample.ts +27 -0
- package/src/components/camera2d.tsx +82 -0
- package/src/components/context.ts +48 -0
- package/src/components/group.tsx +68 -0
- package/src/components/index.ts +28 -0
- package/src/components/sprite-layer.tsx +312 -0
- package/src/components/sprite.tsx +72 -0
- package/src/components/tile-layer.tsx +160 -0
- package/src/components/view2d.tsx +151 -0
- package/src/dispatch.ts +7 -7
- package/src/frames.ts +63 -14
- package/src/index.ts +27 -11
- package/src/input.ts +50 -0
- package/src/layer.ts +384 -245
- package/src/oversample-math.ts +1 -1
- package/src/records.ts +33 -138
- package/src/shaders.ts +65 -3
- package/src/tiles.ts +195 -42
- package/src/views.ts +294 -0
- package/src/camera-motion.ts +0 -532
- package/src/components.tsx +0 -611
package/src/oversample-math.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The pure oversample math: arithmetic on plain numbers, no GPU or window
|
|
2
2
|
// imports, so the headless check (checks/oversample-check.ts) drives it
|
|
3
|
-
// directly. oversample.ts wraps it with the device limits; components
|
|
3
|
+
// directly. oversample.ts wraps it with the device limits; components/
|
|
4
4
|
// adds the measured inputs (window box, display scale, camera).
|
|
5
5
|
|
|
6
6
|
/** Tolerance under a whole number when rounding a display scale up, so a
|
package/src/records.ts
CHANGED
|
@@ -20,26 +20,14 @@
|
|
|
20
20
|
// (addSprite/setSprite/...) are shared with the node layer; picking here is
|
|
21
21
|
// the JS reverse walk (pointInSprite), since records have no nodes.
|
|
22
22
|
import { getOwner, onCleanup } from "@solidrt/core"
|
|
23
|
-
import {
|
|
24
|
-
beginBufferWrite,
|
|
25
|
-
createBuffer,
|
|
26
|
-
createPipelineTexture,
|
|
27
|
-
destroyBuffer,
|
|
28
|
-
destroyTexture,
|
|
29
|
-
endBufferWrite,
|
|
30
|
-
setDraw,
|
|
31
|
-
setTargetParams,
|
|
32
|
-
setTargetSize,
|
|
33
|
-
} from "@solidrt/core/gpu"
|
|
23
|
+
import { beginBufferWrite, createBuffer, destroyBuffer, endBufferWrite } from "@solidrt/core/gpu"
|
|
34
24
|
import type { BufferId, TextureId } from "@solidrt/core/gpu"
|
|
35
|
-
import { checkCamera, projectCamera, unprojectCamera } from "./camera.ts"
|
|
36
25
|
import { FULL_FRAME, writeFrame } from "./frames.ts"
|
|
37
|
-
import { spriteDispatch } from "./dispatch.ts"
|
|
38
26
|
import { checkTint, readFrame } from "./layer.ts"
|
|
39
|
-
import type { LayerBase,
|
|
27
|
+
import type { LayerBase, Sprite, SpriteLayerOptions, SpriteOptions, SpriteState } from "./layer.ts"
|
|
40
28
|
import { pointInSprite } from "./pick.ts"
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
29
|
+
import { createSpritePipeline, INSTANCE_ATTRIBUTES, VERTEX } from "./shaders.ts"
|
|
30
|
+
import { createViews } from "./views.ts"
|
|
43
31
|
|
|
44
32
|
// Floats per instance record:
|
|
45
33
|
// [cx, cy, w, h, u0, v0, u1, v1, rot, tintR, tintG, tintB, tintA]
|
|
@@ -92,33 +80,21 @@ export type RecordLayer = LayerBase & {
|
|
|
92
80
|
}
|
|
93
81
|
|
|
94
82
|
/**
|
|
95
|
-
* Create a records layer
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* out with `{ autoFree: false }`).
|
|
83
|
+
* Create a records layer over one atlas texture (the atlas is NOT owned).
|
|
84
|
+
* Like createSpriteLayer it renders nothing by itself: `createView` is
|
|
85
|
+
* where it shows. Disposed automatically with the owning reactive scope
|
|
86
|
+
* (opt out with `{ autoFree: false }`).
|
|
99
87
|
*/
|
|
100
|
-
export function createRecordLayer(
|
|
101
|
-
width: number,
|
|
102
|
-
height: number,
|
|
103
|
-
atlas: TextureId,
|
|
104
|
-
opts?: RecordLayerOptions,
|
|
105
|
-
): RecordLayer {
|
|
88
|
+
export function createRecordLayer(atlas: TextureId, opts?: RecordLayerOptions): RecordLayer {
|
|
106
89
|
let capacity = opts?.capacity ?? 1024
|
|
107
90
|
if (!(capacity > 0 && Number.isInteger(capacity))) {
|
|
108
91
|
throw new Error(`createRecordLayer: capacity must be a positive integer, got ${capacity}`)
|
|
109
92
|
}
|
|
110
93
|
let label = opts?.label ?? "sprites"
|
|
111
|
-
// One unit quad (triangle strip), reused by every instance.
|
|
112
|
-
let quad = createBuffer(new Float32Array([-0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5]), {
|
|
113
|
-
label: `${label}-quad`,
|
|
114
|
-
autoFree: false,
|
|
115
|
-
})
|
|
116
94
|
let records: BufferId = createBuffer(capacity * FLOATS_PER_SPRITE * 4, {
|
|
117
95
|
label: `${label}-records`,
|
|
118
96
|
autoFree: false,
|
|
119
97
|
})
|
|
120
|
-
let oversample = opts?.oversample ?? 1
|
|
121
|
-
checkOversample("createRecordLayer", oversample, width, height)
|
|
122
98
|
let tint = opts?.tint ?? [1, 1, 1, 1]
|
|
123
99
|
checkTint("createRecordLayer", tint)
|
|
124
100
|
let orderBy = opts?.orderBy
|
|
@@ -128,37 +104,8 @@ export function createRecordLayer(
|
|
|
128
104
|
: orderBy === "y"
|
|
129
105
|
? { field: Y_FIELD_OFFSET }
|
|
130
106
|
: { field: orderBy.field, descending: orderBy.descending }
|
|
131
|
-
let
|
|
132
|
-
let texture = createPipelineTexture(
|
|
133
|
-
VERTEX,
|
|
134
|
-
FRAGMENT,
|
|
135
|
-
width * oversample,
|
|
136
|
-
height * oversample,
|
|
137
|
-
{ uViewport: [width, height], uCamera: [0, 0, 1, 1], uCameraRot: [1, 0, 0, 0], uTint: tint },
|
|
138
|
-
{
|
|
139
|
-
label,
|
|
140
|
-
topology: "triangle-strip",
|
|
141
|
-
vertexCount: 4,
|
|
142
|
-
attributes: [{ name: "aPos", format: "vec2" }],
|
|
143
|
-
buffer: quad,
|
|
144
|
-
instanceAttributes: INSTANCE_ATTRIBUTES,
|
|
145
|
-
instanceBuffer: records,
|
|
146
|
-
instanceOrder,
|
|
147
|
-
instanceCount: 0,
|
|
148
|
-
blend: "alpha",
|
|
149
|
-
textures: { uAtlas: atlas },
|
|
150
|
-
clearColor: opts?.clearColor ?? [0, 0, 0, 0],
|
|
151
|
-
autoFree: false,
|
|
152
|
-
},
|
|
153
|
-
)
|
|
107
|
+
let gpu = createSpritePipeline(label, VERTEX, INSTANCE_ATTRIBUTES)
|
|
154
108
|
|
|
155
|
-
// Camera state, mirrored for picking (the inverse mapping).
|
|
156
|
-
let camX = 0
|
|
157
|
-
let camY = 0
|
|
158
|
-
let camZoom = 1
|
|
159
|
-
let camRot = 0
|
|
160
|
-
let camPivotX = 0
|
|
161
|
-
let camPivotY = 0
|
|
162
109
|
let disposed = false
|
|
163
110
|
let dirty = false
|
|
164
111
|
let scheduled = false
|
|
@@ -167,7 +114,7 @@ export function createRecordLayer(
|
|
|
167
114
|
// The GPU buffer's record capacity; the canonical array grows ahead of it
|
|
168
115
|
// (addSprite) and the publish catches the buffer up: a larger buffer is
|
|
169
116
|
// created, written in full, swapped in, and the old one destroyed. The
|
|
170
|
-
//
|
|
117
|
+
// entries hold the old buffer alive until the swaps land, so the destroy
|
|
171
118
|
// is safe to issue right after.
|
|
172
119
|
let gpuCapacity = capacity
|
|
173
120
|
let flush = () => {
|
|
@@ -185,7 +132,8 @@ export function createRecordLayer(
|
|
|
185
132
|
out.set(layer.records.subarray(0, count * FLOATS_PER_SPRITE))
|
|
186
133
|
endBufferWrite(target, count * FLOATS_PER_SPRITE * 4)
|
|
187
134
|
if (grown !== null) {
|
|
188
|
-
|
|
135
|
+
views.setBuffers({ instanceBuffer: grown })
|
|
136
|
+
views.setCount(count)
|
|
189
137
|
if (instanceOrder !== undefined) {
|
|
190
138
|
// The growth publish above landed BEFORE the swap (the entry must
|
|
191
139
|
// never point at an unwritten buffer), so the order had not yet
|
|
@@ -200,7 +148,7 @@ export function createRecordLayer(
|
|
|
200
148
|
records = grown
|
|
201
149
|
published = count
|
|
202
150
|
} else if (count !== published) {
|
|
203
|
-
|
|
151
|
+
views.setCount(count)
|
|
204
152
|
published = count
|
|
205
153
|
}
|
|
206
154
|
}
|
|
@@ -239,71 +187,31 @@ export function createRecordLayer(
|
|
|
239
187
|
}
|
|
240
188
|
}
|
|
241
189
|
|
|
242
|
-
let
|
|
190
|
+
let views = createViews({
|
|
191
|
+
label,
|
|
192
|
+
pipeline: gpu.pipeline,
|
|
193
|
+
quad: gpu.quad,
|
|
194
|
+
atlas,
|
|
195
|
+
buffers: () => ({ instanceBuffer: records }),
|
|
196
|
+
count: () => published,
|
|
197
|
+
tint: () => tint,
|
|
198
|
+
pick: (x, y) => layer.pick(x, y),
|
|
199
|
+
order: instanceOrder,
|
|
200
|
+
})
|
|
243
201
|
|
|
244
202
|
let layer: RecordLayer = {
|
|
245
|
-
texture,
|
|
246
|
-
handlers: undefined as unknown as SpriteHandlers,
|
|
247
203
|
get count() {
|
|
248
204
|
return layer._order.length
|
|
249
205
|
},
|
|
250
|
-
get width() {
|
|
251
|
-
return width
|
|
252
|
-
},
|
|
253
|
-
get height() {
|
|
254
|
-
return height
|
|
255
|
-
},
|
|
256
|
-
setSize(w, h) {
|
|
257
|
-
if (disposed || (w === width && h === height)) return
|
|
258
|
-
checkOversample("setSize", oversample, w, h)
|
|
259
|
-
width = w
|
|
260
|
-
height = h
|
|
261
|
-
setTargetSize(texture, w * oversample, h * oversample)
|
|
262
|
-
setTargetParams(texture, { uViewport: [w, h] })
|
|
263
|
-
},
|
|
264
|
-
listen(listener) {
|
|
265
|
-
listeners.add(listener)
|
|
266
|
-
return () => {
|
|
267
|
-
listeners.delete(listener)
|
|
268
|
-
}
|
|
269
|
-
},
|
|
270
|
-
get oversample() {
|
|
271
|
-
return oversample
|
|
272
|
-
},
|
|
273
|
-
setOversample(n) {
|
|
274
|
-
if (disposed || n === oversample) return
|
|
275
|
-
checkOversample("setOversample", n, width, height)
|
|
276
|
-
thrash()
|
|
277
|
-
oversample = n
|
|
278
|
-
setTargetSize(texture, width * n, height * n)
|
|
279
|
-
},
|
|
280
|
-
setCamera(update) {
|
|
281
|
-
if (disposed) return
|
|
282
|
-
checkCamera(update)
|
|
283
|
-
if (update.x !== undefined) camX = update.x
|
|
284
|
-
if (update.y !== undefined) camY = update.y
|
|
285
|
-
if (update.zoom !== undefined) camZoom = update.zoom
|
|
286
|
-
if (update.rotation !== undefined) camRot = update.rotation
|
|
287
|
-
if (update.pivotX !== undefined) camPivotX = update.pivotX
|
|
288
|
-
if (update.pivotY !== undefined) camPivotY = update.pivotY
|
|
289
|
-
setTargetParams(texture, {
|
|
290
|
-
uCamera: [camX, camY, camZoom, camZoom],
|
|
291
|
-
uCameraRot: [Math.cos(camRot), Math.sin(camRot), camPivotX, camPivotY],
|
|
292
|
-
})
|
|
293
|
-
},
|
|
294
|
-
camera() {
|
|
295
|
-
return { x: camX, y: camY, zoom: camZoom, rotation: camRot, pivotX: camPivotX, pivotY: camPivotY }
|
|
296
|
-
},
|
|
297
|
-
project(x, y) {
|
|
298
|
-
return projectCamera(layer.camera(), x, y)
|
|
299
|
-
},
|
|
300
|
-
unproject(x, y) {
|
|
301
|
-
return unprojectCamera(layer.camera(), x, y)
|
|
302
|
-
},
|
|
303
206
|
setTint(next) {
|
|
304
207
|
if (disposed) return
|
|
305
208
|
checkTint("setTint", next)
|
|
306
|
-
|
|
209
|
+
tint = next
|
|
210
|
+
views.setTint(next)
|
|
211
|
+
},
|
|
212
|
+
createView(vopts) {
|
|
213
|
+
if (disposed) throw new Error("createView: layer is disposed")
|
|
214
|
+
return views.create(vopts)
|
|
307
215
|
},
|
|
308
216
|
pick(x, y) {
|
|
309
217
|
// Topmost first: reverse draw order, exact rotated-rect containment.
|
|
@@ -317,18 +225,14 @@ export function createRecordLayer(
|
|
|
317
225
|
}
|
|
318
226
|
return out
|
|
319
227
|
},
|
|
320
|
-
handlersFor(layout) {
|
|
321
|
-
return dispatch(layout)
|
|
322
|
-
},
|
|
323
228
|
dispose() {
|
|
324
229
|
if (disposed) return
|
|
325
230
|
disposed = true
|
|
326
|
-
listeners.clear()
|
|
327
231
|
for (let sprite of layer._order) sprite.layer = null
|
|
328
232
|
layer._order.length = 0
|
|
329
|
-
|
|
233
|
+
views.dispose()
|
|
330
234
|
destroyBuffer(records)
|
|
331
|
-
|
|
235
|
+
gpu.dispose()
|
|
332
236
|
},
|
|
333
237
|
_add(opts) {
|
|
334
238
|
if (opts?.parent) {
|
|
@@ -405,15 +309,6 @@ export function createRecordLayer(
|
|
|
405
309
|
},
|
|
406
310
|
_order: [],
|
|
407
311
|
}
|
|
408
|
-
let dispatch = spriteDispatch({
|
|
409
|
-
size: () => [width, height],
|
|
410
|
-
camera: () => layer.camera(),
|
|
411
|
-
pick: (x, y) => layer.pick(x, y),
|
|
412
|
-
root: layer,
|
|
413
|
-
listeners,
|
|
414
|
-
})
|
|
415
|
-
layer.handlers = dispatch(null)
|
|
416
|
-
|
|
417
312
|
if (opts?.autoFree !== false && getOwner()) onCleanup(() => layer.dispose())
|
|
418
313
|
return layer
|
|
419
314
|
}
|
package/src/shaders.ts
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
// projectCamera in camera.ts. The differential checks guard the JS side
|
|
21
21
|
// against oracles but NOT against these shaders - if you touch one
|
|
22
22
|
// rotation, touch all.
|
|
23
|
-
import { glsl } from "@solidrt/core/gpu"
|
|
24
|
-
import type { InstanceAttribute, VertexAttribute } from "@solidrt/core/gpu"
|
|
23
|
+
import { compileShader, createBuffer, createRenderPipeline, destroyBuffer, destroyProgram, destroyRenderPipeline, destroyShader, glsl, linkProgram } from "@solidrt/core/gpu"
|
|
24
|
+
import type { BufferId, InstanceAttribute, RenderPipelineId, VertexAttribute } from "@solidrt/core/gpu"
|
|
25
25
|
|
|
26
26
|
export let VERTEX = glsl`
|
|
27
27
|
in vec2 aPos;
|
|
@@ -31,6 +31,7 @@ export let VERTEX = glsl`
|
|
|
31
31
|
in float iRot;
|
|
32
32
|
in vec4 iTint;
|
|
33
33
|
out vec2 vUv;
|
|
34
|
+
flat out vec4 vFrame;
|
|
34
35
|
out vec4 vTint;
|
|
35
36
|
uniform vec2 uViewport;
|
|
36
37
|
uniform vec4 uCamera;
|
|
@@ -45,6 +46,7 @@ export let VERTEX = glsl`
|
|
|
45
46
|
// World and clip are both y-down, so the mapping carries no flip.
|
|
46
47
|
gl_Position = vec4(screen / uViewport * 2.0 - 1.0, 0.0, 1.0);
|
|
47
48
|
vUv = mix(iUv.xy, iUv.zw, aPos + 0.5);
|
|
49
|
+
vFrame = iUv;
|
|
48
50
|
vTint = iTint;
|
|
49
51
|
}
|
|
50
52
|
`
|
|
@@ -52,14 +54,31 @@ export let VERTEX = glsl`
|
|
|
52
54
|
// uTint is the LAYER tint, multiplied over the per-instance vTint (identity
|
|
53
55
|
// [1, 1, 1, 1] - GLSL uniforms default to ZERO, so every pipeline creation
|
|
54
56
|
// must pin it explicitly or everything renders transparent black).
|
|
57
|
+
//
|
|
58
|
+
// The sample is clamped to its frame, pulled in by half a texel: a quad at
|
|
59
|
+
// a fractional position lands edge samples so close to the frame's edge
|
|
60
|
+
// that a linear tap (and float interpolation under nearest) reaches the
|
|
61
|
+
// cell next door, painting a one-texel line of the neighbour along the
|
|
62
|
+
// sprite's edge. The clamp keeps the quad's texel mapping 1:1 and only
|
|
63
|
+
// moves those edge samples onto the edge texel, so touching cells do not
|
|
64
|
+
// bleed and no gutter or inset is needed at mip level 0. It cannot help a
|
|
65
|
+
// mip chain, whose texels straddle the cell edge before sampling - that is
|
|
66
|
+
// okf/backlog/2d-atlas-extrude.md. min/max because a flipped frame stores
|
|
67
|
+
// its UVs swapped, and the inner min/max because a sub-texel frame would
|
|
68
|
+
// otherwise hand clamp() a lower bound above its upper one.
|
|
55
69
|
export let FRAGMENT = glsl`
|
|
56
70
|
in vec2 vUv;
|
|
71
|
+
flat in vec4 vFrame;
|
|
57
72
|
in vec4 vTint;
|
|
58
73
|
uniform sampler2D uAtlas;
|
|
59
74
|
uniform vec4 uTint;
|
|
60
75
|
|
|
61
76
|
void main() {
|
|
62
|
-
|
|
77
|
+
vec2 halfTexel = 0.5 / vec2(textureSize(uAtlas, 0));
|
|
78
|
+
vec2 lo = min(vFrame.xy, vFrame.zw) + halfTexel;
|
|
79
|
+
vec2 hi = max(vFrame.xy, vFrame.zw) - halfTexel;
|
|
80
|
+
vec2 uv = clamp(vUv, min(lo, hi), max(lo, hi));
|
|
81
|
+
fragColor = texture(uAtlas, uv) * vTint * uTint;
|
|
63
82
|
}
|
|
64
83
|
`
|
|
65
84
|
|
|
@@ -80,6 +99,7 @@ export let VERTEX_SPLIT = glsl`
|
|
|
80
99
|
in vec4 iUv;
|
|
81
100
|
in vec4 iTint;
|
|
82
101
|
out vec2 vUv;
|
|
102
|
+
flat out vec4 vFrame;
|
|
83
103
|
out vec4 vTint;
|
|
84
104
|
uniform vec2 uViewport;
|
|
85
105
|
uniform vec4 uCamera;
|
|
@@ -94,6 +114,7 @@ export let VERTEX_SPLIT = glsl`
|
|
|
94
114
|
// World and clip are both y-down, so the mapping carries no flip.
|
|
95
115
|
gl_Position = vec4(screen / uViewport * 2.0 - 1.0, 0.0, 1.0);
|
|
96
116
|
vUv = mix(iUv.xy, iUv.zw, aPos + 0.5);
|
|
117
|
+
vFrame = iUv;
|
|
97
118
|
vTint = iTint;
|
|
98
119
|
}
|
|
99
120
|
`
|
|
@@ -108,3 +129,44 @@ export const INSTANCE_ATTRIBUTES_SPLIT: InstanceAttribute[] = [
|
|
|
108
129
|
{ name: "iTint", format: "vec4", slot: 1 },
|
|
109
130
|
{ name: "iRenderOrder", format: "f32", slot: 1 },
|
|
110
131
|
]
|
|
132
|
+
|
|
133
|
+
/** What a layer's targets draw with: the unit quad every instance reuses
|
|
134
|
+
* and the pipeline over one of the two layouts above. */
|
|
135
|
+
export type SpritePipeline = { quad: BufferId; pipeline: RenderPipelineId; dispose(): void }
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Compile one sprite pipeline: `vertex` with its matching attribute list
|
|
139
|
+
* (VERTEX + INSTANCE_ATTRIBUTES, or the split pair) over the shared
|
|
140
|
+
* fragment stage, alpha-blended triangle strips. Spelled out (not the
|
|
141
|
+
* fused createPipelineTexture) so a layer's targets - its own and every
|
|
142
|
+
* view - add entries over the one pipeline. The program lives as long as
|
|
143
|
+
* the pipeline; `dispose` frees both and the quad.
|
|
144
|
+
*/
|
|
145
|
+
export function createSpritePipeline(label: string, vertex: string, instanceAttributes: InstanceAttribute[]): SpritePipeline {
|
|
146
|
+
// One unit quad (triangle strip), reused by every instance.
|
|
147
|
+
let quad = createBuffer(new Float32Array([-0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5]), {
|
|
148
|
+
label: `${label}-quad`,
|
|
149
|
+
autoFree: false,
|
|
150
|
+
})
|
|
151
|
+
let vs = compileShader("vertex", vertex, { header: true })
|
|
152
|
+
let fs = compileShader("fragment", FRAGMENT, { header: true })
|
|
153
|
+
let program = linkProgram(vs, fs, { label })
|
|
154
|
+
destroyShader(vs)
|
|
155
|
+
destroyShader(fs)
|
|
156
|
+
let pipeline = createRenderPipeline(program, {
|
|
157
|
+
label,
|
|
158
|
+
topology: "triangle-strip",
|
|
159
|
+
attributes: [{ name: "aPos", format: "vec2" }],
|
|
160
|
+
instanceAttributes,
|
|
161
|
+
blend: "alpha",
|
|
162
|
+
})
|
|
163
|
+
return {
|
|
164
|
+
quad,
|
|
165
|
+
pipeline,
|
|
166
|
+
dispose() {
|
|
167
|
+
destroyRenderPipeline(pipeline)
|
|
168
|
+
destroyProgram(program)
|
|
169
|
+
destroyBuffer(quad)
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
}
|