const Self = @This(); const Gltf = @import("zgltf"); const rl = @import("raylib"); const std = @import("std"); const RaylibChip = @import("raylib-chip.zig"); const Light = @import("./light.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; allocator: Allocator, materials: []rl.Material, models: std.ArrayList(rl.Model), static_models: std.ArrayList(*rl.Model), buttons: [16]*rl.Model, power_switch: *rl.Model, power_light_position: rl.Vector3, bbox: rl.BoundingBox, position: rl.Vector3, button_state: *[16]bool, powered: *bool, fn getExtensionByMimeType(mime_type: []const u8) ?[:0]const u8 { if (std.mem.eql(u8, mime_type, "image/png")) { return ".png\x00"; } else if (std.mem.eql(u8, mime_type, "image/jpg")) { return ".jpg\x00"; } else { return null; } } fn loadGltfImage(image: Gltf.Image) !rl.Image { assert(image.uri == null); assert(image.data != null); assert(image.mime_type != null); const image_data = image.data.?; const mime_type = image.mime_type.?; const file_type = getExtensionByMimeType(mime_type); assert(file_type != null); const rl_image = rl.LoadImageFromMemory(file_type.?, @ptrCast(image_data.ptr), @intCast(image_data.len)); if (@as(?*anyopaque, @ptrCast(rl_image.data)) == null) { return error.Failed; } return rl_image; } fn loadGltfMaterial(data: Gltf.Data, material: Gltf.Material) !rl.Material { var rl_material = rl.LoadMaterialDefault(); errdefer rl.UnloadMaterial(rl_material); var albedo_map: *rl.MaterialMap = &rl_material.maps.?[@intFromEnum(rl.MaterialMapIndex.MATERIAL_MAP_ALBEDO)]; if (material.metallic_roughness.base_color_texture) |base_color_texture| { const texture = data.textures.items[base_color_texture.index]; const image = data.images.items[texture.source.?]; const rl_image = try loadGltfImage(image); defer rl.UnloadImage(rl_image); albedo_map.texture = rl.LoadTextureFromImage(rl_image); } albedo_map.color.r = @intFromFloat(material.metallic_roughness.base_color_factor[0]*255); albedo_map.color.g = @intFromFloat(material.metallic_roughness.base_color_factor[1]*255); albedo_map.color.b = @intFromFloat(material.metallic_roughness.base_color_factor[2]*255); albedo_map.color.a = @intFromFloat(material.metallic_roughness.base_color_factor[3]*255); return rl_material; } fn loadGltfPrimitive(gltf: Gltf, primitive: Gltf.Primitive) !rl.Mesh { var allocator = std.heap.c_allocator; assert(primitive.mode == .triangles); var rl_mesh = std.mem.zeroes(rl.Mesh); errdefer rl.UnloadMesh(rl_mesh); var bin = gltf.glb_binary.?; var f32_buffer = std.ArrayList(f32).init(allocator); defer f32_buffer.deinit(); for (primitive.attributes.items) |attribute| { switch (attribute) { .position => |accessor_index| { const accessor = gltf.data.accessors.items[accessor_index]; assert(accessor.component_type == .float); assert(accessor.type == .vec3); f32_buffer.clearAndFree(); gltf.getDataFromBufferView(f32, &f32_buffer, accessor, bin); var vertices = try allocator.dupe(f32, f32_buffer.items); rl_mesh.vertexCount = @intCast(accessor.count); rl_mesh.vertices = @ptrCast(vertices); }, .normal => |accessor_index| { const accessor = gltf.data.accessors.items[accessor_index]; assert(accessor.component_type == .float); assert(accessor.type == .vec3); f32_buffer.clearRetainingCapacity(); gltf.getDataFromBufferView(f32, &f32_buffer, accessor, bin); var normals = try allocator.dupe(f32, f32_buffer.items); rl_mesh.normals = @ptrCast(normals); }, .tangent => |accessor_index| { const accessor = gltf.data.accessors.items[accessor_index]; assert(accessor.component_type == .float); assert(accessor.type == .vec4); f32_buffer.clearRetainingCapacity(); gltf.getDataFromBufferView(f32, &f32_buffer, accessor, bin); var tangents = try allocator.dupe(f32, f32_buffer.items); rl_mesh.tangents = @ptrCast(tangents); }, .texcoord => |accessor_index| { const accessor = gltf.data.accessors.items[accessor_index]; assert(accessor.component_type == .float); assert(accessor.type == .vec2); f32_buffer.clearRetainingCapacity(); gltf.getDataFromBufferView(f32, &f32_buffer, accessor, bin); var texcoords = try allocator.dupe(f32, f32_buffer.items); rl_mesh.texcoords = @ptrCast(texcoords); }, else => {} } } if (primitive.indices) |accessor_index| { const accessor = gltf.data.accessors.items[accessor_index]; rl_mesh.triangleCount = @divExact(accessor.count, 3); const accessor_count: usize = @intCast(accessor.count); var indices = try allocator.alloc(u16, accessor_count); rl_mesh.indices = @ptrCast(indices); if (accessor.component_type == Gltf.ComponentType.unsigned_short) { var u16_buffer = std.ArrayList(u16).init(allocator); defer u16_buffer.deinit(); gltf.getDataFromBufferView(u16, &u16_buffer, accessor, bin); @memcpy(indices, u16_buffer.items); } else if (accessor.component_type == Gltf.ComponentType.unsigned_integer) { var u32_buffer = std.ArrayList(u32).init(allocator); defer u32_buffer.deinit(); gltf.getDataFromBufferView(u32, &u32_buffer, accessor, bin); for (0..accessor_count) |i| { indices[i] = @truncate(u32_buffer.items[i]); } rl.TraceLog(@intFromEnum(rl.TraceLogLevel.LOG_WARNING), "MODEL: Indices data converted from u32 to u16, possible loss of data"); } else { @panic("Unknown GLTF primitives indices component type. Use u16 or u32"); } } else { rl_mesh.triangleCount = @divExact(rl_mesh.vertexCount, 3); } rl.UploadMesh(@ptrCast(&rl_mesh), false); return rl_mesh; } fn loadGltfMesh(materials: []rl.Material, gltf: Gltf, node: Gltf.Node) !rl.Model { const allocator = std.heap.c_allocator; const transform = Gltf.getGlobalTransform(&gltf.data, node); var model = std.mem.zeroes(rl.Model); errdefer rl.UnloadModel(model); model.transform = rl.Matrix{ .m0 = transform[0][0], .m4 = transform[1][0], .m8 = transform[2][0], .m12 = transform[3][0], .m1 = transform[0][1], .m5 = transform[1][1], .m9 = transform[2][1], .m13 = transform[3][1], .m2 = transform[0][2], .m6 = transform[1][2], .m10 = transform[2][2], .m14 = transform[3][2], .m3 = transform[0][3], .m7 = transform[1][3], .m11 = transform[2][3], .m15 = transform[3][3] }; if (node.mesh) |mesh_idx| { const mesh = gltf.data.meshes.items[mesh_idx]; const primitives: []Gltf.Primitive = mesh.primitives.items; var meshes = try allocator.alloc(rl.Mesh, primitives.len); model.meshCount = @intCast(primitives.len); model.meshes = @ptrCast(meshes.ptr); var mesh_material = try allocator.alloc(i32, primitives.len); model.meshMaterial = @ptrCast(mesh_material.ptr); @memset(mesh_material, 0); var used_material_ids = try allocator.alloc(usize, materials.len); var used_materials_count: usize = 0; defer allocator.free(used_material_ids); for (0.., primitives) |j, primitive| { meshes[j] = try loadGltfPrimitive(gltf, primitive); var mtl: usize = 0; if (primitive.material) |material| { mtl = material + 1; } var mtl_index = std.mem.indexOfScalar(usize, used_material_ids[0..used_materials_count], mtl); if (mtl_index == null) { mtl_index = used_materials_count; used_material_ids[used_materials_count] = mtl; used_materials_count += 1; } mesh_material[j] = @intCast(mtl_index.?); } var used_materials = try allocator.alloc(rl.Material, used_materials_count+1); model.materials = @ptrCast(used_materials); model.materialCount = 0; for (0..used_materials_count) |i| { used_materials[i] = materials[used_material_ids[i]]; const max_material_maps = 12; const maps = try allocator.dupe(rl.MaterialMap, used_materials[i].maps.?[0..max_material_maps]); used_materials[i].maps = @ptrCast(maps); model.materialCount += 1; } } return model; } 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")); const scene = gltf.data.scenes.items[gltf.data.scene.?]; const scene_nodes: std.ArrayList(Gltf.Index) = scene.nodes.?; const material_count: usize = @intCast(gltf.data.materials.items.len); var materials = try allocator.alloc(rl.Material, material_count+1); @memset(materials, std.mem.zeroes(rl.Material)); errdefer allocator.free(materials); materials[0] = rl.LoadMaterialDefault(); errdefer { for (materials) |mtl| { rl.UnloadMaterial(mtl); } } for (0..material_count, gltf.data.materials.items) |i, material| { materials[i+1] = try loadGltfMaterial(gltf.data, material); } var models = try std.ArrayList(rl.Model).initCapacity(allocator, scene_nodes.items.len); errdefer models.deinit(); errdefer { for (models.items) |model| { rl.UnloadModel(model); } } 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; var power_light: ?*rl.Model = null; for (scene_nodes.items) |node_index| { const node = gltf.data.nodes.items[node_index]; if (node.mesh == null) continue; models.appendAssumeCapacity(try loadGltfMesh(materials, gltf, node)); var model: *rl.Model = &models.items[models.items.len-1]; const name = node.name; if (std.mem.eql(u8, name, "Power switch")) { power_switch = model; } else if (std.mem.startsWith(u8, name, "Buttons ")) { var space = std.mem.indexOfScalar(u8, name, ' ').?; const button_idx = try std.fmt.parseInt(usize, name[space+1..], 16); buttons[button_idx] = model; } else { try static_models.append(model); } if (std.mem.eql(u8, name, "Screen")) { const screen_material = &model.materials.?[0]; rl.SetMaterialTexture(@ptrCast(screen_material), rl.MATERIAL_MAP_DIFFUSE, screen_texture.texture); } else if (std.mem.eql(u8, name, "Power indicator")) { power_light = model; } } var power_light_position = rl.Vector3.zero(); if (power_light) |model| { power_light_position = matrixGetTranslation(model.transform); } return Self{ .allocator = allocator, .materials = materials, .static_models = static_models, .models = models, .buttons = buttons, .power_switch = power_switch, .powered = powered, .button_state = button_state, .power_light_position = power_light_position, .bbox = rl.GetModelBoundingBox(static_models.items[0].*), .position = rl.Vector3{ .x = 0, .y = 0, .z = 0 }, }; } pub fn setShader(self: *Self, shader: rl.Shader) void { for (self.models.items) |*model| { if (model.materials == null) continue; for (0..@intCast(model.materialCount)) |i| { model.materials.?[i].shader = shader; } } } pub fn deinit(self: *Self) void { for (self.models.items) |model| { rl.UnloadModel(model); } for (self.materials) |mtl| { rl.UnloadMaterial(mtl); } self.allocator.free(self.materials); self.static_models.deinit(); self.models.deinit(); } 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; } 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; } fn matrixGetTranslation(mat: rl.Matrix) rl.Vector3 { return rl.Vector3{ .x = mat.m12, .y = mat.m13, .z = mat.m14, }; } 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.button_state[i]) { position.z += 0.035; } rl.DrawModel(button.*, position, 1.0, rl.WHITE); } for (self.static_models.items) |model| { rl.DrawModel(model.*, 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); } } pub fn get_power_light_position(self: *Self) rl.Vector3 { return self.power_light_position.add(self.position); } pub fn get_power_light_color(self: *Self) rl.Color { _ = self; return rl.GREEN; }