0xc3 3 هفته پیش
والد
کامیت
873ee162da
1فایلهای تغییر یافته به همراه316 افزوده شده و 120 حذف شده
  1. 316 120
      src/cmd/sandbox/main.odin

+ 316 - 120
src/cmd/sandbox/main.odin

@@ -2,6 +2,10 @@ package main
 
 import "base:runtime"
 import "core:fmt"
+import "core:math"
+import "core:math/linalg"
+
+// Vendor libs
 import mu "third-party:microui"
 import sapp "third-party:sokol/app"
 import sg "third-party:sokol/gfx"
@@ -9,20 +13,46 @@ import sgl "third-party:sokol/gl"
 import sglue "third-party:sokol/glue"
 import slog "third-party:sokol/log"
 
-State :: struct {
-	pip:         sg.Pipeline,
-	bind:        sg.Bindings,
+// --- Data ---
+
+Camera :: struct {
+	pos:   linalg.Vector3f32,
+	pitch: f32,
+	yaw:   f32,
+}
+
+Input_State :: struct {
+	keys:     map[sapp.Keycode]bool,
+	mouse_dx: f32,
+	mouse_dy: f32,
+	locked:   bool, // Курсор захвачен окном?
+}
+
+App_State :: struct {
+	// Sokol GFX resources
 	pass_action: sg.Pass_Action,
+
+	// MicroUI context
 	mu_ctx:      mu.Context,
 
-	// Ресурсы
+	// UI Resources
 	atlas_img:   sg.Image,
 	atlas_view:  sg.View,
 	atlas_smp:   sg.Sampler,
 	ui_pip:      sgl.Pipeline,
+
+	// 3D Resources
+	cube_pip:    sgl.Pipeline,
+
+	// Runtime data
+	dpi_scale:   f32,
+	camera:      Camera,
+	input:       Input_State,
 }
 
-state: State
+state: App_State
+
+// --- Logic / Input ---
 
 map_mouse_button :: proc(btn: sapp.Mousebutton) -> (mu.Mouse, bool) {
 	switch btn {
@@ -54,40 +84,125 @@ map_key :: proc(key: sapp.Keycode) -> (mu.Key, bool) {
 	return nil, false
 }
 
-event :: proc "c" (ev: ^sapp.Event) {
-	context = runtime.default_context()
+// Чистая функция обновления камеры. Принимает состояние, ввод и время.
+// Возвращает матрицы 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)
+	}
 
-	#partial switch ev.type {
-	case .MOUSE_DOWN:
-		if btn, ok := map_mouse_button(ev.mouse_button); ok {
-			mu.input_mouse_down(&state.mu_ctx, i32(ev.mouse_x), i32(ev.mouse_y), btn)
-		}
+	// 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),
+	}
 
-	case .MOUSE_UP:
-		if btn, ok := map_mouse_button(ev.mouse_button); ok {
-			mu.input_mouse_up(&state.mu_ctx, i32(ev.mouse_x), i32(ev.mouse_y), btn)
-		}
+	// Вычисляем right вектор (cross product с мировым UP)
+	world_up := linalg.Vector3f32{0, 1, 0}
+	right := linalg.normalize(linalg.cross(forward, world_up))
+
+	// 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
+	}
 
-	case .MOUSE_MOVE:
-		mu.input_mouse_move(&state.mu_ctx, i32(ev.mouse_x), i32(ev.mouse_y))
+	// 4. Матрицы
+	// LookAt
+	center := cam.pos - forward // -forward потому что в OpenGL камера смотрит в -Z
+	view = linalg.matrix4_look_at_f32(cam.pos, center, world_up)
 
-	case .MOUSE_SCROLL:
-		mu.input_scroll(&state.mu_ctx, 0, i32(ev.scroll_y))
+	// Perspective
+	aspect := sapp.widthf() / sapp.heightf()
+	proj = linalg.matrix4_perspective_f32(60.0 * (math.PI / 180.0), aspect, 0.1, 100.0)
 
-	case .KEY_DOWN:
-		if k, ok := map_key(ev.key_code); ok {
-			mu.input_key_down(&state.mu_ctx, k)
-		}
+	return view, proj
+}
 
+// --- Callbacks ---
+
+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:
-		if k, ok := map_key(ev.key_code); ok {
-			mu.input_key_up(&state.mu_ctx, k)
+		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:
+	}
 
