364 lines
14 KiB
Zig
364 lines
14 KiB
Zig
const Self = @This();
|
|
const rl = @import("raylib");
|
|
const std = @import("std");
|
|
const Gltf = @import("zgltf");
|
|
const GlobalContext = @import("./global-context.zig");
|
|
const EmulatorModel = @import("./emulator-model.zig");
|
|
|
|
const ChipContext = @import("chip.zig");
|
|
const RaylibChip = @import("raylib-chip.zig");
|
|
|
|
const assert = std.debug.assert;
|
|
const Allocator = std.mem.Allocator;
|
|
const StringList = std.ArrayList([]const u8);
|
|
|
|
ctx: *GlobalContext,
|
|
allocator: Allocator,
|
|
|
|
emulator: EmulatorModel,
|
|
|
|
camera_turn_vel: rl.Vector3 = rl.Vector3{ .x = 0, .y = 0, .z = 0 },
|
|
camera_target_orientation: ?rl.Vector3 = null,
|
|
previous_click_time: f64 = 0.0,
|
|
|
|
shader: rl.Shader,
|
|
lights: [2]Light,
|
|
|
|
chip: *ChipContext,
|
|
raylib_chip: *RaylibChip,
|
|
chip_sound: rl.Sound,
|
|
|
|
pub fn genSinWave(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);
|
|
}
|
|
}
|
|
|
|
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 getLightShaderLocation(shader: rl.Shader, idx: usize, comptime name: []const u8) i32 {
|
|
var buf: [128]u8 = undefined;
|
|
var fba = std.heap.FixedBufferAllocator.init(&buf);
|
|
const prop_name = std.fmt.allocPrintZ(fba.allocator(), "lights[{d}]." ++ name, .{idx}) catch unreachable;
|
|
return rl.GetShaderLocation(shader, prop_name);
|
|
}
|
|
|
|
pub fn init(idx: usize, 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 = Light.getLightShaderLocation(shader, idx, "enabled"),
|
|
.typeLoc = Light.getLightShaderLocation(shader, idx, "type"),
|
|
.positionLoc = Light.getLightShaderLocation(shader, idx, "position"),
|
|
.targetLoc = Light.getLightShaderLocation(shader, idx, "target"),
|
|
.colorLoc = Light.getLightShaderLocation(shader, idx, "color"),
|
|
};
|
|
light.update_values(shader);
|
|
return light;
|
|
}
|
|
|
|
pub fn update_values(self: *Light, shader: rl.Shader) void {
|
|
const enabled: i32 = @intFromBool(self.enabled);
|
|
rl.SetShaderValue(shader, self.enabledLoc, &enabled, rl.ShaderUniformDataType.SHADER_UNIFORM_INT);
|
|
|
|
const lightType: i32 = @intFromEnum(self.type);
|
|
rl.SetShaderValue(shader, self.typeLoc, &lightType, rl.ShaderUniformDataType.SHADER_UNIFORM_INT);
|
|
|
|
const position = [3]f32{ self.position.x, self.position.y, self.position.z };
|
|
rl.SetShaderValue(shader, self.positionLoc, &position, rl.ShaderUniformDataType.SHADER_UNIFORM_VEC3);
|
|
|
|
const target = [3]f32{ self.target.x, self.target.y, self.target.z };
|
|
rl.SetShaderValue(shader, self.targetLoc, &target, rl.ShaderUniformDataType.SHADER_UNIFORM_VEC3);
|
|
|
|
const color = [4]f32{
|
|
@as(f32, @floatFromInt(self.color.r)) / 255.0,
|
|
@as(f32, @floatFromInt(self.color.g)) / 255.0,
|
|
@as(f32, @floatFromInt(self.color.b)) / 255.0,
|
|
@as(f32, @floatFromInt(self.color.a)) / 255.0,
|
|
};
|
|
rl.SetShaderValue(shader, self.colorLoc, &color, rl.ShaderUniformDataType.SHADER_UNIFORM_VEC4);
|
|
}
|
|
};
|
|
|
|
fn getCameraProjection(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 getScreenDirectionFromCamera(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 getPrefferedDistanceToBox(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 = getCameraProjection(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 = getScreenDirectionFromCamera(&mat_proj, &mat_view, screen_middle);
|
|
const top_middle_dir = getScreenDirectionFromCamera(&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 init(allocator: Allocator, ctx: *GlobalContext) !Self {
|
|
const shader = rl.LoadShader("src/shaders/main.vs", "src/shaders/main.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.6, 0.6, 1, 1.0 }, .SHADER_UNIFORM_VEC4);
|
|
|
|
var light1 = Light.init(0, .DIRECTIONAL, rl.Vector3.new(0.2, 0, -0.2), rl.Vector3.zero(), rl.WHITE, shader);
|
|
var light2 = Light.init(1, .DIRECTIONAL, rl.Vector3.new(0.2, 0, 0.2), rl.Vector3.zero(), rl.WHITE, shader);
|
|
|
|
var chip = try allocator.create(ChipContext);
|
|
chip.* = try ChipContext.init(allocator);
|
|
|
|
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),
|
|
};
|
|
|
|
genSinWave(&chip_wave, 440);
|
|
var chip_sound = rl.LoadSoundFromWave(chip_wave);
|
|
rl.SetSoundVolume(chip_sound, 0.2);
|
|
|
|
var raylib_chip = try allocator.create(RaylibChip);
|
|
raylib_chip.* = RaylibChip.init(chip, chip_sound);
|
|
|
|
var emulator = try EmulatorModel.init(allocator, raylib_chip);
|
|
emulator.setShader(shader);
|
|
|
|
return Self {
|
|
.allocator = allocator,
|
|
.ctx = ctx,
|
|
.emulator = emulator,
|
|
.shader = shader,
|
|
.lights = .{light1, light2},
|
|
|
|
.chip = chip,
|
|
.raylib_chip = raylib_chip,
|
|
.chip_sound = chip_sound,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Self) void {
|
|
self.emulator.deinit();
|
|
rl.UnloadSound(self.chip_sound);
|
|
self.chip.deinit();
|
|
self.allocator.destroy(self.raylib_chip);
|
|
self.allocator.destroy(self.chip);
|
|
}
|
|
|
|
fn updateCamera(self: *Self, dt: f32) void {
|
|
const mouse_delta = rl.GetMouseDelta();
|
|
const camera = &self.ctx.camera;
|
|
const emulator = &self.emulator;
|
|
|
|
if (rl.IsWindowResized()) {
|
|
const distance = getPrefferedDistanceToBox(camera, emulator.bbox);
|
|
const direction = camera.position.sub(emulator.position).normalize();
|
|
camera.position = emulator.position.add(direction.scale(distance));
|
|
}
|
|
|
|
if (rl.Vector3Equals(camera.position, rl.Vector3Zero()) == 1) {
|
|
const distance = getPrefferedDistanceToBox(camera, self.emulator.bbox);
|
|
camera.target = emulator.position;
|
|
camera.position = emulator.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) {
|
|
self.camera_turn_vel = self.camera_turn_vel.scale(0.90); // Holding drag
|
|
}
|
|
}
|
|
|
|
if (rl.IsMouseButtonPressed(rl.MouseButton.MOUSE_BUTTON_LEFT)) {
|
|
self.camera_target_orientation = null;
|
|
|
|
const now = rl.GetTime();
|
|
const duration_between_clicks = now - self.previous_click_time;
|
|
if (duration_between_clicks < 0.3) {
|
|
const ray = rl.GetMouseRay(rl.GetMousePosition(), camera.*);
|
|
const collision = rl.GetRayCollisionBox(ray, self.emulator.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) {
|
|
self.camera_target_orientation = front_face_normal;
|
|
} else if (rl.Vector3Equals(collision.normal, back_face_normal) == 1) {
|
|
self.camera_target_orientation = back_face_normal;
|
|
}
|
|
}
|
|
}
|
|
self.previous_click_time = now;
|
|
}
|
|
|
|
if (self.camera_target_orientation) |target| {
|
|
const current_direction = camera.position.sub(emulator.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) {
|
|
self.camera_turn_vel.x = 0;
|
|
self.camera_target_orientation = null;
|
|
} else {
|
|
self.camera_turn_vel.x = diff_angle*3;
|
|
}
|
|
}
|
|
|
|
self.camera_turn_vel = self.camera_turn_vel.scale(0.95); // Ambient drag
|
|
self.camera_turn_vel = self.camera_turn_vel.add(camera_turn_acc.scale(dt));
|
|
|
|
const camera_min_vel = 0;
|
|
if (self.camera_turn_vel.length() > camera_min_vel) {
|
|
const rotation = rl.MatrixRotate(camera.up.normalize(), self.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);
|
|
}
|
|
}
|
|
|
|
pub fn update(self: *Self, dt: f32) void {
|
|
self.updateCamera(dt);
|
|
|
|
const camera = &self.ctx.camera;
|
|
const cameraPos = [3]f32{ camera.position.x, camera.position.y, camera.position.z };
|
|
rl.SetShaderValue(self.shader, self.shader.locs.?[@intFromEnum(rl.ShaderLocationIndex.SHADER_LOC_VECTOR_VIEW)], &cameraPos, rl.ShaderUniformDataType.SHADER_UNIFORM_VEC3);
|
|
for (&self.lights) |*light| {
|
|
light.update_values(self.shader);
|
|
}
|
|
|
|
self.emulator.updateDisplay();
|
|
|
|
// {
|
|
// 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;
|
|
// }
|
|
}
|
|
|
|
pub fn draw(self: *Self) void {
|
|
rl.BeginShaderMode(self.shader);
|
|
{
|
|
self.emulator.draw();
|
|
}
|
|
rl.EndShaderMode();
|
|
}
|