#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 */ /* support for billboards and flat two sided quads */ /* texture painting */ /* bones with mesh animations, snapping to grid, with no weights */ /* billboard render to specified angles */ /* 1 point light primitive lighting */ /* live edited textures are capped at 128x128 */ /* assumptions: */ /* up is always (0,1,0) */ /* preallocations everywhere */ 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[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, uint8_t tex_scale, int16_t tex_x, int16_t tex_y) { Object *o = &state.objects[object]; o->faces_sz++; o->faces[o->faces_sz-1] = (Face) { .p = {p0, p1, p2, p3}, .texture = texture, .tex_scale = tex_scale, .tex_x = tex_x, .tex_y = tex_y, }; return o->faces_sz-1; } static uint8_t push_texture(uint8_t object, char *texture) { Object *o = &state.objects[object]; /* check whether it's already here */ for (uint8_t i = 0; i < o->textures_sz; ++i) if (SDL_strcmp(o->textures[i], texture) == 0) return i; o->textures_sz++; o->textures[o->textures_sz-1] = SDL_strdup(texture); return o->textures_sz-1; } /* TODO: use tombstones instead? it would be easier to maintain, by a lot */ /* note: make sure nothing depends on none */ static void pop_face(uint8_t object, uint16_t face) { Object *o = &state.objects[object]; if (face != o->faces_sz-1) o->faces[face] = o->faces[o->faces_sz-1]; o->faces_sz--; } 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 void extend_operation(Operation operation) { uint8_t op = state.op_stack_ptr % UNDO_STACK_SIZE; Operation ext = state.op_stack[op]; ext.chained = true; state.op_stack[op] = operation; push_operation(ext, state.op_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 inline Vec2 project_texture_coordinate(Vec2 origin, Vec3 plane, Vec3 point, float scale) { Vec3 right = vec3_norm(vec3_cross(plane, fabsf(0.0f - plane.y) < 1e-9f ? (Vec3){0,1,0} : (Vec3){1,0,0})); Vec3 up = vec3_norm(vec3_cross(right, plane)); Vec2 pp = { vec3_dot(point, right), vec3_dot(point, up) }; return vec2_scale(vec2_sub(origin, pp), scale); } 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]; if (f->p[2] != 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]); if (state.solid_display_mode) { Vec3 n = vec3_norm(vec3_cross(vec3_sub(p1, p0), vec3_sub(p2, p0))); Vec2 to = { f->tex_x / (float)POINTS_PER_METER, f->tex_y / (float)POINTS_PER_METER, }; Vec2 tul = project_texture_coordinate(to, n, p0, (float)f->tex_scale); Vec2 tdl = project_texture_coordinate(to, n, p1, (float)f->tex_scale); Vec2 tdr = project_texture_coordinate(to, n, p2, (float)f->tex_scale); draw_triangle(o->textures[f->texture], p0, p1, p2, tul, tdl, tdr, (Color){255,255,255,255}, (Color){255,255,255,255}, (Color){255,255,255,255}); if (f->p[3] != INVALID_POINT) { Vec3 p3 = point_to_vec3(object, f->p[3]); Vec2 tur = project_texture_coordinate(to, n, p3, (float)f->tex_scale); draw_triangle(o->textures[f->texture], p2, p3, p0, tdr, tur, tul, (Color){255,255,255,255}, (Color){255,255,255,255}, (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}); if (f->p[3] == INVALID_POINT) draw_line_3d(p2, p0, 1, (Color){255,255,255,255}); else { Vec3 p3 = point_to_vec3(object, f->p[3]); draw_line_3d(p2, p3, 1, (Color){255,255,255,255}); draw_line_3d(p3, p0, 1, (Color){255,255,255,255}); } } } else 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, p2, p3, p0, p1, tex, 128, 0, 0); push_face(object, p5, p4, p7, p6, tex, 128, 0, 0); push_face(object, p1, p0, p4, p5, tex, 128, 0, 0); push_face(object, p6, p7, p3, p2, tex, 128, 0, 0); push_face(object, p2, p1, p5, p6, tex, 128, 0, 0); push_face(object, p0, p3, p7, p4, tex, 128, 0, 0); return object; } static void process_camera_rotation(void) { 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}); Vec3 local_position = vec3_sub(state.active_center, state.camera_position); Vec3 new_local_position = vec3_rotate(local_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) { new_local_position = vec3_rotate(new_local_position, vertical_rotation, front); state.camera_direction = new_rot; } state.camera_position = vec3_sub(state.active_center, new_local_position); } static void process_camera_translation(void) { Vec3 right = vec3_norm(vec3_cross(state.camera_direction, (Vec3){0,1,0})); Vec3 up = vec3_norm(vec3_cross(state.camera_direction, right)); Vec3 was = state.camera_position; if (input_action_pressed("camera_rotate_left")) state.camera_position = vec3_sub(state.camera_position, vec3_scale(right, CAMERA_TRANSLATION_SPEED)); if (input_action_pressed("camera_rotate_right")) state.camera_position = vec3_add(state.camera_position, vec3_scale(right, CAMERA_TRANSLATION_SPEED)); if (input_action_pressed("camera_rotate_up")) state.camera_position = vec3_sub(state.camera_position, vec3_scale(up, CAMERA_TRANSLATION_SPEED)); if (input_action_pressed("camera_rotate_down")) state.camera_position = vec3_add(state.camera_position, vec3_scale(up, CAMERA_TRANSLATION_SPEED)); state.active_center = vec3_add(state.active_center, vec3_sub(state.camera_position, was)); draw_billboard("/data/camera.png", vec3_add(state.camera_position, vec3_scale(state.camera_direction, vec3_length(state.camera_position))), (Vec2){0.2f,0.2f}, (Rect){0}, (Color){255,255,255,255}, false); /* show relation to origin */ draw_billboard("/data/center.png", (Vec3){0}, (Vec2){0.1f,0.1f}, (Rect){0}, (Color){255,255,255,255}, false); draw_line_3d((Vec3){0}, state.active_center, 1, (Color){255,255,255,255}); } 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"); input_action("camera_lock_rotation", "SPACE"); if (input_action_pressed("camera_lock_rotation")) { process_camera_translation(); } else { process_camera_rotation(); } } 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) { if (f->p[pi] == INVALID_POINT) break; 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 bool find_closest_face(uint8_t* object_result, uint16_t *face_result) { DrawCameraUnprojectResult cam = unproject_point(ctx.mouse_position); uint8_t closest_obj = INVALID_OBJECT; uint16_t closest_face = INVALID_FACE; float closest_distance = INFINITY; for (uint8_t oi = 0; oi < state.objects_sz; ++oi) { Object *o = &state.objects[oi]; for (uint16_t fi = 0; fi < o->faces_sz; ++fi) { Face *f = &o->faces[fi]; if (f->p[1] == INVALID_POINT) continue; Vec3 p0 = point_to_vec3(oi, f->p[0]); Vec3 p1 = point_to_vec3(oi, f->p[1]); Vec3 p2 = point_to_vec3(oi, f->p[2]); Vec3 n = vec3_norm(vec3_cross(vec3_sub(p1, p0), vec3_sub(p2, p0))); /* culling */ if (vec3_dot(state.camera_direction, n) >= 0) continue; Vec3 i; if (!vector_plane_intersection(cam.position, cam.direction, p0, n, &i)) continue; float dist = vec3_length(vec3_sub(p0, i)); if (dist >= closest_distance) continue; /* left normals are used to determine whether point lies to the left for all forming lines */ Vec3 ln0 = vec3_norm(vec3_cross(n, vec3_sub(p1, p0))); if (vec3_dot(ln0, vec3_sub(p0, i)) >= 0) continue; Vec3 ln1 = vec3_norm(vec3_cross(n, vec3_sub(p2, p1))); if (vec3_dot(ln1, vec3_sub(p1, i)) >= 0) continue; if (f->p[3] == INVALID_POINT) { /* triangle */ Vec3 ln2 = vec3_norm(vec3_cross(n, vec3_sub(p0, p2))); if (vec3_dot(ln2, vec3_sub(p2, i)) >= 0) continue; } else { /* quad */ Vec3 p3 = point_to_vec3(oi, f->p[3]); Vec3 ln2 = vec3_norm(vec3_cross(n, vec3_sub(p3, p2))); if (vec3_dot(ln2, vec3_sub(p2, i)) >= 0) continue; Vec3 ln3 = vec3_norm(vec3_cross(n, vec3_sub(p0, p3))); if (vec3_dot(ln3, vec3_sub(p3, i)) >= 0) continue; } closest_distance = dist; closest_face = fi; closest_obj = oi; } } if (closest_face == INVALID_FACE) return false; if (object_result) *object_result = closest_obj; if (face_result) *face_result = closest_face; return true; } 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 bool 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 false; } 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}, (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)(roundf(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)(roundf(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)(roundf(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); return true; } return false; } 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 reverse_operation_set_texture(Operation *op) { SDL_assert(op->kind == OPERATION_SET_TEXTURE); Object *o = &state.objects[op->data.set_texture.object]; o->faces[op->data.set_texture.face].texture += op->data.set_texture.delta_texture; audio_play("/data/drop.ogg", NULL, false, 0.4f, 0.0f); } static void reverse_triangulation(Operation *op) { SDL_assert(op->kind == OPERATION_TRIANGULATE); Object *o = &state.objects[op->data.set_texture.object]; Face *fn = &o->faces[op->data.triangulate.new_face]; Face *fo = &o->faces[op->data.triangulate.old_face]; fo->p[3] = fo->p[2]; fo->p[2] = fn->p[1]; pop_face(op->data.set_texture.object, op->data.triangulate.new_face); } /* TODO: reverse of this */ static void try_subdividing_from_moving(uint8_t object, uint16_t point) { Object *o = &state.objects[object]; bool not_first = false; for (uint16_t fi = 0; fi < o->faces_sz; ++fi) { Face *f = &o->faces[fi]; if (f->p[3] == INVALID_POINT) continue; for (uint16_t pi = 0; pi < 4; ++pi) { if (f->p[pi] == point) { Face new0 = *f; new0.p[0] = f->p[pi]; new0.p[1] = f->p[(pi + 1) % 4]; new0.p[2] = f->p[(pi + 3) % 4]; new0.p[3] = INVALID_POINT; uint16_t newf = push_face( object, f->p[(pi + 1) % 4], f->p[(pi + 2) % 4], f->p[(pi + 3) % 4], INVALID_POINT, f->texture, f->tex_scale, f->tex_x, f->tex_y); *f = new0; extend_operation((Operation){ .kind = OPERATION_TRIANGULATE, .data = { .triangulate = { .new_face = newf, .old_face = fi, .object = object} }, .chained = not_first, }); not_first = true; } } } } static void process_undo(void) { /* 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; } case OPERATION_SET_TEXTURE: { reverse_operation_set_texture(op); break; } case OPERATION_TRIANGULATE: { reverse_triangulation(op); break; } default: (void)0; } /* pop another if they're chained together */ if (op->chained) process_undo(); } static void process_operations(void) { if (input_action_just_pressed("undo")) process_undo(); if (!state.op_active) { /* point dragging */ if (!state.solid_display_mode) { 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}, (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, .object = obj_select } }, }, true ); } /* texture setting */ } else { uint8_t obj_select; uint16_t face_select; if (find_closest_face(&obj_select, &face_select)) { state.current_hovered_face = face_select; state.current_hovered_obj = obj_select; if (input_action_just_pressed("rotate")) { Face *f = &state.objects[obj_select].faces[face_select]; int16_t tex_x = f->tex_x; int16_t tex_y = f->tex_y; f->tex_x = -tex_y; f->tex_y = tex_x; } if (input_action_pressed("select") && state.current_texture) { uint8_t new_tex = push_texture(obj_select, state.current_texture); uint8_t cur_tex = state.objects[obj_select].faces[face_select].texture; if (new_tex != cur_tex) { state.objects[obj_select].faces[face_select].texture = new_tex; audio_play("/data/bong.ogg", NULL, false, 0.5f, 0.0f); push_operation((Operation){ .kind = OPERATION_SET_TEXTURE, .data = { .set_texture = { .face = face_select, .object = obj_select, .delta_texture = cur_tex - new_tex, }}, }, false ); } } } } } if (state.op_active) { Operation *op = &state.op_stack[state.op_stack_ptr % UNDO_STACK_SIZE]; switch (op->kind) { case OPERATION_MOVE_POINT: { bool update = process_operation_move_point(op); if (update) try_subdividing_from_moving(op->data.move_point.object, op->data.move_point.point); break; } case OPERATION_SET_TEXTURE: case OPERATION_TRIANGULATE: default: (void)0; } } } static void draw_axes(void) { /* axis helpers */ /* idea: double selection of axes for diagonal edits */ draw_line_3d(state.active_center, (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_add(state.active_center, (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(state.active_center, (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_add(state.active_center, (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(state.active_center, (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_add(state.active_center, (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); } static void draw_hovered_face_border(void) { if (state.current_hovered_obj == INVALID_OBJECT || state.current_hovered_face == INVALID_FACE) return; Object *o = &state.objects[state.current_hovered_obj]; Face *f = &o->faces[state.current_hovered_face]; if (f->p[3] != INVALID_POINT) { Vec3 p0 = point_to_vec3(state.current_hovered_obj, f->p[0]); Vec3 p1 = point_to_vec3(state.current_hovered_obj, f->p[1]); Vec3 p2 = point_to_vec3(state.current_hovered_obj, f->p[2]); Vec3 p3 = point_to_vec3(state.current_hovered_obj, f->p[3]); Vec3 center = vec3_scale(vec3_add(p2, p0), 0.5); float size = sinf(ctx.frame_number / 10) * 0.05f + 0.1f; Vec3 c0 = vec3_add(p0, vec3_scale(vec3_sub(p0, center), size)); Vec3 c1 = vec3_add(p1, vec3_scale(vec3_sub(p1, center), size)); Vec3 c2 = vec3_add(p2, vec3_scale(vec3_sub(p2, center), size)); Vec3 c3 = vec3_add(p3, vec3_scale(vec3_sub(p3, center), size)); draw_line_3d(c0, c1, 1, (Color){255,255,255,255}); draw_line_3d(c1, c2, 1, (Color){255,255,255,255}); draw_line_3d(c2, c3, 1, (Color){255,255,255,255}); draw_line_3d(c3, c0, 1, (Color){255,255,255,255}); } else { Vec3 p0 = point_to_vec3(state.current_hovered_obj, f->p[0]); Vec3 p1 = point_to_vec3(state.current_hovered_obj, f->p[1]); Vec3 p2 = point_to_vec3(state.current_hovered_obj, f->p[2]); Vec3 center = vec3_scale(vec3_add(p0, vec3_add(p1, p2)), 0.33f); float size = sinf(ctx.frame_number / 10) * 0.05f + 0.1f; Vec3 c0 = vec3_add(p0, vec3_scale(vec3_sub(p0, center), size)); Vec3 c1 = vec3_add(p1, vec3_scale(vec3_sub(p1, center), size)); Vec3 c2 = vec3_add(p2, vec3_scale(vec3_sub(p2, center), size)); draw_line_3d(c0, c1, 1, (Color){255,255,255,255}); draw_line_3d(c1, c2, 1, (Color){255,255,255,255}); draw_line_3d(c2, c0, 1, (Color){255,255,255,255}); } } static void display_textures(void) { String list = file_read("/data/assets/", ":images"); if (!list.data) return; draw_rectangle((Rect){0, 480 - 50, 640, 480}, (Color){0,0,0,126}); char *selected = NULL; char *saveptr = NULL; int count = 0; char const *part = SDL_strtokr(list.data, "\n", &saveptr); do { Rect box = (Rect){(float)count * 50, 480 - 48, 48, 48}; draw_sprite(part, box, (Rect){0,0,64,64}, (Color){255,255,255,255}, 0, false, false, true); count++; if (rect_intersects(box, (Rect){ctx.mouse_position.x, ctx.mouse_position.y, 1, 1})) selected = SDL_strdup(part); } while ((part = SDL_strtokr(NULL, "\n", &saveptr))); if (selected) { draw_text(selected, (Vec2){0, 480 - 48 - 24}, 24, (Color){255,255,255,255}, NULL); if (input_action_just_pressed("select")) { if (state.current_texture) SDL_free(state.current_texture); state.current_texture = selected; } else SDL_free(selected); } } 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; } state.current_hovered_face = INVALID_FACE; state.current_hovered_obj = INVALID_OBJECT; input_action("toggle_display_mode", "Q"); input_action("toggle_projection", "TAB"); input_action("toggle_x_axis", "Z"); input_action("toggle_y_axis", "X"); input_action("toggle_z_axis", "C"); input_action("select", "LCLICK"); input_action("rotate", "R"); 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(); /* helpres */ draw_axes(); draw_hovered_face_border(); for (uint8_t obj = 0; obj < state.objects_sz; ++obj) render_object(obj); if (state.solid_display_mode) display_textures(); 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.current_texture) SDL_free(state.current_texture); }