From 6aa44042d75e60a833df1cde709827856ff40c94 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sat, 30 Dec 2023 01:08:53 +0000 Subject: [PATCH] particles, sprite animation --- scripts/components.js | 169 +++++++++++-------------------- scripts/engine.js | 1 + source/engine/jsffi.c | 4 + source/engine/particle.c | 182 ++++++++++++++++++++++++++++++---- source/engine/particle.h | 44 ++++---- source/engine/render.c | 3 + source/engine/render.h | 2 + source/engine/sprite.c | 1 - source/engine/texture.c | 14 ++- source/engine/texture.h | 6 +- source/engine/yugine.c | 11 +- source/shaders/bezier.sglsl | 19 ++++ source/shaders/particle.sglsl | 45 +++++++++ 13 files changed, 350 insertions(+), 151 deletions(-) create mode 100644 source/shaders/bezier.sglsl create mode 100644 source/shaders/particle.sglsl diff --git a/scripts/components.js b/scripts/components.js index ced6fda..dc4273f 100644 --- a/scripts/components.js +++ b/scripts/components.js @@ -79,19 +79,61 @@ component.sprite = Object.copy(component, { _enghook: make_sprite, }); -Object.hide(component.sprite, 'rect'); - component.sprite.impl = { + toJSON() { + var j = {}; + Object.keys(this).forEach(k => j[k] = this[k]); + delete j.rect; + return j; + }, + set path(x) { - //cmd(12,this.id,prototypes.resani(this.gameobject.__proto__.toString(), x),this.rect); - cmd(12,this.id,x,this.rect); + if (this.cancel) { + this.cancel(); + this.cancel = undefined; + } + + if (!Resources.is_animation(x)) { + this.rect = component.sprite.rect; + cmd(12,this.id,x,this.rect); + } + else { + this.rect = SpriteAnim.make(x).frames[0].rect; + cmd(12,this.id,x,this.rect); + } }, get path() { var s = cmd(116,this.id); if (s === "icons/no_tex.gif") return undefined; return s; - //return prototypes.resavi(this.gameobject.__proto__.toString(), cmd(116,this.id)); }, + + play() { + var frame = 0; + var anim = SpriteAnim.make(this.path); + var advance = function() { + frame = (frame+1)%anim.frames.length; + this.rect = anim.frames[frame].rect; + cmd(12,this.id,this.path,this.rect); + this.cancel = this.gameobject.delay(advance.bind(this), anim.frames[frame].time); + } + advance.call(this); + + }, + + stop() { + if (!this.cancel) return; + this.cancel(); + this.cancel = undefined; + }, + setframe(f) { + this.stop(); + var anim = SpriteAnim.make(this.path); + if (!anim) return; + this.rect = anim.frames[f].rect; + cmd(12,this.id,this.path,this.rect); + }, + toString() { return "sprite"; }, hide() { this.enabled = false; }, show() { this.enabled = true; }, @@ -163,12 +205,18 @@ sprite.inputs.kp2 = function() { this.pos = this.dimensions().scale([-0.5,-1]); sprite.inputs.kp1 = function() { this.pos = this.dimensions().scale([-1,-1]); }; Object.seal(sprite); +/* sprite anim returns a data structure for the given file path + frames: array of frames + rect: frame rectangle + time: miliseconds to hold the frame for + loop: true if it should be looped +*/ var SpriteAnim = { make(path) { if (path.ext() === 'gif') return SpriteAnim.gif(path); - else if (Resources.is_image(path)) - return SpriteAnim.strip(path); + else + return undefined; }, gif(path) { var anim = {}; @@ -187,11 +235,13 @@ var SpriteAnim = { frame.time = 0.05; anim.frames.push(frame); } + var times = cmd(224,path); + for (var i = 0; i < frames; i++) + anim.frames[i].time = times[i]/1000; anim.loop = true; var dim = Resources.texture.dimensions(path); dim.y /= frames; anim.dim = dim; - anim.toJSON = function() { return {}; }; return anim; }, @@ -268,106 +318,6 @@ SpriteAnim.strip.doc = 'Given a path and number of frames, converts a horizontal SpriteAnim.aseprite.doc = 'Given an aseprite json metadata, returns an object of animations defined in the aseprite file.'; SpriteAnim.find.doc = 'Given a path, find the relevant animation for the file.'; -/* Container to play sprites and anim2ds */ - -component.char2d = Object.create(component.sprite); -component.char2dimpl = { - boundingbox() { - var dim = this.acur.dim.slice(); - dim = dim.scale(this.gameobject.scale); - var realpos = this.pos.slice(); - realpos.x = realpos.x * dim.x + (dim.x/2); - realpos.y = realpos.y * dim.y + (dim.y/2); - return cwh2bb(realpos,dim); - }, - - anims:{}, - acur:{}, - frame: 0, - - play_anim(anim) { - this.acur = anim; - this.frame = 0; - this.gameobject.delay(this.advance.bind(this), this.acur.frames[this.frame].time); - this.setsprite(); - }, - - play(name) { - if (!(name in this)) { - Log.info("Can't find an animation named " + name); - return; - } - - if (this.acur === this[name]) { - this.timer.start(); - return; - } - - this.acur = this[name]; - this.frame = 0; - this.timer.time = this.acur.frames[this.frame].time; - this.timer.start(); - this.setsprite(); - }, - - setsprite() { - this.rect = this.acur.frames[this.frame].rect; - this.path = this.path; - }, - - advance() { - this.frame = (this.frame + 1) % this.acur.frames.length; - this.setsprite(); - this.gameobject.delay(this.advance.bind(this), this.acur.frames[this.frame].time); - }, - - devance() { - this.frame = (this.frame - 1); - if (this.frame === -1) this.frame = this.acur.frames-1; - this.setsprite(); - }, - - setframe(frame) { - this.frame = frame; - this.setsprite(); - }, - - pause() { - this.timer.pause(); - }, - - stop() { - this.setframe(0); - this.timer.stop(); - }, - - kill() { cmd(9, this.id); }, - - add_anim(anim,name) { - if (name in this) return; - this[name] = function() { - this.play_anim(anim); - } - }, -}; - -component.char2dimpl.doc = { - doc: "An animation player for sprites.", - frame: "The current frame of animation.", - anims: "A list of all animations in this player.", - acur: "The currently playing animation object.", - advance: "Advance the animation by one frame.", - devance: "Go back one frame in the animation.", - setframe: "Set a specific frame of animation.", - stop: "Stops the animation and returns to the first frame.", - pause: "Pauses the animation sequence in place.", - play: "Given an animation string, play it. Equivalent to anim.[name].play().", - play_anim: "Play a given animation object.", - add_anim: "Add an animation object with the given name." -}; - -Object.hide(component.char2dimpl, "doc"); - /* Returns points specifying this geometry, with ccw */ var Geometry = { box(w, h) { @@ -907,7 +857,8 @@ component.circle2d.impl = Object.mix({ get pos() { return this.offset; }, set pos(x) { this.offset = x; }, -}, collider2d.impl);; +}, collider2d.impl); + /* ASSETS */ diff --git a/scripts/engine.js b/scripts/engine.js index 2049ff0..b2cc576 100644 --- a/scripts/engine.js +++ b/scripts/engine.js @@ -82,6 +82,7 @@ var timer = { kill() { Register.unregister_obj(this); + this.fn = undefined; }, delay(fn, secs) { diff --git a/source/engine/jsffi.c b/source/engine/jsffi.c index 5dd93a3..5e8f52b 100644 --- a/source/engine/jsffi.c +++ b/source/engine/jsffi.c @@ -1382,6 +1382,10 @@ JSValue duk_cmd(JSContext *js, JSValueConst this, int argc, JSValueConst *argv) ret = ptr2js(cpGearJointNew(js2gameobject(argv[1])->body, js2gameobject(argv[2])->body, js2number(argv[3]), js2number(argv[4]))); cpSpaceAddConstraint(space,js2ptr(ret)); break; + case 224: + str = js2str(argv[1]); + ret = ints2js(gif_delays(str)); + break; } if (str) diff --git a/source/engine/particle.c b/source/engine/particle.c index aa0abeb..379eac8 100644 --- a/source/engine/particle.c +++ b/source/engine/particle.c @@ -1,36 +1,184 @@ #include "particle.h" #include "stb_ds.h" -#include "freelist.h" +#include "render.h" +#include "particle.sglsl.h" +#include "log.h" -struct emitter make_emitter() { - struct emitter e = {0}; - arrsetcap(e.particles, 200); +static emitter **emitters; + +static sg_shader par_shader; +static sg_pipeline par_pipe; +static sg_bindings par_bind; +static int draw_count; + +struct par_vert { + HMM_Vec2 pos; + float angle; + float scale; + struct rgba color; +}; + +typedef struct par_vert par_vert; + +void particle_init() +{ + par_shader = sg_make_shader(particle_shader_desc(sg_query_backend())); + + par_pipe = sg_make_pipeline(&(sg_pipeline_desc){ + .shader = par_shader, + .layout = { + .attrs = { + [1].format = SG_VERTEXFORMAT_FLOAT2, + [2].format = SG_VERTEXFORMAT_FLOAT, + [3].format = SG_VERTEXFORMAT_FLOAT, + [4].format = SG_VERTEXFORMAT_UBYTE4N, + [0].format = SG_VERTEXFORMAT_FLOAT2, + [0].buffer_index = 1 + }, + .buffers[0].step_func = SG_VERTEXSTEP_PER_INSTANCE, + }, + .primitive_type = SG_PRIMITIVETYPE_TRIANGLE_STRIP, + .label = "particle pipeline", + .cull_mode = SG_CULLMODE_BACK, + .colors[0].blend = blend_trans, + .depth = { + .write_enabled = true, + .compare = SG_COMPAREFUNC_LESS_EQUAL, + .pixel_format = SG_PIXELFORMAT_DEPTH + } + }); + + par_bind.vertex_buffers[0] = sg_make_buffer(&(sg_buffer_desc){ + .size = sizeof(par_vert)*500, + .type = SG_BUFFERTYPE_VERTEXBUFFER, + .usage = SG_USAGE_STREAM, + .label = "particle buffer" + }); + + float circleverts[8] = { + 0,0, + 0,1, + 1,0, + 1,1, + }; + + par_bind.vertex_buffers[1] = sg_make_buffer(&(sg_buffer_desc){ + .data = (sg_range){.ptr = circleverts, .size = sizeof(float)*8}, + .usage = SG_USAGE_IMMUTABLE + }); + + par_bind.fs.samplers[0] = sg_make_sampler(&(sg_sampler_desc){}); +} + +emitter *make_emitter() { + emitter *e = NULL; + e = malloc(sizeof(*e)); + e->max = 20; + arrsetlen(e->particles, e->max); + for (int i = 0; i < arrlen(e->particles); i++) + e->particles[i].life = 0; + + e->life = 10; + e->explosiveness = 0; + e->tte = e->life/e->max; + e->color = color_white; + e->scale = 20; + arrpush(emitters,e); return e; } -void free_emitter(struct emitter e) { - arrfree(e.particles); +void free_emitter(emitter *e) +{ + arrfree(e->particles); + for (int i = arrlen(emitters)-1; i >= 0; i--) + if (emitters[i] == e) { + arrdelswap(emitters,i); + break; + } } -void start_emitter(struct emitter e) { - +void start_emitter(emitter *e) +{ + } -void pause_emitter(struct emitter e) { +void pause_emitter(emitter *e) { } -void stop_emitter(struct emitter e) { +void stop_emitter(emitter *e) { } -void emitter_step(struct emitter e, double dt) { - for (int i = 0; i < arrlen(e.particles); i++) { - e.particles[i].pos = HMM_AddV3(e.particles[i].pos, HMM_MulV3F(e.particles[i].v, dt)); - e.particles[i].angle = HMM_MulQ(e.particles[i].angle, HMM_MulQF(e.particles[i].angle, dt)); - e.particles[i].life -= dt; +int emitter_spawn(emitter *e) +{ + for (int i = 0; i < e->max; i++) { + if (e->particles[i].life > 0) continue; + e->particles[i].life = e->life; + e->particles[i].pos = (HMM_Vec3){0,0,0}; + e->particles[i].v = (HMM_Vec3){20,1,0}; + e->particles[i].angle = 0; + e->particles[i].av = 1; + return 1; + } + return 0; +} - if (e.particles[i].life <= 0) - arrdelswap(e.particles, i); +void emitter_emit(emitter *e, int count) +{ + for (int i = 0; i < count; i++) + if (!emitter_spawn(e)) return; +} + +void emitters_step(double dt) +{ + for (int i = 0; i < arrlen(emitters); i++) + emitter_step(emitters[i], dt); +} + +void emitters_draw() +{ + + draw_count = 0; + + for (int i = 0; i < arrlen(emitters); i++) { + emitter *e = emitters[i]; + par_bind.fs.images[0] = e->texture->id; + for (int j = 0; j < arrlen(e->particles); j++) { + particle *p = &e->particles[j]; + if (p->life <= 0) continue; + struct par_vert pv; + pv.pos = p->pos.xy; + pv.angle = p->angle; + pv.scale = p->scale; + pv.color = p->color; + sg_append_buffer(par_bind.vertex_buffers[0], &(sg_range){.ptr=&pv, .size=sizeof(struct par_vert)}); + draw_count++; + } + } + if (draw_count == 0) return; + sg_apply_pipeline(par_pipe); + sg_apply_uniforms(SG_SHADERSTAGE_VS, 0, SG_RANGE_REF(projection)); + sg_apply_bindings(&par_bind); + sg_draw(0, 4, draw_count); +} + +void emitter_step(emitter *e, double dt) { + for (int i = 0; i < arrlen(e->particles); i++) { + if (e->particles[i].life <= 0) continue; + e->particles[i].pos = HMM_AddV3(e->particles[i].pos, HMM_MulV3F(e->particles[i].v, dt)); + e->particles[i].angle += e->particles[i].av*dt; + e->particles[i].life -= dt; + e->particles[i].color = e->color; + e->particles[i].scale = e->scale; + + if (e->particles[i].life <= 0) + e->particles[i].life = 0; + } + + e->tte-=dt; + if (e->tte <= 0) { + emitter_emit(e,1); + e->tte = e->life/e->max; } } diff --git a/source/engine/particle.h b/source/engine/particle.h index 1fbae39..cd096b0 100644 --- a/source/engine/particle.h +++ b/source/engine/particle.h @@ -3,34 +3,42 @@ #include "HandmadeMath.h" #include "transform.h" +#include "texture.h" -struct particle { +typedef struct particle { HMM_Vec3 pos; HMM_Vec3 v; /* velocity */ - HMM_Quat angle; - HMM_Quat av; /* angular velocity */ - - union { - double life; - unsigned int next; - }; -}; + float angle; + float av; /* angular velocity */ + float scale; + double life; + rgba color; +} particle; typedef struct emitter { struct particle *particles; transform3d t; - int max; - double life; - void (*seeder)(struct particle *p); /* Called to initialize each particle */ + float explosiveness; /* 0 for a stream, 1 for all at once. Range of values allowed. */ + int max; /* number of particles */ + double life; /* how long a particle lasts */ + double tte; /* time to emit */ + rgba color; + float scale; + texture *texture; } emitter; -struct emitter make_emitter(); -void free_emitter(struct emitter e); +void particle_init(); -void start_emitter(struct emitter e); -void pause_emitter(struct emitter e); -void stop_emitter(struct emitter e); +emitter *make_emitter(); +void free_emitter(emitter *e); -void emitter_step(struct emitter e, double dt); +void start_emitter(emitter *e); +void pause_emitter(emitter *e); +void stop_emitter(emitter *e); + +void emitter_emit(emitter *e, int count); +void emitters_step(double dt); +void emitters_draw(); +void emitter_step(emitter *e, double dt); #endif diff --git a/source/engine/render.c b/source/engine/render.c index b52a530..8f8f6c7 100644 --- a/source/engine/render.c +++ b/source/engine/render.c @@ -7,6 +7,7 @@ #include "gameobject.h" #include "log.h" #include "sprite.h" +#include "particle.h" #include "window.h" #include "model.h" #include "stb_ds.h" @@ -512,6 +513,8 @@ void full_2d_pass(struct window *window) model_draw_all(); call_draw(); + emitters_draw(); + //// DEBUG if (debugDrawPhysics) { gameobject_draw_debugs(); diff --git a/source/engine/render.h b/source/engine/render.h index 6338aba..d326b2b 100644 --- a/source/engine/render.h +++ b/source/engine/render.h @@ -91,6 +91,8 @@ struct rgba { unsigned char a; }; +typedef struct rgba rgba; + struct boundingbox { float t; float b; diff --git a/source/engine/sprite.c b/source/engine/sprite.c index d80c008..11231dc 100644 --- a/source/engine/sprite.c +++ b/source/engine/sprite.c @@ -277,7 +277,6 @@ void slice9_draw(const char *img, HMM_Vec2 pos, HMM_Vec2 dimensions, struct rgba sg_draw(sprite_count * 4, 4, 1); sprite_count++; - } void sprite_setframe(struct sprite *sprite, struct glrect *frame) { diff --git a/source/engine/texture.c b/source/engine/texture.c index 1ad8914..762e571 100644 --- a/source/engine/texture.c +++ b/source/engine/texture.c @@ -76,6 +76,12 @@ int gif_nframes(const char *path) return t->frames; } +int *gif_delays(const char *path) +{ + struct Texture *t = texture_pullfromfile(path); + return t->delays; +} + /* If an empty string or null is put for path, loads default texture */ struct Texture *texture_pullfromfile(const char *path) { if (!path) return texture_notex(); @@ -110,7 +116,13 @@ struct Texture *texture_pullfromfile(const char *path) { tex->height = qoi.height; n = qoi.channels; } else if (!strcmp(ext, ".gif")) { - data = stbi_load_gif_from_memory(raw, rawlen, NULL, &tex->width, &tex->height, &tex->frames, &n, 4); + data = stbi_load_gif_from_memory(raw, rawlen, &tex->delays, &tex->width, &tex->height, &tex->frames, &n, 4); + int *dd = tex->delays; + tex->delays = NULL; + arrsetlen(tex->delays, tex->frames); + for (int i = 0; i < tex->frames;i++) + tex->delays[i] = dd[i]; + free(dd); tex->height *= tex->frames; } else if (!strcmp(ext, ".svg")) { #ifndef NSVG diff --git a/source/engine/texture.h b/source/engine/texture.h index 28ee8a0..6bb6ce6 100644 --- a/source/engine/texture.h +++ b/source/engine/texture.h @@ -30,9 +30,12 @@ struct Texture { int height; unsigned char *data; struct TextureOptions opts; - int frames; + int frames; + int *delays; }; +typedef struct Texture texture; + struct Image { struct Texture *tex; struct glrect frame; @@ -47,6 +50,7 @@ void texture_sync(const char *path); char * tex_get_path(struct Texture *tex); // Get image path for texture int gif_nframes(const char *path); +int *gif_delays(const char *path); struct glrect tex_get_rect(struct Texture *tex); HMM_Vec2 tex_get_dimensions(struct Texture *tex); diff --git a/source/engine/yugine.c b/source/engine/yugine.c index 13b99b7..ebb34c8 100644 --- a/source/engine/yugine.c +++ b/source/engine/yugine.c @@ -9,6 +9,7 @@ #include "resources.h" #include "spline.h" #include +#include "particle.h" #include "datastream.h" @@ -101,6 +102,11 @@ void c_init() { window_set_icon("icons/moon.gif"); window_resize(sapp_width(), sapp_height()); script_evalf("Game.init();"); + + particle_init(); +// emitter *e = make_emitter(); +// e->texture = texture_pullfromfile("bolt.gif"); +// start_emitter(e); } int frame_fps() { return 1.0/sapp_frame_duration(); } @@ -114,10 +120,7 @@ static void process_frame() /* Timers all update every frame - once per monitor refresh */ timer_update(elapsed, timescale); - /* Update at a high level:: - * Update scene graph - * - */ + emitters_step(elapsed); if (sim_play == SIM_PLAY || sim_play == SIM_STEP) { if (stm_sec(stm_diff(frame_t, updatelast)) > updateMS) { diff --git a/source/shaders/bezier.sglsl b/source/shaders/bezier.sglsl new file mode 100644 index 0000000..d244ea6 --- /dev/null +++ b/source/shaders/bezier.sglsl @@ -0,0 +1,19 @@ +@vs vs +in vec2 pos; + +uniform vs_p { mat4 proj; }; + +void main() +{ + gl_Position = proj * vec4(pos,0.0,1.0); +} +@end + +@fs fs +void main() +{ + +} +@end + +@program particle vs fs \ No newline at end of file diff --git a/source/shaders/particle.sglsl b/source/shaders/particle.sglsl new file mode 100644 index 0000000..0c9b255 --- /dev/null +++ b/source/shaders/particle.sglsl @@ -0,0 +1,45 @@ +@vs vs +in vec2 vertex; +in vec2 apos; +in float angle; +in float scale; +in vec4 vc; + +out vec4 fcolor; +out vec2 uv; + +uniform vs_p { mat4 proj; }; + +void main() +{ + fcolor = vc; + uv = vertex; + vec2 v = vertex - 0.5; + vec2 p = vec2( + cos(angle)*v.x-sin(angle)*v.y, + sin(angle)*v.x+cos(angle)*v.y + ); + p += 0.5; + p *= scale; + p += apos; + + gl_Position = proj * vec4(p, 0.0, 1.0); +} +@end + +@fs fs +in vec4 fcolor; +out vec4 color; + +in vec2 uv; +uniform texture2D image; +uniform sampler smp; + +void main() +{ + color = texture(sampler2D(image,smp),uv); + color *= fcolor; +} +@end + +@program particle vs fs \ No newline at end of file