-	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)
+	// Выход из захвата по ESC
+	if ev.type == .KEY_DOWN && ev.key_code == .ESCAPE {
+		state.input.locked = !state.input.locked
+		sapp.lock_mouse(state.input.locked)
+	}
+
+	// Логика UI (только если мышь НЕ захвачена игрой)
+	if !state.input.locked {
+		// Нормализация координат для UI
+		mouse_x := i32(ev.mouse_x / dpi)
+		mouse_y := 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, 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)
+			}
 		}
 	}
 }
@@ -98,153 +213,229 @@ init :: proc "c" () {
 	sg.setup({environment = sglue.environment(), logger = {func = slog.func}})
 	sgl.setup({logger = {func = slog.func}})
 
+	// Init 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)
-	defer delete(pixels)
-
+	pixels := make([]u32, width * height, 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),
+			pixel_format = .RGBA8,
+			data = {mip_levels = {0 = {ptr = raw_data(pixels), size = len(pixels) * 4}}},
+		},
+	)
+	state.atlas_view = sg.make_view({texture = {image = state.atlas_img}})
+	state.atlas_smp = sg.make_sampler({min_filter = .NEAREST, mag_filter = .NEAREST})
 
-	img_desc: sg.Image_Desc
-	img_desc.width = i32(width)
-	img_desc.height = i32(height)
-	img_desc.pixel_format = .RGBA8
-	img_desc.data.mip_levels[0] = {
-		ptr  = raw_data(pixels),
-		size = len(pixels) * 4,
-	}
-	state.atlas_img = sg.make_image(img_desc)
-
-	view_desc: sg.View_Desc
-	view_desc.texture.image = state.atlas_img
-	state.atlas_view = sg.make_view(view_desc)
-
-	smp_desc: sg.Sampler_Desc
-	smp_desc.min_filter = .NEAREST
-	smp_desc.mag_filter = .NEAREST
-	state.atlas_smp = sg.make_sampler(smp_desc)
-
+	// 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)
 
