const rl = @import("raylib"); const std = @import("std"); const print = std.debug.print; const Allocator = std.mem.Allocator; const ChipContext = @import("chip.zig"); const RaylibChip = @import("raylib-chip.zig"); const UI = @import("ui.zig").UI; const MemoryView = @import("memory-view.zig").MemoryView; const Range = @import("memory-view.zig").Range; const assert = std.debug.assert; const Tab = enum { MemoryView }; pub fn gen_sin_wave(wave: *rl.Wave, frequency: f32) void { assert(wave.sampleSize == 16); // Only 16 bits are supported const sample_rate: f32 = @floatFromInt(wave.sampleRate); const sample_size: u5 = @truncate(wave.sampleSize); const max_sample_value: f32 = @floatFromInt(@shlExact(@as(u32, 1), sample_size - 1)); const data: [*]i16 = @ptrCast(@alignCast(wave.data)); for (0..wave.frameCount) |i| { const i_f32: f32 = @floatFromInt(i); const sin_value: f32 = @sin(std.math.pi*2*frequency/sample_rate*i_f32); data[i] = @intFromFloat(sin_value*max_sample_value); } } fn megabytes(amount: usize) usize { return amount * 1024 * 1024; } const Light = struct { const LightType = enum(i32) { DIRECTIONAL = 0, POINT = 1, }; type: LightType, enabled: bool, position: rl.Vector3, target: rl.Vector3, color: rl.Color, attenuation: f32 = 0.0, enabledLoc: i32, typeLoc: i32, positionLoc: i32, targetLoc: i32, colorLoc: i32, attenuationLoc: i32 = 0, fn create(light_type: LightType, postion: rl.Vector3, target: rl.Vector3, color: rl.Color, shader: rl.Shader) Light { var light = Light{ .type = light_type, .enabled = true, .position = postion, .target = target, .color = color, .enabledLoc = rl.GetShaderLocation(shader, "lights[0].enabled"), .typeLoc = rl.GetShaderLocation(shader, "lights[0].type"), .positionLoc = rl.GetShaderLocation(shader, "lights[0].position"), .targetLoc = rl.GetShaderLocation(shader, "lights[0].target"), .colorLoc = rl.GetShaderLocation(shader, "lights[0].color"), }; light.update_values(shader); return light; } fn update_values(light: *Light, shader: rl.Shader) void { const enabled: i32 = @intFromBool(light.enabled); rl.SetShaderValue(shader, light.enabledLoc, &enabled, rl.ShaderUniformDataType.SHADER_UNIFORM_INT); const lightType: i32 = @intFromEnum(light.type); rl.SetShaderValue(shader, light.typeLoc, &lightType, rl.ShaderUniformDataType.SHADER_UNIFORM_INT); const position = [3]f32{ light.position.x, light.position.y, light.position.z }; rl.SetShaderValue(shader, light.positionLoc, &position, rl.ShaderUniformDataType.SHADER_UNIFORM_VEC3); const target = [3]f32{ light.target.x, light.target.y, light.target.z }; rl.SetShaderValue(shader, light.targetLoc, &target, rl.ShaderUniformDataType.SHADER_UNIFORM_VEC3); const color = [4]f32{ @as(f32, @floatFromInt(light.color.r)) / 255.0, @as(f32, @floatFromInt(light.color.g)) / 255.0, @as(f32, @floatFromInt(light.color.b)) / 255.0, @as(f32, @floatFromInt(light.color.a)) / 255.0, }; rl.SetShaderValue(shader, light.colorLoc, &color, rl.ShaderUniformDataType.SHADER_UNIFORM_VEC4); } }; fn get_camera_projection(camera: *const rl.Camera3D) rl.Matrix { const screen_width: f32 = @floatFromInt(rl.GetScreenWidth()); const screen_height: f32 = @floatFromInt(rl.GetScreenHeight()); if (camera.projection == .CAMERA_PERSPECTIVE) { return rl.MatrixPerspective(camera.fovy*rl.DEG2RAD, screen_width/screen_height, rl.RL_CULL_DISTANCE_NEAR, rl.RL_CULL_DISTANCE_FAR); } else if (camera.projection == .CAMERA_ORTHOGRAPHIC) { const aspect = screen_width/screen_height; const top = camera.fovy/2.0; const right = top*aspect; return rl.MatrixOrtho(-right, right, -top, top, rl.RL_CULL_DISTANCE_NEAR, rl.RL_CULL_DISTANCE_FAR); } else { unreachable; } } fn get_screen_direction_from_camera(mat_proj: *const rl.Matrix, mat_view: *const rl.Matrix, point: rl.Vector2) rl.Vector3 { const screen_width: f32 = @floatFromInt(rl.GetScreenWidth()); const screen_height: f32 = @floatFromInt(rl.GetScreenHeight()); const ndc_x = (2.0*point.x) / screen_width - 1.0; const ndc_y = 1.0 - (2.0*point.y) / screen_height; var near_point = rl.Vector3Unproject(.{ .x = ndc_x, .y = ndc_y, .z = 0.0 }, mat_proj.*, mat_view.*); var far_point = rl.Vector3Unproject(.{ .x = ndc_x, .y = ndc_y, .z = 1.0 }, mat_proj.*, mat_view.*); return rl.Vector3Subtract(far_point, near_point).normalize(); } fn get_preffered_distance_to_box(camera: *const rl.Camera3D, box: rl.BoundingBox) f32 { const screen_width: f32 = @floatFromInt(rl.GetScreenWidth()); const screen_height: f32 = @floatFromInt(rl.GetScreenHeight()); const margin = @min(screen_width, screen_height)*0.1; const box_size = box.max.sub(box.min); const max_model_scale = @min((screen_width-2*margin)/box_size.x, (screen_height-2*margin)/box_size.y); // const model_screen_width = box_size.x * max_model_scale; const model_screen_height = box_size.y * max_model_scale; const mat_proj = get_camera_projection(camera); const mat_view = rl.MatrixIdentity(); // rl.MatrixLookAt(camera.position, camera.target, camera.up); const screen_middle = rl.Vector2{ .x = screen_width/2, .y = screen_height/2 }; const box_top_middle = screen_middle.add(.{ .y = -model_screen_height/2 }); const middle_dir = get_screen_direction_from_camera(&mat_proj, &mat_view, screen_middle); const top_middle_dir = get_screen_direction_from_camera(&mat_proj, &mat_view, box_top_middle); const angle = top_middle_dir.angleBetween(middle_dir); const distance = 1/@tan(angle) * (box_size.y/2) + box_size.z/4; return distance; } pub fn main() anyerror!void { var program_memory = try std.heap.page_allocator.alloc(u8, megabytes(2)); var fba = std.heap.FixedBufferAllocator.init(program_memory); const allocator = fba.allocator(); var chip = try ChipContext.init(&allocator); defer chip.deinit(); chip.set_memory(0x200, @embedFile("ROMs/br8kout.ch8")); // const file = try std.fs.cwd().openFile("ROMs/morse_demo.ch8", .{ .mode = .read_only }); // defer file.close(); // try chip.set_memory_from_file(0x200, file); const pixel_size = 20; const initial_screen_width: i32 = @as(i32, chip.display_width) * pixel_size; const initial_screen_height: i32 = @as(i32, chip.display_height) * pixel_size; rl.SetConfigFlags(rl.ConfigFlags{ .FLAG_WINDOW_RESIZABLE = true }); rl.InitWindow(initial_screen_width, initial_screen_height, "CHIP-8"); defer rl.CloseWindow(); rl.InitAudioDevice(); defer rl.CloseAudioDevice(); rl.SetTargetFPS(60); const font_size = 24; const font_ttf_default_numchars = 95; // TTF font generation default charset: 95 glyphs (ASCII 32..126) const font = rl.LoadFontEx("src/fonts/generic-mono.otf", font_size, null, font_ttf_default_numchars); defer rl.UnloadFont(font); const sample_rate = 44100; var data = try allocator.alloc(i16, sample_rate); defer allocator.free(data); var chip_wave = rl.Wave{ .frameCount = sample_rate, .sampleRate = sample_rate, .sampleSize = 16, .channels = 1, .data = @ptrCast(data.ptr), }; gen_sin_wave(&chip_wave, 440); var chip_sound = rl.LoadSoundFromWave(chip_wave); defer rl.UnloadSound(chip_sound); rl.SetSoundVolume(chip_sound, 0.2); var raylib_chip = RaylibChip.init(&chip, &chip_sound); // var raylib_chip = RaylibChip.init(&chip, null); raylib_chip.tick_speed = 500; raylib_chip.timer_speed = 60; var edit_mode = false; var tab: Tab = .MemoryView; // var temp_mem = [1]u8{0xAA} ** (16*80 + 10); // var memory_view = MemoryView.init(&temp_mem, &font, 32); // var memory_view = MemoryView.init(chip.memory, &font, font_size, &allocator); // var selected_memory = Range{}; // var ui = UI.init(); var camera = rl.Camera3D{ .position = rl.Vector3.new(0.0, 0, 0.0), .target = rl.Vector3.new(0.0, 0.0, 0.0), .up = rl.Vector3.new(0.0, 1.0, 0.0), .fovy = 45.0, .projection = rl.CameraProjection.CAMERA_PERSPECTIVE, }; var model = rl.LoadModel("src/assets/models/emulator.obj"); var model_bbox = rl.GetModelBoundingBox(model); var model_position = rl.Vector3{ }; var model_size = model_bbox.max.sub(model_bbox.min); const shader = rl.LoadShader("src/shaders/lighting.vs", "src/shaders/lighting.fs"); shader.locs.?[@intFromEnum(rl.ShaderLocationIndex.SHADER_LOC_VECTOR_VIEW)] = rl.GetShaderLocation(shader, "viewPos"); const ambientLoc = rl.GetShaderLocation(shader, "ambient"); rl.SetShaderValue(shader, ambientLoc, &[4]f32{ 0.2, 0.2, 0.2, 1.0 }, rl.ShaderUniformDataType.SHADER_UNIFORM_VEC4); for (0..@intCast(model.materialCount)) |i| { model.materials.?[i].shader = shader; } var light = Light.create(Light.LightType.DIRECTIONAL, rl.Vector3.new(0.2, 0, -0.2), rl.Vector3.zero(), rl.WHITE, shader); std.debug.print("dimensions {}", .{model_size}); var camera_turn_vel = rl.Vector3Zero(); var camera_target_orientation: ?rl.Vector3 = null; var previous_click_time: f64 = 0.0; // rl.DisableCursor(); while (!rl.WindowShouldClose()) { var dt = rl.GetFrameTime(); raylib_chip.update(dt); if (rl.IsKeyPressed(rl.KeyboardKey.KEY_TAB)) { edit_mode = !edit_mode; } if (edit_mode) { if (rl.IsKeyPressed(rl.KeyboardKey.KEY_ONE)) { tab = .MemoryView; } } const mouse_delta = rl.GetMouseDelta(); if (rl.IsWindowResized()) { const distance = get_preffered_distance_to_box(&camera, model_bbox); const direction = camera.position.sub(model_position).normalize(); camera.position = model_position.add(direction.scale(distance)); } if (rl.Vector3Equals(camera.position, rl.Vector3Zero()) == 1) { const distance = get_preffered_distance_to_box(&camera, model_bbox); camera.target = model_position; camera.position = model_position.sub(rl.Vector3.new(0, 0, 1).scale(distance)); } var camera_turn_acc = rl.Vector3Zero(); if (rl.IsMouseButtonDown(rl.MouseButton.MOUSE_BUTTON_LEFT)) { if (@fabs(mouse_delta.x) > 5) { const rotation_speed = 2; // Radians/second camera_turn_acc.x = -rotation_speed*mouse_delta.x; } if (@fabs(mouse_delta.x) < 5) { camera_turn_vel = camera_turn_vel.scale(0.90); // Holding drag } } if (rl.IsMouseButtonPressed(rl.MouseButton.MOUSE_BUTTON_LEFT)) { camera_target_orientation = null; const now = rl.GetTime(); const duration_between_clicks = now - previous_click_time; if (duration_between_clicks < 0.3) { const ray = rl.GetMouseRay(rl.GetMousePosition(), camera); const collision = rl.GetRayCollisionBox(ray, model_bbox); if (collision.hit) { const front_face_normal = rl.Vector3.new(0, 0, -1); const back_face_normal = rl.Vector3.new(0, 0, 1); if (rl.Vector3Equals(collision.normal, front_face_normal) == 1) { camera_target_orientation = front_face_normal; } else if (rl.Vector3Equals(collision.normal, back_face_normal) == 1) { camera_target_orientation = back_face_normal; } } } previous_click_time = now; } if (camera_target_orientation) |target| { const current_direction = camera.position.sub(model_position).normalize(); const current_angle = std.math.atan2(f32, current_direction.z, current_direction.x); const target_angle = std.math.atan2(f32, target.z, target.x); const diff_angle = std.math.pi - @mod((target_angle - current_angle) + 3*std.math.pi, 2*std.math.pi); if (@fabs(diff_angle) < 0.001) { camera_turn_vel.x = 0; camera_target_orientation = null; } else { camera_turn_vel.x = diff_angle*3; } } camera_turn_vel = camera_turn_vel.scale(0.95); // Ambient drag camera_turn_vel = camera_turn_vel.add(camera_turn_acc.scale(dt)); const camera_min_vel = 0; if (camera_turn_vel.length() > camera_min_vel) { const rotation = rl.MatrixRotate(camera.up.normalize(), camera_turn_vel.x*dt); var view = rl.Vector3Subtract(camera.position, camera.target); view = rl.Vector3Transform(view, rotation); camera.position = rl.Vector3Add(camera.target, view); } // { // var matProj = rl.MatrixIdentity(); // // projection = CAMERA_PERSPECTIVE // matProj = rl.MatrixPerspective(camera.fovy*rl.DEG2RAD, (screen_width/screen_height), rl.RL_CULL_DISTANCE_NEAR, rl.RL_CULL_DISTANCE_FAR); // // var matView = rl.MatrixLookAt(camera.position, camera.target, camera.up); // // Convert world position vector to quaternion // var worldPos = rl.Vector4{ .x = position.x, .y = position.y, .z = position.z, .w = 1.0 }; // // std.debug.print("worldPos {}\n", .{worldPos}); // // Transform world position to view // worldPos = rl.QuaternionTransform(worldPos, matView); // // // Transform result to projection (clip space position) // worldPos = rl.QuaternionTransform(worldPos, matProj); // // // Calculate normalized device coordinates (inverted y) // var ndcPos = rl.Vector3.new( worldPos.x/worldPos.w, -worldPos.y/worldPos.w, worldPos.z/worldPos.w ); // // // Calculate 2d screen position vector // screen_position = rl.Vector2{ .x = (ndcPos.x + 1.0)/2.0*screen_width, .y = (ndcPos.y + 1.0)/2.0*screen_height }; // } // const target_screen_position = rl.Vector2{ .x = screen_width/2, .y = screen_height*0.1 }; // { // var matProj = get_camera_projection(&camera); // var matView = rl.MatrixLookAt(camera.position, camera.target, camera.up); // // const ndc_x = (2.0*target_screen_position.x) / screen_width - 1.0; // const ndc_y = 1.0 - (2.0*target_screen_position.y) / screen_height; // // var near_point = rl.Vector3Unproject(.{ .x = ndc_x, .y = ndc_y, .z = 0.0 }, matProj, matView); // var far_point = rl.Vector3Unproject(.{ .x = ndc_x, .y = ndc_y, .z = 1.0 }, matProj, matView); // // var direction = rl.Vector3Subtract(far_point, near_point).normalize(); // // var origin: rl.Vector3 = undefined; // if (camera.projection == .CAMERA_PERSPECTIVE) { // origin = camera.position; // } else { // origin = rl.Vector3Unproject(.{ .x = ndc_x, .y = ndc_y, .z = -1.0 }, matProj, matView); // } // // var world_pos = origin.add(direction.scale(3)); // // model_position = world_pos; // } const cameraPos = [3]f32{ camera.position.x, camera.position.y, camera.position.z }; rl.SetShaderValue(shader, shader.locs.?[@intFromEnum(rl.ShaderLocationIndex.SHADER_LOC_VECTOR_VIEW)], &cameraPos, rl.ShaderUniformDataType.SHADER_UNIFORM_VEC3); light.update_values(shader); rl.BeginDrawing(); rl.ClearBackground(.{ .r = 33, .g = 33, .b = 33, .a = 255 }); rl.BeginMode3D(camera); rl.DrawModel(model, model_position, 1.0, rl.WHITE); // rl.rlPushMatrix(); // rl.rlSetLineWidth(2); // rl.rlTranslatef(model_position.x, model_position.y, model_position.z); // rl.DrawBoundingBox(model_bbox, rl.GREEN); // rl.rlPopMatrix(); rl.EndMode3D(); rl.EndDrawing(); // if (!edit_mode) { // rl.ClearBackground(rl.DARKGRAY); // // const scale_x = @divFloor(screen_width, chip.display_width); // const scale_y = @divFloor(screen_height, chip.display_height); // const min_scale = @min(scale_x, scale_y); // // const display_width = chip.display_width * min_scale; // const display_height = chip.display_height * min_scale; // const display_x = @divFloor(screen_width - display_width, 2); // const display_y = @divFloor(screen_height - display_height, 2); // raylib_chip.render(display_x, display_y, display_width, display_height); // } else { // rl.ClearBackground(rl.RAYWHITE); // ui.update(); // // if (tab == .MemoryView) { // try memory_view.show(&ui, 0, 0, @floatFromInt(screen_width), @floatFromInt(screen_height), &selected_memory); // } // } } }