|
3 | 3 | using ZeroElectric.Vinculum; |
4 | 4 | using Color = Clay.Color; |
5 | 5 | using Vector2 = System.Numerics.Vector2; |
| 6 | +using RayColor = ZeroElectric.Vinculum.Color; |
6 | 7 |
|
7 | 8 | // Window configuration |
8 | 9 | const int InitialWidth = 1440; |
|
193 | 194 | bool assetBrowserOpen = true; |
194 | 195 | bool consoleOpen = true; |
195 | 196 |
|
| 197 | +// ============ 3D Scene Setup ============ |
| 198 | + |
| 199 | +// Render texture for 3D viewport (resized each frame to match viewport panel) |
| 200 | +var viewportRT = Raylib.LoadRenderTexture(InitialWidth, InitialHeight); |
| 201 | +var viewportData = new ViewportTextureData { RenderTexture = viewportRT }; |
| 202 | + |
| 203 | +// Orbit camera state |
| 204 | +var cameraYaw = 45f * MathF.PI / 180f; |
| 205 | +var cameraPitch = 30f * MathF.PI / 180f; |
| 206 | +var cameraDistance = 20f; |
| 207 | +var cameraTarget = new System.Numerics.Vector3(0, 1, 0); |
| 208 | +bool viewportHovered = false; |
| 209 | +bool orbitDragging = false; |
| 210 | +bool panDragging = false; |
| 211 | +var lastMousePos = new Vector2(0, 0); |
| 212 | + |
| 213 | +// Entity 3D shape types for rendering |
| 214 | +string[] entityShapes = |
| 215 | +[ |
| 216 | + "camera", "light", "capsule", "capsule", "capsule", |
| 217 | + "plane", "cone", "cone", "cube", "sphere", "none", "none" |
| 218 | +]; |
| 219 | + |
| 220 | +System.Numerics.Vector3 CameraPosition() |
| 221 | +{ |
| 222 | + float x = cameraTarget.X + cameraDistance * MathF.Cos(cameraPitch) * MathF.Cos(cameraYaw); |
| 223 | + float y = cameraTarget.Y + cameraDistance * MathF.Sin(cameraPitch); |
| 224 | + float z = cameraTarget.Z + cameraDistance * MathF.Cos(cameraPitch) * MathF.Sin(cameraYaw); |
| 225 | + return new System.Numerics.Vector3(x, y, z); |
| 226 | +} |
| 227 | + |
196 | 228 | // Main loop |
197 | 229 | while (!Raylib.WindowShouldClose()) |
198 | 230 | { |
|
213 | 245 |
|
214 | 246 | ForwardKeyboardInput(); |
215 | 247 |
|
| 248 | + // ===== Camera Controls (when viewport is hovered) ===== |
| 249 | + var currentMousePos = new Vector2(mousePos.X, mousePos.Y); |
| 250 | + var mouseDelta = new Vector2(currentMousePos.X - lastMousePos.X, currentMousePos.Y - lastMousePos.Y); |
| 251 | + lastMousePos = currentMousePos; |
| 252 | + |
| 253 | + if (viewportHovered) |
| 254 | + { |
| 255 | + // Right-click drag to orbit |
| 256 | + if (Raylib.IsMouseButtonDown(MouseButton.MOUSE_BUTTON_RIGHT)) |
| 257 | + { |
| 258 | + if (!orbitDragging) orbitDragging = true; |
| 259 | + cameraYaw -= mouseDelta.X * 0.005f; |
| 260 | + cameraPitch += mouseDelta.Y * 0.005f; |
| 261 | + cameraPitch = Math.Clamp(cameraPitch, -1.4f, 1.4f); |
| 262 | + } |
| 263 | + else orbitDragging = false; |
| 264 | + |
| 265 | + // Middle-click drag to pan |
| 266 | + if (Raylib.IsMouseButtonDown(MouseButton.MOUSE_BUTTON_MIDDLE)) |
| 267 | + { |
| 268 | + if (!panDragging) panDragging = true; |
| 269 | + float panSpeed = cameraDistance * 0.003f; |
| 270 | + var right = new System.Numerics.Vector3(MathF.Sin(cameraYaw), 0, -MathF.Cos(cameraYaw)); |
| 271 | + cameraTarget.X -= right.X * mouseDelta.X * panSpeed; |
| 272 | + cameraTarget.Z -= right.Z * mouseDelta.X * panSpeed; |
| 273 | + cameraTarget.Y += mouseDelta.Y * panSpeed; |
| 274 | + } |
| 275 | + else panDragging = false; |
| 276 | + |
| 277 | + // Scroll to zoom |
| 278 | + if (scrollDelta.Y != 0) |
| 279 | + { |
| 280 | + cameraDistance -= scrollDelta.Y * cameraDistance * 0.1f; |
| 281 | + cameraDistance = Math.Clamp(cameraDistance, 2f, 100f); |
| 282 | + } |
| 283 | + } |
| 284 | + else |
| 285 | + { |
| 286 | + orbitDragging = false; |
| 287 | + panDragging = false; |
| 288 | + } |
| 289 | + |
| 290 | + // ===== Resize viewport render texture if needed ===== |
| 291 | + { |
| 292 | + int vpW = Math.Max(1, Raylib.GetScreenWidth()); |
| 293 | + int vpH = Math.Max(1, Raylib.GetScreenHeight()); |
| 294 | + if (viewportRT.texture.width != vpW || viewportRT.texture.height != vpH) |
| 295 | + { |
| 296 | + Raylib.UnloadRenderTexture(viewportRT); |
| 297 | + viewportRT = Raylib.LoadRenderTexture(vpW, vpH); |
| 298 | + viewportData.RenderTexture = viewportRT; |
| 299 | + } |
| 300 | + } |
| 301 | + |
| 302 | + // ===== Render 3D Scene to Texture ===== |
| 303 | + { |
| 304 | + var camPos = CameraPosition(); |
| 305 | + var camera = new Camera3D |
| 306 | + { |
| 307 | + position = camPos, |
| 308 | + target = cameraTarget, |
| 309 | + up = new System.Numerics.Vector3(0, 1, 0), |
| 310 | + fovy = 60f, |
| 311 | + Projection = CameraProjection.CAMERA_PERSPECTIVE |
| 312 | + }; |
| 313 | + |
| 314 | + Raylib.BeginTextureMode(viewportRT); |
| 315 | + Raylib.ClearBackground(new RayColor { r = 30, g = 32, b = 38, a = 255 }); |
| 316 | + Raylib.BeginMode3D(camera); |
| 317 | + |
| 318 | + // Draw grid |
| 319 | + if (editorShowGrid) |
| 320 | + { |
| 321 | + Raylib.DrawGrid(40, 1.0f); |
| 322 | + } |
| 323 | + |
| 324 | + // Draw entities |
| 325 | + for (int i = 0; i < sceneEntities.Length; i++) |
| 326 | + { |
| 327 | + if (!entityActive[i]) continue; |
| 328 | + var pos = new System.Numerics.Vector3(entityPosX[i], entityPosY[i], entityPosZ[i]); |
| 329 | + var scale = new System.Numerics.Vector3(entityScaleX[i], entityScaleY[i], entityScaleZ[i]); |
| 330 | + bool isSel = i == selectedEntity; |
| 331 | + var baseColor = entityTags[i] switch |
| 332 | + { |
| 333 | + "Player" => new RayColor { r = 50, g = 150, b = 255, a = 255 }, |
| 334 | + "Enemy" => new RayColor { r = 220, g = 60, b = 60, a = 255 }, |
| 335 | + "Environment" => new RayColor { r = 80, g = 180, b = 80, a = 255 }, |
| 336 | + "FX" => new RayColor { r = 255, g = 160, b = 40, a = 255 }, |
| 337 | + "Light" => new RayColor { r = 255, g = 240, b = 150, a = 255 }, |
| 338 | + "MainCamera" => new RayColor { r = 180, g = 180, b = 200, a = 255 }, |
| 339 | + "UI" => new RayColor { r = 150, g = 100, b = 220, a = 255 }, |
| 340 | + "Audio" => new RayColor { r = 100, g = 200, b = 200, a = 255 }, |
| 341 | + _ => new RayColor { r = 160, g = 160, b = 160, a = 255 } |
| 342 | + }; |
| 343 | + |
| 344 | + switch (entityShapes[i]) |
| 345 | + { |
| 346 | + case "capsule": |
| 347 | + Raylib.DrawCapsule( |
| 348 | + new System.Numerics.Vector3(pos.X, pos.Y, pos.Z), |
| 349 | + new System.Numerics.Vector3(pos.X, pos.Y + scale.Y * 1.5f, pos.Z), |
| 350 | + 0.4f * scale.X, 8, 4, baseColor); |
| 351 | + if (isSel) |
| 352 | + Raylib.DrawCapsuleWires( |
| 353 | + new System.Numerics.Vector3(pos.X, pos.Y, pos.Z), |
| 354 | + new System.Numerics.Vector3(pos.X, pos.Y + scale.Y * 1.5f, pos.Z), |
| 355 | + 0.42f * scale.X, 8, 4, Raylib.YELLOW); |
| 356 | + break; |
| 357 | + case "cube": |
| 358 | + Raylib.DrawCube(pos, scale.X, scale.Y, scale.Z, baseColor); |
| 359 | + if (isSel) Raylib.DrawCubeWires(pos, scale.X + 0.05f, scale.Y + 0.05f, scale.Z + 0.05f, Raylib.YELLOW); |
| 360 | + break; |
| 361 | + case "sphere": |
| 362 | + Raylib.DrawSphere(pos, 0.5f * scale.X, baseColor); |
| 363 | + if (isSel) Raylib.DrawSphereWires(pos, 0.52f * scale.X, 12, 12, Raylib.YELLOW); |
| 364 | + break; |
| 365 | + case "plane": |
| 366 | + Raylib.DrawPlane(pos, new Vector2(scale.X, scale.Z), baseColor); |
| 367 | + if (isSel) Raylib.DrawCubeWires(pos, scale.X, 0.02f, scale.Z, Raylib.YELLOW); |
| 368 | + break; |
| 369 | + case "cone": |
| 370 | + Raylib.DrawCylinder(pos, 0, 0.6f * scale.X, scale.Y * 2f, 8, baseColor); |
| 371 | + if (isSel) Raylib.DrawCylinderWires(pos, 0, 0.62f * scale.X, scale.Y * 2f + 0.04f, 8, Raylib.YELLOW); |
| 372 | + break; |
| 373 | + case "light": |
| 374 | + // Draw a small sphere for light |
| 375 | + Raylib.DrawSphere(pos, 0.3f, baseColor); |
| 376 | + if (isSel) Raylib.DrawSphereWires(pos, 0.32f, 8, 8, Raylib.YELLOW); |
| 377 | + // Draw light direction line |
| 378 | + Raylib.DrawLine3D(pos, new System.Numerics.Vector3(pos.X, pos.Y - 3, pos.Z), baseColor); |
| 379 | + break; |
| 380 | + case "camera": |
| 381 | + // Draw camera as a small wireframe box |
| 382 | + Raylib.DrawCube(pos, 0.6f, 0.4f, 0.8f, baseColor); |
| 383 | + Raylib.DrawCubeWires(pos, 0.6f, 0.4f, 0.8f, isSel ? Raylib.YELLOW : Raylib.DARKGRAY); |
| 384 | + // Lens cone |
| 385 | + Raylib.DrawCylinder( |
| 386 | + new System.Numerics.Vector3(pos.X, pos.Y, pos.Z - 0.5f), |
| 387 | + 0.15f, 0.3f, 0.3f, 6, baseColor); |
| 388 | + break; |
| 389 | + } |
| 390 | + |
| 391 | + // Draw gizmo for selected entity |
| 392 | + if (isSel && editorShowGizmos) |
| 393 | + { |
| 394 | + float gizLen = 1.5f; |
| 395 | + // X axis - red |
| 396 | + Raylib.DrawLine3D(pos, new System.Numerics.Vector3(pos.X + gizLen, pos.Y, pos.Z), |
| 397 | + new RayColor { r = 230, g = 50, b = 50, a = 255 }); |
| 398 | + // Y axis - green |
| 399 | + Raylib.DrawLine3D(pos, new System.Numerics.Vector3(pos.X, pos.Y + gizLen, pos.Z), |
| 400 | + new RayColor { r = 50, g = 230, b = 50, a = 255 }); |
| 401 | + // Z axis - blue |
| 402 | + Raylib.DrawLine3D(pos, new System.Numerics.Vector3(pos.X, pos.Y, pos.Z + gizLen), |
| 403 | + new RayColor { r = 50, g = 100, b = 230, a = 255 }); |
| 404 | + } |
| 405 | + } |
| 406 | + |
| 407 | + Raylib.EndMode3D(); |
| 408 | + |
| 409 | + // Draw viewport overlay text |
| 410 | + Raylib.DrawText($"FPS: {Raylib.GetFPS()}", 10, 10, |
| 411 | + 16, new RayColor { r = 200, g = 200, b = 200, a = 180 }); |
| 412 | + |
| 413 | + if (isPlaying) |
| 414 | + { |
| 415 | + Raylib.DrawText("PLAY MODE", viewportRT.texture.width / 2 - 60, 10, |
| 416 | + 20, new RayColor { r = 60, g = 200, b = 80, a = 220 }); |
| 417 | + if (isPaused) |
| 418 | + Raylib.DrawText("PAUSED", viewportRT.texture.width / 2 - 40, 34, |
| 419 | + 16, new RayColor { r = 230, g = 180, b = 50, a = 220 }); |
| 420 | + } |
| 421 | + |
| 422 | + Raylib.EndTextureMode(); |
| 423 | + } |
| 424 | + |
216 | 425 | // ===== Root Layout ===== |
217 | 426 | ClayUI.BeginVertical(gap: 0, style: new LayoutStyle |
218 | 427 | { |
|
292 | 501 | Raylib.EndDrawing(); |
293 | 502 | } |
294 | 503 |
|
| 504 | +Raylib.UnloadRenderTexture(viewportRT); |
295 | 505 | Clay.Clay.Shutdown(); |
296 | 506 | Raylib.CloseWindow(); |
297 | 507 |
|
@@ -604,48 +814,20 @@ void RenderHierarchyContent() |
604 | 814 |
|
605 | 815 | void RenderViewportContent() |
606 | 816 | { |
607 | | - // Viewport content area (simulated 3D view) |
608 | | - ClayUI.Spacer(); |
609 | | - |
610 | | - // Center info overlay |
611 | | - ClayUI.BeginHorizontal(alignment: ChildAlignment.Center, style: new LayoutStyle |
| 817 | + // Emit a Custom element that fills the viewport — the renderer will blit the 3D texture here |
| 818 | + var vpId = Clay.Clay.Id("ViewportTexture"); |
| 819 | + using (Clay.Clay.Element(new ElementDeclaration |
612 | 820 | { |
613 | | - Sizing = new Sizing(SizingAxis.Grow(), SizingAxis.Fit()) |
614 | | - }); |
615 | | - ClayUI.BeginVertical(gap: 8, alignment: ChildAlignment.Center, style: new LayoutStyle |
616 | | - { |
617 | | - Padding = Padding.All(20), |
618 | | - BackgroundColor = Color.Rgba(0, 0, 0, 80), |
619 | | - CornerRadius = CornerRadius.All(8), |
620 | | - ClipContent = true |
621 | | - }); |
622 | | - |
623 | | - if (isPlaying) |
624 | | - { |
625 | | - ClayUI.Heading("PLAY MODE", new HeadingStyle { TextColor = colSuccess, FontSize = 18 }); |
626 | | - ClayUI.Label($"Scene: MainScene | Time: {playTime:F2}s | FPS: {Raylib.GetFPS()}", new LabelStyle { TextColor = colText, FontSize = 13 }); |
627 | | - if (isPaused) |
628 | | - ClayUI.Label("PAUSED", new LabelStyle { TextColor = colWarning, FontSize = 14 }); |
629 | | - } |
630 | | - else |
| 821 | + Id = vpId, |
| 822 | + Layout = new LayoutConfig { Sizing = Sizing.Fill() }, |
| 823 | + Custom = CustomConfig.Create(viewportData) |
| 824 | + })) |
631 | 825 | { |
632 | | - ClayUI.Label("3D Scene Viewport", new LabelStyle { TextColor = colTextDim, FontSize = 14 }); |
633 | | - ClayUI.Label($"Selected: {sceneEntities[selectedEntity]} | FPS: {Raylib.GetFPS()}", new LabelStyle { TextColor = colText, FontSize = 13 }); |
634 | | - ClayUI.Label($"Tool: {(gizmoMode == 0 ? "Move" : gizmoMode == 1 ? "Rotate" : "Scale")} ({(gizmoLocal ? "Local" : "Global")})", new LabelStyle { TextColor = colTextDim, FontSize = 12 }); |
| 826 | + // No children — the Custom render handler draws the 3D scene texture |
635 | 827 | } |
636 | 828 |
|
637 | | - ClayUI.EndVertical(); |
638 | | - ClayUI.EndHorizontal(); |
639 | | - |
640 | | - ClayUI.Spacer(); |
641 | | - |
642 | | - // Bottom-left camera info |
643 | | - ClayUI.BeginHorizontal(style: new LayoutStyle |
644 | | - { |
645 | | - Padding = Padding.Symmetric(8, 4) |
646 | | - }); |
647 | | - ClayUI.Label("Persp | Free Camera | FOV: 60", new LabelStyle { TextColor = Color.Rgba(100, 100, 110), FontSize = 11 }); |
648 | | - ClayUI.EndHorizontal(); |
| 829 | + // Track hover state for camera controls |
| 830 | + viewportHovered = Clay.Clay.PointerOver(vpId); |
649 | 831 | } |
650 | 832 |
|
651 | 833 | // ============ Inspector Panel ============ |
|
0 commit comments