automatically use spritesheets

This commit is contained in:
John Alanbrook 2024-10-05 08:43:51 -05:00
parent a8ce3106e1
commit f928f3e8a2
10 changed files with 1672 additions and 106 deletions

View file

@ -12,15 +12,14 @@ var make_point_obj = function (o, p) {
};
};
var fullrect = [0, 0, 1, 1];
var sprite_addbucket = function (sprite) {
if (!sprite.image) return;
var layer = 1000000 + sprite.gameobject.drawlayer * 1000 - sprite.gameobject.pos.y;
sprite_buckets[layer] ??= {};
sprite_buckets[layer][sprite.path] ??= [];
sprite_buckets[layer][sprite.path].push(sprite);
sprite_buckets[layer][sprite.image.texture.path] ??= [];
sprite_buckets[layer][sprite.image.texture.path].push(sprite);
sprite._oldlayer = layer;
sprite._oldpath = sprite.path;
sprite._oldpath = sprite.image.texture.path;
};
var sprite_rmbucket = function (sprite) {
@ -28,88 +27,101 @@ var sprite_rmbucket = function (sprite) {
else for (var layer of Object.values(sprite_buckets)) for (var path of Object.values(layer)) path.remove(sprite);
};
/* an anim is simply an array of images */
/* an anim set is like this
frog = {
walk: [],
hop: [],
...etc
}
*/
var sprite = {
loop: true,
rect: fullrect,
anim: {},
playing: 0,
image: undefined,
get diffuse() { return this.image.texture; },
set diffuse(x) {},
anim_speed: 1,
play(str = 0, fn, loop = true, reverse = false) {
play(str, loop = true, reverse = false) {
str ??= this.anim;
if (!str) return;
if (typeof str === 'string')
str = this.animset[str];
var playing = str;
this.del_anim?.();
var self = this;
var stop;
self.del_anim = function () {
self.del_anim = undefined;
self = undefined;
advance = undefined;
stop?.();
};
var playing = self.anim[str];
if (!playing) return;
var f = 0;
if (reverse) f = playing.frames.length - 1;
self.path = playing.path;
var f = 0;
if (reverse) f = playing.length - 1;
function advance(time) {
if (!self) return;
if (!self.gameobject) return;
self.frame = playing.frames[f].rect;
self.rect = [self.frame.x, self.frame.y, self.frame.w, self.frame.h];
self.update_dimensions();
var done = false;
if (reverse) {
f = (((f - 1) % playing.frames.length) + playing.frames.length) % playing.frames.length;
if (f === playing.frames.length - 1) done = true;
f = (((f - 1) % playing.length) + playing.length) % playing.length;
if (f === playing.length - 1) done = true;
} else {
f = (f + 1) % playing.frames.length;
f = (f + 1) % playing.length;
if (f === 0) done = true;
}
self.image = playing[f];
if (done) {
fn?.();
self.anim_done?.();
if (!loop) {
self?.stop();
return;
}
}
return playing.frames[f].time/self.anim_speed;
return playing[f].time/self.anim_speed;
}
stop = self.gameobject.delay(advance, playing.frames[f].time/self.anim_speed);
stop = self.gameobject.delay(advance, playing[f].time/self.anim_speed);
advance();
},
tex_sync() {
if (this.anim) this.stop();
this.rect = fullrect;
var anim = SpriteAnim.make(this.path);
this.update_dimensions();
this.sync();
if (!anim) return;
this.anim = anim;
this.play();
this.pos = this.dimensions().scale(this.anchor);
},
stop() {
this.del_anim?.();
},
set path(p) {
p = Resources.find_image(p, this.gameobject._root);
if (!p) {
var image = game.texture(p);
if (!image) {
console.warn(`Could not find image ${p}.`);
return;
}
if (p === this.path) return;
this._p = p;
this.del_anim?.();
this.texture = game.texture(p);
this.diffuse = this.texture;
this.image = image;
if (Array.isArray(image)) {
this.anim = image;
this.image = image[0];
}
if (Object.values(image)[0][0]) {
// it is an anims set
this.animset = image;
this.image = Object.values(image)[0][0];
}
this.tex_sync();
},
get path() {
@ -147,21 +159,6 @@ var sprite = {
var realpos = dim.scale(0.5).add(this.pos);
return bbox.fromcwh(realpos, dim);
},
update_dimensions() {
this._dimensions = [this.texture.width * this.rect[2], this.texture.height * this.rect[3]];
component.sprite_dim_hook?.(this);
},
dimensions() {
return this._dimensions;
},
width() {
return this.dimensions().x;
},
height() {
return this.dimensions().y;
},
};
globalThis.allsprites = [];
var sprite_buckets = [];
@ -247,7 +244,6 @@ component.sprite = function (obj) {
sp.transform = obj.transform;
sp.guid = prosperon.guid();
allsprites.push(sp);
if (component.sprite.make_hook) component.sprite.make_hook(sp);
sprite_addbucket(sp);
return sp;
};
@ -271,37 +267,38 @@ var SpriteAnim = {};
SpriteAnim.hotreload = function(path) {
delete animcache[path];
}
SpriteAnim.make = function (path) {
SpriteAnim.make = function (path, tex) {
var anim;
if (animcache[path]) return animcache[path];
if (!tex) return;
profile.report(`animation_${path}`);
if (io.exists(path.set_ext(".ase"))) anim = SpriteAnim.aseprite(path.set_ext(".ase"));
else if (io.exists(path.set_ext(".json"))) anim = SpriteAnim.aseprite(path.set_ext(".json"));
else if (path.ext() === "ase") anim = SpriteAnim.aseprite(path);
else if (path.ext() === "gif") anim = SpriteAnim.gif(path);
if (io.exists(path.set_ext(".ase"))) anim = SpriteAnim.aseprite(path.set_ext(".ase"), tex);
else if (io.exists(path.set_ext(".json"))) anim = SpriteAnim.aseprite(path.set_ext(".json"), tex);
else if (path.ext() === "ase") anim = SpriteAnim.aseprite(path, tex);
else if (path.ext() === "gif") anim = SpriteAnim.gif(path, tex);
else anim = undefined;
profile.endreport(`animation_${path}`);
animcache[path] = anim;
return animcache[path];
};
SpriteAnim.gif = function (path) {
SpriteAnim.gif = function (path, tex) {
var anim = {};
anim.frames = [];
anim.path = path;
var tex = game.texture(path).texture;
var frames = tex.frames;
if (frames === 1) return undefined;
var yslice = 1 / frames;
for (var f = 0; f < frames; f++) {
var frame = {};
frame.rect = {
x: 0,
w: 1,
y: yslice * f,
h: yslice,
};
frame.rect = [
0,
yslice * f,
1,
yslice,
];
frame.time = 0.05;
anim.frames.push(frame);
}
@ -340,12 +337,12 @@ SpriteAnim.aseprite = function (path) {
var ase_make_frame = function (ase_frame) {
var f = ase_frame.frame;
var frame = {};
frame.rect = {
x: f.x / dim.w,
w: f.w / dim.w,
y: f.y / dim.h,
h: f.h / dim.h,
};
frame.rect = [
f.x / dim.w,
f.y / dim.h,
f.w / dim.w,
f.h / dim.h,
];
frame.time = ase_frame.duration / 1000;
anim.frames.push(frame);
};

View file

@ -21,7 +21,6 @@ mum.base = {
font_size: 16,
scale: 1,
angle: 0,
inset: null,
anchor: [0, 1], // where to draw the item from, relative to the cursor. [0,1] is from the top left corner. [1,0] is from the bottom right
background_image: null,
slice: null, // pass to slice an image as a 9 slice. see render.slice9 for its format
@ -47,7 +46,10 @@ mum.base = {
image_repeat_offset: [0, 0],
debug: false /* set to true to draw debug boxes */,
hide: false,
child_gap: 0,
child_layout: 'top2bottom', /* top2bottom, left2right */
tooltip: null,
children: [],
};
// data is passed into each function, and various stats are generated
@ -64,9 +66,46 @@ var context = mum.base;
var context_stack = [];
var cursor = [0,0];
function computeContainerSize(context)
{
var sizing = context.sizing.value;
var content_dim = [0,0];
var max_child_dim = [0,0];
if (context.layout === 'left2right') {
for (var child of context.children) {
content_dim.x += child.size.x + context.child_gap;
if (child.size.y > max_child_dim.y)
max_child_dim.y = child.size.y;
}
content_dim.width -= context.child_gap; // remove extra gap after last child
content_dim.y = max_child_dim.y;
} else {
for (var child of context.children) {
content_dim.y += child.size.y + context.child_gap;
if (child.size.x > max_child_dim.x)
max_child_dim.x = child.size.x;
}
content_dim.y -= child_gap;
content_dim.x = max_child_dim.x;
}
content_dim = content_dim.add(context.padding.scale(2));
var container_size = [0,0];
if (context.sizing.x.type === 'fit')
container_size.x = content_dim.x;
else if (container.sizing.x.type === 'grow') {
}
}
mum.container = function(data, cb) {
context_stack.push(context);
data.__proto__ = mum.base;
data.children = [];
var container_context = {
pos:cursor.slice(),
size:[0,0],
@ -194,8 +233,9 @@ mum.label = function (str, data = {}) {
mum.image = function (path, data = {}) {
if (pre(data)) return;
path ??= data.background_image;
var tex = path;
if (typeof path === "string") tex = game.texture(path);
var image = game.texture(path);
var tex = image.texture;
if (!data.height)
if (data.width) data.height = tex.height * (data.width / tex.width);
@ -212,12 +252,12 @@ mum.image = function (path, data = {}) {
data.drawpos = data.drawpos.add(aa.scale([data.width, data.height]));
if (data.slice) render.slice9(tex, data.drawpos, data.slice, [data.width, data.height]);
else data.bb = render.image(tex, data.drawpos, [data.width, data.height]);
else data.bb = render.image(image, data.drawpos, [data.width, data.height]);
end(data);
};
mum.rectangle = function (data = {}) {
mum.rectangle = function (data = {}, cb) {
if (pre(data)) return;
var aa = [0, 0].sub(data.anchor);
data.drawpos = data.drawpos.add(aa.scale([data.width, data.height]));

View file

@ -101,6 +101,7 @@ function update_emitters(dt) {
var arr = [];
function draw_emitters() {
return;
ssbo ??= render.make_textssbo();
render.use_shader("shaders/baseparticle.cg");
var buckets = {};

View file

@ -61,7 +61,7 @@ game.engine_start = function (s) {
function () {
global.mixin("scripts/sound.js");
world_start();
window.set_icon(game.texture("moon"));
window.set_icon(game.texture("moon").texture);
Object.readonly(window.__proto__, "vsync");
Object.readonly(window.__proto__, "enable_dragndrop");
Object.readonly(window.__proto__, "enable_clipboard");
@ -225,6 +225,65 @@ game.tex_hotreload = function () {
}
};
var image = {};
image.dimensions = function()
{
return [this.texture.width, this.texture.height].scale([this.rect[2], this.rect[3]]);
}
texture_proto.copy = function(src, pos, rect)
{
var pixel_rect = {
x: rect[0]*src.width,
y: rect[1]*src.height,
w: rect[2]*src.width,
h: rect[3]*src.height
};
this.blit(src, {
x: pos[0],
y: pos[1],
w: rect[2]*src.width,
h: rect[3]*src.height
}, pixel_rect, false);
}
var spritesheet;
var sheet_frames = [];
var sheetsize = 1024;
function pack_into_sheet(images)
{
if (!Array.isArray(images)) images = [images];
if (images[0].texture.width > 300 && images[0].texture.height > 300) return;
sheet_frames = sheet_frames.concat(images);
var sizes = sheet_frames.map(x => [x.rect[2]*x.texture.width, x.rect[3]*x.texture.height]);
var pos = os.rectpack(sheetsize, sheetsize, sizes);
if (!pos) {
console.error(`did not make spritesheet properly from images ${images}`);
console.info(sizes);
return;
}
var newsheet = os.make_tex_data(sheetsize,sheetsize);
for (var i = 0; i < pos.length; i++) {
// Copy the texture to the new sheet
newsheet.copy(sheet_frames[i].texture, pos[i], sheet_frames[i].rect);
// Update the frame's rect to the new position in normalized coordinates
sheet_frames[i].rect[0] = pos[i][0] / newsheet.width;
sheet_frames[i].rect[1] = pos[i][1] / newsheet.height;
sheet_frames[i].rect[2] = sizes[i][0] / newsheet.width;
sheet_frames[i].rect[3] = sizes[i][1] / newsheet.height;
sheet_frames[i].texture = newsheet;
}
newsheet.load_gpu();
spritesheet = newsheet;
return spritesheet;
}
game.texture = function (path) {
if (!path) return game.texture("icons/no_tex.gif");
path = Resources.find_image(path);
@ -236,8 +295,40 @@ game.texture = function (path) {
return game.texture.cache[path];
}
if (game.texture.cache[path]) return game.texture.cache[path];
game.texture.cache[path] = os.make_texture(path);
var tex = os.make_texture(path);
var image;
var anim = SpriteAnim.make(path, tex);
if (!anim) {
image = {
texture: tex,
rect:[0,0,1,1]
};
if (pack_into_sheet([image]))
tex = spritesheet;
} else if (Object.keys(anim).length === 1) {
image = Object.values(anim)[0].frames;
image.forEach(x => x.texture = tex);
if (pack_into_sheet(image))
tex = spritesheet;
} else {
image = {};
var packs = [];
for (var a in anim) {
image[a] = anim[a].frames.slice();
image[a].forEach(x => x.texture = tex);
packs = packs.concat(image[a]);
}
if (pack_into_sheet(packs))
tex = spritesheet;
}
game.texture.cache[path] = image;
game.texture.time_cache[path] = io.mod(path);
tex.load_gpu();
return game.texture.cache[path];
};
game.texture.cache = {};
@ -246,14 +337,14 @@ game.texture.time_cache = {};
game.texture.total_size = function()
{
var size = 0;
Object.values(game.texture.cache).forEach(x => size += x.inram() ? x.width*x.height*4 : 0);
// Object.values(game.texture.cache).forEach(x => size += x.texture.inram() ? x..texture.width*x.texture.height*4 : 0);
return size;
}
game.texture.total_vram = function()
{
var vram = 0;
Object.values(game.texture.cache).forEach(x => vram += x.vram);
// Object.values(game.texture.cache).forEach(x => vram += x.vram);
return vram;
}

View file

@ -1,3 +1,46 @@
/*
Anatomy of rendering an image
render.image(path)
Path can be a file like "toad"
If this is a gif, this would display the entire range of the animation
It can be a frame of animation, like "frog.0"
If it's an aseprite, it can have multiple animations, like "frog.walk.0"
file^ frame^ idx
render.image("frog.walk.0",
game.image("frog.walk.0") ==> retrieve
image = {
texture: "spritesheet.png",
rect: [x,y,w,h],
time: 100
},
frames: {
toad: {
x: 4,
y: 5,
w: 10,
h: 10
},
frog: {
walk: [
{ texture: spritesheet.png, x: 10, y:10, w:6,h:6, time: 100 },
{ texture: spritesheet.png, x:16,y:10,w:6,h:6,time:100} <--- two frame walk animation
],
},
},
}
texture frog {
texture: {"frog.png"}, <--- this is the actual thing to send to the gpu
x:0,
y:0,
w:10,
h:10
},
*/
render.doc = {
doc: "Functions for rendering modes.",
normal: "Final render with all lighting.",
@ -886,7 +929,6 @@ function img_e() {
var e = {
transform: os.make_transform(),
shade: Color.white,
rect: [0, 0, 1, 1],
};
img_cache.push(e);
return e;
@ -954,7 +996,9 @@ render.invertmask = function()
render.mask = function mask(tex, pos, scale, rotation = 0, ref = 1)
{
if (typeof tex === 'string') tex = game.texture(tex);
if (typeof tex === 'string')
tex = game.texture(tex);
var pipe = stencil_writer(ref);
render.use_shader('shaders/sprite.cg', pipe);
var t = os.make_transform();
@ -962,16 +1006,18 @@ render.mask = function mask(tex, pos, scale, rotation = 0, ref = 1)
t.scale = scale.div(tex.dimensions);
set_model(t);
render.use_mat({
diffuse:tex,
rect: [0,0,1,1],
diffuse:tex.texture,
rect: tex.rect,
shade: Color.white
});
render.draw(shape.quad);
}
render.image = function image(tex, pos, scale, rotation = 0, color = Color.white) {
if (typeof tex === "string")
tex = game.texture(tex);
render.image = function image(image, pos, scale, rotation = 0, color = Color.white) {
if (typeof image === "string")
image = game.texture(image);
var tex = image.texture;
if (scale)
scale = scale.div([tex.width, tex.height]);
@ -992,8 +1038,8 @@ render.image = function image(tex, pos, scale, rotation = 0, color = Color.white
var e = img_e();
e.transform.trs(pos, undefined, scale);
e.shade = color;
e.texture = tex;
e.image = image;
e.shade = color;
return;
var bb = {};

View file

@ -273,7 +273,7 @@ Cmdline.register_order(
if (io.exists("game.js")) global.app = actor.spawn("game.js");
else global.app = actor.spawn("scripts/nogame.js");
if (project.icon) window.set_icon(game.texture(project.icon));
if (project.icon) window.set_icon(game.texture(project.icon).texture);
game.camera = world.spawn("scripts/camera2d");
});
},

File diff suppressed because it is too large Load diff

View file

@ -1174,7 +1174,8 @@ JSC_CCALL(render_make_sprite_ssbo,
JSValue sub = js_getpropidx(array,i);
transform *tr = js2transform(js_getpropstr(sub, "transform"));
texture *t = js2texture(js_getpropstr(sub, "texture"));
JSValue image = js_getpropstr(sub, "image");
texture *t = js2texture(js_getpropstr(image, "texture"));
HMM_Vec3 tscale;
if (t) {
@ -1185,7 +1186,7 @@ JSC_CCALL(render_make_sprite_ssbo,
}
ms[i].model = transform2mat(tr);
ms[i].rect = js2vec4(js_getpropstr(sub,"rect"));
ms[i].rect = js2vec4(js_getpropstr(image,"rect"));
ms[i].shade = js2vec4(js_getpropstr(sub,"shade"));
if (t)
@ -3274,6 +3275,10 @@ JSC_SCALL(os_make_texture,
JS_SetPropertyStr(js, ret, "path", JS_DupValue(js,argv[0]));
)
JSC_SCALL(os_make_aseprite,
)
JSC_SCALL(os_texture_swap,
texture *old = js2texture(argv[1]);
texture *tex = texture_from_file(str);
@ -3510,8 +3515,9 @@ JSC_CCALL(os_rectpack,
stbrp_init_target(ctx, width, height, nodes, width);
int packed = stbrp_pack_rects(ctx, rects, num);
if (!packed)
if (!packed) {
return JS_UNDEFINED;
}
ret = JS_NewArray(js);
for (int i = 0; i < num; i++) {

View file

@ -15,6 +15,9 @@
#include "qoi.h"
#define CUTE_ASEPRITE_IMPLEMENTATION
#include "cute_aseprite.h"
#ifndef NSVG
#include "nanosvgrast.h"
#endif
@ -177,8 +180,32 @@ struct texture *texture_from_file(const char *path) {
int n;
char *ext = strrchr(path, '.');
if (!strcmp(ext, ".qoi")) {
if (!strcmp(ext, ".ase")) {
ase_t *ase = cute_aseprite_load_from_memory(raw, rawlen, NULL);
// frame is ase->w, ase->h
/* sprite anim is
anim = {
frames: [
rect: { <---- gathered after rect packing
x:
y:
w:
h:
},
time: ase->duration_milliseconds / 1000 (so it's in seconds)
],
path: path,
};
*/
for (int i = 0; i < ase->frame_count; i++) {
ase_frame_t *frame = ase->frames+i;
// add to thing with frame->pixels
}
cute_aseprite_free(ase);
} else if (!strcmp(ext, ".qoi")) {
qoi_desc qoi;
data = qoi_decode(raw, rawlen, &qoi, 4);
tex->width = qoi.width;
@ -220,8 +247,6 @@ struct texture *texture_from_file(const char *path) {
}
tex->data = data;
texture_load_gpu(tex);
return tex;
}
@ -232,9 +257,9 @@ void texture_free(texture *tex)
if (tex->data)
free(tex->data);
if (tex->delays) arrfree(tex->delays);
sg_destroy_image(tex->id);
if (tex->simgui.id)
simgui_destroy_image(tex->simgui);
simgui_destroy_image(tex->simgui);
free(tex);
}
@ -510,9 +535,9 @@ void texture_load_gpu(texture *tex)
.num_mipmaps = 1,
.data = img_data
});
} else {
} //else {
// Simple update
sg_image_data img_data = tex_img_data(tex,0);
sg_update_image(tex->id, &img_data);
}
// sg_image_data img_data = tex_img_data(tex,0);
// sg_update_image(tex->id, &img_data);
// }
}

View file

@ -5,6 +5,8 @@
#include "HandmadeMath.h"
#include "render.h"
#include "stb_rect_pack.h";
#include "sokol_app.h"
#include "sokol/util/sokol_imgui.h"