particles, sprite animation

This commit is contained in:
John Alanbrook 2023-12-30 01:08:53 +00:00
parent 5bdf311da9
commit 6aa44042d7
13 changed files with 350 additions and 151 deletions

View file

@ -79,19 +79,61 @@ component.sprite = Object.copy(component, {
_enghook: make_sprite, _enghook: make_sprite,
}); });
Object.hide(component.sprite, 'rect');
component.sprite.impl = { component.sprite.impl = {
toJSON() {
var j = {};
Object.keys(this).forEach(k => j[k] = this[k]);
delete j.rect;
return j;
},
set path(x) { set path(x) {
//cmd(12,this.id,prototypes.resani(this.gameobject.__proto__.toString(), x),this.rect); if (this.cancel) {
cmd(12,this.id,x,this.rect); 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() { get path() {
var s = cmd(116,this.id); var s = cmd(116,this.id);
if (s === "icons/no_tex.gif") return undefined; if (s === "icons/no_tex.gif") return undefined;
return s; 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"; }, toString() { return "sprite"; },
hide() { this.enabled = false; }, hide() { this.enabled = false; },
show() { this.enabled = true; }, 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]); }; sprite.inputs.kp1 = function() { this.pos = this.dimensions().scale([-1,-1]); };
Object.seal(sprite); 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 = { var SpriteAnim = {
make(path) { make(path) {
if (path.ext() === 'gif') if (path.ext() === 'gif')
return SpriteAnim.gif(path); return SpriteAnim.gif(path);
else if (Resources.is_image(path)) else
return SpriteAnim.strip(path); return undefined;
}, },
gif(path) { gif(path) {
var anim = {}; var anim = {};
@ -187,11 +235,13 @@ var SpriteAnim = {
frame.time = 0.05; frame.time = 0.05;
anim.frames.push(frame); 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; anim.loop = true;
var dim = Resources.texture.dimensions(path); var dim = Resources.texture.dimensions(path);
dim.y /= frames; dim.y /= frames;
anim.dim = dim; anim.dim = dim;
anim.toJSON = function() { return {}; };
return anim; 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.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.'; 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 */ /* Returns points specifying this geometry, with ccw */
var Geometry = { var Geometry = {
box(w, h) { box(w, h) {
@ -907,7 +857,8 @@ component.circle2d.impl = Object.mix({
get pos() { return this.offset; }, get pos() { return this.offset; },
set pos(x) { this.offset = x; }, set pos(x) { this.offset = x; },
}, collider2d.impl);; }, collider2d.impl);
/* ASSETS */ /* ASSETS */

View file

@ -82,6 +82,7 @@ var timer = {
kill() { kill() {
Register.unregister_obj(this); Register.unregister_obj(this);
this.fn = undefined;
}, },
delay(fn, secs) { delay(fn, secs) {

View file

@ -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]))); ret = ptr2js(cpGearJointNew(js2gameobject(argv[1])->body, js2gameobject(argv[2])->body, js2number(argv[3]), js2number(argv[4])));
cpSpaceAddConstraint(space,js2ptr(ret)); cpSpaceAddConstraint(space,js2ptr(ret));
break; break;
case 224:
str = js2str(argv[1]);
ret = ints2js(gif_delays(str));
break;
} }
if (str) if (str)

View file

@ -1,36 +1,184 @@
#include "particle.h" #include "particle.h"
#include "stb_ds.h" #include "stb_ds.h"
#include "freelist.h" #include "render.h"
#include "particle.sglsl.h"
#include "log.h"
struct emitter make_emitter() { static emitter **emitters;
struct emitter e = {0};
arrsetcap(e.particles, 200); 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; return e;
} }
void free_emitter(struct emitter e) { void free_emitter(emitter *e)
arrfree(e.particles); {
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) { int emitter_spawn(emitter *e)
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)); for (int i = 0; i < e->max; i++) {
e.particles[i].angle = HMM_MulQ(e.particles[i].angle, HMM_MulQF(e.particles[i].angle, dt)); if (e->particles[i].life > 0) continue;
e.particles[i].life -= dt; 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) void emitter_emit(emitter *e, int count)
arrdelswap(e.particles, i); {
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;
} }
} }

View file

@ -3,34 +3,42 @@
#include "HandmadeMath.h" #include "HandmadeMath.h"
#include "transform.h" #include "transform.h"
#include "texture.h"
struct particle { typedef struct particle {
HMM_Vec3 pos; HMM_Vec3 pos;
HMM_Vec3 v; /* velocity */ HMM_Vec3 v; /* velocity */
HMM_Quat angle; float angle;
HMM_Quat av; /* angular velocity */ float av; /* angular velocity */
float scale;
union { double life;
double life; rgba color;
unsigned int next; } particle;
};
};
typedef struct emitter { typedef struct emitter {
struct particle *particles; struct particle *particles;
transform3d t; transform3d t;
int max; float explosiveness; /* 0 for a stream, 1 for all at once. Range of values allowed. */
double life; int max; /* number of particles */
void (*seeder)(struct particle *p); /* Called to initialize each particle */ double life; /* how long a particle lasts */
double tte; /* time to emit */
rgba color;
float scale;
texture *texture;
} emitter; } emitter;
struct emitter make_emitter(); void particle_init();
void free_emitter(struct emitter e);
void start_emitter(struct emitter e); emitter *make_emitter();
void pause_emitter(struct emitter e); void free_emitter(emitter *e);
void stop_emitter(struct 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 #endif

View file

@ -7,6 +7,7 @@
#include "gameobject.h" #include "gameobject.h"
#include "log.h" #include "log.h"
#include "sprite.h" #include "sprite.h"
#include "particle.h"
#include "window.h" #include "window.h"
#include "model.h" #include "model.h"
#include "stb_ds.h" #include "stb_ds.h"
@ -512,6 +513,8 @@ void full_2d_pass(struct window *window)
model_draw_all(); model_draw_all();
call_draw(); call_draw();
emitters_draw();
//// DEBUG //// DEBUG
if (debugDrawPhysics) { if (debugDrawPhysics) {
gameobject_draw_debugs(); gameobject_draw_debugs();

View file

@ -91,6 +91,8 @@ struct rgba {
unsigned char a; unsigned char a;
}; };
typedef struct rgba rgba;
struct boundingbox { struct boundingbox {
float t; float t;
float b; float b;

View file

@ -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); sg_draw(sprite_count * 4, 4, 1);
sprite_count++; sprite_count++;
} }
void sprite_setframe(struct sprite *sprite, struct glrect *frame) { void sprite_setframe(struct sprite *sprite, struct glrect *frame) {

View file

@ -76,6 +76,12 @@ int gif_nframes(const char *path)
return t->frames; 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 */ /* If an empty string or null is put for path, loads default texture */
struct Texture *texture_pullfromfile(const char *path) { struct Texture *texture_pullfromfile(const char *path) {
if (!path) return texture_notex(); if (!path) return texture_notex();
@ -110,7 +116,13 @@ struct Texture *texture_pullfromfile(const char *path) {
tex->height = qoi.height; tex->height = qoi.height;
n = qoi.channels; n = qoi.channels;
} else if (!strcmp(ext, ".gif")) { } 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; tex->height *= tex->frames;
} else if (!strcmp(ext, ".svg")) { } else if (!strcmp(ext, ".svg")) {
#ifndef NSVG #ifndef NSVG

View file

@ -31,8 +31,11 @@ struct Texture {
unsigned char *data; unsigned char *data;
struct TextureOptions opts; struct TextureOptions opts;
int frames; int frames;
int *delays;
}; };
typedef struct Texture texture;
struct Image { struct Image {
struct Texture *tex; struct Texture *tex;
struct glrect frame; 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 char * tex_get_path(struct Texture *tex); // Get image path for texture
int gif_nframes(const char *path); int gif_nframes(const char *path);
int *gif_delays(const char *path);
struct glrect tex_get_rect(struct Texture *tex); struct glrect tex_get_rect(struct Texture *tex);
HMM_Vec2 tex_get_dimensions(struct Texture *tex); HMM_Vec2 tex_get_dimensions(struct Texture *tex);

View file

@ -9,6 +9,7 @@
#include "resources.h" #include "resources.h"
#include "spline.h" #include "spline.h"
#include <stdio.h> #include <stdio.h>
#include "particle.h"
#include "datastream.h" #include "datastream.h"
@ -101,6 +102,11 @@ void c_init() {
window_set_icon("icons/moon.gif"); window_set_icon("icons/moon.gif");
window_resize(sapp_width(), sapp_height()); window_resize(sapp_width(), sapp_height());
script_evalf("Game.init();"); 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(); } 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 */ /* Timers all update every frame - once per monitor refresh */
timer_update(elapsed, timescale); timer_update(elapsed, timescale);
/* Update at a high level:: emitters_step(elapsed);
* Update scene graph
*
*/
if (sim_play == SIM_PLAY || sim_play == SIM_STEP) { if (sim_play == SIM_PLAY || sim_play == SIM_STEP) {
if (stm_sec(stm_diff(frame_t, updatelast)) > updateMS) { if (stm_sec(stm_diff(frame_t, updatelast)) > updateMS) {

View file

@ -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

View file

@ -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