#include "twn_game_api.h" #include "state.h" #include "twn_vec.h" #include /* 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 */ /* assumptions: */ /* up is always (0,1,0) */ 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; float dist = vec3_length(vec3_add(p, b)); 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; } /* o = vector origin */ /* v = vector direction, normalized */ /* p = any point on plane */ /* n = normal of a plane */ static bool vector_plane_intersection(Vec3 o, Vec3 v, Vec3 p, Vec3 n, Vec3 *out) { float dot = vec3_dot(n, v); if (fabsf(dot) > FLT_EPSILON) { Vec3 w = vec3_sub(o, p); float fac = -vec3_dot(n, w) / dot; *out = vec3_add(o, vec3_scale(v, fac)); return true; } /* vector and plane are perpendicular, assume that it lies exactly on it */ return false; } static void show_snap_lines(Vec3 p) { float step = 1.0f / ((float)POINTS_PER_METER / state.grid_snap_granularity); int const lines_per_side = (SNAP_LINES_SHOW - 1) / 2; for (int l = -lines_per_side; l <= lines_per_side; ++l) { if (!state.axis_mask[0]) { Vec3 c = vec3_add(p, vec3_scale((Vec3){1,0,0}, step * (float)l)); draw_line_3d( vec3_add(c, vec3_scale((Vec3){0,0,1}, SNAP_LINES_WIDTH)), vec3_add(c, vec3_scale((Vec3){0,0,1}, -SNAP_LINES_WIDTH)), 1, SNAP_LINES_COLOR); } if (!state.axis_mask[1]) { Vec3 axis = fabsf(vec3_dot(state.camera_direction, (Vec3){0,0,1})) >= 0.5f ? (Vec3){1,0,0} : (Vec3){0,0,1}; Vec3 c = vec3_add(p, vec3_scale((Vec3){0,1,0}, step * (float)l)); draw_line_3d( vec3_add(c, vec3_scale(axis, SNAP_LINES_WIDTH)), vec3_add(c, vec3_scale(axis, -SNAP_LINES_WIDTH)), 1, SNAP_LINES_COLOR); } if (!state.axis_mask[2]) { Vec3 c = vec3_add(p, vec3_scale((Vec3){0,0,1}, step * (float)l)); draw_line_3d( vec3_add(c, vec3_scale((Vec3){1,0,0}, SNAP_LINES_WIDTH)), vec3_add(c, vec3_scale((Vec3){1,0,0}, -SNAP_LINES_WIDTH)), 1, SNAP_LINES_COLOR); } } } static void process_operation_move_point(Operation *op) { /* finish dragging around */ /* TODO: dont keep empty ops on stack? */ 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 cam = unproject_point(ctx.mouse_position); Vec3 p = point_to_vec3(op->data.move_point.object, op->data.move_point.point); bool point_moved = false; /* figure out which planes are angled acutely against the viewing direction */ bool y_closer_than_horizon = fabsf(vec3_dot(state.camera_direction, (Vec3){0,1,0})) >= 0.5f; Vec3 plane_normal_x = y_closer_than_horizon ? (Vec3){0,1,0} : (Vec3){0,0,1}; Vec3 plane_normal_z = y_closer_than_horizon ? (Vec3){0,1,0} : (Vec3){1,0,0}; /* show snapping in lines */ show_snap_lines(p); Vec3 s; if (!state.axis_mask[0]) { if (vector_plane_intersection(cam.position, cam.direction, p, plane_normal_x, &s)) { int16_t xch = (int16_t)(floorf(vec3_dot(vec3_sub(s, p), (Vec3){1,0,0}) * (float)POINTS_PER_METER)); xch -= xch % state.grid_snap_granularity; state.points[op->data.move_point.point].x += xch; op->data.move_point.delta_x += xch; if (xch != 0) point_moved = true; } } if (!state.axis_mask[1]) { if (vector_plane_intersection(cam.position, cam.direction, p, (Vec3){0,0,1}, &s)) { int16_t ych = (int16_t)(floorf(vec3_dot(vec3_sub(s, p), (Vec3){0,1,0}) * (float)POINTS_PER_METER)); ych -= ych % state.grid_snap_granularity; state.points[op->data.move_point.point].y += ych; op->data.move_point.delta_y += ych; if (ych != 0) point_moved = true; } } if (!state.axis_mask[2]) { if (vector_plane_intersection(cam.position, cam.direction, p, plane_normal_z, &s)) { int16_t zch = (int16_t)(floorf(vec3_dot(vec3_sub(s, p), (Vec3){0,0,1}) * (float)POINTS_PER_METER)); zch -= zch % state.grid_snap_granularity; state.points[op->data.move_point.point].z += zch; op->data.move_point.delta_z += zch; if (zch != 0) point_moved = true; } } if (point_moved) audio_play("/data/bong.ogg", NULL, false, 0.12f, 0.0f); } static void reverse_operation_move_point(Operation *op) { SDL_assert(op->kind == OPERATION_MOVE_POINT); state.points[op->data.move_point.point].x -= op->data.move_point.delta_x; state.points[op->data.move_point.point].y -= op->data.move_point.delta_y; state.points[op->data.move_point.point].z -= op->data.move_point.delta_z; audio_play("/data/drop.ogg", NULL, false, 0.4f, 0.0f); } static void process_operations(void) { if (input_action_just_pressed("undo")) { /* TODO: checks and defined limit */ Operation *op = &state.op_stack[state.op_stack_ptr % UNDO_STACK_SIZE]; state.op_stack_ptr--; switch (op->kind) { case OPERATION_MOVE_POINT: { reverse_operation_move_point(op); break; } default: (void)0; } } 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; state.grid_snap_granularity = 16; state.axis_mask[1] = 1; state.axis_mask[2] = 1; init = true; } input_action("toggle_display_mode", "Q"); input_action("toggle_projection", "E"); input_action("toggle_x_axis", "Z"); input_action("toggle_y_axis", "X"); input_action("toggle_z_axis", "C"); input_action("select", "LCLICK"); input_action("undo", "F"); 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; } if (input_action_just_pressed("toggle_x_axis")) { state.axis_mask[0] = 0; state.axis_mask[1] = 1; state.axis_mask[2] = 1; } if (input_action_just_pressed("toggle_y_axis")) { state.axis_mask[0] = 1; state.axis_mask[1] = 0; state.axis_mask[2] = 1; } if (input_action_just_pressed("toggle_z_axis")) { state.axis_mask[0] = 1; state.axis_mask[1] = 1; state.axis_mask[2] = 0; } 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){256,0,0}, 1, state.axis_mask[0] ? (Color){0,0,0,75} : (Color){255,0,0,125}); draw_billboard("/data/x.png", (Vec3){2,0,0}, (Vec2){0.1f, 0.1f}, (Rect){0}, state.axis_mask[0] ? (Color){0,0,0,255} : (Color){255,0,0,255}, false); draw_line_3d((Vec3){0}, (Vec3){0,256,0}, 1, state.axis_mask[1] ? (Color){0,0,0,75} : (Color){75,125,25,125}); draw_billboard("/data/y.png", (Vec3){0,1.5f,0}, (Vec2){0.1f, 0.1f}, (Rect){0}, state.axis_mask[1] ? (Color){0,0,0,255} : (Color){75,125,25,255}, false); draw_line_3d((Vec3){0}, (Vec3){0,0,256}, 1, state.axis_mask[2] ? (Color){0,0,0,75} : (Color){0,0,255,125}); draw_billboard("/data/z.png", (Vec3){0,0,2}, (Vec2){0.1f, 0.1f}, (Rect){0}, state.axis_mask[2] ? (Color){0,0,0,255} : (Color){0,0,255,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); }