524 lines
18 KiB
Zig
524 lines
18 KiB
Zig
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;
|
|
}
|