From fbc61362ef276e5495e267ba35cd146eca2ea35d Mon Sep 17 00:00:00 2001 From: tga Date: Tue, 17 Oct 2023 13:27:13 +0800 Subject: [PATCH] start adding offscreen canvas --- src/gfx.ts | 151 +++++++++++++++++++++-------------------- src/kaboom.ts | 184 +++++++++++++++++++++----------------------------- src/types.ts | 29 ++++++++ src/utils.ts | 4 +- 4 files changed, 186 insertions(+), 182 deletions(-) diff --git a/src/gfx.ts b/src/gfx.ts index 2f612d7bb..a3c34809c 100644 --- a/src/gfx.ts +++ b/src/gfx.ts @@ -14,20 +14,7 @@ import { deepEq, } from "./utils" -export type GfxCtx = { - gl: WebGLRenderingContext, - onDestroy: (action: () => void) => void, - pushTexture: (ty: GLenum, tex: WebGLTexture) => void, - popTexture: (ty: GLenum) => void, - pushBuffer: (ty: GLenum, tex: WebGLBuffer) => void, - popBuffer: (ty: GLenum) => void, - pushFramebuffer: (ty: GLenum, tex: WebGLFramebuffer) => void, - popFramebuffer: (ty: GLenum) => void, - pushRenderbuffer: (ty: GLenum, tex: WebGLRenderbuffer) => void, - popRenderbuffer: (ty: GLenum) => void, - setVertexFormat: (fmt: VertexFormat) => void, - destroy: () => void, -} +export type GfxCtx = ReturnType export class Texture { @@ -96,11 +83,11 @@ export class Texture { } bind() { - this.ctx.pushTexture(this.ctx.gl.TEXTURE_2D, this.glTex) + this.ctx.pushTexture2D(this.glTex) } unbind() { - this.ctx.popTexture(this.ctx.gl.TEXTURE_2D) + this.ctx.popTexture2D() } free() { @@ -185,15 +172,15 @@ export class FrameBuffer { } bind() { - const gl = this.ctx.gl - this.ctx.pushFramebuffer(gl.FRAMEBUFFER, this.glFramebuffer) - this.ctx.pushRenderbuffer(gl.RENDERBUFFER, this.glRenderbuffer) + this.ctx.pushFramebuffer(this.glFramebuffer) + this.ctx.pushRenderbuffer(this.glRenderbuffer) + this.ctx.pushViewport({ x: 0, y: 0, w: this.width, h: this.height }) } unbind() { - const gl = this.ctx.gl - this.ctx.popFramebuffer(gl.FRAMEBUFFER) - this.ctx.popRenderbuffer(gl.RENDERBUFFER) + this.ctx.popFramebuffer() + this.ctx.popRenderbuffer() + this.ctx.popViewport() } free() { @@ -247,11 +234,11 @@ export class Shader { } bind() { - this.ctx.gl.useProgram(this.glProgram) + this.ctx.pushProgram(this.glProgram) } unbind() { - this.ctx.gl.useProgram(null) + this.ctx.popProgram() } send(uniform: Uniform) { @@ -313,14 +300,14 @@ export class BatchRenderer { this.maxIndices = maxIndices this.glVBuf = gl.createBuffer() - ctx.pushBuffer(gl.ARRAY_BUFFER, this.glVBuf) + ctx.pushArrayBuffer(this.glVBuf) gl.bufferData(gl.ARRAY_BUFFER, maxVertices * 4, gl.DYNAMIC_DRAW) - ctx.popBuffer(gl.ARRAY_BUFFER) + ctx.popArrayBuffer() this.glIBuf = gl.createBuffer() - ctx.pushBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glIBuf) + ctx.pushElementArrayBuffer(this.glIBuf) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, maxIndices * 4, gl.DYNAMIC_DRAW) - ctx.popBuffer(gl.ELEMENT_ARRAY_BUFFER) + ctx.popElementArrayBuffer() } @@ -368,9 +355,9 @@ export class BatchRenderer { const gl = this.ctx.gl - this.ctx.pushBuffer(gl.ARRAY_BUFFER, this.glVBuf) + this.ctx.pushArrayBuffer(this.glVBuf) gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(this.vqueue)) - this.ctx.pushBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glIBuf) + this.ctx.pushElementArrayBuffer(this.glIBuf) gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, new Uint16Array(this.iqueue)) this.ctx.setVertexFormat(this.vertexFormat) this.curShader.bind() @@ -380,8 +367,8 @@ export class BatchRenderer { this.curTex?.unbind() this.curShader.unbind() - this.ctx.popBuffer(gl.ARRAY_BUFFER) - this.ctx.popBuffer(gl.ELEMENT_ARRAY_BUFFER) + this.ctx.popArrayBuffer() + this.ctx.popElementArrayBuffer() this.vqueue = [] this.iqueue = [] @@ -413,14 +400,14 @@ export class Mesh { this.ctx = ctx this.glVBuf = gl.createBuffer() - ctx.pushBuffer(gl.ARRAY_BUFFER, this.glVBuf) + ctx.pushArrayBuffer(this.glVBuf) gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW) - ctx.popBuffer(gl.ARRAY_BUFFER) + ctx.popArrayBuffer() this.glIBuf = gl.createBuffer() - ctx.pushBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glIBuf) + ctx.pushElementArrayBuffer(this.glIBuf) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW) - ctx.popBuffer(gl.ELEMENT_ARRAY_BUFFER) + ctx.popElementArrayBuffer() this.count = indices.length @@ -428,12 +415,12 @@ export class Mesh { draw(primitive?: GLenum) { const gl = this.ctx.gl - this.ctx.pushBuffer(gl.ARRAY_BUFFER, this.glVBuf) - this.ctx.pushBuffer(gl.ELEMENT_ARRAY_BUFFER, this.glIBuf) + this.ctx.pushArrayBuffer(this.glVBuf) + this.ctx.pushElementArrayBuffer(this.glIBuf) this.ctx.setVertexFormat(this.vertexFormat) gl.drawElements(primitive ?? gl.TRIANGLES, this.count, gl.UNSIGNED_SHORT, 0) - this.ctx.popBuffer(gl.ARRAY_BUFFER) - this.ctx.popBuffer(gl.ELEMENT_ARRAY_BUFFER) + this.ctx.popArrayBuffer() + this.ctx.popElementArrayBuffer() } free() { @@ -445,36 +432,22 @@ export class Mesh { } -// TODO: support useProgram -function genBinder(func: (ty: GLenum, item: T) => void) { - const bindings = {} - return { - cur: (ty: GLenum) => { - const stack = bindings[ty] ?? [] - return stack[stack.length - 1] - }, - push: (ty: GLenum, item: T) => { - if (!bindings[ty]) bindings[ty] = [] - const stack = bindings[ty] - stack.push(item) - func(ty, item) - }, - pop: (ty: GLenum) => { - const stack = bindings[ty] - if (!stack) throw new Error(`Unknown WebGL type: ${ty}`) - if (stack.length <= 0) throw new Error("Can't unbind texture when there's no texture bound") - stack.pop() - func(ty, stack[stack.length - 1] ?? null) - }, +function genStack(setFunc: (item: T) => void) { + const stack: T[] = [] + const push = (item: T) => { + stack.push(item) + setFunc(item) } + const pop = () => { + stack.pop() + setFunc(cur() ?? null) + } + const cur = () => stack[stack.length - 1] + return [push, pop, cur] as const } -export default (gl: WebGLRenderingContext): GfxCtx => { +export default function initGfx(gl: WebGLRenderingContext) { - const textureBinder = genBinder(gl.bindTexture.bind(gl)) - const bufferBinder = genBinder(gl.bindBuffer.bind(gl)) - const framebufferBinder = genBinder(gl.bindFramebuffer.bind(gl)) - const renderbufferBinder = genBinder(gl.bindRenderbuffer.bind(gl)) const gc: Array<() => void> = [] function onDestroy(action) { @@ -499,18 +472,48 @@ export default (gl: WebGLRenderingContext): GfxCtx => { }, 0) } + const [ pushTexture2D, popTexture2D ] = + genStack((t) => gl.bindTexture(gl.TEXTURE_2D, t)) + + const [ pushArrayBuffer, popArrayBuffer ] = + genStack((b) => gl.bindBuffer(gl.ARRAY_BUFFER, b)) + + const [ pushElementArrayBuffer, popElementArrayBuffer ] = + genStack((b) => gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, b)) + + const [ pushFramebuffer, popFramebuffer ] = + genStack((b) => gl.bindFramebuffer(gl.FRAMEBUFFER, b)) + + const [ pushRenderbuffer, popRenderbuffer ] = + genStack((b) => gl.bindRenderbuffer(gl.RENDERBUFFER, b)) + + const [ pushViewport, popViewport ] = + genStack<{ x: number, y: number, w: number, h: number }>(({ x, y, w, h }) => { + gl.viewport(x, y, w, h) + }) + + const [ pushProgram, popProgram ] = genStack((p) => gl.useProgram(p)) + + pushViewport({ x: 0, y: 0, w: gl.drawingBufferWidth, h: gl.drawingBufferHeight }) + return { gl, onDestroy, destroy, - pushTexture: textureBinder.push, - popTexture: textureBinder.pop, - pushBuffer: bufferBinder.push, - popBuffer: bufferBinder.pop, - pushFramebuffer: framebufferBinder.push, - popFramebuffer: framebufferBinder.pop, - pushRenderbuffer: renderbufferBinder.push, - popRenderbuffer: renderbufferBinder.pop, + pushTexture2D, + popTexture2D, + pushArrayBuffer, + popArrayBuffer, + pushElementArrayBuffer, + popElementArrayBuffer, + pushFramebuffer, + popFramebuffer, + pushRenderbuffer, + popRenderbuffer, + pushViewport, + popViewport, + pushProgram, + popProgram, setVertexFormat, } diff --git a/src/kaboom.ts b/src/kaboom.ts index 91a8df27d..42a20f799 100644 --- a/src/kaboom.ts +++ b/src/kaboom.ts @@ -54,7 +54,7 @@ import easings from "./easings" import TexPacker from "./texPacker" import { - IDList, + Registry, Event, EventHandler, download, @@ -64,6 +64,7 @@ import { uid, isDataURL, getExt, + overload2, dataURLToArrayBuffer, EventController, getErrorMessage, @@ -460,8 +461,6 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { ) } - // TODO: use this instead of change UV - // gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true) gl.enable(gl.BLEND) gl.blendFuncSeparate( gl.SRC_ALPHA, @@ -696,7 +695,6 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { game.root.use(timer()) - // TODO: accept Asset? // wrap individual loaders with global loader counter, for stuff like progress bar function load(prom: Promise): Asset { return assets.custom.add(null, prom) @@ -1329,6 +1327,10 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { anchor?: Anchor | Vec2, } + function makeCanvas(w: number, h: number) { + return new FrameBuffer(ggl, w, h) + } + function makeShader( vertSrc: string | null = DEF_VERT, fragSrc: string | null = DEF_FRAG, @@ -1423,7 +1425,6 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { // clear backbuffer gl.clear(gl.COLOR_BUFFER_BIT) gfx.frameBuffer.bind() - gl.viewport(0, 0, gfx.frameBuffer.width, gfx.frameBuffer.height) // clear framebuffer gl.clear(gl.COLOR_BUFFER_BIT) @@ -2638,6 +2639,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { draw(this: GameObj) { if (this.hidden) return + if (this.canvas) this.canvas.bind() const f = gfx.fixed if (this.fixed) gfx.fixed = true pushTransform() @@ -2665,6 +2667,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { } popTransform() gfx.fixed = f + if (this.canvas) this.canvas.unbind() }, drawInspect(this: GameObj) { @@ -2954,7 +2957,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { // add an event to a tag function on(event: string, tag: Tag, cb: (obj: GameObj, ...args) => void): EventController { if (!game.objEvents[event]) { - game.objEvents[event] = new IDList() + game.objEvents[event] = new Registry() } return game.objEvents.on(event, (obj, ...args) => { if (obj.is(tag)) { @@ -2963,57 +2966,47 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { }) } - // add update event to a tag or global update - const onUpdate = ((tag: Tag | (() => void), action?: (obj: GameObj) => void) => { - if (typeof tag === "function" && action === undefined) { - const obj = add([{ update: tag }]) - return { - get paused() { - return obj.paused - }, - set paused(p) { - obj.paused = p - }, - cancel: () => obj.destroy(), - } - } else if (typeof tag === "string") { - return on("update", tag, action) + const onUpdate = overload2((action: () => void): EventController => { + const obj = add([{ update: action }]) + return { + get paused() { + return obj.paused + }, + set paused(p) { + obj.paused = p + }, + cancel: () => obj.destroy(), } - }) as KaboomCtx["onUpdate"] + }, (tag: Tag, action: (obj: GameObj) => void) => { + return on("update", tag, action) + }) - // add draw event to a tag or global draw - const onDraw = ((tag: Tag | (() => void), action?: (obj: GameObj) => void) => { - if (typeof tag === "function" && action === undefined) { - const obj = add([{ draw: tag }]) - return { - get paused() { - return obj.hidden - }, - set paused(p) { - obj.hidden = p - }, - cancel: () => obj.destroy(), - } - } else if (typeof tag === "string") { - return on("draw", tag, action) + const onDraw = overload2((action: () => void): EventController => { + const obj = add([{ draw: action }]) + return { + get paused() { + return obj.hidden + }, + set paused(p) { + obj.hidden = p + }, + cancel: () => obj.destroy(), } - }) as KaboomCtx["onDraw"] + }, (tag: Tag, action: (obj: GameObj) => void) => { + return on("draw", tag, action) + }) - function onAdd(tag: Tag | ((obj: GameObj) => void), action?: (obj: GameObj) => void) { - if (typeof tag === "function" && action === undefined) { - return game.events.on("add", tag) - } else if (typeof tag === "string") { - return on("add", tag, action) - } - } + const onAdd = overload2((action: (obj: GameObj) => void) => { + return game.events.on("add", action) + }, (tag: Tag, action: (obj: GameObj) => void) => { + return on("add", tag, action) + }) - function onDestroy(tag: Tag | ((obj: GameObj) => void), action?: (obj: GameObj) => void) { - if (typeof tag === "function" && action === undefined) { - return game.events.on("destroy", tag) - } else if (typeof tag === "string") { - return on("destroy", tag, action) - } - } + const onDestroy = overload2((action: (obj: GameObj) => void) => { + return game.events.on("destroy", action) + }, (tag: Tag, action: (obj: GameObj) => void) => { + return on("destroy", tag, action) + }) // add an event that runs with objs with t1 collides with objs with t2 function onCollide( @@ -3045,20 +3038,17 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { onAdd(t, action) } - // add an event that runs when objs with tag t is clicked - function onClick(tag: Tag | (() => void), action?: (obj: GameObj) => void): EventController { - if (typeof tag === "function") { - return app.onMousePress(tag) - } else { - const events = [] - forAllCurrentAndFuture(tag, (obj) => { - if (!obj.area) - throw new Error("onClick() requires the object to have area() component") - events.push(obj.onClick(() => action(obj))) - }) - return EventController.join(events) - } - } + const onClick = overload2((action: () => void) => { + return app.onMousePress(action) + }, (tag: Tag, action: (obj: GameObj) => void) => { + const events = [] + forAllCurrentAndFuture(tag, (obj) => { + if (!obj.area) + throw new Error("onClick() requires the object to have area() component") + events.push(obj.onClick(() => action(obj))) + }) + return EventController.join(events) + }) // add an event that runs once when objs with tag t is hovered function onHover(t: Tag, action: (obj: GameObj) => void): EventController { @@ -3093,38 +3083,6 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { return EventController.join(events) } - function enterDebugMode() { - - app.onKeyPress("f1", () => { - debug.inspect = !debug.inspect - }) - - app.onKeyPress("f2", () => { - debug.clearLog() - }) - - app.onKeyPress("f8", () => { - debug.paused = !debug.paused - }) - - app.onKeyPress("f7", () => { - debug.timeScale = toFixed(clamp(debug.timeScale - 0.2, 0, 2), 1) - }) - - app.onKeyPress("f9", () => { - debug.timeScale = toFixed(clamp(debug.timeScale + 0.2, 0, 2), 1) - }) - - app.onKeyPress("f10", () => { - debug.stepFrame() - }) - - } - - function enterBurpMode() { - app.onKeyPress("b", () => burp()) - } - function setGravity(g: number) { game.gravity = g } @@ -4455,11 +4413,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { maxHP = n }, setHP(this: GameObj, n: number) { - if (maxHP) { - hp = Math.min(maxHP, n) - } else { - hp = n - } + hp = maxHP ? Math.min(maxHP, n) : n if (hp <= 0) { this.trigger("death") } @@ -4639,6 +4593,14 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { } } + function drawon(c: FrameBuffer) { + return { + add(this: GameObj) { + this.canvas = c + }, + } + } + function onLoad(cb: () => void): void { if (assets.loaded) { cb() @@ -5510,7 +5472,6 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { function boom(speed: number = 2, size: number = 1): Comp { let time = 0 return { - id: "boom", require: [ "scale" ], update(this: GameObj) { const s = Math.sin(time * speed) * size @@ -6236,11 +6197,20 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { }) if (gopt.debug !== false) { - enterDebugMode() + app.onKeyPress("f1", () => debug.inspect = !debug.inspect) + app.onKeyPress("f2", () => debug.clearLog()) + app.onKeyPress("f8", () => debug.paused = !debug.paused) + app.onKeyPress("f7", () => { + debug.timeScale = toFixed(clamp(debug.timeScale - 0.2, 0, 2), 1) + }) + app.onKeyPress("f9", () => { + debug.timeScale = toFixed(clamp(debug.timeScale + 0.2, 0, 2), 1) + }) + app.onKeyPress("f10", () => debug.stepFrame()) } if (gopt.burp) { - enterBurpMode() + app.onKeyPress("b", () => burp()) } } @@ -6346,6 +6316,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { state, fadeIn, mask, + drawon, tile, agent, // group events @@ -6461,6 +6432,7 @@ export default (gopt: KaboomOpt = {}): KaboomCtx => { pushRotate, pushMatrix, usePostEffect, + makeCanvas, // debug debug, // scene diff --git a/src/types.ts b/src/types.ts index c25fcb79b..4df328292 100644 --- a/src/types.ts +++ b/src/types.ts @@ -604,6 +604,7 @@ export interface KaboomCtx { * @since v3000.2 */ mask(maskType?: Mask): MaskComp, + drawon(canvas: FrameBuffer): Comp, /** * A tile on a tile map. * @@ -2236,6 +2237,12 @@ export interface KaboomCtx { * ``` */ formatText(options: DrawTextOpt): FormattedText, + /** + * Create a canvas to draw stuff offscreen. + * + * @since v3000.2 + */ + makeCanvas(w: number, h: number): FrameBuffer /** * @section Debug * @@ -2711,6 +2718,12 @@ export interface GameObjRaw { * A unique number ID for each game object. */ id: GameObjID | null, + /** + * The canvas to draw this game object on + * + * @since v3000.2 + */ + canvas: FrameBuffer | null, onKeyDown(key: Key, action: (key: Key) => void): EventController, onKeyDown(action: (key: Key) => void): EventController, onKeyPress(key: Key, action: (key: Key) => void): EventController, @@ -3131,6 +3144,22 @@ export declare class Texture { free(): void } +export declare class FrameBuffer { + ctx: GfxCtx + tex: Texture + glFramebuffer: WebGLFramebuffer + glRenderbuffer: WebGLRenderbuffer + constructor(ctx: GfxCtx, w: number, h: number, opt?: TextureOpt) + width: number + height: number + toImageData(): ImageData + toDataURL(): string + draw(action: () => void): void + bind(): void + unbind(): void + free(): void +} + export interface GfxFont { tex: Texture, map: Record, diff --git a/src/utils.ts b/src/utils.ts index f70a43f3b..b8c8366be 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -export class IDList extends Map { +export class Registry extends Map { private lastID: number constructor(...args) { super(...args) @@ -34,7 +34,7 @@ export class EventController { } export class Event { - private handlers: IDList<(...args: Args) => void> = new IDList() + private handlers: Registry<(...args: Args) => void> = new Registry() add(action: (...args: Args) => void): EventController { const cancel = this.handlers.pushd((...args: Args) => { if (ev.paused) return