kaplay 3001.0.0-alpha.1

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,541 @@
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
+ clear() {
170
+ const gl = this.ctx.gl
171
+ gl.clear(gl.COLOR_BUFFER_BIT)
172
+ }
173
+
174
+ draw(action: () => void) {
175
+ this.bind()
176
+ action()
177
+ this.unbind()
178
+ }
179
+
180
+ bind() {
181
+ this.ctx.pushFramebuffer(this.glFramebuffer)
182
+ this.ctx.pushRenderbuffer(this.glRenderbuffer)
183
+ this.ctx.pushViewport({ x: 0, y: 0, w: this.width, h: this.height })
184
+ }
185
+
186
+ unbind() {
187
+ this.ctx.popFramebuffer()
188
+ this.ctx.popRenderbuffer()
189
+ this.ctx.popViewport()
190
+ }
191
+
192
+ free() {
193
+ const gl = this.ctx.gl
194
+ gl.deleteFramebuffer(this.glFramebuffer)
195
+ gl.deleteRenderbuffer(this.glRenderbuffer)
196
+ this.tex.free()
197
+ }
198
+
199
+ }
200
+
201
+ export class Shader {
202
+
203
+ ctx: GfxCtx
204
+ glProgram: WebGLProgram
205
+
206
+ constructor(ctx: GfxCtx, vert: string, frag: string, attribs: string[]) {
207
+
208
+ this.ctx = ctx
209
+ ctx.onDestroy(() => this.free())
210
+
211
+ const gl = ctx.gl
212
+ const vertShader = gl.createShader(gl.VERTEX_SHADER)
213
+ const fragShader = gl.createShader(gl.FRAGMENT_SHADER)
214
+
215
+ gl.shaderSource(vertShader, vert)
216
+ gl.shaderSource(fragShader, frag)
217
+ gl.compileShader(vertShader)
218
+ gl.compileShader(fragShader)
219
+
220
+ const prog = gl.createProgram()
221
+ this.glProgram = prog
222
+
223
+ gl.attachShader(prog, vertShader)
224
+ gl.attachShader(prog, fragShader)
225
+
226
+ attribs.forEach((attrib, i) => gl.bindAttribLocation(prog, i, attrib))
227
+
228
+ gl.linkProgram(prog)
229
+
230
+ if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
231
+ const vertError = gl.getShaderInfoLog(vertShader)
232
+ if (vertError) throw new Error("VERTEX SHADER " + vertError)
233
+ const fragError = gl.getShaderInfoLog(fragShader)
234
+ if (fragError) throw new Error("FRAGMENT SHADER " + fragError)
235
+ }
236
+
237
+ gl.deleteShader(vertShader)
238
+ gl.deleteShader(fragShader)
239
+
240
+ }
241
+
242
+ bind() {
243
+ this.ctx.pushProgram(this.glProgram)
244
+ }
245
+
246
+ unbind() {
247
+ this.ctx.popProgram()
248
+ }
249
+
250
+ send(uniform: Uniform) {
251
+ const gl = this.ctx.gl
252
+ for (const name in uniform) {
253
+ const val = uniform[name]
254
+ const loc = gl.getUniformLocation(this.glProgram, name)
255
+ if (typeof val === "number") {
256
+ gl.uniform1f(loc, val)
257
+ } else if (val instanceof Mat4) {
258
+ gl.uniformMatrix4fv(loc, false, new Float32Array(val.m))
259
+ } else if (val instanceof Color) {
260
+ gl.uniform3f(loc, val.r, val.g, val.b)
261
+ } else if (val instanceof Vec2) {
262
+ gl.uniform2f(loc, val.x, val.y)
263
+ } else if (Array.isArray(val)) {
264
+ const first = val[0]
265
+ if (typeof first === "number") {
266
+ gl.uniform1fv(loc, val as number[])
267
+ } else if (first instanceof Vec2) {
268
+ gl.uniform2fv(loc, val.map(v => [v.x, v.y]).flat())
269
+ } else if (first instanceof Color) {
270
+ gl.uniform3fv(loc, val.map(v => [v.r, v.g, v.b]).flat())
271
+ }
272
+ } else {
273
+ throw new Error("Unsupported uniform data type")
274
+ }
275
+ }
276
+ }
277
+
278
+ free() {
279
+ this.ctx.gl.deleteProgram(this.glProgram)
280
+ }
281
+
282
+ }
283
+
284
+ export type VertexFormat = {
285
+ name: string,
286
+ size: number,
287
+ }[]
288
+
289
+ export class BatchRenderer {
290
+
291
+ ctx: GfxCtx
292
+
293
+ glVBuf: WebGLBuffer
294
+ glIBuf: WebGLBuffer
295
+ vqueue: number[] = []
296
+ iqueue: number[] = []
297
+ stride: number
298
+ maxVertices: number
299
+ maxIndices: number
300
+
301
+ vertexFormat: VertexFormat
302
+ numDraws: number = 0
303
+
304
+ curPrimitive: GLenum | null = null
305
+ curTex: Texture | null = null
306
+ curShader: Shader | null = null
307
+ curUniform: Uniform = {}
308
+
309
+ constructor(ctx: GfxCtx, format: VertexFormat, maxVertices: number, maxIndices: number) {
310
+
311
+ const gl = ctx.gl
312
+
313
+ this.vertexFormat = format
314
+ this.ctx = ctx
315
+ this.stride = format.reduce((sum, f) => sum + f.size, 0)
316
+ this.maxVertices = maxVertices
317
+ this.maxIndices = maxIndices
318
+
319
+ this.glVBuf = gl.createBuffer()
320
+ ctx.pushArrayBuffer(this.glVBuf)
321
+ gl.bufferData(gl.ARRAY_BUFFER, maxVertices * 4, gl.DYNAMIC_DRAW)
322
+ ctx.popArrayBuffer()
323
+
324
+ this.glIBuf = gl.createBuffer()
325
+ ctx.pushElementArrayBuffer(this.glIBuf)
326
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, maxIndices * 4, gl.DYNAMIC_DRAW)
327
+ ctx.popElementArrayBuffer()
328
+
329
+ }
330
+
331
+ push(
332
+ primitive: GLenum,
333
+ verts: number[],
334
+ indices: number[],
335
+ shader: Shader,
336
+ tex: Texture | null = null,
337
+ uniform: Uniform = {},
338
+ ) {
339
+ if (
340
+ primitive !== this.curPrimitive
341
+ || tex !== this.curTex
342
+ || shader !== this.curShader
343
+ || !deepEq(this.curUniform, uniform)
344
+ || this.vqueue.length + verts.length * this.stride > this.maxVertices
345
+ || this.iqueue.length + indices.length > this.maxIndices
346
+ ) {
347
+ this.flush()
348
+ }
349
+ const indexOffset = this.vqueue.length / this.stride
350
+ for (const v of verts) {
351
+ this.vqueue.push(v)
352
+ }
353
+ for (const i of indices) {
354
+ this.iqueue.push(i + indexOffset)
355
+ }
356
+ this.curPrimitive = primitive
357
+ this.curShader = shader
358
+ this.curTex = tex
359
+ this.curUniform = uniform
360
+ }
361
+
362
+ flush() {
363
+
364
+ if (
365
+ !this.curPrimitive
366
+ || !this.curShader
367
+ || this.vqueue.length === 0
368
+ || this.iqueue.length === 0
369
+ ) {
370
+ return
371
+ }
372
+
373
+ const gl = this.ctx.gl
374
+
375
+ this.ctx.pushArrayBuffer(this.glVBuf)
376
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(this.vqueue))
377
+ this.ctx.pushElementArrayBuffer(this.glIBuf)
378
+ gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, new Uint16Array(this.iqueue))
379
+ this.ctx.setVertexFormat(this.vertexFormat)
380
+ this.curShader.bind()
381
+ this.curShader.send(this.curUniform)
382
+ this.curTex?.bind()
383
+ gl.drawElements(this.curPrimitive, this.iqueue.length, gl.UNSIGNED_SHORT, 0)
384
+ this.curTex?.unbind()
385
+ this.curShader.unbind()
386
+
387
+ this.ctx.popArrayBuffer()
388
+ this.ctx.popElementArrayBuffer()
389
+
390
+ this.vqueue = []
391
+ this.iqueue = []
392
+ this.numDraws++
393
+
394
+ }
395
+
396
+ free() {
397
+ const gl = this.ctx.gl
398
+ gl.deleteBuffer(this.glVBuf)
399
+ gl.deleteBuffer(this.glIBuf)
400
+ }
401
+
402
+ }
403
+
404
+ export class Mesh {
405
+
406
+ ctx: GfxCtx
407
+ glVBuf: WebGLBuffer
408
+ glIBuf: WebGLBuffer
409
+ vertexFormat: VertexFormat
410
+ count: number
411
+
412
+ constructor(ctx: GfxCtx, format: VertexFormat, verts: number[], indices: number[]) {
413
+
414
+ const gl = ctx.gl
415
+
416
+ this.vertexFormat = format
417
+ this.ctx = ctx
418
+
419
+ this.glVBuf = gl.createBuffer()
420
+ ctx.pushArrayBuffer(this.glVBuf)
421
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW)
422
+ ctx.popArrayBuffer()
423
+
424
+ this.glIBuf = gl.createBuffer()
425
+ ctx.pushElementArrayBuffer(this.glIBuf)
426
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW)
427
+ ctx.popElementArrayBuffer()
428
+
429
+ this.count = indices.length
430
+
431
+ }
432
+
433
+ draw(primitive?: GLenum) {
434
+ const gl = this.ctx.gl
435
+ this.ctx.pushArrayBuffer(this.glVBuf)
436
+ this.ctx.pushElementArrayBuffer(this.glIBuf)
437
+ this.ctx.setVertexFormat(this.vertexFormat)
438
+ gl.drawElements(primitive ?? gl.TRIANGLES, this.count, gl.UNSIGNED_SHORT, 0)
439
+ this.ctx.popArrayBuffer()
440
+ this.ctx.popElementArrayBuffer()
441
+ }
442
+
443
+ free() {
444
+ const gl = this.ctx.gl
445
+ gl.deleteBuffer(this.glVBuf)
446
+ gl.deleteBuffer(this.glIBuf)
447
+ }
448
+
449
+
450
+ }
451
+
452
+ function genStack<T>(setFunc: (item: T) => void) {
453
+ const stack: T[] = []
454
+ // TODO: don't do anything if pushed item is the same as the one on top?
455
+ const push = (item: T) => {
456
+ stack.push(item)
457
+ setFunc(item)
458
+ }
459
+ const pop = () => {
460
+ stack.pop()
461
+ setFunc(cur() ?? null)
462
+ }
463
+ const cur = () => stack[stack.length - 1]
464
+ return [push, pop, cur] as const
465
+ }
466
+
467
+ export default function initGfx(gl: WebGLRenderingContext, opts: {
468
+ texFilter?: TexFilter,
469
+ } = {}) {
470
+
471
+ const gc: Array<() => void> = []
472
+
473
+ function onDestroy(action) {
474
+ gc.push(action)
475
+ }
476
+
477
+ function destroy() {
478
+ gc.forEach((action) => action())
479
+ gl.getExtension("WEBGL_lose_context").loseContext()
480
+ }
481
+
482
+ let curVertexFormat = null
483
+
484
+ function setVertexFormat(fmt: VertexFormat) {
485
+ if (deepEq(fmt, curVertexFormat)) return
486
+ curVertexFormat = fmt
487
+ const stride = fmt.reduce((sum, f) => sum + f.size, 0)
488
+ fmt.reduce((offset, f, i) => {
489
+ gl.vertexAttribPointer(i, f.size, gl.FLOAT, false, stride * 4, offset)
490
+ gl.enableVertexAttribArray(i)
491
+ return offset + f.size * 4
492
+ }, 0)
493
+ }
494
+
495
+ const [ pushTexture2D, popTexture2D ] =
496
+ genStack<WebGLTexture>((t) => gl.bindTexture(gl.TEXTURE_2D, t))
497
+
498
+ const [ pushArrayBuffer, popArrayBuffer ] =
499
+ genStack<WebGLBuffer>((b) => gl.bindBuffer(gl.ARRAY_BUFFER, b))
500
+
501
+ const [ pushElementArrayBuffer, popElementArrayBuffer ] =
502
+ genStack<WebGLBuffer>((b) => gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, b))
503
+
504
+ const [ pushFramebuffer, popFramebuffer ] =
505
+ genStack<WebGLFramebuffer>((b) => gl.bindFramebuffer(gl.FRAMEBUFFER, b))
506
+
507
+ const [ pushRenderbuffer, popRenderbuffer ] =
508
+ genStack<WebGLRenderbuffer>((b) => gl.bindRenderbuffer(gl.RENDERBUFFER, b))
509
+
510
+ const [ pushViewport, popViewport ] =
511
+ genStack<{ x: number, y: number, w: number, h: number }>(({ x, y, w, h }) => {
512
+ gl.viewport(x, y, w, h)
513
+ })
514
+
515
+ const [ pushProgram, popProgram ] = genStack<WebGLProgram>((p) => gl.useProgram(p))
516
+
517
+ pushViewport({ x: 0, y: 0, w: gl.drawingBufferWidth, h: gl.drawingBufferHeight })
518
+
519
+ return {
520
+ gl,
521
+ opts,
522
+ onDestroy,
523
+ destroy,
524
+ pushTexture2D,
525
+ popTexture2D,
526
+ pushArrayBuffer,
527
+ popArrayBuffer,
528
+ pushElementArrayBuffer,
529
+ popElementArrayBuffer,
530
+ pushFramebuffer,
531
+ popFramebuffer,
532
+ pushRenderbuffer,
533
+ popRenderbuffer,
534
+ pushViewport,
535
+ popViewport,
536
+ pushProgram,
537
+ popProgram,
538
+ setVertexFormat,
539
+ }
540
+
541
+ }