Add obj model loading and suzanne test model

This commit is contained in:
Riyyi
2026-10-26 15:14:04 +01:00
parent 30de42caca
commit 426504c748
4 changed files with 2205 additions and 49 deletions
+2027
View File
File diff suppressed because it is too large Load Diff
+148
View File
@@ -0,0 +1,148 @@
package core
import "core:fmt"
import "core:math/linalg"
import "core:os"
import "core:strconv"
import "core:strings"
import "sindri:platform"
// Minimal OBJ loader: reads v, vt, vn and triangulates convex polygon faces.
// Produces one GPU vertex per face corner, then centers and normalizes the
// model so it fits roughly inside a [-1, 1] cube at the origin
load_obj_model :: proc(
path: string,
) -> (
vertices: []platform.Vertex,
indices: []u16,
ok: bool,
) {
data, err := os.read_entire_file_from_path(path, context.allocator)
if err != os.ERROR_NONE {
fmt.eprintfln("[core] failed to read model: %s", path)
return nil, nil, false
}
defer delete(data)
positions: [dynamic]platform.Vec3
uvs: [dynamic]platform.Vec2
normals: [dynamic]platform.Vec3
temp_vertices: [dynamic]platform.Vertex
temp_indices: [dynamic]u16
defer {
delete(positions)
delete(uvs)
delete(normals)
delete(temp_vertices)
delete(temp_indices)
}
text := string(data)
for line in strings.split_lines_iterator(&text) {
trimmed := strings.trim_space(line)
if len(trimmed) == 0 || trimmed[0] == '#' do continue
fields := strings.fields(trimmed)
defer delete(fields)
if len(fields) == 0 do continue
switch fields[0] {
case "v":
if len(fields) < 4 do continue
x, _ := strconv.parse_f32(fields[1])
y, _ := strconv.parse_f32(fields[2])
z, _ := strconv.parse_f32(fields[3])
append(&positions, platform.Vec3{x, y, z})
case "vt":
if len(fields) < 3 do continue
u, _ := strconv.parse_f32(fields[1])
v, _ := strconv.parse_f32(fields[2])
append(&uvs, platform.Vec2{u, v})
case "vn":
if len(fields) < 4 do continue
x, _ := strconv.parse_f32(fields[1])
y, _ := strconv.parse_f32(fields[2])
z, _ := strconv.parse_f32(fields[3])
append(&normals, platform.Vec3{x, y, z})
case "f":
face_indices: [dynamic]u16
defer delete(face_indices)
for i := 1; i < len(fields); i += 1 {
token := fields[i]
slash := strings.index_byte(token, '/')
vi_str := token
vti_str := ""
if slash != -1 {
vi_str = token[:slash]
slash2 := strings.index_byte(token[slash + 1:], '/')
if slash2 != -1 {
vti_str = token[slash + 1:slash + 1 + slash2]
} else {
vti_str = token[slash + 1:]
}
}
vi, _ := strconv.parse_int(vi_str)
vi -= 1 // OBJ indices are 1-based
if vi < 0 || vi >= len(positions) do continue
uv := platform.Vec2{0, 0}
if len(vti_str) > 0 {
vti, _ := strconv.parse_int(vti_str)
vti -= 1
if vti >= 0 && vti < len(uvs) {
uv = uvs[vti]
}
} else if vi < len(uvs) {
uv = uvs[vi]
}
append(
&temp_vertices,
platform.Vertex {
pos = positions[vi],
col = {1, 1, 1},
uv = uv,
},
)
append(&face_indices, cast(u16)(len(temp_vertices) - 1))
}
// Triangulate convex polygon by fanning from the first vertex
for i := 2; i < len(face_indices); i += 1 {
append(&temp_indices, face_indices[0])
append(&temp_indices, face_indices[i - 1])
append(&temp_indices, face_indices[i])
}
}
}
if len(temp_vertices) == 0 {
return nil, nil, false
}
// Center and normalize: fit the model inside a roughly [-1, 1] cube
min_p := temp_vertices[0].pos
max_p := temp_vertices[0].pos
for v in temp_vertices {
min_p = linalg.min(min_p, v.pos)
max_p = linalg.max(max_p, v.pos)
}
center := (min_p + max_p) * 0.5
size := max_p - min_p
max_size := max(size.x, size.y, size.z)
scale := 1.8 / max_size
for &v in temp_vertices {
v.pos = (v.pos - center) * scale
}
vertices = make([]platform.Vertex, len(temp_vertices))
indices = make([]u16, len(temp_indices))
copy(vertices, temp_vertices[:])
copy(indices, temp_indices[:])
return vertices, indices, true
}
+12
View File
@@ -5,6 +5,7 @@ import "core:time"
import "game:game" import "game:game"
import "sindri:base" import "sindri:base"
import "sindri:core"
import "sindri:platform" import "sindri:platform"
VERSION :: #config(VERSION, "dev") VERSION :: #config(VERSION, "dev")
@@ -41,6 +42,7 @@ main :: proc() {
// Initialize GPU resources // Initialize GPU resources
platform.instance_init(settings) platform.instance_init(settings)
defer platform.instance_destroy() defer platform.instance_destroy()
load_test_model()
api.init_once() api.init_once()
@@ -84,3 +86,13 @@ test :: proc(e: ^base.Key_Press_Event, _: rawptr) -> bool {
fmt.println("main: @@@@@@@@@@@", e) fmt.println("main: @@@@@@@@@@@", e)
return true return true
} }
// Load model from disk and upload it
load_test_model :: proc() {
vertices, indices, model_ok := core.load_obj_model("assets/suzanne.obj")
if !model_ok do fmt.panicf("[main] could not load model")
defer delete(vertices)
defer delete(indices)
platform.fill_buffers_with_model(vertices, indices)
}
+18 -49
View File
@@ -4,6 +4,7 @@ import "base:runtime"
import "core:fmt" import "core:fmt"
import "core:math" import "core:math"
import "core:math/linalg" import "core:math/linalg"
import "core:os"
import "core:slice" import "core:slice"
import "wgpu:wgpu" import "wgpu:wgpu"
@@ -19,7 +20,7 @@ Vec3 :: [3]f32
Vertex :: struct #align (4) { Vertex :: struct #align (4) {
pos: Vec3, pos: Vec3,
col: Vec3, col: Vec3,
uv: Vec2, uv: Vec2, // texture coordinate
} }
Camera_Uniform :: struct { Camera_Uniform :: struct {
@@ -245,6 +246,21 @@ frame :: proc "c" () {
wgpu.SurfacePresent(state.surface) wgpu.SurfacePresent(state.surface)
} }
// Upload arbitrary vertex/index data to GPU buffers
fill_buffers_with_model :: proc(vertices: []Vertex, indices: []u16) {
state.vertex_buffer = wgpu.DeviceCreateBufferWithData(
state.device,
&{label = "model vertices", usage = {.Vertex}},
vertices,
)
state.index_buffer = wgpu.DeviceCreateBufferWithData(
state.device,
&{label = "model indices", usage = {.Index}},
indices,
)
state.index_count = cast(u32)len(indices)
}
// ----------------------------------------- // -----------------------------------------
// Private functions // Private functions
@@ -399,9 +415,6 @@ on_device :: proc "c" (
// ---------------------------------------- // ----------------------------------------
// Buffer shape // Buffer shape
// Fill vertex/index buffers
fill_buffers_with_quad()
vertex_attributes := [?]wgpu.VertexAttribute { vertex_attributes := [?]wgpu.VertexAttribute {
{ {
format = .Float32x3, format = .Float32x3,
@@ -562,50 +575,6 @@ create_depth_texture :: proc() {
state.depth_texture_view = wgpu.TextureCreateView(state.depth_texture, nil) state.depth_texture_view = wgpu.TextureCreateView(state.depth_texture, nil)
} }
@(private = "file")
fill_buffers_with_quad :: proc() {
vertices := [?]Vertex {
// Near quad: z = 0, colourful, drawn first
{pos = {-0.4, -0.4, 0.0}, col = {1, 0, 0}, uv = {0, 1}}, // 0 bottom-left red
{pos = {0.4, -0.4, 0.0}, col = {0, 1, 0}, uv = {1, 1}}, // 1 bottom-right green
{pos = {0.4, 0.4, 0.0}, col = {0, 0, 1}, uv = {1, 0}}, // 2 top-right blue
{pos = {-0.4, 0.4, 0.0}, col = {1, 1, 0}, uv = {0, 0}}, // 3 top-left yellow
// Far quad: z = -0.5, cyan, drawn second
{pos = {-0.8, -0.8, -0.5}, col = {0, 1, 1}, uv = {0, 1}}, // 4 bottom-left
{pos = {0.8, -0.8, -0.5}, col = {0, 1, 1}, uv = {1, 1}}, // 5 bottom-right
{pos = {0.8, 0.8, -0.5}, col = {0, 1, 1}, uv = {1, 0}}, // 6 top-right
{pos = {-0.8, 0.8, -0.5}, col = {0, 1, 1}, uv = {0, 0}}, // 7 top-left
}
indices := [?]u16 {
0,
1,
2,
2,
3,
0, // near quad
4,
5,
6,
6,
7,
4, // far quad
}
state.vertex_buffer = wgpu.DeviceCreateBufferWithData(
state.device,
&{label = "quad vertices", usage = {.Vertex}},
vertices[:],
)
state.index_buffer = wgpu.DeviceCreateBufferWithData(
state.device,
&{label = "quad indices", usage = {.Index}},
indices[:],
)
state.index_count = cast(u32)len(indices)
}
@(private = "file") @(private = "file")
framebuffer_aspect :: proc() -> f32 { framebuffer_aspect :: proc() -> f32 {
width, height := os_get_framebuffer_size() width, height := os_get_framebuffer_size()
@@ -638,7 +607,7 @@ camera_perspective :: proc(
camera_mvp :: proc(aspect: f32) -> linalg.Matrix4f32 { camera_mvp :: proc(aspect: f32) -> linalg.Matrix4f32 {
projection := camera_perspective(60, aspect, 0.1, 100) projection := camera_perspective(60, aspect, 0.1, 100)
view := linalg.matrix4_look_at_f32({0, 0.75, 2}, {0, 0, 0}, {0, 1, 0}) view := linalg.matrix4_look_at_f32({0, 0.75, 2}, {0, 0, 0}, {0, 1, 0})
// model = identity: the quad lives at the origin, no pose of its own yet // model = identity: the loader already centered the model at the origin
return projection * view return projection * view
} }