368 lines
12 KiB
C
368 lines
12 KiB
C
#include "twn_game_api.h"
|
|
#include "state.h"
|
|
#include "twn_vec.h"
|
|
|
|
#include <SDL.h>
|
|
|
|
/* planned features: */
|
|
/* grid-based, bounded space (65536 / POINTS_PER_METER meters), which allows for efficient storage */
|
|
/* 65534 point limit, 16 object limit, 2048 faces per object, 32 textures per object */
|
|
/* triangles and quads only */
|
|
/* texture painting */
|
|
/* bones with mesh animations, snapping to grid, with no weights */
|
|
/* billboard render to specified angles */
|
|
/* 1 point light primitive lighting */
|
|
|
|
static State state;
|
|
static bool init;
|
|
|
|
|
|
static uint8_t new_object(const char *name) {
|
|
if (state.objects_sz >= OBJECT_LIMIT)
|
|
return INVALID_OBJECT;
|
|
state.objects_sz++;
|
|
state.objects = SDL_realloc(state.objects, state.objects_sz * sizeof (*state.objects));
|
|
SDL_memset(&state.objects[state.objects_sz-1], 0, sizeof (Object));
|
|
state.objects[state.objects_sz-1].name = SDL_strdup(name);
|
|
return state.objects_sz-1;
|
|
}
|
|
|
|
|
|
static uint16_t new_point(int16_t x, int16_t y, int16_t z) {
|
|
if (state.points_sz >= POINT_LIMIT)
|
|
return INVALID_POINT;
|
|
state.points_sz++;
|
|
state.points[state.points_sz-1] = (Point){x, y, z};
|
|
return state.points_sz-1;
|
|
}
|
|
|
|
|
|
static uint16_t push_face(uint8_t object, uint16_t p0, uint16_t p1, uint16_t p2, uint16_t p3, uint8_t texture) {
|
|
Object *o = &state.objects[object];
|
|
o->faces_sz++;
|
|
o->faces[o->faces_sz-1] = (Face) {.p = {p0, p1, p2, p3}, .texture = texture};
|
|
return o->faces_sz-1;
|
|
}
|
|
|
|
|
|
static uint8_t push_texture(uint8_t object, char *texture) {
|
|
/* TODO: search and combine if it already exists */
|
|
Object *o = &state.objects[object];
|
|
o->textures_sz++;
|
|
o->textures[o->textures_sz-1] = SDL_strdup(texture);
|
|
return o->textures_sz-1;
|
|
}
|
|
|
|
|
|
static void push_operation(Operation operation, bool active) {
|
|
state.op_stack_ptr++;
|
|
uint8_t op = state.op_stack_ptr % UNDO_STACK_SIZE;
|
|
state.op_stack[op] = operation;
|
|
state.op_active = active;
|
|
}
|
|
|
|
|
|
static inline Vec3 point_to_vec3(uint8_t object, uint16_t point) {
|
|
Object *o = &state.objects[object];
|
|
return (Vec3){ (o->position.x + state.points[point].x) / (float)POINTS_PER_METER,
|
|
(o->position.y + state.points[point].y) / (float)POINTS_PER_METER,
|
|
(o->position.z + state.points[point].z) / (float)POINTS_PER_METER };
|
|
}
|
|
|
|
|
|
static void render_object(uint8_t object) {
|
|
Object *o = &state.objects[object];
|
|
|
|
if (o->is_invisible)
|
|
return;
|
|
|
|
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
|
|
Face *f = &o->faces[fi];
|
|
/* quads */
|
|
if (f->p[3] != INVALID_POINT) {
|
|
Vec3 p0 = point_to_vec3(object, f->p[0]);
|
|
Vec3 p1 = point_to_vec3(object, f->p[1]);
|
|
Vec3 p2 = point_to_vec3(object, f->p[2]);
|
|
Vec3 p3 = point_to_vec3(object, f->p[3]);
|
|
|
|
if (state.solid_display_mode)
|
|
draw_quad(o->textures[f->texture],
|
|
p0, p1, p2, p3,
|
|
(Rect){0,0,32,32},
|
|
(Color){255,255,255,255} );
|
|
else {
|
|
draw_line_3d(p0, p1, 1, (Color){255,255,255,255});
|
|
draw_line_3d(p1, p2, 1, (Color){255,255,255,255});
|
|
draw_line_3d(p2, p3, 1, (Color){255,255,255,255});
|
|
draw_line_3d(p3, p0, 1, (Color){255,255,255,255});
|
|
}
|
|
} else {
|
|
/* triangles */
|
|
/* TODO: */
|
|
SDL_assert_always(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static uint8_t new_cube(Point pos, Point size) {
|
|
uint8_t object = new_object("cube");
|
|
|
|
uint16_t p0 = new_point(pos.x - size.x / 2, pos.y - size.y / 2, pos.z - size.z / 2);
|
|
uint16_t p1 = new_point(pos.x - size.x / 2, pos.y + size.y / 2, pos.z - size.z / 2);
|
|
uint16_t p2 = new_point(pos.x + size.x / 2, pos.y + size.y / 2, pos.z - size.z / 2);
|
|
uint16_t p3 = new_point(pos.x + size.x / 2, pos.y - size.y / 2, pos.z - size.z / 2);
|
|
uint16_t p4 = new_point(pos.x - size.x / 2, pos.y - size.y / 2, pos.z + size.z / 2);
|
|
uint16_t p5 = new_point(pos.x - size.x / 2, pos.y + size.y / 2, pos.z + size.z / 2);
|
|
uint16_t p6 = new_point(pos.x + size.x / 2, pos.y + size.y / 2, pos.z + size.z / 2);
|
|
uint16_t p7 = new_point(pos.x + size.x / 2, pos.y - size.y / 2, pos.z + size.z / 2);
|
|
|
|
uint8_t tex = push_texture(object, "/data/placeholder.png");
|
|
push_face(object, p0, p1, p2, p3, tex);
|
|
push_face(object, p7, p6, p5, p4, tex);
|
|
push_face(object, p4, p5, p1, p0, tex);
|
|
push_face(object, p6, p7, p3, p2, tex);
|
|
push_face(object, p2, p1, p5, p6, tex);
|
|
push_face(object, p0, p3, p7, p4, tex);
|
|
|
|
return object;
|
|
}
|
|
|
|
|
|
static void process_camera_movement(void) {
|
|
input_action("camera_rotate_left", "A");
|
|
input_action("camera_rotate_right", "D");
|
|
input_action("camera_rotate_up", "W");
|
|
input_action("camera_rotate_down", "S");
|
|
|
|
float horizontal_rotation = 0;
|
|
float vertical_rotation = 0;
|
|
|
|
if (input_action_pressed("camera_rotate_left"))
|
|
horizontal_rotation -= CAMERA_ROTATION_SPEED;
|
|
if (input_action_pressed("camera_rotate_right"))
|
|
horizontal_rotation += CAMERA_ROTATION_SPEED;
|
|
if (input_action_pressed("camera_rotate_up"))
|
|
vertical_rotation -= CAMERA_ROTATION_SPEED;
|
|
if (input_action_pressed("camera_rotate_down"))
|
|
vertical_rotation += CAMERA_ROTATION_SPEED;
|
|
|
|
Vec3 front = vec3_cross(state.camera_direction, (Vec3){0,1,0});
|
|
|
|
state.camera_position = vec3_rotate(state.camera_position, horizontal_rotation, (Vec3){0,1,0});
|
|
state.camera_direction = vec3_rotate(state.camera_direction, horizontal_rotation, (Vec3){0,1,0});
|
|
|
|
Vec3 new_rot = vec3_rotate(state.camera_direction, vertical_rotation, front);
|
|
|
|
/* only apply if it's in limits */
|
|
float d = vec3_dot(new_rot, (Vec3){0,-1,0});
|
|
if (fabsf(d) <= 0.999f) {
|
|
state.camera_position = vec3_rotate(state.camera_position, vertical_rotation, front);
|
|
state.camera_direction = new_rot;
|
|
}
|
|
}
|
|
|
|
|
|
static inline DrawCameraUnprojectResult unproject_point(Vec2 point) {
|
|
return draw_camera_unproject(
|
|
point,
|
|
state.camera_position,
|
|
state.camera_direction,
|
|
(Vec3){0, 1, 0},
|
|
state.camera_is_orthographic ? 0 : CAMERA_FOV,
|
|
state.camera_zoom,
|
|
100 );
|
|
}
|
|
|
|
|
|
static bool find_closest_point(uint8_t* object_result, uint16_t *point_result) {
|
|
DrawCameraUnprojectResult pos_and_ray = unproject_point(ctx.mouse_position);
|
|
|
|
/* step over every selectable object and find points closest to the view ray */
|
|
/* by constructing triangles and finding their height, from perpendicular */
|
|
uint16_t closest_point = INVALID_POINT;
|
|
uint8_t closest_obj = INVALID_OBJECT;
|
|
float closest_distance = INFINITY;
|
|
for (uint8_t obj = 0; obj < state.objects_sz; ++obj) {
|
|
Object *o = &state.objects[obj];
|
|
|
|
if (o->is_invisible)
|
|
continue;
|
|
|
|
/* TODO: is it possible to skip repeated points? does it matter? */
|
|
/* as we limit the point could we could actually have bool array preallocated for this */
|
|
for (uint16_t fi = 0; fi < o->faces_sz; ++fi) {
|
|
Face *f = &o->faces[fi];
|
|
for (uint16_t pi = 0; pi < 4; ++pi) {
|
|
Vec3 p = point_to_vec3(obj, f->p[pi]);
|
|
Vec3 d = vec3_sub(pos_and_ray.position, p);
|
|
Vec3 b = vec3_cross(d, pos_and_ray.direction);
|
|
float ray_dist = vec3_length(b);
|
|
if (ray_dist > ((float)SELECTION_SPHERE_RADIUS / POINTS_PER_METER))
|
|
continue;
|
|
Vec3 i = vec3_add(p, b);
|
|
float dist = vec3_length(i);
|
|
if (dist < closest_distance) {
|
|
closest_distance = dist;
|
|
closest_obj = obj;
|
|
closest_point = f->p[pi];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (closest_point == INVALID_POINT)
|
|
return false;
|
|
|
|
if (object_result)
|
|
*object_result = closest_obj;
|
|
if (point_result)
|
|
*point_result = closest_point;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
static void process_operation_move_point(Operation *op) {
|
|
/* finish dragging around */
|
|
if (input_action_just_released("select")) {
|
|
state.op_active = false;
|
|
return;
|
|
}
|
|
|
|
float size = sinf(ctx.frame_number / 10) * 0.05f + 0.05f;
|
|
draw_billboard("/data/grab.png",
|
|
point_to_vec3(op->data.move_point.object, op->data.move_point.point),
|
|
(Vec2){size, size},
|
|
(Rect){0,0,16,16},
|
|
(Color){255,255,255,255},
|
|
false);
|
|
|
|
DrawCameraUnprojectResult pos_and_ray = unproject_point(ctx.mouse_position);
|
|
|
|
Vec3 p = point_to_vec3(op->data.move_point.object, op->data.move_point.point);
|
|
Vec3 d = vec3_sub(pos_and_ray.position, p);
|
|
Vec3 b = vec3_cross(d, pos_and_ray.direction);
|
|
|
|
/* TODO: show thresholds */
|
|
|
|
/* change along axes, delta is accumulated when threshold is met*/
|
|
int16_t zch = (int16_t)(floorf(vec3_dot(b, (Vec3){0,0,1}) * (float)POINTS_PER_METER));
|
|
op->data.move_point.delta_x += zch;
|
|
state.points[op->data.move_point.point].x += zch;
|
|
}
|
|
|
|
|
|
static void process_operations(void) {
|
|
if (!state.op_active) {
|
|
uint16_t point_select; uint8_t obj_select;
|
|
if (find_closest_point(&obj_select, &point_select)) {
|
|
draw_billboard("/data/point.png",
|
|
point_to_vec3(obj_select, point_select),
|
|
(Vec2){0.05f, 0.05f},
|
|
(Rect){0,0,16,16},
|
|
(Color){255,255,255,255},
|
|
false);
|
|
|
|
if (input_action_just_pressed("select"))
|
|
push_operation((Operation){
|
|
.kind = OPERATION_MOVE_POINT,
|
|
.data = { .move_point = { .point = point_select } },
|
|
}, true );
|
|
}
|
|
}
|
|
|
|
if (state.op_active) {
|
|
Operation *op = &state.op_stack[state.op_stack_ptr % UNDO_STACK_SIZE];
|
|
switch (op->kind) {
|
|
case OPERATION_MOVE_POINT: {
|
|
process_operation_move_point(op);
|
|
|
|
break;
|
|
}
|
|
default:
|
|
(void)0;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void game_tick(void) {
|
|
if (!init) {
|
|
/* default state */
|
|
new_cube((Point){0,0,0}, (Point){POINTS_PER_METER,POINTS_PER_METER,POINTS_PER_METER});
|
|
state.camera_position = (Vec3){2,1,2};
|
|
state.camera_direction = vec3_norm(((Vec3){-2,-1,-2}));
|
|
state.camera_zoom = 0.5f;
|
|
init = true;
|
|
}
|
|
|
|
input_action("toggle_display_mode", "Q");
|
|
input_action("toggle_projection", "E");
|
|
|
|
input_action("select", "LCLICK");
|
|
|
|
if (input_action_just_pressed("toggle_display_mode")) {
|
|
audio_play("/data/click.wav", NULL, false, 0.7f, 0.0f);
|
|
state.solid_display_mode = !state.solid_display_mode;
|
|
}
|
|
|
|
if (input_action_just_pressed("toggle_projection")) {
|
|
audio_play("/data/pop.wav", NULL, false, 0.8f, 0.0f);
|
|
state.camera_is_orthographic = !state.camera_is_orthographic;
|
|
}
|
|
|
|
process_camera_movement();
|
|
process_operations();
|
|
|
|
/* axis helpers */
|
|
/* idea: black out inactives when dragging points */
|
|
/* idea: double selection of axes for diagonal edits */
|
|
draw_line_3d((Vec3){0}, (Vec3){0,0,256}, 1, (Color){255,0,0,125});
|
|
draw_billboard("/data/x.png",
|
|
(Vec3){0,0,2},
|
|
(Vec2){0.1f, 0.1f},
|
|
(Rect){0},
|
|
(Color){255,0,0,255},
|
|
false);
|
|
draw_line_3d((Vec3){0}, (Vec3){256,0,0}, 1, (Color){0,0,255,125});
|
|
draw_billboard("/data/y.png",
|
|
(Vec3){2,0,0},
|
|
(Vec2){0.1f, 0.1f},
|
|
(Rect){0},
|
|
(Color){0,0,255,255},
|
|
false);
|
|
draw_line_3d((Vec3){0}, (Vec3){0,256,0}, 1, (Color){75,125,25,125});
|
|
draw_billboard("/data/z.png",
|
|
(Vec3){0,1.5f,0},
|
|
(Vec2){0.1f, 0.1f},
|
|
(Rect){0},
|
|
(Color){75,125,25,255},
|
|
false);
|
|
|
|
for (uint8_t obj = 0; obj < state.objects_sz; ++obj)
|
|
render_object(obj);
|
|
|
|
draw_text("twndel\x03", (Vec2){0, 2}, 32, (Color){255,255,255,200}, NULL);
|
|
draw_camera(
|
|
state.camera_position,
|
|
state.camera_direction,
|
|
(Vec3){0, 1, 0},
|
|
state.camera_is_orthographic ? 0 : CAMERA_FOV,
|
|
state.camera_zoom,
|
|
100
|
|
);
|
|
}
|
|
|
|
|
|
void game_end(void) {
|
|
for (uint8_t obj = 0; obj < state.objects_sz; ++obj) {
|
|
Object *o = &state.objects[obj];
|
|
for (uint8_t t = 0; t < o->textures_sz; ++t)
|
|
SDL_free(o->textures[t]);
|
|
SDL_free(o->name);
|
|
}
|
|
if (state.objects_sz != 0)
|
|
SDL_free(state.objects);
|
|
}
|