static struct lockstep update_step = { demo_update_read, DT };
+float demo_play_blend(void)
+{
+ return lockstep_blend(&update_step);
+}
+
/*---------------------------------------------------------------------------*/
static struct demo demo_replay;
void demo_play_step(void);
void demo_play_stat(int, int, int);
void demo_play_stop(void);
+float demo_play_blend(void);
int demo_saved (void);
void demo_rename(const char *);
/*---------------------------------------------------------------------------*/
+#define CURR 0
+#define PREV 1
+
static struct game_draw gd;
+static struct game_lerp gl;
static float timer = 0.0f; /* Clock time */
static int status = GAME_NONE; /* Outcome of the game */
static int got_tilt_axes;
+static int next_update;
+
/*---------------------------------------------------------------------------*/
static void game_run_cmd(const union cmd *cmd)
{
if (gd.state)
{
+ struct game_view *view = &gl.view[CURR];
+ struct game_tilt *tilt = &gl.tilt[CURR];
+
struct s_vary *vary = &gd.file.vary;
struct v_item *hp;
- struct v_ball *up;
float v[3];
float dt;
- int i;
+
+ if (next_update)
+ {
+ game_lerp_copy(&gl);
+ next_update = 0;
+ }
switch (cmd->type)
{
case CMD_END_OF_UPDATE:
-
got_tilt_axes = 0;
+ next_update = 1;
+
if (first_update)
{
+ game_lerp_copy(&gl);
+ /* Hack to sync state before the next update. */
+ game_lerp_apply(&gl, &gd, 1.0f);
first_update = 0;
break;
}
/* Compute gravity for particle effects. */
if (status == GAME_GOAL)
- game_tilt_grav(v, GRAVITY_UP, &gd.tilt);
+ game_tilt_grav(v, GRAVITY_UP, tilt);
else
- game_tilt_grav(v, GRAVITY_DN, &gd.tilt);
+ game_tilt_grav(v, GRAVITY_DN, tilt);
/* Step particle, goal and jump effects. */
{
dt = 1.0f / (float) ups;
- if (gd.goal_e && gd.goal_k < 1.0f)
- gd.goal_k += dt;
+ if (gd.goal_e && gl.goal_k[CURR] < 1.0f)
+ gl.goal_k[CURR] += dt;
if (gd.jump_b)
{
- gd.jump_dt += dt;
+ gl.jump_dt[CURR] += dt;
- if (1.0f < gd.jump_dt)
+ if (1.0f < gl.jump_dt[CURR])
gd.jump_b = 0;
}
case CMD_MAKE_BALL:
/* Allocate a new ball and mark it as the current ball. */
- if ((up = realloc(vary->uv, sizeof (*up) * (vary->uc + 1))))
- {
- vary->uv = up;
- curr_ball = vary->uc;
- vary->uc++;
- }
+ if (sol_lerp_cmd(&gl.lerp, cmd))
+ curr_ball = gl.lerp.uc - 1;
+
break;
case CMD_MAKE_ITEM:
case CMD_TILT_ANGLES:
if (!got_tilt_axes)
- game_tilt_axes(&gd.tilt, gd.view.e);
+ game_tilt_axes(tilt, view->e);
- gd.tilt.rx = cmd->tiltangles.x;
- gd.tilt.rz = cmd->tiltangles.z;
+ tilt->rx = cmd->tiltangles.x;
+ tilt->rz = cmd->tiltangles.z;
break;
case CMD_SOUND:
case CMD_JUMP_ENTER:
gd.jump_b = 1;
gd.jump_e = 0;
- gd.jump_dt = 0.0f;
+ gl.jump_dt[CURR] = 0.0f;
break;
case CMD_JUMP_EXIT:
break;
case CMD_BODY_PATH:
- vary->bv[cmd->bodypath.bi].pi = cmd->bodypath.pi;
+ sol_lerp_cmd(&gl.lerp, cmd);
break;
case CMD_BODY_TIME:
- vary->bv[cmd->bodytime.bi].t = cmd->bodytime.t;
+ sol_lerp_cmd(&gl.lerp, cmd);
break;
case CMD_GOAL_OPEN:
if (!gd.goal_e)
{
gd.goal_e = 1;
- gd.goal_k = first_update ? 1.0f : 0.0f;
+ gl.goal_k[CURR] = first_update ? 1.0f : 0.0f;
}
break;
break;
case CMD_BALL_RADIUS:
- vary->uv[curr_ball].r = cmd->ballradius.r;
+ sol_lerp_cmd(&gl.lerp, cmd);
break;
case CMD_CLEAR_ITEMS:
break;
case CMD_CLEAR_BALLS:
- if (vary->uv)
- {
- free(vary->uv);
- vary->uv = NULL;
- }
- vary->uc = 0;
+ sol_lerp_cmd(&gl.lerp, cmd);
break;
case CMD_BALL_POSITION:
- up = vary->uv + curr_ball;
-
- v_cpy(up->p, cmd->ballpos.p);
+ sol_lerp_cmd(&gl.lerp, cmd);
break;
case CMD_BALL_BASIS:
- up = vary->uv + curr_ball;
-
- v_cpy(up->e[0], cmd->ballbasis.e[0]);
- v_cpy(up->e[1], cmd->ballbasis.e[1]);
- v_crs(up->e[2], up->e[0], up->e[1]);
+ sol_lerp_cmd(&gl.lerp, cmd);
break;
case CMD_BALL_PEND_BASIS:
- up = vary->uv + curr_ball;
-
- v_cpy(up->E[0], cmd->ballpendbasis.E[0]);
- v_cpy(up->E[1], cmd->ballpendbasis.E[1]);
- v_crs(up->E[2], up->E[0], up->E[1]);
+ sol_lerp_cmd(&gl.lerp, cmd);
break;
case CMD_VIEW_POSITION:
- v_cpy(gd.view.p, cmd->viewpos.p);
+ v_cpy(view->p, cmd->viewpos.p);
break;
case CMD_VIEW_CENTER:
- v_cpy(gd.view.c, cmd->viewcenter.c);
+ v_cpy(view->c, cmd->viewcenter.c);
break;
case CMD_VIEW_BASIS:
- v_cpy(gd.view.e[0], cmd->viewbasis.e[0]);
- v_cpy(gd.view.e[1], cmd->viewbasis.e[1]);
- v_crs(gd.view.e[2], gd.view.e[0], gd.view.e[1]);
+ v_cpy(view->e[0], cmd->viewbasis.e[0]);
+ v_cpy(view->e[1], cmd->viewbasis.e[1]);
+ v_crs(view->e[2], view->e[0], view->e[1]);
break;
case CMD_CURRENT_BALL:
+ sol_lerp_cmd(&gl.lerp, cmd);
curr_ball = cmd->currball.ui;
break;
break;
case CMD_STEP_SIMULATION:
- /*
- * Simulate body motion.
- *
- * This is done on the client side due to replay file size
- * concerns and isn't done as part of CMD_END_OF_UPDATE to
- * match the server state as closely as possible. Body time
- * is still synchronized with the server on a semi-regular
- * basis and path indices are handled through CMD_BODY_PATH,
- * thus this code doesn't need to be as sophisticated as
- * sol_body_step.
- */
-
- dt = cmd->stepsim.dt;
-
- for (i = 0; i < vary->bc; i++)
- {
- struct v_body *bp = vary->bv + i;
- struct v_path *pp = vary->pv + bp->pi;
-
- if (bp->pi >= 0 && pp->f)
- bp->t += dt;
- }
+ sol_lerp_cmd(&gl.lerp, cmd);
break;
case CMD_MAP:
* Note a version (mis-)match between the loaded map and what
* the server has. (This doesn't actually load a map.)
*/
-
game_compat_map = version.x == cmd->map.version.x;
break;
case CMD_TILT_AXES:
got_tilt_axes = 1;
- v_cpy(gd.tilt.x, cmd->tiltaxes.x);
- v_cpy(gd.tilt.z, cmd->tiltaxes.z);
+ v_cpy(tilt->x, cmd->tiltaxes.x);
+ v_cpy(tilt->z, cmd->tiltaxes.z);
break;
case CMD_NONE:
}
}
}
+
void game_client_sync(fs_file demo_fp)
{
union cmd *cmdp;
gd.state = 1;
- game_tilt_init(&gd.tilt);
+ /* Initialize game state. */
- /* Initialize jump and goal states. */
+ game_tilt_init(&gd.tilt);
+ game_view_init(&gd.view);
- gd.jump_e = 1;
- gd.jump_b = 0;
+ gd.jump_e = 1;
+ gd.jump_b = 0;
+ gd.jump_dt = 0.0f;
gd.goal_e = 0;
gd.goal_k = 0.0f;
- /* Initialise the level, background, particles, fade, and view. */
+ /* Initialize interpolation. */
+
+ game_lerp_init(&gl, &gd);
+
+ /* Initialize fade. */
gd.fade_k = 1.0f;
gd.fade_d = -2.0f;
+ /* Load level info. */
version.x = 0;
version.y = 0;
game_compat_map = version.x == 1;
+ /* Initialize particles. */
+
part_reset(GOAL_HEIGHT, JUMP_HEIGHT);
+ /* Initialize command state. */
+
ups = 0;
first_update = 1;
+ /* Initialize background. */
+
back_init(grad_name);
sol_load_full(&gd.back, back_name, 0);
if (gd.state)
{
game_proxy_clr();
+ game_lerp_free(&gl);
sol_free_full(&gd.file);
sol_free_full(&gd.back);
back_free();
/*---------------------------------------------------------------------------*/
-void game_client_draw(int pose, float t)
+void game_client_draw(int pose, float t, float a)
{
+ game_lerp_apply(&gl, &gd, a);
game_draw(&gd, pose, t);
}
/*---------------------------------------------------------------------------*/
-
void game_look(float phi, float theta)
{
- gd.view.c[0] = gd.view.p[0] + fsinf(V_RAD(theta)) * fcosf(V_RAD(phi));
- gd.view.c[1] = gd.view.p[1] + fsinf(V_RAD(phi));
- gd.view.c[2] = gd.view.p[2] - fcosf(V_RAD(theta)) * fcosf(V_RAD(phi));
+ struct game_view *view = &gl.view[CURR];
+
+ view->c[0] = view->p[0] + fsinf(V_RAD(theta)) * fcosf(V_RAD(phi));
+ view->c[1] = view->p[1] + fsinf(V_RAD(phi));
+ view->c[2] = view->p[2] - fcosf(V_RAD(theta)) * fcosf(V_RAD(phi));
}
/*---------------------------------------------------------------------------*/
void game_client_fly(float k)
{
- game_view_fly(&gd.view, &gd.file.vary, k);
+ game_view_fly(&gl.view[CURR], &gd.file.vary, k);
}
/*---------------------------------------------------------------------------*/
int game_client_init(const char *);
void game_client_free(void);
void game_client_sync(fs_file);
-void game_client_draw(int, float);
+void game_client_draw(int, float, float);
int curr_clock(void);
int curr_coins(void);
void lockstep_run(struct lockstep *, float);
void lockstep_scl(struct lockstep *, float);
+#define lockstep_blend(ls) ((ls)->at / (ls)->dt)
+
/*---------------------------------------------------------------------------*/
#endif
}
/*---------------------------------------------------------------------------*/
+
+#define CURR 0
+#define PREV 1
+
+void game_lerp_init(struct game_lerp *gl, struct game_draw *gd)
+{
+ sol_load_lerp(&gl->lerp, &gd->file.vary);
+
+ gl->tilt[PREV] = gl->tilt[CURR] = gd->tilt;
+ gl->view[PREV] = gl->view[CURR] = gd->view;
+
+ gl->goal_k[PREV] = gl->goal_k[CURR] = gd->goal_k;
+ gl->jump_dt[PREV] = gl->jump_dt[CURR] = gd->jump_dt;
+}
+
+void game_lerp_free(struct game_lerp *gl)
+{
+ sol_free_lerp(&gl->lerp);
+}
+
+void game_lerp_copy(struct game_lerp *gl)
+{
+ sol_lerp_copy(&gl->lerp);
+
+ gl->tilt[PREV] = gl->tilt[CURR];
+ gl->view[PREV] = gl->view[CURR];
+
+ gl->goal_k[PREV] = gl->goal_k[CURR];
+ gl->jump_dt[PREV] = gl->jump_dt[CURR];
+}
+
+void game_lerp_apply(struct game_lerp *gl, struct game_draw *gd, float a)
+{
+ /* Solid. */
+
+ sol_lerp_apply(&gl->lerp, a);
+
+ /* Tilt. */
+
+ v_lerp(gd->tilt.x, gl->tilt[PREV].x, gl->tilt[CURR].x, a);
+ v_lerp(gd->tilt.z, gl->tilt[PREV].z, gl->tilt[CURR].z, a);
+
+ gd->tilt.rx = (gl->tilt[PREV].rx * (1.0f - a) + gl->tilt[CURR].rx * a);
+ gd->tilt.rz = (gl->tilt[PREV].rz * (1.0f - a) + gl->tilt[CURR].rz * a);
+
+ /* View. */
+
+ v_lerp(gd->view.c, gl->view[PREV].c, gl->view[CURR].c, a);
+ v_lerp(gd->view.p, gl->view[PREV].p, gl->view[CURR].p, a);
+ e_lerp(gd->view.e, gl->view[PREV].e, gl->view[CURR].e, a);
+
+ /* Effects. */
+
+ gd->goal_k = (gl->goal_k[PREV] * (1.0f - a) + gl->goal_k[CURR] * a);
+ gd->jump_dt = (gl->jump_dt[PREV] * (1.0f - a) + gl->jump_dt[CURR] * a);
+}
+
+/*---------------------------------------------------------------------------*/
#include "solid_draw.h"
#include "game_common.h"
+/*---------------------------------------------------------------------------*/
+
struct game_draw
{
int state;
void game_draw(const struct game_draw *, int, float);
+/*---------------------------------------------------------------------------*/
+
+struct game_lerp
+{
+ struct s_lerp lerp;
+
+ struct game_tilt tilt[2];
+ struct game_view view[2];
+
+ float goal_k[2];
+ float jump_dt[2];
+};
+
+void game_lerp_init(struct game_lerp *, struct game_draw *);
+void game_lerp_free(struct game_lerp *);
+void game_lerp_copy(struct game_lerp *);
+void game_lerp_apply(struct game_lerp *, struct game_draw *, float);
+
+/*---------------------------------------------------------------------------*/
+
#endif
{
sol_quit_sim();
sol_free_vary(&file);
+ sol_free_base(&base);
server_state = 0;
}
}
lockstep_run(&server_step, dt);
}
+float game_server_blend(void)
+{
+ return lockstep_blend(&server_step);
+}
+
/*---------------------------------------------------------------------------*/
void game_set_goal(void)
int game_server_init(const char *, int, int);
void game_server_free(void);
void game_server_step(float);
+float game_server_blend(void);
void game_set_goal(void);
void game_clr_goal(void);
if (config_cheat())
toggle_wire();
break;
-
case SDLK_RETURN:
d = st_buttn(config_get_d(CONFIG_JOYSTICK_BUTTON_A), 1);
break;
video_clear();
game_client_fly(1.0f);
game_kill_fade();
- game_client_draw(POSE_LEVEL, 0);
+ game_client_draw(POSE_LEVEL, 0, 1.0f);
SDL_GL_SwapBuffers();
image_snap(filename);
}
video_pop_matrix();
- game_client_draw(POSE_BALL, t);
+ game_client_draw(POSE_BALL, t, demo_play_blend());
gui_paint(id);
}
static void demo_play_paint(int id, float t)
{
- game_client_draw(0, t);
+ game_client_draw(0, t, demo_play_blend());
if (show_hud)
hud_paint();
static void demo_end_paint(int id, float t)
{
- game_client_draw(0, t);
+ game_client_draw(0, t, demo_play_blend());
gui_paint(id);
if (demo_paused)
static void help_demo_paint(int id, float t)
{
- game_client_draw(0, t);
+ game_client_draw(0, t, demo_play_blend());
}
static void help_demo_timer(int id, float dt)
static void poser_paint(int id, float t)
{
- game_client_draw(POSE_LEVEL, t);
+ game_client_draw(POSE_LEVEL, t, 1.0f);
}
static int poser_buttn(int c, int d)
video_pop_matrix();
}
else
- game_client_draw(0, t);
+ game_client_draw(0, t, 1.0f);
gui_paint(id);
}
static void play_ready_paint(int id, float t)
{
- game_client_draw(0, t);
+ game_client_draw(0, t, 1.0f);
hud_view_paint();
gui_paint(id);
}
static void play_set_paint(int id, float t)
{
- game_client_draw(0, t);
+ game_client_draw(0, t, 1.0f);
hud_view_paint();
gui_paint(id);
}
static void play_loop_paint(int id, float t)
{
- game_client_draw(0, t);
+ game_client_draw(0, t, game_server_blend());
if (show_hud)
hud_paint();
static void look_paint(int id, float t)
{
- game_client_draw(0, t);
+ game_client_draw(0, t, 1.0f);
}
static void look_point(int id, int x, int y, int dx, int dy)
void shared_paint(int id, float t)
{
- game_client_draw(0, t);
+ game_client_draw(0, t, 1.0f);
gui_paint(id);
}
}
/*---------------------------------------------------------------------------*/
+
+#define CURR 0
+#define PREV 1
+
+static int curr_ball;
+
+int sol_lerp_cmd(struct s_lerp *fp, const union cmd *cmd)
+{
+ struct l_ball (*uv)[2];
+ struct l_ball *up;
+
+ int i, rc = 0;
+
+ switch (cmd->type)
+ {
+ case CMD_MAKE_BALL:
+ if ((uv = realloc(fp->uv, sizeof (*uv) * (fp->uc + 1))))
+ {
+ struct v_ball *up;
+
+ fp->uv = uv;
+ fp->uc++;
+
+ /* Sync the main structure. */
+
+ if ((up = realloc(fp->vary->uv, sizeof (*up) * fp->uc)))
+ {
+ fp->vary->uv = up;
+ fp->vary->uc = fp->uc;
+
+ curr_ball = fp->uc - 1;
+ rc = 1;
+ }
+ }
+ break;
+
+ case CMD_BODY_PATH:
+ fp->bv[cmd->bodypath.bi][CURR].pi = cmd->bodypath.pi;
+ break;
+
+ case CMD_BODY_TIME:
+ fp->bv[cmd->bodytime.bi][CURR].t = cmd->bodytime.t;
+ break;
+
+ case CMD_BALL_RADIUS:
+ fp->uv[curr_ball][CURR].r = cmd->ballradius.r;
+ break;
+
+ case CMD_CLEAR_BALLS:
+ free(fp->uv);
+ fp->uv = NULL;
+ fp->uc = 0;
+
+ free(fp->vary->uv);
+ fp->vary->uv = NULL;
+ fp->vary->uc = 0;
+ break;
+
+ case CMD_BALL_POSITION:
+ up = &fp->uv[curr_ball][CURR];
+ v_cpy(up->p, cmd->ballpos.p);
+ break;
+
+ case CMD_BALL_BASIS:
+ up = &fp->uv[curr_ball][CURR];
+ v_cpy(up->e[0], cmd->ballbasis.e[0]);
+ v_cpy(up->e[1], cmd->ballbasis.e[1]);
+ v_crs(up->e[2], up->e[0], up->e[1]);
+ break;
+
+ case CMD_BALL_PEND_BASIS:
+ up = &fp->uv[curr_ball][CURR];
+ v_cpy(up->E[0], cmd->ballpendbasis.E[0]);
+ v_cpy(up->E[1], cmd->ballpendbasis.E[1]);
+ v_crs(up->E[2], up->E[0], up->E[1]);
+ break;
+
+ case CMD_CURRENT_BALL:
+ curr_ball = cmd->currball.ui;
+ break;
+
+ case CMD_STEP_SIMULATION:
+ /*
+ * Simulate body motion.
+ *
+ * This is done on the client side due to replay file size
+ * concerns and isn't done as part of CMD_END_OF_UPDATE to
+ * match the server state as closely as possible. Body time
+ * is still synchronized with the server on a semi-regular
+ * basis and path indices are handled through CMD_BODY_PATH,
+ * thus this code doesn't need to be as sophisticated as
+ * sol_body_step.
+ */
+
+ for (i = 0; i < fp->bc; i++)
+ {
+ struct l_body *bp = &fp->bv[i][CURR];
+ struct v_path *pp = &fp->vary->pv[bp->pi];
+
+ if (bp->pi >= 0 && pp->f)
+ bp->t += cmd->stepsim.dt;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return rc;
+}
+
+void sol_lerp_copy(struct s_lerp *fp)
+{
+ int i;
+
+ for (i = 0; i < fp->bc; i++)
+ fp->bv[i][PREV] = fp->bv[i][CURR];
+
+ for (i = 0; i < fp->uc; i++)
+ fp->uv[i][PREV] = fp->uv[i][CURR];
+}
+
+void sol_lerp_apply(struct s_lerp *fp, float a)
+{
+ int i;
+
+ for (i = 0; i < fp->bc; i++)
+ {
+ if (fp->bv[i][PREV].pi == fp->bv[i][CURR].pi)
+ fp->vary->bv[i].t = (fp->bv[i][PREV].t * (1.0f - a) +
+ fp->bv[i][CURR].t * a);
+ else
+ fp->vary->bv[i].t = fp->bv[i][CURR].t * a;
+
+ fp->vary->bv[i].pi = fp->bv[i][CURR].pi;
+ }
+
+ for (i = 0; i < fp->uc; i++)
+ {
+ e_lerp(fp->vary->uv[i].e, fp->uv[i][PREV].e, fp->uv[i][CURR].e, a);
+ v_lerp(fp->vary->uv[i].p, fp->uv[i][PREV].p, fp->uv[i][CURR].p, a);
+ e_lerp(fp->vary->uv[i].E, fp->uv[i][PREV].E, fp->uv[i][CURR].E, a);
+
+ fp->vary->uv[i].r = (fp->uv[i][PREV].r * (1.0f - a) +
+ fp->uv[i][CURR].r * a);
+ }
+}
+
+int sol_load_lerp(struct s_lerp *fp, struct s_vary *vary)
+{
+ int i;
+
+ fp->vary = vary;
+
+ if (fp->vary->bc)
+ {
+ fp->bv = calloc(fp->vary->bc, sizeof (*fp->bv));
+ fp->bc = fp->vary->bc;
+
+ for (i = 0; i < fp->vary->bc; i++)
+ fp->bv[i][CURR].pi = fp->vary->bv[i].pi;
+ }
+
+ if (fp->vary->uc)
+ {
+ fp->uv = calloc(fp->vary->uc, sizeof (*fp->uv));
+ fp->uc = fp->vary->uc;
+
+ for (i = 0; i < fp->vary->uc; i++)
+ {
+ e_cpy(fp->uv[i][CURR].e, fp->vary->uv[i].e);
+ v_cpy(fp->uv[i][CURR].p, fp->vary->uv[i].p);
+ e_cpy(fp->uv[i][CURR].E, fp->vary->uv[i].E);
+
+ fp->uv[i][CURR].r = fp->vary->uv[i].r;
+ }
+ }
+
+ sol_lerp_copy(fp);
+
+ return 1;
+}
+
+void sol_free_lerp(struct s_lerp *fp)
+{
+ if (fp->bv) free(fp->bv);
+ if (fp->uv) free(fp->uv);
+
+ memset(fp, 0, sizeof (*fp));
+}
+
+/*---------------------------------------------------------------------------*/
/*---------------------------------------------------------------------------*/
+/*
+ * Buffers changes to the varying SOL data for interpolation purposes.
+ */
+
+struct l_body
+{
+ float t; /* time on current path */
+
+ int pi;
+};
+
+struct l_ball
+{
+ float e[3][3]; /* basis of orientation */
+ float p[3]; /* position vector */
+ float E[3][3]; /* basis of pendulum */
+ float r; /* radius */
+};
+
+struct s_lerp
+{
+ struct s_vary *vary;
+
+ int bc;
+ int uc;
+
+ struct l_body (*bv)[2];
+ struct l_ball (*uv)[2];
+};
+
+/*---------------------------------------------------------------------------*/
+
+#include "cmd.h"
+
+int sol_load_lerp(struct s_lerp *, struct s_vary *);
+void sol_free_lerp(struct s_lerp *);
+
+void sol_lerp_copy(struct s_lerp *);
+void sol_lerp_apply(struct s_lerp *, float);
+int sol_lerp_cmd(struct s_lerp *, const union cmd *);
+
+/*---------------------------------------------------------------------------*/
+
#endif
(u)[2] = (p)[2] + (v)[2] * (t); \
} while (0)
-/*---------------------------------------------------------------------------*/
+#define v_lerp(u, v, w, a) { \
+ (u)[0] = (v)[0] * (1.0f - (a)) + (w)[0] * (a); \
+ (u)[1] = (v)[1] * (1.0f - (a)) + (w)[1] * (a); \
+ (u)[2] = (v)[2] * (1.0f - (a)) + (w)[2] * (a); \
+}
+
+#define e_cpy(d, e) { \
+ v_cpy((d)[0], (e)[0]); \
+ v_cpy((d)[1], (e)[1]); \
+ v_cpy((d)[2], (e)[2]); \
+}
+
+#define e_lerp(c, d, e, a) { \
+ v_lerp((c)[0], (d)[0], (e)[0], a); \
+ v_lerp((c)[1], (d)[1], (e)[1], a); \
+ v_lerp((c)[2], (d)[2], (e)[2], a); \
+}
+/*---------------------------------------------------------------------------*/
void v_nrm(float *, const float *);
void v_crs(float *, const float *, const float *);