kaplay 3000.1.17

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/src/gfx.ts ADDED
@@ -0,0 +1,524 @@
1
+ import type {
2
+ ImageSource,
3
+ TextureOpt,
4
+ TexFilter,
5
+ Uniform,
6
+ } from "./types"
7
+
8
+ import {
9
+ Mat4,
10
+ Vec2,
11
+ Color,
12
+ } from "./math"
13
+
14
+ import {
15
+ deepEq,
16
+ } from "./utils"
17
+
18
+ export type GfxCtx = ReturnType<typeof initGfx>
19
+
20
+ export class Texture {
21
+
22
+ ctx: GfxCtx
23
+ src: null | ImageSource = null
24
+ glTex: WebGLTexture
25
+ width: number
26
+ height: number
27
+
28
+ constructor(ctx: GfxCtx, w: number, h: number, opt: TextureOpt = {}) {
29
+
30
+ this.ctx = ctx
31
+ const gl = ctx.gl
32
+ this.glTex = ctx.gl.createTexture()
33
+ ctx.onDestroy(() => this.free())
34
+
35
+ this.width = w
36
+ this.height = h
37
+
38
+ // TODO: no default
39
+ const filter = {
40
+ "linear": gl.LINEAR,
41
+ "nearest": gl.NEAREST,
42
+ }[opt.filter ?? ctx.opts.texFilter] ?? gl.NEAREST
43
+
44
+ const wrap = {
45
+ "repeat": gl.REPEAT,
46
+ "clampToEadge": gl.CLAMP_TO_EDGE,
47
+ }[opt.wrap] ?? gl.CLAMP_TO_EDGE
48
+
49
+ this.bind()
50
+
51
+ if (w && h) {
52
+ gl.texImage2D(
53
+ gl.TEXTURE_2D,
54
+ 0, gl.RGBA,
55
+ w,
56
+ h,
57
+ 0,
58
+ gl.RGBA,
59
+ gl.UNSIGNED_BYTE,
60
+ null,
61
+ )
62
+ }
63
+
64
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter)
65
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter)
66
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap)
67
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap)
68
+ this.unbind()
69
+
70
+ }
71
+
72
+ static fromImage(ctx: GfxCtx, img: ImageSource, opt: TextureOpt = {}): Texture {
73
+ const tex = new Texture(ctx, img.width, img.height, opt)
74
+ tex.update(img)
75
+ tex.src = img
76
+ return tex
77
+ }
78
+
79
+ update(img: ImageSource, x = 0, y = 0) {
80
+ const gl = this.ctx.gl
81
+ this.bind()
82
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, gl.RGBA, gl.UNSIGNED_BYTE, img)
83
+ this.unbind()
84
+ }
85
+
86
+ bind() {
87
+ this.ctx.pushTexture2D(this.glTex)
88
+ }
89
+
90
+ unbind() {
91
+ this.ctx.popTexture2D()
92
+ }
93
+
94
+ free() {
95
+ this.ctx.gl.deleteTexture(this.glTex)
96
+ }
97
+
98
+ }
99
+
100
+ export class FrameBuffer {
101
+
102
+ ctx: GfxCtx
103
+ tex: Texture
104
+ glFramebuffer: WebGLFramebuffer
105
+ glRenderbuffer: WebGLRenderbuffer
106
+
107
+ constructor(ctx: GfxCtx, w: number, h: number, opt: TextureOpt = {}) {
108
+
109
+ this.ctx = ctx
110
+ const gl = ctx.gl
111
+ ctx.onDestroy(() => this.free())
112
+ this.tex = new Texture(ctx, w, h, opt)
113
+ this.glFramebuffer = gl.createFramebuffer()
114
+ this.glRenderbuffer = gl.createRenderbuffer()
115
+ this.bind()
116
+ gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, w, h)
117
+ gl.framebufferTexture2D(
118
+ gl.FRAMEBUFFER,
119
+ gl.COLOR_ATTACHMENT0,
120
+ gl.TEXTURE_2D,
121
+ this.tex.glTex,
122
+ 0,
123
+ )
124
+ gl.framebufferRenderbuffer(
125
+ gl.FRAMEBUFFER,
126
+ gl.DEPTH_STENCIL_ATTACHMENT,
127
+ gl.RENDERBUFFER,
128
+ this.glRenderbuffer,
129
+ )
130
+ this.unbind()
131
+ }
132
+
133
+ get width() {
134
+ return this.tex.width
135
+ }
136
+
137
+ get height() {
138
+ return this.tex.height
139
+ }
140
+
141
+ toImageData() {
142
+ const gl = this.ctx.gl
143
+ const data = new Uint8ClampedArray(this.width * this.height * 4)
144
+ this.bind()
145
+ gl.readPixels(0, 0, this.width, this.height, gl.RGBA, gl.UNSIGNED_BYTE, data)
146
+ this.unbind()
147
+ // flip vertically
148
+ const bytesPerRow = this.width * 4
149
+ const temp = new Uint8Array(bytesPerRow)
150
+ for (let y = 0; y < (this.height / 2 | 0); y++) {
151
+ const topOffset = y * bytesPerRow
152
+ const bottomOffset = (this.height - y - 1) * bytesPerRow
153
+ temp.set(data.subarray(topOffset, topOffset + bytesPerRow))
154
+ data.copyWithin(topOffset, bottomOffset, bottomOffset + bytesPerRow)
155
+ data.set(temp, bottomOffset)
156
+ }
157
+ return new ImageData(data, this.width, this.height)
158
+ }
159
+
160
+ toDataURL() {
161
+ const canvas = document.createElement("canvas")
162
+ const ctx = canvas.getContext("2d")
163
+ canvas.width = this.width
164
+ canvas.height = this.height
165
+ ctx.putImageData(this.toImageData(), 0, 0)
166
+ return canvas.toDataURL()
167
+ }
168
+
169
+ draw(action: () => void) {
170
+ this.bind()
171
+ action()
172
+ this.unbind()
173
+ }
174
+
175
+ bind() {
176
+ this.ctx.pushFramebuffer(this.glFramebuffer)
177
+ this.ctx.pushRenderbuffer(this.glRenderbuffer)
178
+ this.ctx.pushViewport({ x: 0, y: 0, w: this.width, h: this.height })
179
+ }
180
+
181
+ unbind() {
182
+ this.ctx.popFramebuffer()
183
+ this.ctx.popRenderbuffer()
184
+ this.ctx.popViewport()
185
+ }
186
+
187
+ free() {
188
+ const gl = this.ctx.gl
189
+ gl.deleteFramebuffer(this.glFramebuffer)
190
+ gl.deleteRenderbuffer(this.glRenderbuffer)
191
+ this.tex.free()
192
+ }
193
+
194
+ }
195
+
196
+ export class Shader {
197
+
198
+ ctx: GfxCtx
199
+ glProgram: WebGLProgram
200
+
201
+ constructor(ctx: GfxCtx, vert: string, frag: string, attribs: string[]) {
202
+
203
+ this.ctx = ctx
204
+ ctx.onDestroy(() => this.free())
205
+
206
+ const gl = ctx.gl
207
+ const vertShader = gl.createShader(gl.VERTEX_SHADER)
208
+ const fragShader = gl.createShader(gl.FRAGMENT_SHADER)
209
+
210
+ gl.shaderSource(vertShader, vert)
211
+ gl.shaderSource(fragShader, frag)
212
+ gl.compileShader(vertShader)
213
+ gl.compileShader(fragShader)
214
+
215
+ const prog = gl.createProgram()
216
+ this.glProgram = prog
217
+
218
+ gl.attachShader(prog, vertShader)
219
+ gl.attachShader(prog, fragShader)
220
+
221
+ attribs.forEach((attrib, i) => gl.bindAttribLocation(prog, i, attrib))
222
+
223
+ gl.linkProgram(prog)
224
+
225
+ if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
226
+ const vertError = gl.getShaderInfoLog(vertShader)
227
+ if (vertError) throw new Error("VERTEX SHADER " + vertError)
228
+ const fragError = gl.getShaderInfoLog(fragShader)
229
+ if (fragError) throw new Error("FRAGMENT SHADER " + fragError)
230
+ }
231
+
232
+ gl.deleteShader(vertShader)
233
+ gl.deleteShader(fragShader)
234
+
235
+ }
236
+
237
+ bind() {
238
+ this.ctx.pushProgram(this.glProgram)
239
+ }
240
+
241
+ unbind() {
242
+ this.ctx.popProgram()
243
+ }
244
+
245
+ send(uniform: Uniform) {
246
+ const gl = this.ctx.gl
247
+ for (const name in uniform) {
248
+ const val = uniform[name]
249
+ const loc = gl.getUniformLocation(this.glProgram, name)
250
+ if (typeof val === "number") {
251
+ gl.uniform1f(loc, val)
252
+ } else if (val instanceof Mat4) {
253
+ gl.uniformMatrix4fv(loc, false, new Float32Array(val.m))
254
+ } else if (val instanceof Color) {
255
+ gl.uniform3f(loc, val.r, val.g, val.b)
256
+ } else if (val instanceof Vec2) {
257
+ gl.uniform2f(loc, val.x, val.y)
258
+ }
259
+ }
260
+ }
261
+
262
+ free() {
263
+ this.ctx.gl.deleteProgram(this.glProgram)
264
+ }
265
+
266
+ }
267
+
268
+ export type VertexFormat = {
269
+ name: string,
270
+ size: number,
271
+ }[]
272
+
273
+ export class BatchRenderer {
274
+
275
+ ctx: GfxCtx
276
+
277
+ glVBuf: WebGLBuffer
278
+ glIBuf: WebGLBuffer
279
+ vqueue: number[] = []
280
+ iqueue: number[] = []
281
+ stride: number
282
+ maxVertices: number
283
+ maxIndices: number
284
+
285
+ vertexFormat: VertexFormat
286
+ numDraws: number = 0
287
+
288
+ curPrimitive: GLenum | null = null
289
+ curTex: Texture | null = null
290
+ curShader: Shader | null = null
291
+ curUniform: Uniform = {}
292
+
293
+ constructor(ctx: GfxCtx, format: VertexFormat, maxVertices: number, maxIndices: number) {
294
+
295
+ const gl = ctx.gl
296
+
297
+ this.vertexFormat = format
298
+ this.ctx = ctx
299
+ this.stride = format.reduce((sum, f) => sum + f.size, 0)
300
+ this.maxVertices = maxVertices
301
+ this.maxIndices = maxIndices
302
+
303
+ this.glVBuf = gl.createBuffer()
304
+ ctx.pushArrayBuffer(this.glVBuf)
305
+ gl.bufferData(gl.ARRAY_BUFFER, maxVertices * 4, gl.DYNAMIC_DRAW)
306
+ ctx.popArrayBuffer()
307
+
308
+ this.glIBuf = gl.createBuffer()
309
+ ctx.pushElementArrayBuffer(this.glIBuf)
310
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, maxIndices * 4, gl.DYNAMIC_DRAW)
311
+ ctx.popElementArrayBuffer()
312
+
313
+ }
314
+
315
+ push(
316
+ primitive: GLenum,
317
+ verts: number[],
318
+ indices: number[],
319
+ shader: Shader,
320
+ tex: Texture | null = null,
321
+ uniform: Uniform = {},
322
+ ) {
323
+ if (
324
+ primitive !== this.curPrimitive
325
+ || tex !== this.curTex
326
+ || shader !== this.curShader
327
+ || !deepEq(this.curUniform, uniform)
328
+ || this.vqueue.length + verts.length * this.stride > this.maxVertices
329
+ || this.iqueue.length + indices.length > this.maxIndices
330
+ ) {
331
+ this.flush()
332
+ }
333
+ const indexOffset = this.vqueue.length / this.stride
334
+ for (const v of verts) {
335
+ this.vqueue.push(v)
336
+ }
337
+ for (const i of indices) {
338
+ this.iqueue.push(i + indexOffset)
339
+ }
340
+ this.curPrimitive = primitive
341
+ this.curShader = shader
342
+ this.curTex = tex
343
+ this.curUniform = uniform
344
+ }
345
+
346
+ flush() {
347
+
348
+ if (
349
+ !this.curPrimitive
350
+ || !this.curShader
351
+ || this.vqueue.length === 0
352
+ || this.iqueue.length === 0
353
+ ) {
354
+ return
355
+ }
356
+
357
+ const gl = this.ctx.gl
358
+
359
+ this.ctx.pushArrayBuffer(this.glVBuf)
360
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(this.vqueue))
361
+ this.ctx.pushElementArrayBuffer(this.glIBuf)
362
+ gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, new Uint16Array(this.iqueue))
363
+ this.ctx.setVertexFormat(this.vertexFormat)
364
+ this.curShader.bind()
365
+ this.curShader.send(this.curUniform)
366
+ this.curTex?.bind()
367
+ gl.drawElements(this.curPrimitive, this.iqueue.length, gl.UNSIGNED_SHORT, 0)
368
+ this.curTex?.unbind()
369
+ this.curShader.unbind()
370
+
371
+ this.ctx.popArrayBuffer()
372
+ this.ctx.popElementArrayBuffer()
373
+
374
+ this.vqueue = []
375
+ this.iqueue = []
376
+ this.numDraws++
377
+
378
+ }
379
+
380
+ free() {
381
+ const gl = this.ctx.gl
382
+ gl.deleteBuffer(this.glVBuf)
383
+ gl.deleteBuffer(this.glIBuf)
384
+ }
385
+
386
+ }
387
+
388
+ export class Mesh {
389
+
390
+ ctx: GfxCtx
391
+ glVBuf: WebGLBuffer
392
+ glIBuf: WebGLBuffer
393
+ vertexFormat: VertexFormat
394
+ count: number
395
+
396
+ constructor(ctx: GfxCtx, format: VertexFormat, verts: number[], indices: number[]) {
397
+
398
+ const gl = ctx.gl
399
+
400
+ this.vertexFormat = format
401
+ this.ctx = ctx
402
+
403
+ this.glVBuf = gl.createBuffer()
404
+ ctx.pushArrayBuffer(this.glVBuf)
405
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW)
406
+ ctx.popArrayBuffer()
407
+
408
+ this.glIBuf = gl.createBuffer()
409
+ ctx.pushElementArrayBuffer(this.glIBuf)
410
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW)
411
+ ctx.popElementArrayBuffer()
412
+
413
+ this.count = indices.length
414
+
415
+ }
416
+
417
+ draw(primitive?: GLenum) {
418
+ const gl = this.ctx.gl
419
+ this.ctx.pushArrayBuffer(this.glVBuf)
420
+ this.ctx.pushElementArrayBuffer(this.glIBuf)
421
+ this.ctx.setVertexFormat(this.vertexFormat)
422
+ gl.drawElements(primitive ?? gl.TRIANGLES, this.count, gl.UNSIGNED_SHORT, 0)
423
+ this.ctx.popArrayBuffer()
424
+ this.ctx.popElementArrayBuffer()
425
+ }
426
+
427
+ free() {
428
+ const gl = this.ctx.gl
429
+ gl.deleteBuffer(this.glVBuf)
430
+ gl.deleteBuffer(this.glIBuf)
431
+ }
432
+
433
+
434
+ }
435
+
436
+ function genStack<T>(setFunc: (item: T) => void) {
437
+ const stack: T[] = []
438
+ const push = (item: T) => {
439
+ stack.push(item)
440
+ setFunc(item)
441
+ }
442
+ const pop = () => {
443
+ stack.pop()
444
+ setFunc(cur() ?? null)
445
+ }
446
+ const cur = () => stack[stack.length - 1]
447
+ return [push, pop, cur] as const
448
+ }
449
+
450
+ export default function initGfx(gl: WebGLRenderingContext, opts: {
451
+ texFilter?: TexFilter,
452
+ } = {}) {
453
+
454
+ const gc: Array<() => void> = []
455
+
456
+ function onDestroy(action) {
457
+ gc.push(action)
458
+ }
459
+
460
+ function destroy() {
461
+ gc.forEach((action) => action())
462
+ gl.getExtension("WEBGL_lose_context").loseContext()
463
+ }
464
+
465
+ let curVertexFormat = null
466
+
467
+ function setVertexFormat(fmt: VertexFormat) {
468
+ if (deepEq(fmt, curVertexFormat)) return
469
+ curVertexFormat = fmt
470
+ const stride = fmt.reduce((sum, f) => sum + f.size, 0)
471
+ fmt.reduce((offset, f, i) => {
472
+ gl.vertexAttribPointer(i, f.size, gl.FLOAT, false, stride * 4, offset)
473
+ gl.enableVertexAttribArray(i)
474
+ return offset + f.size * 4
475
+ }, 0)
476
+ }
477
+
478
+ const [ pushTexture2D, popTexture2D ] =
479
+ genStack<WebGLTexture>((t) => gl.bindTexture(gl.TEXTURE_2D, t))
480
+
481
+ const [ pushArrayBuffer, popArrayBuffer ] =
482
+ genStack<WebGLBuffer>((b) => gl.bindBuffer(gl.ARRAY_BUFFER, b))
483
+
484
+ const [ pushElementArrayBuffer, popElementArrayBuffer ] =
485
+ genStack<WebGLBuffer>((b) => gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, b))
486
+
487
+ const [ pushFramebuffer, popFramebuffer ] =
488
+ genStack<WebGLFramebuffer>((b) => gl.bindFramebuffer(gl.FRAMEBUFFER, b))
489
+
490
+ const [ pushRenderbuffer, popRenderbuffer ] =
491
+ genStack<WebGLRenderbuffer>((b) => gl.bindRenderbuffer(gl.RENDERBUFFER, b))
492
+
493
+ const [ pushViewport, popViewport ] =
494
+ genStack<{ x: number, y: number, w: number, h: number }>(({ x, y, w, h }) => {
495
+ gl.viewport(x, y, w, h)
496
+ })
497
+
498
+ const [ pushProgram, popProgram ] = genStack<WebGLProgram>((p) => gl.useProgram(p))
499
+
500
+ pushViewport({ x: 0, y: 0, w: gl.drawingBufferWidth, h: gl.drawingBufferHeight })
501
+
502
+ return {
503
+ gl,
504
+ opts,
505
+ onDestroy,
506
+ destroy,
507
+ pushTexture2D,
508
+ popTexture2D,
509
+ pushArrayBuffer,
510
+ popArrayBuffer,
511
+ pushElementArrayBuffer,
512
+ popElementArrayBuffer,
513
+ pushFramebuffer,
514
+ popFramebuffer,
515
+ pushRenderbuffer,
516
+ popRenderbuffer,
517
+ pushViewport,
518
+ popViewport,
519
+ pushProgram,
520
+ popProgram,
521
+ setVertexFormat,
522
+ }
523
+
524
+ }