diff --git a/src/assets/models/emulator.blend b/src/assets/models/emulator.blend index 86038ef..cbb6d49 100644 Binary files a/src/assets/models/emulator.blend and b/src/assets/models/emulator.blend differ diff --git a/src/chip.zig b/src/chip.zig index a40466f..4bd7dce 100644 --- a/src/chip.zig +++ b/src/chip.zig @@ -70,6 +70,20 @@ pub fn init(allocator: Allocator) !Self { return self; } +pub fn reset(self: *Self) void { + @memset(self.memory, 0); + @memset(&self.input, false); + @memset(&self.stack, 0); + @memset(&self.V, 0); + @memset(self.display, false); + + self.I = 0; + self.PC = 0x200; + self.SP = 0; + self.DT = 0; + self.ST = 0; +} + pub fn deinit(self: *Self) void { self.allocator.free(self.display); self.allocator.free(self.memory); diff --git a/src/emulator-model.zig b/src/emulator-model.zig index b6a0f17..1fa09d0 100644 --- a/src/emulator-model.zig +++ b/src/emulator-model.zig @@ -18,9 +18,9 @@ power_switch: *rl.Model, bbox: rl.BoundingBox, position: rl.Vector3, -screen_texture: rl.RenderTexture2D, -rl_chip: *const RaylibChip, +button_state: *[16]bool, +powered: *bool, fn getExtensionByMimeType(mime_type: []const u8) ?[:0]const u8 { if (std.mem.eql(u8, mime_type, "image/png")) { @@ -243,7 +243,7 @@ fn loadGltfMesh(materials: []rl.Material, gltf: Gltf, node: Gltf.Node) !rl.Model return model; } -pub fn init(allocator: Allocator, rl_chip: *const RaylibChip) !Self { +pub fn init(allocator: Allocator, powered: *bool, button_state: *[16]bool, screen_texture: rl.RenderTexture2D) !Self { var gltf = Gltf.init(allocator); defer gltf.deinit(); try gltf.parse(@embedFile("assets/models/emulator.glb")); @@ -278,6 +278,7 @@ pub fn init(allocator: Allocator, rl_chip: *const RaylibChip) !Self { var static_models = std.ArrayList(*rl.Model).init(allocator); errdefer static_models.deinit(); + var buttons: [16]*rl.Model = undefined; var power_switch: *rl.Model = undefined; @@ -286,7 +287,7 @@ pub fn init(allocator: Allocator, rl_chip: *const RaylibChip) !Self { if (node.mesh == null) continue; models.appendAssumeCapacity(try loadGltfMesh(materials, gltf, node)); - const model = &models.items[models.items.len-1]; + var model: *rl.Model = &models.items[models.items.len-1]; if (std.mem.eql(u8, node.name, "Power switch")) { power_switch = model; @@ -297,20 +298,11 @@ pub fn init(allocator: Allocator, rl_chip: *const RaylibChip) !Self { } else { try static_models.append(model); } - } - const screen_texture = rl.LoadRenderTexture(rl_chip.chip.display_width, rl_chip.chip.display_height); - errdefer rl.UnloadRenderTexture(screen_texture); - - { // Link screen render target to shader - var screen_mtl_idx: ?usize = null; - for (1.., gltf.data.materials.items) |idx, mtl| { - if (std.mem.eql(u8, mtl.name, "Screen")) { - screen_mtl_idx = idx; - break; - } + if (std.mem.eql(u8, node.name, "Screen")) { + const screen_material = &model.materials.?[0]; + rl.SetMaterialTexture(@ptrCast(screen_material), rl.MATERIAL_MAP_DIFFUSE, screen_texture.texture); } - rl.SetMaterialTexture(@ptrCast(&materials[screen_mtl_idx.?]), rl.MATERIAL_MAP_DIFFUSE, screen_texture.texture); } return Self{ @@ -322,11 +314,11 @@ pub fn init(allocator: Allocator, rl_chip: *const RaylibChip) !Self { .models = models, .buttons = buttons, .power_switch = power_switch, + .powered = powered, + .button_state = button_state, .bbox = rl.GetModelBoundingBox(static_models.items[0].*), - .screen_texture = screen_texture, .position = rl.Vector3{ .x = 0, .y = 0, .z = 0 }, - .rl_chip = rl_chip, }; } @@ -350,35 +342,140 @@ pub fn deinit(self: *Self) void { self.allocator.free(self.materials); self.static_models.deinit(); self.models.deinit(); - rl.UnloadRenderTexture(self.screen_texture); } -pub fn updateDisplay(self: *Self) void { - rl.BeginTextureMode(self.screen_texture); - self.rl_chip.render(); - rl.EndTextureMode(); +fn getRayCollisionModel(ray: rl.Ray, model: rl.Model, position: rl.Vector3) rl.RayCollision { + var closest_hit_info = std.mem.zeroes(rl.RayCollision); + const transform = rl.MatrixMultiply(model.transform, rl.MatrixTranslate(position.x, position.y, position.z)); + + for (0..@intCast(model.meshCount)) |i| { + if (model.meshes == null) break; + const mesh = model.meshes.?[i]; + + const hit_info = rl.GetRayCollisionMesh(ray, mesh, transform); + if (!hit_info.hit) continue; + + if ((!closest_hit_info.hit) or (closest_hit_info.distance > hit_info.distance)) { + closest_hit_info = hit_info; + } + } + + return closest_hit_info; } -pub fn isMouseOverPowerSwitch(self: *const Self) bool { - _ = self; +const ModelCollision = struct { collision: rl.RayCollision, model: *rl.Model }; +fn getRayCollisionModels(ray: rl.Ray, models: []rl.Model, position: rl.Vector3) ?ModelCollision { + var closest_hit_info: ?rl.RayCollision = null; + var closest_model: ?*rl.Model = null; + + for (models) |*model| { + const hit_info = getRayCollisionModel(ray, model.*, position); + if (!hit_info.hit) continue; + + if ((closest_hit_info == null) or (closest_hit_info.?.distance > hit_info.distance)) { + closest_hit_info = hit_info; + closest_model = model; + } + } + + if (closest_hit_info == null) { + return null; + } + + return ModelCollision{ + .collision = closest_hit_info.?, + .model = closest_model.? + }; +} + +pub fn isOverPowerSwitch(self: *const Self, ray: rl.Ray) bool { + const collision = getRayCollisionModels(ray, self.models.items, self.position); + if (collision) |c| { + return c.model == self.power_switch; + } + return false; } -pub fn setPowerSwitch(self: *const Self, enabled: bool) void { - _ = enabled; - _ = self; - return false; +fn matrixGetTranslation(mat: rl.Matrix) rl.Vector3 { + return rl.Vector3{ + .x = mat.m12, + .y = mat.m13, + .z = mat.m14, + }; } -pub fn getPowerSwitch(self: *const Self) bool { - _ = self; - return false; +fn matrixGetScale(mat: rl.Matrix) rl.Vector3 { + return rl.Vector3{ + .x = rl.Vector3Length(rl.Vector3{ .x = mat.m0, .y = mat.m1, .z = mat.m2 }), + .y = rl.Vector3Length(rl.Vector3{ .x = mat.m4, .y = mat.m5, .z = mat.m6 }), + .z = rl.Vector3Length(rl.Vector3{ .x = mat.m8, .y = mat.m9, .z = mat.m10 }), + }; +} + +// https://stackoverflow.com/a/64336115 +fn matrixGetRotation(mat: rl.Matrix) rl.Vector3 { + const scale = matrixGetScale(mat); + const R11 = mat.m0 / scale.x; + const R12 = mat.m1 / scale.x; + const R13 = mat.m2 / scale.x; + + const R21 = mat.m4 / scale.y; + // const R22 = mat.m5 / scale.y; + // const R23 = mat.m6 / scale.y; + + const R31 = mat.m8 / scale.z; + const R32 = mat.m9 / scale.z; + const R33 = mat.m10 / scale.z; + + const asin = std.math.asin; + const cos = std.math.cos; + const atan2 = std.math.atan2; + const pi = rl.PI; + + var roll: f32 = undefined; + var pitch: f32 = undefined; + var yaw: f32 = undefined; + if (R31 != 1 and R31 != -1) { + const pitch_1 = -1 * asin(R31); + // const pitch_2 = pi - pitch_1; + const roll_1 = atan2(f32, R32 / cos(pitch_1), R33 / cos(pitch_1)); + // const roll_2 = atan2( R32 / cos(pitch_2) , R33 / cos(pitch_2)); + const yaw_1 = atan2(f32, R21 / cos(pitch_1), R11 / cos(pitch_1)); + // const yaw_2 = atan2( R21 / cos(pitch_2) , R11 / cos(pitch_2)); + + // IMPORTANT NOTE here, there is more than one solution but we choose the first for this case for simplicity ! + // You can insert your own domain logic here on how to handle both solutions appropriately (see the reference publication link for more info). + pitch = pitch_1; + roll = roll_1; + yaw = yaw_1; + } else { + yaw = 0; // anything (we default this to zero) + if (R31 == -1) { + pitch = pi / 2; + roll = yaw + atan2(f32, R12, R13); + } else { + pitch = -pi / 2; + roll = -1*yaw + atan2(f32, -1*R12, -1*R13); + } + } + + // convert from radians to degrees + roll = roll * rl.RAD2DEG; + pitch = pitch * rl.RAD2DEG; + yaw = yaw * rl.RAD2DEG; + + return rl.Vector3{ + .x = roll, + .y = pitch, + .z = yaw, + }; } pub fn draw(self: *Self) void { for (self.buttons, 0..) |button, i| { var position = self.position; - if (self.rl_chip.chip.is_input_pressed(@intCast(i))) { + if (self.button_state[i]) { position.z += 0.035; } rl.DrawModel(button.*, position, 1.0, rl.WHITE); @@ -388,5 +485,18 @@ pub fn draw(self: *Self) void { rl.DrawModel(model.*, self.position, 1.0, rl.WHITE); } - rl.DrawModel(self.power_switch.*, self.position, 1.0, rl.WHITE); + { // Power switch + const on_angle: f32 = 45; + const off_angle: f32 = -45; + const target_angle = if (self.powered.*) on_angle else off_angle; + + var transform = self.power_switch.transform; + const rotation = matrixGetRotation(transform); + const dt = rl.GetFrameTime(); + const delta_angle = rotation.z - target_angle; + transform = rl.MatrixMultiply(rl.MatrixRotateZ((delta_angle * dt * 0.2)), transform); + + self.power_switch.transform = transform; + rl.DrawModel(self.power_switch.*, self.position, 1.0, rl.WHITE); + } } diff --git a/src/main-scene.zig b/src/main-scene.zig index 543eacf..efee315 100644 --- a/src/main-scene.zig +++ b/src/main-scene.zig @@ -4,6 +4,7 @@ const std = @import("std"); const Gltf = @import("zgltf"); const GlobalContext = @import("./global-context.zig"); const EmulatorModel = @import("./emulator-model.zig"); +const ROM = @import("./roms.zig").ROM; const ChipContext = @import("chip.zig"); const RaylibChip = @import("raylib-chip.zig"); @@ -24,9 +25,12 @@ previous_click_time: f64 = 0.0, shader: rl.Shader, lights: [2]Light, +powered: *bool, chip: *ChipContext, raylib_chip: *RaylibChip, chip_sound: rl.Sound, +screen_texture: rl.RenderTexture2D, +rom: ?ROM = null, pub fn genSinWave(wave: *rl.Wave, frequency: f32) void { assert(wave.sampleSize == 16); // Only 16 bits are supported @@ -195,7 +199,14 @@ pub fn init(allocator: Allocator, ctx: *GlobalContext) !Self { var raylib_chip = try allocator.create(RaylibChip); raylib_chip.* = RaylibChip.init(chip, chip_sound); - var emulator = try EmulatorModel.init(allocator, raylib_chip); + const screen_texture = rl.LoadRenderTexture(raylib_chip.chip.display_width, raylib_chip.chip.display_height); + errdefer rl.UnloadRenderTexture(screen_texture); + + const powered = try allocator.create(bool); + errdefer allocator.destroy(powered); + + var emulator = try EmulatorModel.init(allocator, powered, &chip.input, screen_texture); + errdefer emulator.deinit(); emulator.setShader(shader); return Self { @@ -208,6 +219,8 @@ pub fn init(allocator: Allocator, ctx: *GlobalContext) !Self { .chip = chip, .raylib_chip = raylib_chip, .chip_sound = chip_sound, + .screen_texture = screen_texture, + .powered = powered }; } @@ -215,11 +228,40 @@ pub fn deinit(self: *Self) void { self.emulator.deinit(); rl.UnloadShader(self.shader); rl.UnloadSound(self.chip_sound); + rl.UnloadRenderTexture(self.screen_texture); + self.chip.deinit(); + self.allocator.destroy(self.powered); self.allocator.destroy(self.raylib_chip); self.allocator.destroy(self.chip); } +pub fn set_rom(self: *Self, rom: ROM) void { + self.rom = rom; + self.chip.reset(); + self.chip.set_memory(0x200, rom.data); +} + +pub fn turn_on(self: *Self) void { + self.powered.* = true; +} + +pub fn turn_off(self: *Self) void { + self.powered.* = false; + self.chip.reset(); + if (self.rom) |rom| { + self.chip.set_memory(0x200, rom.data); + } +} + +pub fn toggle_power(self: *Self) void { + if (self.powered.*) { + self.turn_off(); + } else { + self.turn_on(); + } +} + fn updateCamera(self: *Self, dt: f32) void { const mouse_delta = rl.GetMouseDelta(); const camera = &self.ctx.camera; @@ -304,7 +346,23 @@ pub fn update(self: *Self, dt: f32) void { light.update_values(self.shader); } - self.emulator.updateDisplay(); + const ray = rl.GetMouseRay(rl.GetMousePosition(), self.ctx.camera); + if (self.emulator.isOverPowerSwitch(ray)) { + if (rl.IsMouseButtonPressed(rl.MouseButton.MOUSE_BUTTON_LEFT)) { + self.toggle_power(); + } + } + + if (self.powered.*) { + self.raylib_chip.update_input(); + self.raylib_chip.update(dt); + } + + rl.BeginTextureMode(self.screen_texture); + { + self.raylib_chip.render(); + } + rl.EndTextureMode(); // { // var matProj = rl.MatrixIdentity(); diff --git a/src/main.zig b/src/main.zig index a644aff..65ad5b6 100755 --- a/src/main.zig +++ b/src/main.zig @@ -7,17 +7,12 @@ const ChipContext = @import("chip.zig"); const RaylibChip = @import("raylib-chip.zig"); const GlobalContext = @import("./global-context.zig"); const MainScene = @import("./main-scene.zig"); -const options = @import("options"); +const listROMs = @import("./roms.zig").listROMs; fn megabytes(amount: usize) usize { return amount * 1024 * 1024; } -const ROM = struct { - name: []const u8, - data: []const u8, -}; - pub fn main() anyerror!void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); @@ -38,19 +33,8 @@ pub fn main() anyerror!void { var main_scene = try MainScene.init(allocator, &ctx); defer main_scene.deinit(); - comptime var roms = [1]ROM{ undefined } ** options.roms.len; - comptime { - var i = 0; - for (options.roms) |file| { - roms[i] = ROM{ - .name = file, - .data = @embedFile(file) - }; - i += 1; - } - } - - main_scene.chip.set_memory(0x200, roms[2].data); + const roms = comptime listROMs(); + main_scene.set_rom(roms[3]); const font_size = 24; const font_ttf_default_numchars = 95; // TTF font generation default charset: 95 glyphs (ASCII 32..126) @@ -59,9 +43,6 @@ pub fn main() anyerror!void { while (!rl.WindowShouldClose()) { var dt = rl.GetFrameTime(); - main_scene.raylib_chip.update_input(); - main_scene.raylib_chip.update(dt); - main_scene.update(dt); rl.BeginDrawing(); diff --git a/src/roms.zig b/src/roms.zig new file mode 100644 index 0000000..f4441a1 --- /dev/null +++ b/src/roms.zig @@ -0,0 +1,20 @@ +const options = @import("options"); +const rom_count = options.roms.len; + +pub const ROM = struct { + name: []const u8, + data: []const u8, +}; + +pub fn listROMs() [options.roms.len]ROM { + var roms: [options.roms.len]ROM = undefined; + + for (0.., options.roms) |i, file| { + roms[i] = ROM{ + .name = file, + .data = @embedFile(file) + }; + } + + return roms; +}