package main import "core:c" import "core:fmt" import "core:math" import "core:mem" import "core:os" import "core:time" import "core:path/filepath" import "core:strings" import "core:math/linalg" import sdl "vendor:sdl3" modelIndex := 0 Vec3 :: [3]f32 Mat4 :: [16]f32 // column-major Camera :: struct { center: Vec3, distance: f32, orientation: Rotor, fov_deg: f32, } Debug_State :: struct { enabled: bool, accum: f32, } Rotor :: struct { s, x, y, z: f32, } Cube_Instance :: struct { pos: Vec3, scale: f32, color: Vec3, } Vertex :: struct { pos: Vec3, } Push_Constants :: struct { mvp: matrix[4,4]f32, color: [4]f32, } mat4_identity :: proc() -> Mat4 { return Mat4{1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1} } mat4_mul :: proc(a, b: Mat4) -> Mat4 { r := Mat4{} for c in 0 ..< 4 { for row in 0 ..< 4 { r[c*4+row] = a[0*4+row] * b[c*4+0] + a[1*4+row] * b[c*4+1] + a[2*4+row] * b[c*4+2] + a[3*4+row] * b[c*4+3] } } return r } vec3_sub :: proc(a, b: Vec3) -> Vec3 { return Vec3{a[0] - b[0], a[1] - b[1], a[2] - b[2]} } vec3_add :: proc(a, b: Vec3) -> Vec3 { return Vec3{a[0] + b[0], a[1] + b[1], a[2] + b[2]} } vec3_scale :: proc(v: Vec3, s: f32) -> Vec3 { return Vec3{v[0] * s, v[1] * s, v[2] * s} } vec3_dot :: proc(a, b: Vec3) -> f32 { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] } vec3_cross :: proc(a, b: Vec3) -> Vec3 { return Vec3{ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0], } } vec3_normalize :: proc(v: Vec3) -> Vec3 { len2 := vec3_dot(v, v) if len2 <= 0.000001 { return Vec3{0, 0, 0} } inv := 1.0 / math.sqrt(len2) return vec3_scale(v, inv) } rotor_identity :: proc() -> Rotor { return Rotor{1, 0, 0, 0} } rotor_normalize :: proc(r: Rotor) -> Rotor { len2 := r.s*r.s + r.x*r.x + r.y*r.y + r.z*r.z if len2 <= 0.000001 { return rotor_identity() } inv := 1.0 / math.sqrt(len2) return Rotor{r.s * inv, r.x * inv, r.y * inv, r.z * inv} } rotor_mul :: proc(a, b: Rotor) -> Rotor { return Rotor{ a.s*b.s - a.x*b.x - a.y*b.y - a.z*b.z, a.s*b.x + a.x*b.s + a.y*b.z - a.z*b.y, a.s*b.y - a.x*b.z + a.y*b.s + a.z*b.x, a.s*b.z + a.x*b.y - a.y*b.x + a.z*b.s, } } rotor_from_axis_angle :: proc(axis: Vec3, angle: f32) -> Rotor { a := vec3_normalize(axis) h := angle * 0.5 c := math.cos(h) s := math.sin(h) return Rotor{c, a[0] * s, a[1] * s, a[2] * s} } rotate_vec3 :: proc(r: Rotor, v: Vec3) -> Vec3 { u := Vec3{r.x, r.y, r.z} t := vec3_scale(vec3_cross(u, v), 2.0) return vec3_add(v, vec3_add(vec3_scale(t, r.s), vec3_cross(u, t))) } mat4_translate :: proc(p: Vec3) -> Mat4 { m := mat4_identity() m[12] = p[0] m[13] = p[1] m[14] = p[2] return m } mat4_scale_uniform :: proc(s: f32) -> Mat4 { m := mat4_identity() m[0] = s m[5] = s m[10] = s return m } mat4_perspective :: proc(fov_deg, aspect, z_near, z_far: f32) -> Mat4 { f := 1.0 / math.tan((fov_deg * 0.5) * (math.PI / 180.0)) m := Mat4{} m[0] = f / aspect m[5] = f m[10] = (z_far + z_near) / (z_near - z_far) m[11] = -1 m[14] = (2.0 * z_far * z_near) / (z_near - z_far) return m } mat4_look_at :: proc(eye, target, up: Vec3) -> Mat4 { fwd := vec3_normalize(vec3_sub(target, eye)) right := vec3_normalize(vec3_cross(fwd, up)) real_up := vec3_cross(right, fwd) m := mat4_identity() m[0] = right[0] m[1] = real_up[0] m[2] = -fwd[0] m[4] = right[1] m[5] = real_up[1] m[6] = -fwd[1] m[8] = right[2] m[9] = real_up[2] m[10] = -fwd[2] m[12] = -vec3_dot(right, eye) m[13] = -vec3_dot(real_up, eye) m[14] = vec3_dot(fwd, eye) return m } camera_forward :: proc(c: Camera) -> Vec3 { return vec3_normalize(rotate_vec3(c.orientation, Vec3{0, 0, -1})) } camera_right :: proc(c: Camera) -> Vec3 { return vec3_normalize(rotate_vec3(c.orientation, Vec3{1, 0, 0})) } camera_up :: proc(c: Camera) -> Vec3 { return vec3_normalize(rotate_vec3(c.orientation, Vec3{0, 1, 0})) } camera_position :: proc(c: Camera) -> Vec3 { return vec3_sub(c.center, vec3_scale(camera_forward(c), c.distance)) } read_file_or_fail :: proc(path: string) -> []u8 { file_bytes, err := os.read_entire_file(path, context.allocator) if err != nil { fmt.println("failed to read file:", path, "error:", err) return nil } return file_bytes } create_gpu_shader :: proc(device: ^sdl.GPUDevice, path: string, stage: sdl.GPUShaderStage) -> ^sdl.GPUShader { bytes := read_file_or_fail(path) if len(bytes) == 0 { return nil } ci := sdl.GPUShaderCreateInfo{ code_size = uint(len(bytes)), code = raw_data(bytes), entrypoint = cstring("main"), format = sdl.GPUShaderFormat{.SPIRV}, stage = stage, num_samplers = 0, num_storage_textures = 0, num_storage_buffers = 0, num_uniform_buffers = 1, props = 0, } shader := sdl.CreateGPUShader(device, ci) delete(bytes) if shader == nil { fmt.println("CreateGPUShader failed:", path, "error:", sdl.GetError()) } return shader } loadModel :: proc(device: ^sdl.GPUDevice, model: ^RSM_Model, vertices: ^[dynamic]Vertex, indices: ^[dynamic]u16) -> (vbuf: ^sdl.GPUBuffer, ibuf: ^sdl.GPUBuffer) { clear(vertices) clear(indices) for node in model.nodes { fmt.println(node.offset_matrix) fmt.println(node.translation1) fmt.println(node.translation2) fmt.println(node.scale) fmt.println(node.rotation_angle) fmt.println(node.rotation_axis) fmt.println(node.rotation_keyframes[:]) for face in node.faces { for i in 0..<3 { v_idx := face.vertex_position_indices[i] t_idx := face.texture_coordinate_indices[i] pos := node.vertex_positions[v_idx] uv := node.texture_coordinates[t_idx].coordinates append(vertices, Vertex{ pos = pos // uv = uv, }) append(indices, u16(len(vertices)-1)) } } } vbuf_info := sdl.GPUBufferCreateInfo{ usage = sdl.GPUBufferUsageFlags{.VERTEX}, size = u32(len(vertices) * size_of(vertices[0])), props = 0, } vbuf = sdl.CreateGPUBuffer(device, vbuf_info) if vbuf == nil { fmt.println("CreateGPUBuffer failed:", sdl.GetError()) return } ibuf_info := sdl.GPUBufferCreateInfo{ usage = sdl.GPUBufferUsageFlags{.INDEX}, size = u32(len(indices) * size_of(indices[0])), props = 0, } ibuf = sdl.CreateGPUBuffer(device, ibuf_info) if ibuf == nil { fmt.println("CreateGPUBuffer failed:", sdl.GetError()) return } { setup_cmd := sdl.AcquireGPUCommandBuffer(device) if setup_cmd == nil { fmt.println("AcquireGPUCommandBuffer failed:", sdl.GetError()) return } if !copy_data_to_gpu(device, setup_cmd, vertices[:], vbuf, indices[:], ibuf) { _ = sdl.CancelGPUCommandBuffer(setup_cmd) return } if !sdl.SubmitGPUCommandBuffer(setup_cmd) { fmt.println("SubmitGPUCommandBuffer (setup) failed:", sdl.GetError()) return } } return vbuf, ibuf } copy_data_to_gpu :: proc(device: ^sdl.GPUDevice, cmd: ^sdl.GPUCommandBuffer, vertices: []Vertex, vert_buf: ^sdl.GPUBuffer, indices: []u16, ind_buf: ^sdl.GPUBuffer) -> bool { vert_byte_count := len(vertices) * size_of(vertices[0]) ind_byte_count := len(indices) * size_of(indices[0]) tb_info := sdl.GPUTransferBufferCreateInfo{ usage = .UPLOAD, size = u32(vert_byte_count + ind_byte_count), props = 0, } tb := sdl.CreateGPUTransferBuffer(device, tb_info) if tb == nil { fmt.println("CreateGPUTransferBuffer failed:", sdl.GetError()) return false } defer sdl.ReleaseGPUTransferBuffer(device, tb) mapped := transmute([^]byte)sdl.MapGPUTransferBuffer(device, tb, false) if mapped == nil { fmt.println("MapGPUTransferBuffer failed:", sdl.GetError()) return false } mem.copy_non_overlapping(mapped, raw_data(vertices), vert_byte_count) mem.copy_non_overlapping(mapped[vert_byte_count:], raw_data(indices), ind_byte_count) sdl.UnmapGPUTransferBuffer(device, tb) cp := sdl.BeginGPUCopyPass(cmd) if cp == nil { fmt.println("BeginGPUCopyPass failed:", sdl.GetError()) return false } vert_src := sdl.GPUTransferBufferLocation{transfer_buffer = tb, offset = 0} vert_dst := sdl.GPUBufferRegion{buffer = vert_buf, offset = 0, size = u32(vert_byte_count)} sdl.UploadToGPUBuffer(cp, vert_src, vert_dst, false) ind_src := sdl.GPUTransferBufferLocation{transfer_buffer = tb, offset = u32(vert_byte_count)} ind_dst := sdl.GPUBufferRegion{buffer = ind_buf, offset = 0, size = u32(ind_byte_count)} sdl.UploadToGPUBuffer(cp, ind_src, ind_dst, false) sdl.EndGPUCopyPass(cp) return true } GPU_Vertex :: struct { position: [3]f32, uv: [2]f32, } main :: proc() { if !sdl.Init(sdl.INIT_VIDEO) { fmt.println("SDL init failed:", sdl.GetError()) return } defer sdl.Quit() window := sdl.CreateWindow("Odin + SDL3 GPU Cubes", 1280, 720, sdl.WINDOW_RESIZABLE) if window == nil { fmt.println("window create failed:", sdl.GetError()) return } defer sdl.DestroyWindow(window) device := sdl.CreateGPUDevice(sdl.GPUShaderFormat{.SPIRV}, false, cstring("vulkan")) if device == nil { fmt.println("CreateGPUDevice failed:", sdl.GetError()) return } defer sdl.DestroyGPUDevice(device) if !sdl.ClaimWindowForGPUDevice(device, window) { fmt.println("ClaimWindowForGPUDevice failed:", sdl.GetError()) return } defer sdl.ReleaseWindowFromGPUDevice(device, window) driver_name := sdl.GetGPUDeviceDriver(device) fmt.println("SDL GPU driver:", string(driver_name)) if string(driver_name) != "vulkan" { fmt.println("expected Vulkan driver, got:", string(driver_name)) return } vertex_shader := create_gpu_shader(device, "shaders/cubes.vert.spv", .VERTEX) if vertex_shader == nil { return } defer sdl.ReleaseGPUShader(device, vertex_shader) fragment_shader := create_gpu_shader(device, "shaders/cubes.frag.spv", .FRAGMENT) if fragment_shader == nil { return } defer sdl.ReleaseGPUShader(device, fragment_shader) color_format := sdl.GetGPUSwapchainTextureFormat(device, window) if color_format == .INVALID { fmt.println("invalid swapchain format:", sdl.GetError()) return } vb_descs := [1]sdl.GPUVertexBufferDescription{ {slot = 0, pitch = size_of(Vertex), input_rate = .VERTEX, instance_step_rate = 0}, } vattrs := [1]sdl.GPUVertexAttribute{ {location = 0, buffer_slot = 0, format = .FLOAT3, offset = 0}, } blend := sdl.GPUColorTargetBlendState{ src_color_blendfactor = .ONE, dst_color_blendfactor = .ZERO, color_blend_op = .ADD, src_alpha_blendfactor = .ONE, dst_alpha_blendfactor = .ZERO, alpha_blend_op = .ADD, color_write_mask = sdl.GPUColorComponentFlags{.R, .G, .B, .A}, enable_blend = false, enable_color_write_mask = true, } color_targets := [1]sdl.GPUColorTargetDescription{ {format = color_format, blend_state = blend}, } pipeline_ci := sdl.GPUGraphicsPipelineCreateInfo{ vertex_shader = vertex_shader, fragment_shader = fragment_shader, vertex_input_state = sdl.GPUVertexInputState{ vertex_buffer_descriptions = &vb_descs[0], num_vertex_buffers = 1, vertex_attributes = &vattrs[0], num_vertex_attributes = 1, }, primitive_type = .TRIANGLELIST, rasterizer_state = sdl.GPURasterizerState{ fill_mode = .FILL, cull_mode = .BACK, front_face = .COUNTER_CLOCKWISE, depth_bias_constant_factor = 0, depth_bias_clamp = 0, depth_bias_slope_factor = 0, enable_depth_bias = false, enable_depth_clip = true, }, multisample_state = sdl.GPUMultisampleState{ sample_count = ._1, sample_mask = 0, enable_mask = false, enable_alpha_to_coverage = false, }, depth_stencil_state = sdl.GPUDepthStencilState{ compare_op = .LESS, back_stencil_state = sdl.GPUStencilOpState{fail_op = .KEEP, pass_op = .KEEP, depth_fail_op = .KEEP, compare_op = .ALWAYS}, front_stencil_state = sdl.GPUStencilOpState{fail_op = .KEEP, pass_op = .KEEP, depth_fail_op = .KEEP, compare_op = .ALWAYS}, compare_mask = 0, write_mask = 0, enable_depth_test = false, enable_depth_write = false, enable_stencil_test = false, }, target_info = sdl.GPUGraphicsPipelineTargetInfo{ color_target_descriptions = &color_targets[0], num_color_targets = 1, depth_stencil_format = .INVALID, has_depth_stencil_target = false, }, props = 0, } pipeline := sdl.CreateGPUGraphicsPipeline(device, pipeline_ci) if pipeline == nil { fmt.println("CreateGPUGraphicsPipeline failed:", sdl.GetError()) return } defer sdl.ReleaseGPUGraphicsPipeline(device, pipeline) pipeline_create_info := sdl.GPUGraphicsPipelineCreateInfo{ primitive_type = .TRIANGLELIST, rasterizer_state = { fill_mode = .LINE, cull_mode = .NONE, } } models: [dynamic]RSM_Model walk("/home/pavel/neoragnarok_backup/kro_client/data", &models) vertices: [dynamic]Vertex indices: [dynamic]u16 vbuf, ibuf := loadModel(device, &models[modelIndex], &vertices, &indices) defer sdl.ReleaseGPUBuffer(device, ibuf) defer sdl.ReleaseGPUBuffer(device, vbuf) init_yaw := rotor_from_axis_angle(Vec3{0, 1, 0}, 0.9) init_right := rotate_vec3(init_yaw, Vec3{1, 0, 0}) init_pitch := rotor_from_axis_angle(init_right, -0.45) camera := Camera{ center = Vec3{0, 0.5, 0}, distance = 8, orientation = rotor_normalize(rotor_mul(init_pitch, init_yaw)), fov_deg = 60, } cubes := []Cube_Instance{ {pos = Vec3{0, 0, 0}, scale = 1.0, color = Vec3{0.95, 0.45, 0.20}}, {pos = Vec3{2, 0.5, -1}, scale = 0.8, color = Vec3{0.20, 0.70, 0.95}}, {pos = Vec3{-2, -0.2, 1.5}, scale = 1.2, color = Vec3{0.85, 0.85, 0.30}}, {pos = Vec3{1.0, 1.4, 2.0}, scale = 0.6, color = Vec3{0.40, 0.95, 0.60}}, } running := true last := time.tick_now() left_down := false middle_down := false debug := Debug_State{} for running { now := time.tick_now() dt := f32(time.duration_seconds(time.tick_diff(last, now))) last = now event: sdl.Event for sdl.PollEvent(&event) { #partial switch event.type { case .QUIT: running = false case .MOUSE_BUTTON_DOWN: switch event.button.button { case sdl.BUTTON_LEFT: left_down = true case sdl.BUTTON_MIDDLE: middle_down = true } case .MOUSE_BUTTON_UP: switch event.button.button { case sdl.BUTTON_LEFT: left_down = false case sdl.BUTTON_MIDDLE: middle_down = false } case .MOUSE_WHEEL: camera.distance = math.clamp(camera.distance - event.wheel.y * 0.7, 1.5, 80.0) case .KEY_DOWN: if event.key.key == sdl.K_ESCAPE { running = false } else if event.key.scancode == sdl.Scancode.F1 && !event.key.repeat { debug.enabled = !debug.enabled fmt.println("debug:", debug.enabled) } else if event.key.key == sdl.K_N { modelIndex += 1 sdl.ReleaseGPUBuffer(device, ibuf) sdl.ReleaseGPUBuffer(device, vbuf) vbuf, ibuf = loadModel(device, &models[modelIndex], &vertices, &indices) } } } x_rel: f32 = 0 y_rel: f32 = 0 _ = sdl.GetRelativeMouseState(&x_rel, &y_rel) if left_down { yaw_r := rotor_from_axis_angle(Vec3{0, 1, 0}, -x_rel * 0.006) q1 := rotor_normalize(rotor_mul(yaw_r, camera.orientation)) right_axis := vec3_normalize(rotate_vec3(q1, Vec3{1, 0, 0})) pitch_r := rotor_from_axis_angle(right_axis, -y_rel * 0.006) q2 := rotor_normalize(rotor_mul(pitch_r, q1)) fwd2 := camera_forward(Camera{center = camera.center, distance = camera.distance, orientation = q2, fov_deg = camera.fov_deg}) if math.abs(vec3_dot(fwd2, Vec3{0, 1, 0})) < 0.98 { camera.orientation = q2 } else { camera.orientation = q1 } } if middle_down { right := camera_right(camera) up := camera_up(camera) pan_speed := 0.008 * camera.distance camera.center = vec3_add(camera.center, vec3_scale(right, -x_rel * pan_speed)) camera.center = vec3_add(camera.center, vec3_scale(up, y_rel * pan_speed)) } keys := sdl.GetKeyboardState(nil) forward := camera_forward(camera) right := camera_right(camera) up := camera_up(camera) move_speed := camera.distance * dt * 1.4 if keys[sdl.Scancode.W] { camera.center = vec3_add(camera.center, vec3_scale(forward, move_speed)) } if keys[sdl.Scancode.S] { camera.center = vec3_add(camera.center, vec3_scale(forward, -move_speed)) } if keys[sdl.Scancode.A] { camera.center = vec3_add(camera.center, vec3_scale(right, -move_speed)) } if keys[sdl.Scancode.D] { camera.center = vec3_add(camera.center, vec3_scale(right, move_speed)) } if debug.enabled { debug.accum += dt if debug.accum >= 0.2 { debug.accum = 0 eye := camera_position(camera) fmt.println("cam eye:", eye, "center:", camera.center, "fwd:", forward, "right:", right, "up:", up) } } w: c.int = 0 h: c.int = 0 sdl.GetWindowSize(window, &w, &h) if w <= 0 || h <= 0 { continue } cmd := sdl.AcquireGPUCommandBuffer(device) if cmd == nil { fmt.println("AcquireGPUCommandBuffer failed:", sdl.GetError()) break } swap_tex: ^sdl.GPUTexture swap_w: sdl.Uint32 = 0 swap_h: sdl.Uint32 = 0 if !sdl.WaitAndAcquireGPUSwapchainTexture(cmd, window, &swap_tex, &swap_w, &swap_h) { fmt.println("WaitAndAcquireGPUSwapchainTexture failed:", sdl.GetError()) _ = sdl.CancelGPUCommandBuffer(cmd) break } if swap_tex == nil { if !sdl.SubmitGPUCommandBuffer(cmd) { fmt.println("SubmitGPUCommandBuffer failed:", sdl.GetError()) break } continue } clear := sdl.FColor{0.08, 0.09, 0.12, 1.0} cti := sdl.GPUColorTargetInfo{ texture = swap_tex, mip_level = 0, layer_or_depth_plane = 0, clear_color = clear, load_op = .CLEAR, store_op = .STORE, resolve_texture = nil, resolve_mip_level = 0, resolve_layer = 0, cycle = false, cycle_resolve_texture = false, } rp := sdl.BeginGPURenderPass(cmd, &cti, 1, nil) if rp == nil { fmt.println("BeginGPURenderPass failed:", sdl.GetError()) _ = sdl.CancelGPUCommandBuffer(cmd) break } vp := sdl.GPUViewport{x = 0, y = 0, w = f32(swap_w), h = f32(swap_h), min_depth = 0, max_depth = 1} sdl.SetGPUViewport(rp, vp) sdl.BindGPUGraphicsPipeline(rp, pipeline) vb_binding := sdl.GPUBufferBinding{buffer = vbuf, offset = 0} sdl.BindGPUVertexBuffers(rp, 0, &vb_binding, 1) ib_binding := sdl.GPUBufferBinding{buffer = ibuf, offset = 0} sdl.BindGPUIndexBuffer(rp, ib_binding, ._16BIT) aspect := f32(swap_w) / f32(swap_h) proj := linalg.matrix4_perspective(camera.fov_deg, aspect, 0.1, 300.0) view := linalg.matrix4_look_at(camera_position(camera), camera.center, camera_up(camera)) vp_mat := proj * view for node in models[modelIndex].nodes { rotation := linalg.quaternion_angle_axis(node.rotation_angle, node.rotation_axis) if (len(node.rotation_keyframes) > 0) { q := node.rotation_keyframes[0].quaternion rotation = quaternion(imag=q[0], jmag=q[1], kmag=q[2], real=q[3]) } rotation = linalg.quaternion_normalize(rotation) r := linalg.matrix3_from_quaternion(rotation) s := matrix[3,3]f32{ node.scale[0], 0, 0 , 0, node.scale[1], 0 , 0, 0, node.scale[2], } a3 := r * node.offset_matrix * s t := node.translation1 + node.translation2 // 4x4 affine matrix model := matrix[4,4]f32{ a3[0][0], a3[0][1], a3[0][2], t[0], a3[1][0], a3[1][1], a3[1][2], t[1], a3[2][0], a3[2][1], a3[2][2], t[2], 0, 0, 0, 1 , } // append(vertices, Vertex{ // pos = linalg.matrix3_from_quaternion(rotation) * node.offset_matrix * (node.scale * pos) + node.translation1 + node.translation2 // // uv = uv, // }) // model := mat4_mul(node., mat4_scale_uniform(1)) pc := Push_Constants{} pc.mvp = vp_mat * model pc.color = [4]f32{1, 0, 0, 1.0} sdl.PushGPUVertexUniformData(cmd, 0, &pc, u32(size_of(Push_Constants))) sdl.PushGPUFragmentUniformData(cmd, 0, &pc, u32(size_of(Push_Constants))) sdl.DrawGPUIndexedPrimitives(rp, u32(len(indices)), 1, 0, 0, 0) } sdl.EndGPURenderPass(rp) if !sdl.SubmitGPUCommandBuffer(cmd) { fmt.println("SubmitGPUCommandBuffer failed:", sdl.GetError()) break } } _ = sdl.WaitForGPUIdle(device) } walk :: proc(dir: string, models: ^[dynamic]RSM_Model) { f, _ := os.open(dir) entries, ok := os.read_dir(f, 0, context.allocator) for entry in entries { path, _ := filepath.join({dir, entry.name}) if entry.type == .Directory { walk(path, models) } if strings.contains(entry.name, ".rsm") { data, _ := os.read_entire_file(path, context.allocator) parsed, err := parse_rsm(data) if err != nil { fmt.printfln("%v", err) } else { append_elem(models, parsed) } } } }