0xc3 3 viikkoa sitten
vanhempi
sitoutus
f13887f379
1 muutettua tiedostoa jossa 470 lisäystä ja 271 poistoa
  1. 470 271
      src/cmd/sandbox/main.odin

+ 470 - 271
src/cmd/sandbox/main.odin

@@ -4,6 +4,9 @@ import "base:runtime"
 import "core:fmt"
 import "core:math"
 import "core:math/linalg"
+import "core:math/rand"
+import "core:mem"
+import "core:mem/virtual"
 
 // Vendor libs
 import mu "third-party:microui"
@@ -13,38 +16,73 @@ import sgl "third-party:sokol/gl"
 import sglue "third-party:sokol/glue"
 import slog "third-party:sokol/log"
 
-// --- Data ---
+// --- Constants ---
+MAX_ENTITIES :: 100_000
 
-Camera :: struct {
+// --- Data Types ---
+
+Vertex :: struct {
+	pos:   [3]f32,
+	color: [3]f32,
+}
+
+Mesh :: struct {
+	vertices: #soa[dynamic]Vertex,
+	indices:  [dynamic]u16,
+}
+
+// Entity Kind (Tag)
+Entity_Kind :: enum u8 {
+	Player,
+	Enemy,
+}
+
+// The Big Data Struct
+// Благодаря #soa, это превращается в структуру массивов:
+// struct { pos: [N]vec3, vel: [N]vec3, ... }
+Entity :: struct {
 	pos:   linalg.Vector3f32,
+	vel:   linalg.Vector3f32,
+	color: linalg.Vector3f32,
+	kind:  Entity_Kind,
+	scale: f32,
+}
+
+Camera :: struct {
 	pitch: f32,
 	yaw:   f32,
+	dist:  f32, // Расстояние от игрока (вид от 3-го лица)
 }
 
 Input_State :: struct {
 	keys:     map[sapp.Keycode]bool,
 	mouse_dx: f32,
 	mouse_dy: f32,
-	locked:   bool, // Курсор захвачен окном?
+	locked:   bool,
 }
 
 App_State :: struct {
-	// Sokol GFX resources
-	pass_action: sg.Pass_Action,
+	// Memory
+	arena:       virtual.Arena,
+	allocator:   mem.Allocator,
 
-	// MicroUI context
+	// Resources
+	pass_action: sg.Pass_Action,
 	mu_ctx:      mu.Context,
-
-	// UI Resources
 	atlas_img:   sg.Image,
 	atlas_view:  sg.View,
 	atlas_smp:   sg.Sampler,
 	ui_pip:      sgl.Pipeline,
+	scene_pip:   sgl.Pipeline,
+
+	// Assets
+	mesh_cube:   Mesh,
 
-	// 3D Resources
-	cube_pip:    sgl.Pipeline,
+	// ECS (Entity Component System - Lite)
+	// Храним всех в одном массиве. Игрок всегда index 0.
+	entities:    #soa[dynamic]Entity,
 
-	// Runtime data
+	// Runtime
 	dpi_scale:   f32,
 	camera:      Camera,
 	input:       Input_State,
@@ -52,189 +90,289 @@ App_State :: struct {
 
 state: App_State
 
-// --- Logic / Input ---
+// --- Systems ---
 
-map_mouse_button :: proc(btn: sapp.Mousebutton) -> (mu.Mouse, bool) {
-	switch btn {
-	case .LEFT:
-		return .LEFT, true
-	case .RIGHT:
-		return .RIGHT, true
-	case .MIDDLE:
-		return .MIDDLE, true
-	case .INVALID:
-		return nil, false
+// 1. Input System: Управляет скоростью игрока (Entity 0)
+system_input_player :: proc(dt: f32) {
+	if len(state.entities) == 0 do return
+
+	// Получаем указатель на игрока (SoA access)
+	// В Odin доступ к элементу #soa массива возвращает структуру-прокси,
+	// которая ведет себя как ссылка на данные в соответствующих массивах.
+	player := &state.entities[0]
+
+	SPEED :: 10.0
+
+	// Вращение камеры
+	SENSITIVITY :: 0.15 * 0.02
+	if state.input.locked {
+		state.camera.yaw -= state.input.mouse_dx * SENSITIVITY
+		state.camera.pitch += state.input.mouse_dy * SENSITIVITY
+		state.camera.pitch = math.clamp(state.camera.pitch, -1.5, 1.5)
 	}
-	return nil, false
-}
 
-map_key :: proc(key: sapp.Keycode) -> (mu.Key, bool) {
-	#partial switch key {
-	case .LEFT_SHIFT, .RIGHT_SHIFT:
-		return .SHIFT, true
-	case .LEFT_CONTROL, .RIGHT_CONTROL:
-		return .CTRL, true
-	case .LEFT_ALT, .RIGHT_ALT:
-		return .ALT, true
-	case .ENTER:
-		return .RETURN, true
-	case .BACKSPACE:
-		return .BACKSPACE, true
+	// Расчет направления движения относительно камеры
+	sin_yaw, cos_yaw := math.sincos(state.camera.yaw)
+	forward := linalg.Vector3f32{sin_yaw, 0, cos_yaw}
+	right := linalg.Vector3f32{cos_yaw, 0, -sin_yaw}
+
+	move_dir := linalg.Vector3f32{0, 0, 0}
+	if state.input.keys[.W] do move_dir -= forward
+	if state.input.keys[.S] do move_dir += forward
+	if state.input.keys[.A] do move_dir -= right
+	if state.input.keys[.D] do move_dir += right
+
+	// Применяем скорость к игроку (только XZ, Y управляется гравитацией)
+	target_vel_x: f32 = 0.0
+	target_vel_z: f32 = 0.0
+
+	if linalg.length2(move_dir) > 0.01 {
+		move_dir = linalg.normalize(move_dir)
+		target_vel_x = move_dir.x * SPEED
+		target_vel_z = move_dir.z * SPEED
+	}
+
+	// Мгновенная реакция (можно добавить инерцию, если lerp-ить)
+	player.vel.x = target_vel_x
+	player.vel.z = target_vel_z
+
+	// Прыжок
+	if state.input.keys[.SPACE] && player.pos.y <= 0.51 {
+		player.vel.y = 10.0
 	}
-	return nil, false
 }
 
-// Чистая функция обновления камеры. Принимает состояние, ввод и время.
-// Возвращает матрицы View и Projection.
-update_camera :: proc(
-	cam: ^Camera,
-	input: ^Input_State,
-	dt: f32,
-) -> (
-	view, proj: linalg.Matrix4f32,
-) {
-	// Константы настройки
-	SENSITIVITY :: 0.15 * 0.02 // mouse scale * dt factor
-	SPEED :: 6.0
-
-	// 1. Вращение (только если курсор захвачен)
-	if input.locked {
-		cam.yaw -= input.mouse_dx * SENSITIVITY
-		cam.pitch += input.mouse_dy * SENSITIVITY
-
-		// Кламп питча, чтобы не перевернуться
-		cam.pitch = math.clamp(cam.pitch, -1.5, 1.5)
+// 2. Physics System: Обрабатывает ВСЕ сущности линейно
+system_physics :: proc(dt: f32) {
+	GRAVITY :: 30.0
+	FLOOR_Y :: 0.5
+
+	// Итерация по SoA массиву очень быстрая (кэш-френдли).
+	// Компилятор может векторизовать этот цикл.
+	for i in 0 ..< len(state.entities) {
+		e := &state.entities[i]
+
+		// Гравитация
+		e.vel.y -= GRAVITY * dt
+
+		// Интеграция позиции
+		e.pos += e.vel * dt
+
+		// Коллизия с полом (простейшая)
+		if e.pos.y < FLOOR_Y {
+			e.pos.y = FLOOR_Y
+
+			// Отскок
+			if e.kind == .Player {
+				e.vel.y = 0 // Игрок не прыгает как мячик
+			} else {
+				e.vel.y *= -0.8 // Враги прыгают
+
+				// Трение об пол
+				e.vel.x *= 0.95
+				e.vel.z *= 0.95
+			}
+		}
+
+		// Ограничитель мира (чтобы враги не улетали в бесконечность)
+		if e.pos.x > 50 do e.vel.x = -abs(e.vel.x)
+		if e.pos.x < -50 do e.vel.x = abs(e.vel.x)
+		if e.pos.z > 50 do e.vel.z = -abs(e.vel.z)
+		if e.pos.z < -50 do e.vel.z = abs(e.vel.z)
 	}
+}
+
+// 3. Render System
+system_render :: proc(view, proj: linalg.Matrix4f32) {
+	view_copy := view
+	proj_copy := proj
+
+	sgl.defaults()
+	sgl.push_pipeline()
+	sgl.load_pipeline(state.scene_pip)
 
-	// 2. Векторы направления
-	// Вычисляем forward вектор из углов Эйлера
-	forward := linalg.Vector3f32 {
-		math.cos(cam.pitch) * math.sin(cam.yaw),
-		math.sin(cam.pitch),
-		math.cos(cam.pitch) * math.cos(cam.yaw),
+	sgl.matrix_mode_projection()
+	sgl.load_matrix(cast(^f32)&proj_copy)
+	sgl.matrix_mode_modelview()
+	sgl.load_matrix(cast(^f32)&view_copy)
+
+	draw_grid(100, 1.0)
+
+	// Рисуем всех одной пачкой
+	// В реальном движке здесь был бы Instancing, но для SGL просто цикл.
+	for i in 0 ..< len(state.entities) {
+		e := state.entities[i] // Копия для чтения (быстро)
+
+		// Цвет меняем через uniform (в sgl это c3f)
+		// Геометрию берем одну и ту же
+		draw_mesh_instance(&state.mesh_cube, e.pos, e.scale, e.color)
 	}
 
-	// Вычисляем right вектор (cross product с мировым UP)
-	world_up := linalg.Vector3f32{0, 1, 0}
-	right := linalg.normalize(linalg.cross(forward, world_up))
+	sgl.pop_pipeline()
+}
 
-	// 3. Движение
-	move_dir := linalg.Vector3f32{0, 0, 0}
-	if input.keys[.W] do move_dir -= forward
-	if input.keys[.S] do move_dir += forward
-	if input.keys[.A] do move_dir += right
-	if input.keys[.D] do move_dir -= right
-	if input.keys[.SPACE] do move_dir.y += 1.0
-	if input.keys[.LEFT_CONTROL] do move_dir.y -= 1.0
-
-	// Нормализуем, чтобы стрейф не был быстрее
-	if linalg.length2(move_dir) > 0.01 {
-		cam.pos += linalg.normalize(move_dir) * SPEED * dt
+// --- Helpers ---
+
+get_camera_matrices :: proc() -> (view, proj: linalg.Matrix4f32) {
+	if len(state.entities) == 0 {
+		return linalg.MATRIX4F32_IDENTITY, linalg.MATRIX4F32_IDENTITY
 	}
 
-	// 4. Матрицы
-	// LookAt
-	center := cam.pos - forward // -forward потому что в OpenGL камера смотрит в -Z
-	view = linalg.matrix4_look_at_f32(cam.pos, center, world_up)
+	player_pos := state.entities[0].pos
+
+	// Камера смотрит на игрока
+	// Вычисляем позицию камеры на сфере вокруг игрока
+	cam_offset :=
+		linalg.Vector3f32 {
+			math.cos(state.camera.pitch) * math.sin(state.camera.yaw),
+			math.sin(state.camera.pitch),
+			math.cos(state.camera.pitch) * math.cos(state.camera.yaw),
+		} *
+		state.camera.dist
+
+	cam_pos := player_pos + cam_offset
+
+	view = linalg.matrix4_look_at_f32(cam_pos, player_pos, {0, 1, 0})
 
-	// Perspective
 	aspect := sapp.widthf() / sapp.heightf()
-	proj = linalg.matrix4_perspective_f32(60.0 * (math.PI / 180.0), aspect, 0.1, 100.0)
+	proj = linalg.matrix4_perspective_f32(60.0 * (math.PI / 180.0), aspect, 0.1, 200.0)
 
 	return view, proj
 }
 
-// --- Callbacks ---
-
-event :: proc "c" (ev: ^sapp.Event) {
-	context = runtime.default_context()
+spawn_entity :: proc(pos: linalg.Vector3f32, kind: Entity_Kind) {
+	e: Entity
+	e.pos = pos
+	e.kind = kind
+	e.scale = 1.0
+
+	switch kind {
+	case .Player:
+		e.color = {0.2, 0.8, 0.2} // Green
+		e.scale = 1.0
+	case .Enemy:
+		e.color = {0.8, 0.3, 0.3} // Red
+		e.scale = rand.float32_range(0.5, 1.5)
+		// Случайная начальная скорость для врагов
+		e.vel.x = rand.float32_range(-5, 5)
+		e.vel.z = rand.float32_range(-5, 5)
+		e.vel.y = rand.float32_range(5, 15)
+	}
 
-	dpi := sapp.dpi_scale()
+	append_soa(&state.entities, e)
+}
 
-	// Обновляем состояние ввода для игры
-	#partial switch ev.type {
-	case .KEY_DOWN:
-		state.input.keys[ev.key_code] = true
-	case .KEY_UP:
-		state.input.keys[ev.key_code] = false
-	case .MOUSE_MOVE:
-		if state.input.locked {
-			state.input.mouse_dx = ev.mouse_dx
-			state.input.mouse_dy = ev.mouse_dy
-		}
-	case .MOUSE_DOWN:
+// --- Mesh Gen ---
+
+make_cube_mesh :: proc(allocator: mem.Allocator) -> Mesh {
+	m: Mesh
+	// 24 вершины (6 граней * 4 вершины)
+	m.vertices = make(#soa[dynamic]Vertex, 0, 24, allocator)
+	// 36 индексов (6 граней * 2 треугольника * 3 индекса)
+	m.indices = make([dynamic]u16, 0, 36, allocator)
+
+	// Цвета для наглядности сторон
+	c_front := [3]f32{1.0, 0.5, 0.2} // Orange
+	c_back := [3]f32{0.8, 0.4, 0.1}
+	c_left := [3]f32{0.9, 0.45, 0.15}
+	c_right := [3]f32{0.9, 0.45, 0.15}
+	c_top := [3]f32{1.0, 1.0, 1.0} // Lighter
+	c_bottom := [3]f32{1.0, 1.0, 1.0} // Darker
+
+	// Helper для добавления грани (4 вершины + 6 индексов)
+	add_face :: proc(m: ^Mesh, p1, p2, p3, p4: [3]f32, color: [3]f32) {
+		start_idx := u16(len(m.vertices))
+
+		append_soa(&m.vertices, Vertex{p1, color})
+		append_soa(&m.vertices, Vertex{p2, color})
+		append_soa(&m.vertices, Vertex{p3, color})
+		append_soa(&m.vertices, Vertex{p4, color})
+
+		// CCW порядок (Против часовой стрелки)
+		// 1---2
+		// | / |
+		// 0---3
+		append(&m.indices, start_idx + 0, start_idx + 1, start_idx + 2)
+		append(&m.indices, start_idx + 0, start_idx + 2, start_idx + 3)
 	}
 
-	// Выход из захвата по ESC
-	if ev.type == .KEY_DOWN && ev.key_code == .ESCAPE {
-		state.input.locked = !state.input.locked
-		sapp.lock_mouse(state.input.locked)
-	}
+	// Координаты
+	// Z+ (Front), Z- (Back)
+	// Y+ (Top),   Y- (Bottom)
+	// X+ (Right), X- (Left)
+
+	// Front (Z+)
+	add_face(&m, {-0.5, -0.5, 0.5}, {-0.5, 0.5, 0.5}, {0.5, 0.5, 0.5}, {0.5, -0.5, 0.5}, c_front)
+
+	// Back (Z-) - смотрим сзади, порядок точек зеркальный для CCW
+	add_face(
+		&m,
+		{0.5, -0.5, -0.5},
+		{0.5, 0.5, -0.5},
+		{-0.5, 0.5, -0.5},
+		{-0.5, -0.5, -0.5},
+		c_back,
+	)
 
-	// Логика UI (только если мышь НЕ захвачена игрой)
-	if !state.input.locked {
-		// Нормализация координат для UI
-		mouse_x := i32(ev.mouse_x / dpi)
-		mouse_y := i32(ev.mouse_y / dpi)
+	// Top (Y+)
+	add_face(&m, {-0.5, 0.5, 0.5}, {-0.5, 0.5, -0.5}, {0.5, 0.5, -0.5}, {0.5, 0.5, 0.5}, c_top)
+
+	// Bottom (Y-)
+	add_face(
+		&m,
+		{-0.5, -0.5, -0.5},
+		{-0.5, -0.5, 0.5},
+		{0.5, -0.5, 0.5},
+		{0.5, -0.5, -0.5},
+		c_bottom,
+	)
 
-		#partial switch ev.type {
-		case .MOUSE_DOWN:
-			if btn, ok := map_mouse_button(ev.mouse_button); ok {
-				mu.input_mouse_down(&state.mu_ctx, mouse_x, mouse_y, btn)
-			}
-		case .MOUSE_UP:
-			if btn, ok := map_mouse_button(ev.mouse_button); ok {
-				mu.input_mouse_up(&state.mu_ctx, mouse_x, mouse_y, btn)
-			}
-		case .MOUSE_MOVE:
-			mu.input_mouse_move(&state.mu_ctx, mouse_x, mouse_y)
-		case .MOUSE_SCROLL:
-			mu.input_scroll(&state.mu_ctx, 0, i32(ev.scroll_y))
-		case .KEY_DOWN:
-			if k, ok := map_key(ev.key_code); ok {
-				mu.input_key_down(&state.mu_ctx, k)
-			}
-		case .KEY_UP:
-			if k, ok := map_key(ev.key_code); ok {
-				mu.input_key_up(&state.mu_ctx, k)
-			}
-		case .CHAR:
-			if ev.char_code != 127 && ev.char_code >= 32 {
-				text := fmt.tprintf("%r", rune(ev.char_code))
-				mu.input_text(&state.mu_ctx, text)
-			}
-		}
-	}
+	// Right (X+)
+	add_face(&m, {0.5, -0.5, 0.5}, {0.5, 0.5, 0.5}, {0.5, 0.5, -0.5}, {0.5, -0.5, -0.5}, c_right)
+
+	// Left (X-)
+	add_face(
+		&m,
+		{-0.5, -0.5, -0.5},
+		{-0.5, 0.5, -0.5},
+		{-0.5, 0.5, 0.5},
+		{-0.5, -0.5, 0.5},
+		c_left,
+	)
+
+	return m
 }
 
+// --- App Lifecycle ---
+
 init :: proc "c" () {
 	context = runtime.default_context()
 
+	// 1. Memory
+	_ = virtual.arena_init_growing(&state.arena, 64 * mem.Megabyte)
+	state.allocator = virtual.arena_allocator(&state.arena)
+
+	// 2. Sokol
 	sg.setup({environment = sglue.environment(), logger = {func = slog.func}})
 	sgl.setup({logger = {func = slog.func}})
 
-	// Init UI
+	// 3. UI
 	mu.init(&state.mu_ctx)
 	state.mu_ctx.text_width = mu.default_atlas_text_width
 	state.mu_ctx.text_height = mu.default_atlas_text_height
-	state.dpi_scale = 1.0
 
-	// Init Camera
-	state.camera.pos = {0, 1.5, 5} // Чуть выше и назад
-
-	// --- Resources ---
-
-	// 1. UI Atlas Texture
-	width := mu.DEFAULT_ATLAS_WIDTH
-	height := mu.DEFAULT_ATLAS_HEIGHT
-	pixels := make([]u32, width * height, context.temp_allocator)
+	// 4. Resources
+	// UI Atlas
+	w := mu.DEFAULT_ATLAS_WIDTH
+	h := mu.DEFAULT_ATLAS_HEIGHT
+	pixels := make([]u32, w * h, context.temp_allocator)
 	for alpha, i in mu.default_atlas_alpha {
 		pixels[i] = 0x00FFFFFF | (u32(alpha) << 24)
 	}
 	state.atlas_img = sg.make_image(
 		{
-			width = i32(width),
-			height = i32(height),
+			width = i32(w),
+			height = i32(h),
 			pixel_format = .RGBA8,
 			data = {mip_levels = {0 = {ptr = raw_data(pixels), size = len(pixels) * 4}}},
 		},
@@ -242,126 +380,112 @@ init :: proc "c" () {
 	state.atlas_view = sg.make_view({texture = {image = state.atlas_img}})
 	state.atlas_smp = sg.make_sampler({min_filter = .NEAREST, mag_filter = .NEAREST})
 
-	// 2. UI Pipeline
-	pip_desc: sg.Pipeline_Desc
-	pip_desc.colors[0].blend.enabled = true
-	pip_desc.colors[0].blend.src_factor_rgb = .SRC_ALPHA
-	pip_desc.colors[0].blend.dst_factor_rgb = .ONE_MINUS_SRC_ALPHA
-	state.ui_pip = sgl.make_pipeline(pip_desc)
+	// Pipelines
+	ui_pip_desc: sg.Pipeline_Desc
+	ui_pip_desc.colors[0].blend.enabled = true
+	ui_pip_desc.colors[0].blend.src_factor_rgb = .SRC_ALPHA
+	ui_pip_desc.colors[0].blend.dst_factor_rgb = .ONE_MINUS_SRC_ALPHA
+	state.ui_pip = sgl.make_pipeline(ui_pip_desc)
 
-	// 3. 3D Pipeline (Depth enabled)
-	cube_pip_desc: sg.Pipeline_Desc
-	cube_pip_desc.depth.write_enabled = true
-	cube_pip_desc.depth.compare = .LESS_EQUAL
-	state.cube_pip = sgl.make_pipeline(cube_pip_desc)
+	// 3D Pipeline
+	scene_pip_desc: sg.Pipeline_Desc
+	scene_pip_desc.depth.write_enabled = true
+	scene_pip_desc.depth.compare = .LESS_EQUAL
+	scene_pip_desc.cull_mode = .FRONT
+
+	state.scene_pip = sgl.make_pipeline(scene_pip_desc)
 
 	state.pass_action = {
 		colors = {0 = {load_action = .CLEAR, clear_value = {0.1, 0.1, 0.15, 1}}},
 	}
+
+	// 5. Game Data
+	state.mesh_cube = make_cube_mesh(state.allocator)
+
+	// Init Entities (SoA array)
+	// Reserve memory upfront to avoid reallocations
+	state.entities = make(#soa[dynamic]Entity, 0, MAX_ENTITIES, state.allocator)
+
+	// Spawn Player (Index 0)
+	spawn_entity({0, 5, 0}, .Player)
+	state.camera.dist = 10.0
+	state.camera.pitch = 0.5
+
+	// Spawn Enemies
+	for i in 0 ..< 1000 {
+		x := rand.float32_range(-40, 40)
+		z := rand.float32_range(-40, 40)
+		y := rand.float32_range(10, 50)
+		spawn_entity({x, y, z}, .Enemy)
+	}
 }
 
 frame :: proc "c" () {
 	context = runtime.default_context()
+	defer free_all(context.temp_allocator)
+
 	state.dpi_scale = sapp.dpi_scale()
 	dt := f32(sapp.frame_duration())
 
-	// 1. Update Logic
-	view, proj := update_camera(&state.camera, &state.input, dt)
+	// --- Systems Update ---
+	system_input_player(dt)
+	system_physics(dt)
 
-	// Сброс дельты мыши после обработки кадра
+	// Reset input deltas
 	state.input.mouse_dx = 0
 	state.input.mouse_dy = 0
 
-	// 2. UI Logic
+	// --- Render ---
+
+	// UI
 	mu.begin(&state.mu_ctx)
-	if mu.begin_window(&state.mu_ctx, "Controls", {10, 10, 200, 150}) {
-		mu.label(&state.mu_ctx, "WASD + Mouse to move")
-		mu.label(&state.mu_ctx, "Click to lock mouse")
-		mu.label(&state.mu_ctx, "ESC to unlock")
-
-		if .SUBMIT in mu.button(&state.mu_ctx, "Reset Camera") {
-			state.camera.pos = {0, 1.5, 5}
-			state.camera.yaw = 0
-			state.camera.pitch = 0
-		}
+	if mu.begin_window(&state.mu_ctx, "Stats", {10, 10, 200, 120}) {
+		mu.label(&state.mu_ctx, fmt.tprintf("FPS: %.0f", 1.0 / dt))
+		mu.label(&state.mu_ctx, fmt.tprintf("Entities: %d", len(state.entities)))
+		mu.label(&state.mu_ctx, "WASD to Move")
+		mu.label(&state.mu_ctx, "Space to Jump")
 		mu.end_window(&state.mu_ctx)
 	}
 	mu.end(&state.mu_ctx)
 
-	// 3. Rendering
-
-	// 3.1 Draw 3D Scene
-	sgl.defaults()
-	sgl.push_pipeline()
-	sgl.load_pipeline(state.cube_pip) // Pipeline with depth test
-
-	sgl.matrix_mode_projection()
-	sgl.load_matrix(cast(^f32)&proj)
-
-	sgl.matrix_mode_modelview()
-	sgl.load_matrix(cast(^f32)&view)
-
-	draw_cube({0, 0, 0}, 1.0, {1, 0.5, 0.2}) // Orange cube
-	draw_grid(20, 1.0) // Floor grid
-
-	sgl.pop_pipeline()
-
-	// 3.2 Draw UI
+	// Scene
+	view, proj := get_camera_matrices()
+	system_render(view, proj)
 	render_ui()
 
-	// 3.3 Commit
 	sg.begin_pass({action = state.pass_action, swapchain = sglue.swapchain()})
 	sgl.draw()
 	sg.end_pass()
 	sg.commit()
 }
 
-// --- Render Helpers ---
+// --- Render Impl ---
 
-draw_cube :: proc(pos: linalg.Vector3f32, size: f32, color: linalg.Vector3f32) {
+draw_mesh_instance :: proc(m: ^Mesh, pos: linalg.Vector3f32, scale: f32, tint: linalg.Vector3f32) {
 	sgl.push_matrix()
 	sgl.translate(pos.x, pos.y, pos.z)
-	sgl.scale(size, size, size)
-
-	sgl.begin_quads()
-	sgl.c3f(color.x, color.y, color.z)
-
-	// Front
-	sgl.v3f(-0.5, -0.5, 0.5); sgl.v3f(0.5, -0.5, 0.5)
-	sgl.v3f(0.5, 0.5, 0.5); sgl.v3f(-0.5, 0.5, 0.5)
-	// Back
-	sgl.v3f(0.5, -0.5, -0.5); sgl.v3f(-0.5, -0.5, -0.5)
-	sgl.v3f(-0.5, 0.5, -0.5); sgl.v3f(0.5, 0.5, -0.5)
-	// Left
-	sgl.v3f(-0.5, -0.5, -0.5); sgl.v3f(-0.5, -0.5, 0.5)
-	sgl.v3f(-0.5, 0.5, 0.5); sgl.v3f(-0.5, 0.5, -0.5)
-	// Right
-	sgl.v3f(0.5, -0.5, 0.5); sgl.v3f(0.5, -0.5, -0.5)
-	sgl.v3f(0.5, 0.5, -0.5); sgl.v3f(0.5, 0.5, 0.5)
-	// Top
-	sgl.c3f(color.x * 1.2, color.y * 1.2, color.z * 1.2) // Lighter top
-	sgl.v3f(-0.5, 0.5, 0.5); sgl.v3f(0.5, 0.5, 0.5)
-	sgl.v3f(0.5, 0.5, -0.5); sgl.v3f(-0.5, 0.5, -0.5)
-	// Bottom
-	sgl.c3f(color.x * 0.8, color.y * 0.8, color.z * 0.8) // Darker bottom
-	sgl.v3f(-0.5, -0.5, -0.5); sgl.v3f(0.5, -0.5, -0.5)
-	sgl.v3f(0.5, -0.5, 0.5); sgl.v3f(-0.5, -0.5, 0.5)
-
+	sgl.scale(scale, scale, scale)
+
+	sgl.begin_triangles()
+	for i := 0; i < len(m.indices); i += 1 {
+		idx := m.indices[i]
+		v := m.vertices[idx]
+		// Умножаем цвет вершины на цвет сущности (Tint)
+		sgl.c3f(v.color.x * tint.x, v.color.y * tint.y, v.color.z * tint.z)
+		sgl.v3f(v.pos.x, v.pos.y, v.pos.z)
+	}
 	sgl.end()
 	sgl.pop_matrix()
 }
 
 draw_grid :: proc(slices: int, spacing: f32) {
 	sgl.begin_lines()
-	sgl.c3f(0.5, 0.5, 0.5)
-
-	half_size := f32(slices) * spacing * 0.5
+	sgl.c3f(0.3, 0.3, 0.3)
+	half := f32(slices) * spacing * 0.5
 	for i in 0 ..= slices {
-		pos := -half_size + f32(i) * spacing
-		// X lines
-		sgl.v3f(-half_size, 0, pos); sgl.v3f(half_size, 0, pos)
-		// Z lines
-		sgl.v3f(pos, 0, -half_size); sgl.v3f(pos, 0, half_size)
+		p := -half + f32(i) * spacing
+		sgl.v3f(-half, 0, p); sgl.v3f(half, 0, p)
+		sgl.v3f(p, 0, -half); sgl.v3f(p, 0, half)
 	}
 	sgl.end()
 }
@@ -372,25 +496,22 @@ render_ui :: proc() {
 	sgl.enable_texture()
 	sgl.texture(state.atlas_view, state.atlas_smp)
 
-	// 1. Сбрасываем Проекцию
 	sgl.matrix_mode_projection()
 	sgl.push_matrix()
-	sgl.load_identity() // <--- ВАЖНО: Сбрасываем перспективу от 3D сцены
+	sgl.load_identity()
 
-	// Настраиваем Ortho для UI
-	logical_w := sapp.widthf() / state.dpi_scale
-	logical_h := sapp.heightf() / state.dpi_scale
-	sgl.ortho(0.0, logical_w, logical_h, 0.0, -1.0, +1.0)
+	w := sapp.widthf() / state.dpi_scale
+	h := sapp.heightf() / state.dpi_scale
+	sgl.ortho(0.0, w, h, 0.0, -1.0, +1.0)
 
-	// 2. Сбрасываем ModelView (чтобы UI не вращался вместе с камерой)
 	sgl.matrix_mode_modelview()
 	sgl.push_matrix()
-	sgl.load_identity() // <--- ВАЖНО: Сбрасываем поворот камеры
+	sgl.load_identity()
 
 	sgl.begin_quads()
 	cmd: ^mu.Command
-	for variant in mu.next_command_iterator(&state.mu_ctx, &cmd) {
-		#partial switch c in variant {
+	for v in mu.next_command_iterator(&state.mu_ctx, &cmd) {
+		#partial switch c in v {
 		case ^mu.Command_Rect:
 			draw_rect(c.rect, c.color)
 		case ^mu.Command_Text:
@@ -411,62 +532,139 @@ render_ui :: proc() {
 	}
 	sgl.end()
 
-	// Восстанавливаем матрицы (pop в обратном порядке)
-	sgl.matrix_mode_modelview()
 	sgl.pop_matrix()
-
 	sgl.matrix_mode_projection()
 	sgl.pop_matrix()
-
 	sgl.pop_pipeline()
 }
 
-// --- UI Primitives ---
-
-draw_rect :: proc(rect: mu.Rect, color: mu.Color) {
-	push_quad(rect, mu.default_atlas[mu.DEFAULT_ATLAS_WHITE], color)
+draw_rect :: proc(r: mu.Rect, c: mu.Color) {
+	push_quad(r, mu.default_atlas[mu.DEFAULT_ATLAS_WHITE], c)
 }
 
-draw_text :: proc(str: string, pos: mu.Vec2, color: mu.Color) {
+draw_text :: proc(str: string, pos: mu.Vec2, c: mu.Color) {
 	p := pos
-	for char in str {
-		idx := int(char)
+	for ch in str {
+		idx := int(ch)
 		if idx > 127 do idx = 0
 		src := mu.default_atlas[mu.DEFAULT_ATLAS_FONT + idx]
-		dst := mu.Rect{p.x, p.y, src.w, src.h}
-		push_quad(dst, src, color)
+		push_quad({p.x, p.y, src.w, src.h}, src, c)
 		p.x += src.w
 	}
 }
 
-draw_icon :: proc(id: mu.Icon, rect: mu.Rect, color: mu.Color) {
+draw_icon :: proc(id: mu.Icon, r: mu.Rect, c: mu.Color) {
 	src := mu.default_atlas[int(id)]
-	x := rect.x + (rect.w - src.w) / 2
-	y := rect.y + (rect.h - src.h) / 2
-	push_quad({x, y, src.w, src.h}, src, color)
+	x := r.x + (r.w - src.w) / 2
+	y := r.y + (r.h - src.h) / 2
+	push_quad({x, y, src.w, src.h}, src, c)
 }
 
-push_quad :: proc(dst: mu.Rect, src: mu.Rect, color: mu.Color) {
-	w_inv := 1.0 / f32(mu.DEFAULT_ATLAS_WIDTH)
-	h_inv := 1.0 / f32(mu.DEFAULT_ATLAS_HEIGHT)
-
-	u0 := f32(src.x) * w_inv
-	v0 := f32(src.y) * h_inv
-	u1 := f32(src.x + src.w) * w_inv
-	v1 := f32(src.y + src.h) * h_inv
-
+push_quad :: proc(dst, src: mu.Rect, c: mu.Color) {
+	iw := 1.0 / f32(mu.DEFAULT_ATLAS_WIDTH)
+	ih := 1.0 / f32(mu.DEFAULT_ATLAS_HEIGHT)
+	u0, v0 := f32(src.x) * iw, f32(src.y) * ih
+	u1, v1 := f32(src.x + src.w) * iw, f32(src.y + src.h) * ih
 	x0, y0 := f32(dst.x), f32(dst.y)
 	x1, y1 := f32(dst.x + dst.w), f32(dst.y + dst.h)
 
-	sgl.c4b(color.r, color.g, color.b, color.a)
+	sgl.c4b(c.r, c.g, c.b, c.a)
 	sgl.v2f_t2f(x0, y0, u0, v0)
 	sgl.v2f_t2f(x1, y0, u1, v0)
 	sgl.v2f_t2f(x1, y1, u1, v1)
 	sgl.v2f_t2f(x0, y1, u0, v1)
 }
 
+// --- Input Glue ---
+
+map_mouse_button :: proc(btn: sapp.Mousebutton) -> (mu.Mouse, bool) {
+	switch btn {
+	case .LEFT:
+		return .LEFT, true
+	case .RIGHT:
+		return .RIGHT, true
+	case .MIDDLE:
+		return .MIDDLE, true
+	case .INVALID:
+		return nil, false
+	}
+	return nil, false
+}
+
+map_key :: proc(key: sapp.Keycode) -> (mu.Key, bool) {
+	#partial switch key {
+	case .LEFT_SHIFT, .RIGHT_SHIFT:
+		return .SHIFT, true
+	case .LEFT_CONTROL, .RIGHT_CONTROL:
+		return .CTRL, true
+	case .LEFT_ALT, .RIGHT_ALT:
+		return .ALT, true
+	case .ENTER:
+		return .RETURN, true
+	case .BACKSPACE:
+		return .BACKSPACE, true
+	}
+	return nil, false
+}
+
+event :: proc "c" (ev: ^sapp.Event) {
+	context = runtime.default_context()
+	dpi := sapp.dpi_scale()
+
+	#partial switch ev.type {
+	case .KEY_DOWN:
+		state.input.keys[ev.key_code] = true
+	case .KEY_UP:
+		state.input.keys[ev.key_code] = false
+	case .MOUSE_MOVE:
+		if state.input.locked {
+			state.input.mouse_dx = ev.mouse_dx
+			state.input.mouse_dy = ev.mouse_dy
+		}
+	case .MOUSE_DOWN:
+		if ev.mouse_button == .LEFT && !state.input.locked {
+			sapp.lock_mouse(true)
+			state.input.locked = true
+		}
+	}
+
+	if ev.type == .KEY_DOWN && ev.key_code == .ESCAPE {
+		state.input.locked = false
+		sapp.lock_mouse(false)
+	}
+
+	if !state.input.locked {
+		mx := i32(ev.mouse_x / dpi)
+		my := i32(ev.mouse_y / dpi)
+
+		#partial switch ev.type {
+		case .MOUSE_DOWN:
+			if btn, ok := map_mouse_button(ev.mouse_button); ok {
+				mu.input_mouse_down(&state.mu_ctx, mx, my, btn)
+			}
+		case .MOUSE_UP:
+			if btn, ok := map_mouse_button(ev.mouse_button); ok {
+				mu.input_mouse_up(&state.mu_ctx, mx, my, btn)
+			}
+		case .MOUSE_MOVE:
+			mu.input_mouse_move(&state.mu_ctx, mx, my)
+		case .MOUSE_SCROLL:
+			mu.input_scroll(&state.mu_ctx, 0, i32(ev.scroll_y))
+		case .KEY_DOWN:
+			if k, ok := map_key(ev.key_code); ok do mu.input_key_down(&state.mu_ctx, k)
+		case .KEY_UP:
+			if k, ok := map_key(ev.key_code); ok do mu.input_key_up(&state.mu_ctx, k)
+		case .CHAR:
+			if ev.char_code >= 32 && ev.char_code != 127 {
+				mu.input_text(&state.mu_ctx, fmt.tprintf("%r", rune(ev.char_code)))
+			}
+		}
+	}
+}
+
 cleanup :: proc "c" () {
 	context = runtime.default_context()
+	virtual.arena_destroy(&state.arena)
 	sgl.shutdown()
 	sg.shutdown()
 }
@@ -480,10 +678,11 @@ main :: proc() {
 			event_cb = event,
 			width = 1280,
 			height = 720,
-			window_title = "Sokol 3D + UI",
+			window_title = "Sokol DOD",
 			icon = {sokol_default = true},
 			logger = {func = slog.func},
 			high_dpi = true,
+			swap_interval = 0, // Unlock FPS
 		},
 	)
 }