diff --git a/prosperon/emitter.cm b/prosperon/emitter.cm deleted file mode 100644 index b0ea384e..00000000 --- a/prosperon/emitter.cm +++ /dev/null @@ -1,135 +0,0 @@ -var color = use('color') -var graphics = use('graphics') -var transform = use('transform') - -var ex = {} - -ex.emitters = new Set() - -ex.garbage = function() -{ - ex.emitters.delete(this) -} - -ex.update = function(dt) -{ - for (var e of ex.emitters) - try { e.step(dt) } catch(e) { log.error(e) } -} - -ex.step_hook = function(p) -{ - if (p.time < this.grow_for) { - var s = Math.lerp(0, this.scale, p.time / this.grow_for); - p.transform.scale = s; - } else if (p.time > p.life - this.shrink_for) { - var s = Math.lerp(0, this.scale, (p.life - p.time) / this.shrink_for); - p.transform.scale = s; - } else p.transform.scale = [this.scale, this.scale, this.scale]; -} - -ex.step = function(dt) -{ - // update spawning particles - if (this.on && this.pps > 0) { - this.spawn_timer += dt; - var pp = 1 / this.pps; - while (this.spawn_timer > pp) { - this.spawn_timer -= pp; - this.spawn(); - } - } - - // update all particles - for (var p of this.particles) { - p.time += dt; - p.transform.move(p.body.velocity?.scale(dt)); - this.step_hook?.(p); - - if (this.kill_hook?.(p) || p.time >= p.life) { - this.die_hook?.(p); - this.dead.push(p); - this.particles.delete(p); - } - } -} - -ex.burst = function(count,t) -{ - for (var i = 0; i < count; i++) this.spawn(t) -} - -ex.spawn = function(t) -{ - t ??= this.transform - - var par = this.dead.shift() - if (par) { - par.transform.unit() - par.transform.pos = t.pos; - par.transform.scale = this.scale; - this.particles.push(par); - par.time = 0; - this.spawn_hook?.(par); - par.life = this.life; - return; - } - - par = { - transform: new transform, - life: this.life, - time: 0, - color: this.color, - body:{}, - } - - par.transform.scale = this.scale - this.particles.push(par) - this.spawn_hook(par) -} - -ex.stat = function() -{ - var stat = {}; - stat.emitters = emitters.length; - var particles = 0; - for (var e of emitters) particles += e.particles.length; - stat.particles = particles; - return stat; -} - -ex.life = 10 -ex.scale = 1 -ex.grow_for = 0 -ex.spawn_timer = 0 -ex.pps = 0 -ex.color = color.white - -ex.draw = function() -{ -/* var diff = graphics.texture(this.diffuse) - if (!diff) throw new Error("emitter does not have a proper diffuse texture") - - var mesh = graphics.make_sprite_mesh(this.particles) - if (mesh.num_indices == 0) return - render.queue({ - type:'geometry', - mesh, - image:diff, - pipeline, - first_index:0, - num_indices:mesh.num_indices - }) -*/ -} - -return ex - ---- - -this.particles = [] -this.dead = [] - -this.transform = this.overling.transform - -$.emitters.add(this) diff --git a/prosperon/prosperon.cm b/prosperon/prosperon.cm index ffb67f0f..648db8fc 100644 --- a/prosperon/prosperon.cm +++ b/prosperon/prosperon.cm @@ -78,8 +78,6 @@ function worldToScreenRect({x,y,width,height}, camera) { } } - - var gameactor var images = {} @@ -248,62 +246,74 @@ function translate_draw_commands(commands) { break case "tilemap": - // Group tiles by texture to batch draw calls - var textureGroups = {} - var tilePositions = [] + // Get cached geometry commands from tilemap + var geometryCommands = cmd.tilemap.draw() - // Collect all tiles and their positions - tilemap.for(cmd.tilemap, (tile, {x,y}) => { - if (tile) { - tilePositions.push({tile, x, y}) - } - }) - - // Group tiles by texture - tilePositions.forEach(({tile, x, y}) => { - var img = graphics.texture(tile) - if (img && img.gpu) { - var texId = img.gpu.id - if (!textureGroups[texId]) { - textureGroups[texId] = { - texture: img, - tiles: [] - } - } - textureGroups[texId].tiles.push({x, y, img}) - } - }) - - // Generate draw commands for each texture group - Object.keys(textureGroups).forEach(texId => { - var group = textureGroups[texId] - var tiles = group.tiles + // Process each geometry command (one per texture) + for (var geomCmd of geometryCommands) { + var img = graphics.texture(geomCmd.image) + var gpu = img.gpu + if (!gpu) continue - // Create a temporary tilemap with only tiles from this texture - // Apply tilemap position to the offset to shift the world coordinates - var tempMap = { - tiles: [], - offset_x: cmd.tilemap.offset_x + (cmd.tilemap.pos.x / cmd.tilemap.size_x), - offset_y: cmd.tilemap.offset_y + (cmd.tilemap.pos.y / cmd.tilemap.size_y), - size_x: cmd.tilemap.size_x, - size_y: cmd.tilemap.size_y + // Transform geometry through camera and send to renderer + var geom = geomCmd.geometry + + // Transform XY coordinates using camera matrix + var camera_params = [camera.a, camera.c, camera.e, camera.f] + var transformed_xy = geometry.transform_xy_blob(geom.xy, camera_params) + + // Create new geometry object with transformed coordinates + var transformed_geom = { + xy: transformed_xy, + xy_stride: geom.xy_stride, + uv: geom.uv, + uv_stride: geom.uv_stride, + color: geom.color, + color_stride: geom.color_stride, + indices: geom.indices, + num_vertices: geom.num_vertices, + num_indices: geom.num_indices, + size_indices: geom.size_indices, + texture_id: gpu.id } - - // Build sparse array for this texture's tiles - tiles.forEach(({x, y, img}) => { - var arrayX = x - cmd.tilemap.offset_x - var arrayY = y - cmd.tilemap.offset_y - if (!tempMap.tiles[arrayX]) tempMap.tiles[arrayX] = [] - tempMap.tiles[arrayX][arrayY] = img - }) - // Generate geometry for this texture group - var geom = geometry.tilemap_to_data(cmd.tilemap) - geom.texture_id = parseInt(texId) renderer_commands.push({ op: "geometry_raw", - data: geom + data: transformed_geom }) + } + break + + case "geometry": + var img = graphics.texture(cmd.image) + var gpu = img.gpu + if (!gpu) break + + // Transform geometry through camera and send to renderer + var geom = cmd.geometry + + // Transform XY coordinates using camera matrix + var camera_params = [camera.a, camera.c, camera.e, camera.f] + var transformed_xy = geometry.transform_xy_blob(geom.xy, camera_params) + + // Create new geometry object with transformed coordinates + var transformed_geom = { + xy: transformed_xy, + xy_stride: geom.xy_stride, + uv: geom.uv, + uv_stride: geom.uv_stride, + color: geom.color, + color_stride: geom.color_stride, + indices: geom.indices, + num_vertices: geom.num_vertices, + num_indices: geom.num_indices, + size_indices: geom.size_indices, + texture_id: gpu.id + } + + renderer_commands.push({ + op: "geometry_raw", + data: transformed_geom }) break } @@ -412,7 +422,9 @@ prosperon.set_window = function(config) } var renderer_cmds = { - resolution(size) { + resolution(e) { + logical.width = e.width + logical.height = e.height send(video, {kind:"renderer", op:'set', prop:'logicalPresentation', value: {...e}}) } } diff --git a/prosperon/tilemap.cm b/prosperon/tilemap.cm index 84b9f764..34253ad2 100644 --- a/prosperon/tilemap.cm +++ b/prosperon/tilemap.cm @@ -7,6 +7,8 @@ function tilemap() this.offset_y = 0; this.size_x = 32; this.size_y = 32; + this._geometry_cache = null; + this._dirty = true; return this; } @@ -65,13 +67,91 @@ tilemap.prototype = // Set the value this.tiles[x][y] = image; + + // Mark cache as dirty when tiles change + this._dirty = true; }, - draw(pos = {x: 0, y: 0}) { - return { - cmd:'tilemap', - tilemap:this, + // Build cached geometry grouped by texture + _build_geometry_cache(pos = {x: 0, y: 0}) { + var geometry = use('geometry'); + + // Group tiles by texture + var textureGroups = {}; + + // Collect all tiles and their positions + for (var x = 0; x < this.tiles.length; x++) { + if (!this.tiles[x]) continue; + for (var y = 0; y < this.tiles[x].length; y++) { + var tile = this.tiles[x][y]; + if (tile) { + var textureKey = tile; + if (!textureGroups[textureKey]) { + textureGroups[textureKey] = { + tiles: [], + offset_x: this.offset_x, + offset_y: this.offset_y, + size_x: this.size_x, + size_y: this.size_y + }; + } + textureGroups[textureKey].tiles.push({ + x: x + this.offset_x, + y: y + this.offset_y, + image: tile + }); + } + } } + + // Generate geometry for each texture group + var geometryCommands = []; + for (var textureKey in textureGroups) { + var group = textureGroups[textureKey]; + if (group.tiles.length == 0) continue; + + // Create a temporary tilemap for this texture group + var tempMap = { + tiles: [], + offset_x: group.offset_x, + offset_y: group.offset_y, + size_x: group.size_x, + size_y: group.size_y, + pos_x: pos.x, + pos_y: pos.y + }; + + // Build sparse array for this texture's tiles + group.tiles.forEach(({x, y, image}) => { + var arrayX = x - group.offset_x; + var arrayY = y - group.offset_y; + if (!tempMap.tiles[arrayX]) tempMap.tiles[arrayX] = []; + tempMap.tiles[arrayX][arrayY] = image; + }); + + // Generate geometry for this group + var geom = geometry.tilemap_to_data(tempMap); + + geometryCommands.push({ + cmd: "geometry", + geometry: geom, + image: textureKey + }); + } + + this._geometry_cache = geometryCommands; + this._dirty = false; + }, + + draw(pos = {x: 0, y: 0}) { + // Rebuild cache if dirty or position changed + if (this._dirty || !this._geometry_cache || this._last_pos?.x != pos.x || this._last_pos?.y != pos.y) { + this._build_geometry_cache(pos); + this._last_pos = {x: pos.x, y: pos.y}; + } + + // Return cached geometry commands + return this._geometry_cache; }, } diff --git a/scripts/graphics.cm b/scripts/graphics.cm index 797e43c7..342912c4 100644 --- a/scripts/graphics.cm +++ b/scripts/graphics.cm @@ -161,7 +161,10 @@ function makeAnim(frames, loop=true){ function decode_image(bytes, ext) { switch(ext) { - case 'gif': return graphics.make_gif(bytes) + case 'gif': + var g = graphics.make_gif(bytes) + if (g.frames) return g.frames[0] + return g case 'ase': case 'aseprite': return graphics.make_aseprite(bytes) default: return {surface:graphics.make_texture(bytes)} diff --git a/source/qjs_geometry.c b/source/qjs_geometry.c index cab4ae80..dd376f67 100644 --- a/source/qjs_geometry.c +++ b/source/qjs_geometry.c @@ -812,11 +812,25 @@ JSC_CCALL(geometry_tilemap_to_data, // Get tilemap properties double offset_x, offset_y, size_x, size_y; + double pos_x = 0, pos_y = 0; JS_GETPROP(js, offset_x, tilemap_obj, offset_x, number) JS_GETPROP(js, offset_y, tilemap_obj, offset_y, number) JS_GETPROP(js, size_x, tilemap_obj, size_x, number) JS_GETPROP(js, size_y, tilemap_obj, size_y, number) + // Get position properties (optional, default to 0) + JSValue pos_x_val = JS_GetPropertyStr(js, tilemap_obj, "pos_x"); + if (!JS_IsNull(pos_x_val)) { + JS_ToFloat64(js, &pos_x, pos_x_val); + } + JS_FreeValue(js, pos_x_val); + + JSValue pos_y_val = JS_GetPropertyStr(js, tilemap_obj, "pos_y"); + if (!JS_IsNull(pos_y_val)) { + JS_ToFloat64(js, &pos_y, pos_y_val); + } + JS_FreeValue(js, pos_y_val); + JSValue tiles_array = JS_GetPropertyStr(js, tilemap_obj, "tiles"); if (!JS_IsArray(js, tiles_array)) { JS_FreeValue(js, tiles_array); @@ -870,8 +884,8 @@ JSC_CCALL(geometry_tilemap_to_data, // x and y are array indices, need to convert to logical coordinates float logical_x = x + offset_x; float logical_y = y + offset_y; - float world_x = logical_x * size_x; - float world_y = logical_y * size_y; + float world_x = logical_x * size_x + pos_x; + float world_y = logical_y * size_y + pos_y; // Set vertex positions (4 corners of the tile) int base = vertex_idx * 2; @@ -957,6 +971,277 @@ JSC_CCALL(geometry_tilemap_to_data, free(index_data); ) +JSC_CCALL(geometry_sprites_to_data, + JSValue sprites_array = argv[0]; + if (!JS_IsArray(js, sprites_array)) { + return JS_ThrowTypeError(js, "sprites must be an array"); + } + + int sprite_count = JS_ArrayLength(js, sprites_array); + if (sprite_count == 0) { + return JS_NewObject(js); + } + + // Allocate buffers - 4 vertices per sprite + int vertex_count = sprite_count * 4; + int index_count = sprite_count * 6; + + float *xy_data = malloc(vertex_count * 2 * sizeof(float)); + float *uv_data = malloc(vertex_count * 2 * sizeof(float)); + SDL_FColor *color_data = malloc(vertex_count * sizeof(SDL_FColor)); + uint16_t *index_data = malloc(index_count * sizeof(uint16_t)); + + // Generate vertices + int vertex_idx = 0; + int index_idx = 0; + + for (int i = 0; i < sprite_count; i++) { + JSValue sprite = JS_GetPropertyUint32(js, sprites_array, i); + + // Get sprite properties + JSValue pos_val = JS_GetPropertyStr(js, sprite, "pos"); + JSValue texture_val = JS_GetPropertyStr(js, sprite, "texture"); + JSValue color_val = JS_GetPropertyStr(js, sprite, "color"); + + double width = 32, height = 32, anchor_x = 0.5, anchor_y = 0.5; + + // Try to get width/height from sprite first, otherwise use texture dimensions + JSValue width_val = JS_GetPropertyStr(js, sprite, "width"); + if (!JS_IsNull(width_val)) { + JS_ToFloat64(js, &width, width_val); + } else if (!JS_IsNull(texture_val)) { + // Get width from texture + JSValue texture_width = JS_GetPropertyStr(js, texture_val, "width"); + if (!JS_IsNull(texture_width)) { + JS_ToFloat64(js, &width, texture_width); + } + JS_FreeValue(js, texture_width); + } + JS_FreeValue(js, width_val); + + JSValue height_val = JS_GetPropertyStr(js, sprite, "height"); + if (!JS_IsNull(height_val)) { + JS_ToFloat64(js, &height, height_val); + } else if (!JS_IsNull(texture_val)) { + // Get height from texture + JSValue texture_height = JS_GetPropertyStr(js, texture_val, "height"); + if (!JS_IsNull(texture_height)) { + JS_ToFloat64(js, &height, texture_height); + } + JS_FreeValue(js, texture_height); + } + JS_FreeValue(js, height_val); + + JSValue anchor_x_val = JS_GetPropertyStr(js, sprite, "anchor_x"); + if (!JS_IsNull(anchor_x_val)) { + JS_ToFloat64(js, &anchor_x, anchor_x_val); + } + JS_FreeValue(js, anchor_x_val); + + JSValue anchor_y_val = JS_GetPropertyStr(js, sprite, "anchor_y"); + if (!JS_IsNull(anchor_y_val)) { + JS_ToFloat64(js, &anchor_y, anchor_y_val); + } + JS_FreeValue(js, anchor_y_val); + + HMM_Vec2 pos = js2vec2(js, pos_val); + + // Calculate sprite corners with anchor + float half_w = width * 0.5f; + float half_h = height * 0.5f; + float anchor_offset_x = width * anchor_x - half_w; + float anchor_offset_y = height * anchor_y - half_h; + + float left = pos.x - half_w - anchor_offset_x; + float right = pos.x + half_w - anchor_offset_x; + float bottom = pos.y - half_h - anchor_offset_y; + float top = pos.y + half_h - anchor_offset_y; + + // Set vertex positions (4 corners of the sprite) + int base = vertex_idx * 2; + xy_data[base + 0] = left; // bottom-left + xy_data[base + 1] = bottom; + xy_data[base + 2] = right; // bottom-right + xy_data[base + 3] = bottom; + xy_data[base + 4] = left; // top-left + xy_data[base + 5] = top; + xy_data[base + 6] = right; // top-right + xy_data[base + 7] = top; + + // Get UV coordinates from texture (if available) + if (!JS_IsNull(texture_val)) { + JSValue rect_val = JS_GetPropertyStr(js, texture_val, "rect"); + if (!JS_IsNull(rect_val)) { + rect uv_rect = js2rect(js, rect_val); + + // Get texture dimensions to normalize pixel coordinates + double tex_width = 1.0, tex_height = 1.0; + JSValue texture_width = JS_GetPropertyStr(js, texture_val, "width"); + JSValue texture_height = JS_GetPropertyStr(js, texture_val, "height"); + if (!JS_IsNull(texture_width)) { + JS_ToFloat64(js, &tex_width, texture_width); + } + if (!JS_IsNull(texture_height)) { + JS_ToFloat64(js, &tex_height, texture_height); + } + JS_FreeValue(js, texture_width); + JS_FreeValue(js, texture_height); + + // The rect contains pixel coordinates, normalize them + float u0 = uv_rect.x / tex_width; + float v0 = uv_rect.y / tex_height; + float u1 = (uv_rect.x + uv_rect.w) / tex_width; + float v1 = (uv_rect.y + uv_rect.h) / tex_height; + + // Set UVs based on normalized texture rect + uv_data[base + 0] = u0; uv_data[base + 1] = v1; // bottom-left + uv_data[base + 2] = u1; uv_data[base + 3] = v1; // bottom-right + uv_data[base + 4] = u0; uv_data[base + 5] = v0; // top-left + uv_data[base + 6] = u1; uv_data[base + 7] = v0; // top-right + JS_FreeValue(js, rect_val); + } else { + // Default UVs (0-1) + uv_data[base + 0] = 0.0f; uv_data[base + 1] = 1.0f; + uv_data[base + 2] = 1.0f; uv_data[base + 3] = 1.0f; + uv_data[base + 4] = 0.0f; uv_data[base + 5] = 0.0f; + uv_data[base + 6] = 1.0f; uv_data[base + 7] = 0.0f; + } + } else { + // Default UVs (0-1) + uv_data[base + 0] = 0.0f; uv_data[base + 1] = 1.0f; + uv_data[base + 2] = 1.0f; uv_data[base + 3] = 1.0f; + uv_data[base + 4] = 0.0f; uv_data[base + 5] = 0.0f; + uv_data[base + 6] = 1.0f; uv_data[base + 7] = 0.0f; + } + + // Set colors + SDL_FColor default_color = {1.0f, 1.0f, 1.0f, 1.0f}; + if (!JS_IsNull(color_val)) { + HMM_Vec4 color = js2color(js, color_val); + default_color.r = color.r; + default_color.g = color.g; + default_color.b = color.b; + default_color.a = color.a; + } + + for (int j = 0; j < 4; j++) { + color_data[vertex_idx + j] = default_color; + } + + // Set indices (two triangles per sprite) + uint16_t base_idx = vertex_idx; + index_data[index_idx++] = base_idx + 0; // triangle 1: 0,1,2 + index_data[index_idx++] = base_idx + 1; + index_data[index_idx++] = base_idx + 2; + index_data[index_idx++] = base_idx + 1; // triangle 2: 1,3,2 + index_data[index_idx++] = base_idx + 3; + index_data[index_idx++] = base_idx + 2; + + vertex_idx += 4; + + JS_FreeValue(js, pos_val); + JS_FreeValue(js, texture_val); + JS_FreeValue(js, color_val); + JS_FreeValue(js, sprite); + } + + // Create result object with blob data + ret = JS_NewObject(js); + + // Create blobs for each data type + JSValue xy_blob = js_new_blob_stoned_copy(js, xy_data, vertex_count * 2 * sizeof(float)); + JSValue uv_blob = js_new_blob_stoned_copy(js, uv_data, vertex_count * 2 * sizeof(float)); + JSValue color_blob = js_new_blob_stoned_copy(js, color_data, vertex_count * sizeof(SDL_FColor)); + JSValue index_blob = js_new_blob_stoned_copy(js, index_data, index_count * sizeof(uint16_t)); + + JS_SetPropertyStr(js, ret, "xy", xy_blob); + JS_SetPropertyStr(js, ret, "xy_stride", JS_NewInt32(js, 2 * sizeof(float))); + JS_SetPropertyStr(js, ret, "uv", uv_blob); + JS_SetPropertyStr(js, ret, "uv_stride", JS_NewInt32(js, 2 * sizeof(float))); + JS_SetPropertyStr(js, ret, "color", color_blob); + JS_SetPropertyStr(js, ret, "color_stride", JS_NewInt32(js, sizeof(SDL_FColor))); + JS_SetPropertyStr(js, ret, "indices", index_blob); + JS_SetPropertyStr(js, ret, "num_vertices", JS_NewInt32(js, vertex_count)); + JS_SetPropertyStr(js, ret, "num_indices", JS_NewInt32(js, index_count)); + JS_SetPropertyStr(js, ret, "size_indices", JS_NewInt32(js, 2)); // using uint16_t + + free(xy_data); + free(uv_data); + free(color_data); + free(index_data); +) + +JSC_CCALL(geometry_transform_xy_blob, + // argv[0] = xy blob (contains vertex positions as float pairs) + // argv[1] = camera transform parameters [a, c, e, f] + + JSValue xy_blob = argv[0]; + if (!js_is_blob(js, xy_blob)) { + return JS_ThrowTypeError(js, "First argument must be an XY blob"); + } + + JSValue camera_params = argv[1]; + if (!JS_IsArray(js, camera_params) || JS_ArrayLength(js, camera_params) != 4) { + return JS_ThrowTypeError(js, "Second argument must be an array of 4 camera transform parameters [a, c, e, f]"); + } + + // Get camera transform parameters + double a, c, e, f; + JSValue a_val = JS_GetPropertyUint32(js, camera_params, 0); + JSValue c_val = JS_GetPropertyUint32(js, camera_params, 1); + JSValue e_val = JS_GetPropertyUint32(js, camera_params, 2); + JSValue f_val = JS_GetPropertyUint32(js, camera_params, 3); + + JS_ToFloat64(js, &a, a_val); + JS_ToFloat64(js, &c, c_val); + JS_ToFloat64(js, &e, e_val); + JS_ToFloat64(js, &f, f_val); + + JS_FreeValue(js, a_val); + JS_FreeValue(js, c_val); + JS_FreeValue(js, e_val); + JS_FreeValue(js, f_val); + + // Get blob data + size_t xy_size; + float *xy_data = (float*)js_get_blob_data(js, &xy_size, xy_blob); + if (!xy_data) { + return JS_ThrowTypeError(js, "Failed to get XY blob data"); + } + + // Calculate number of vertices (each vertex has 2 floats: x, y) + int vertex_count = xy_size / (2 * sizeof(float)); + if (vertex_count * 2 * sizeof(float) != xy_size) { + return JS_ThrowTypeError(js, "XY blob size is not a multiple of vertex size"); + } + + // Allocate new buffer for transformed coordinates + float *transformed_xy = malloc(xy_size); + if (!transformed_xy) { + return JS_ThrowTypeError(js, "Failed to allocate memory for transformed coordinates"); + } + + // Apply camera transformation to each vertex + for (int i = 0; i < vertex_count; i++) { + float world_x = xy_data[i * 2 + 0]; + float world_y = xy_data[i * 2 + 1]; + + // Apply 2D affine transformation: screen = world * camera_matrix + float screen_x = a * world_x + c; + float screen_y = e * world_y + f; + + transformed_xy[i * 2 + 0] = screen_x; + transformed_xy[i * 2 + 1] = screen_y; + } + + // Create new blob with transformed data + JSValue transformed_blob = js_new_blob_stoned_copy(js, transformed_xy, xy_size); + + free(transformed_xy); + + ret = transformed_blob; +) + static const JSCFunctionListEntry js_geometry_funcs[] = { MIST_FUNC_DEF(geometry, rect_intersection, 2), MIST_FUNC_DEF(geometry, rect_intersects, 2), @@ -969,6 +1254,8 @@ static const JSCFunctionListEntry js_geometry_funcs[] = { MIST_FUNC_DEF(geometry, rect_move, 2), MIST_FUNC_DEF(geometry, rect_transform, 2), MIST_FUNC_DEF(geometry, tilemap_to_data, 1), + MIST_FUNC_DEF(geometry, sprites_to_data, 1), + MIST_FUNC_DEF(geometry, transform_xy_blob, 2), MIST_FUNC_DEF(gpu, tile, 4), MIST_FUNC_DEF(gpu, slice9, 3), MIST_FUNC_DEF(gpu, make_sprite_mesh, 2),