-	vertices := [?]f32 {
-		0.0,
-		0.5,
-		0.5,
-		1.0,
-		0.0,
-		0.0,
-		1.0,
-		0.5,
-		-0.5,
-		0.5,
-		0.0,
-		1.0,
-		0.0,
-		1.0,
-		-0.5,
-		-0.5,
-		0.5,
-		0.0,
-		0.0,
-		1.0,
-		1.0,
-	}
-	state.bind.vertex_buffers[0] = sg.make_buffer(
-		{data = {ptr = &vertices, size = size_of(vertices)}},
-	)
-	state.pip = sg.make_pipeline(
-		{
-			shader = sg.make_shader(triangle_shader_desc(sg.query_backend())),
-			layout = {attrs = {0 = {format = .FLOAT3}, 1 = {format = .FLOAT4}}},
-		},
-	)
+	// 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)
+
 	state.pass_action = {
-		colors = {0 = {load_action = .CLEAR, clear_value = {0.4, 0.4, 0.5, 1}}},
+		colors = {0 = {load_action = .CLEAR, clear_value = {0.1, 0.1, 0.15, 1}}},
 	}
 }
 
 frame :: proc "c" () {
 	context = runtime.default_context()
+	state.dpi_scale = sapp.dpi_scale()
+	dt := f32(sapp.frame_duration())
 
-	mu.begin(&state.mu_ctx)
+	// 1. Update Logic
+	view, proj := update_camera(&state.camera, &state.input, dt)
+
+	// Сброс дельты мыши после обработки кадра
+	state.input.mouse_dx = 0
+	state.input.mouse_dy = 0
 
-	if mu.begin_window(&state.mu_ctx, "Interactive Window", {40, 40, 300, 250}) {
-		mu.label(&state.mu_ctx, "Try interacting!")
-		if .SUBMIT in mu.button(&state.mu_ctx, "Click Me") {
-			fmt.println("Clicked!")
+	// 2. UI Logic
+	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
 		}
 		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
+	render_ui()
+
+	// 3.3 Commit
+	sg.begin_pass({action = state.pass_action, swapchain = sglue.swapchain()})
+	sgl.draw()
+	sg.end_pass()
+	sg.commit()
+}
+
+// --- Render Helpers ---
+
+draw_cube :: proc(pos: linalg.Vector3f32, size: f32, color: 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.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
+	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)
+	}
+	sgl.end()
+}
+
+render_ui :: proc() {
 	sgl.push_pipeline()
 	sgl.load_pipeline(state.ui_pip)
 	sgl.enable_texture()
 	sgl.texture(state.atlas_view, state.atlas_smp)
 
+	// 1. Сбрасываем Проекцию
 	sgl.matrix_mode_projection()
 	sgl.push_matrix()
-	sgl.ortho(0.0, sapp.widthf(), sapp.heightf(), 0.0, -1.0, +1.0)
-	sgl.begin_quads()
+	sgl.load_identity() // <--- ВАЖНО: Сбрасываем перспективу от 3D сцены
+
+	// Настраиваем 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)
+
+	// 2. Сбрасываем ModelView (чтобы UI не вращался вместе с камерой)
+	sgl.matrix_mode_modelview()
+	sgl.push_matrix()
+	sgl.load_identity() // <--- ВАЖНО: Сбрасываем поворот камеры
 
-	current_command: ^mu.Command
-	for cmd_variant in mu.next_command_iterator(&state.mu_ctx, &current_command) {
-		#partial switch cmd in cmd_variant {
+	sgl.begin_quads()
+	cmd: ^mu.Command
+	for variant in mu.next_command_iterator(&state.mu_ctx, &cmd) {
+		#partial switch c in variant {
 		case ^mu.Command_Rect:
-			draw_rect(cmd.rect, cmd.color)
+			draw_rect(c.rect, c.color)
 		case ^mu.Command_Text:
-			draw_text(cmd.str, cmd.pos, cmd.color)
+			draw_text(c.str, c.pos, c.color)
 		case ^mu.Command_Icon:
-			draw_icon(cmd.id, cmd.rect, cmd.color)
+			draw_icon(c.id, c.rect, c.color)
 		case ^mu.Command_Clip:
 			sgl.end()
 			sgl.scissor_rect(
-				f32(cmd.rect.x),
-				f32(cmd.rect.y),
-				f32(cmd.rect.w),
-				f32(cmd.rect.h),
+				f32(c.rect.x) * state.dpi_scale,
+				f32(c.rect.y) * state.dpi_scale,
+				f32(c.rect.w) * state.dpi_scale,
+				f32(c.rect.h) * state.dpi_scale,
 				true,
 			)
 			sgl.begin_quads()
 		}
 	}
 	sgl.end()
+
+	// Восстанавливаем матрицы (pop в обратном порядке)
+	sgl.matrix_mode_modelview()
 	sgl.pop_matrix()
-	sgl.pop_pipeline()
 
-	sg.begin_pass({action = state.pass_action, swapchain = sglue.swapchain()})
-	sg.apply_pipeline(state.pip)
-	sg.apply_bindings(state.bind)
-	sg.draw(0, 3, 1)
-	sgl.draw()
-	sg.end_pass()
-	sg.commit()
+	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_text :: proc(str: string, pos: mu.Vec2, color: mu.Color) {
-	cur_pos := pos
+	p := pos
 	for char in str {
 		idx := int(char)
 		if idx > 127 do idx = 0
-		rect := mu.default_atlas[mu.DEFAULT_ATLAS_FONT + idx]
-		dst := mu.Rect{cur_pos.x, cur_pos.y, rect.w, rect.h}
-		push_quad(dst, rect, color)
-		cur_pos.x += rect.w
+		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)
+		p.x += src.w
 	}
 }
 
@@ -256,10 +447,14 @@ draw_icon :: proc(id: mu.Icon, rect: mu.Rect, color: mu.Color) {
 }
 
 push_quad :: proc(dst: mu.Rect, src: mu.Rect, color: mu.Color) {
-	u0 := f32(src.x) / f32(mu.DEFAULT_ATLAS_WIDTH)
-	v0 := f32(src.y) / f32(mu.DEFAULT_ATLAS_HEIGHT)
-	u1 := f32(src.x + src.w) / f32(mu.DEFAULT_ATLAS_WIDTH)
-	v1 := f32(src.y + src.h) / f32(mu.DEFAULT_ATLAS_HEIGHT)
+	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
+
 	x0, y0 := f32(dst.x), f32(dst.y)
 	x1, y1 := f32(dst.x + dst.w), f32(dst.y + dst.h)
 
@@ -285,9 +480,10 @@ main :: proc() {
 			event_cb = event,
 			width = 1280,
 			height = 720,
-			window_title = "Sokol + MicroUI",
+			window_title = "Sokol 3D + UI",
 			icon = {sokol_default = true},
 			logger = {func = slog.func},
+			high_dpi = true,
 		},
 	)
 }