From 66d9f428b3340269528805c5848dd5e27573d464 Mon Sep 17 00:00:00 2001 From: yhh <359807859@qq.com> Date: Mon, 22 Dec 2025 12:40:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=203D=20=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20-=20=E7=BD=91=E6=A0=BC=E3=80=81=E7=9B=B8?= =?UTF-8?q?=E6=9C=BA=E3=80=81Gizmo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/EngineBridge.ts | 207 +++++++ .../ecs-engine-bindgen/src/types/index.ts | 52 +- .../src/wasm/es_engine.d.ts | 149 ++++- .../editor-app/src/components/Viewport.tsx | 238 +++++++- .../editor-app/src/services/EngineService.ts | 127 ++++ packages/engine/src/backend/webgl2.rs | 76 +++ packages/engine/src/core/engine.rs | 349 +++++++++-- packages/engine/src/core/mod.rs | 2 +- packages/engine/src/lib.rs | 163 +++++ packages/engine/src/renderer/batch/mod.rs | 6 + .../engine/src/renderer/batch/vertex3d.rs | 147 +++++ packages/engine/src/renderer/camera3d.rs | 414 +++++++++++++ packages/engine/src/renderer/gizmo3d.rs | 577 ++++++++++++++++++ packages/engine/src/renderer/grid3d.rs | 450 ++++++++++++++ packages/engine/src/renderer/mod.rs | 18 +- packages/engine/src/renderer/renderer3d.rs | 481 +++++++++++++++ .../engine/src/renderer/shader/builtin.rs | 233 +++++++ packages/engine/src/renderer/shader/mod.rs | 7 +- 18 files changed, 3646 insertions(+), 50 deletions(-) create mode 100644 packages/engine/src/renderer/batch/vertex3d.rs create mode 100644 packages/engine/src/renderer/camera3d.rs create mode 100644 packages/engine/src/renderer/gizmo3d.rs create mode 100644 packages/engine/src/renderer/grid3d.rs create mode 100644 packages/engine/src/renderer/renderer3d.rs diff --git a/packages/ecs-engine-bindgen/src/core/EngineBridge.ts b/packages/ecs-engine-bindgen/src/core/EngineBridge.ts index 680dfcbd..f618f636 100644 --- a/packages/ecs-engine-bindgen/src/core/EngineBridge.ts +++ b/packages/ecs-engine-bindgen/src/core/EngineBridge.ts @@ -652,6 +652,24 @@ export class EngineBridge implements ITextureEngineBridge, ITextureService, IDyn this.getEngine().setTransformMode(mode); } + /** + * Render a 3D gizmo at the specified world position. + * 在指定的世界位置渲染 3D Gizmo。 + * + * Only works in 3D render mode. The gizmo will be rendered + * with the current transform mode (move/rotate/scale). + * 仅在 3D 渲染模式下有效。Gizmo 将使用当前的变换模式渲染。 + * + * @param x - World X position | 世界 X 坐标 + * @param y - World Y position | 世界 Y 坐标 + * @param z - World Z position | 世界 Z 坐标 + * @param scale - Gizmo scale multiplier | Gizmo 缩放倍数 + */ + render3DGizmo(x: number, y: number, z: number, scale: number = 1.0): void { + if (!this.initialized) return; + this.getEngine().render3DGizmo(x, y, z, scale); + } + /** * Set gizmo visibility. * 设置辅助工具可见性。 @@ -1334,6 +1352,195 @@ export class EngineBridge implements ITextureEngineBridge, ITextureService, IDyn } } + // ===== 3D Rendering API ===== + // ===== 3D 渲染 API ===== + + /** + * Render mode enumeration. + * 渲染模式枚举。 + */ + static readonly RenderMode = { + Mode2D: 0, + Mode3D: 1, + } as const; + + /** + * Get current render mode. + * 获取当前渲染模式。 + * + * @returns 0 for 2D mode, 1 for 3D mode | 0 表示 2D 模式,1 表示 3D 模式 + */ + getRenderMode(): number { + if (!this.initialized) return 0; + return this.getEngine().getRenderMode(); + } + + /** + * Set render mode. + * 设置渲染模式。 + * + * When switching to 3D mode for the first time, the 3D renderer + * will be lazily initialized. + * 首次切换到 3D 模式时,3D 渲染器将被延迟初始化。 + * + * @param mode - 0 for 2D, 1 for 3D | 0 表示 2D,1 表示 3D + */ + setRenderMode(mode: number): void { + if (!this.initialized) return; + this.getEngine().setRenderMode(mode); + } + + /** + * Check if 3D renderer is initialized. + * 检查 3D 渲染器是否已初始化。 + */ + has3DRenderer(): boolean { + if (!this.initialized) return false; + return this.getEngine().has3DRenderer(); + } + + /** + * Get 3D camera position. + * 获取 3D 相机位置。 + * + * @returns { x, y, z } or null if 3D renderer is not initialized + * { x, y, z } 或 3D 渲染器未初始化则返回 null + */ + getCamera3DPosition(): { x: number; y: number; z: number } | null { + if (!this.initialized) return null; + const result = this.getEngine().getCamera3DPosition(); + if (!result) return null; + return { x: result[0], y: result[1], z: result[2] }; + } + + /** + * Set 3D camera position. + * 设置 3D 相机位置。 + * + * @param x - X coordinate | X 坐标 + * @param y - Y coordinate | Y 坐标 + * @param z - Z coordinate | Z 坐标 + */ + setCamera3DPosition(x: number, y: number, z: number): void { + if (!this.initialized) return; + this.getEngine().setCamera3DPosition(x, y, z); + } + + /** + * Get 3D camera rotation as Euler angles (in degrees). + * 获取 3D 相机旋转的欧拉角(角度制)。 + * + * @returns { pitch, yaw, roll } in degrees or null + * 角度制的 { pitch, yaw, roll } 或 null + */ + getCamera3DRotation(): { pitch: number; yaw: number; roll: number } | null { + if (!this.initialized) return null; + const result = this.getEngine().getCamera3DRotation(); + if (!result) return null; + return { pitch: result[0], yaw: result[1], roll: result[2] }; + } + + /** + * Set 3D camera rotation from Euler angles (in degrees). + * 使用欧拉角设置 3D 相机旋转(角度制)。 + * + * @param pitch - Pitch angle in degrees | 俯仰角(度) + * @param yaw - Yaw angle in degrees | 偏航角(度) + * @param roll - Roll angle in degrees | 滚转角(度) + */ + setCamera3DRotation(pitch: number, yaw: number, roll: number): void { + if (!this.initialized) return; + this.getEngine().setCamera3DRotation(pitch, yaw, roll); + } + + /** + * Get 3D camera field of view (in degrees). + * 获取 3D 相机视野角(角度制)。 + * + * @returns FOV in degrees or null if 3D renderer not initialized + * 角度制的 FOV 或 null + */ + getCamera3DFov(): number | null { + if (!this.initialized) return null; + const result = this.getEngine().getCamera3DFov(); + return result ?? null; + } + + /** + * Set 3D camera field of view (in degrees). + * 设置 3D 相机视野角(角度制)。 + * + * @param fov - Field of view in degrees (typical: 45-90) + * 视野角(度,通常 45-90) + */ + setCamera3DFov(fov: number): void { + if (!this.initialized) return; + this.getEngine().setCamera3DFov(fov); + } + + /** + * Set 3D camera projection type. + * 设置 3D 相机投影类型。 + * + * @param type - 0 for perspective, 1 for orthographic + * 0 表示透视投影,1 表示正交投影 + * @param orthoSize - Half-height of orthographic view (only used when type = 1) + * 正交视图的半高度(仅在 type = 1 时使用) + */ + setCamera3DProjection(type: number, orthoSize: number = 5.0): void { + if (!this.initialized) return; + this.getEngine().setCamera3DProjection(type, orthoSize); + } + + /** + * Make 3D camera look at a target position. + * 使 3D 相机朝向目标位置。 + * + * @param targetX - Target X coordinate | 目标 X 坐标 + * @param targetY - Target Y coordinate | 目标 Y 坐标 + * @param targetZ - Target Z coordinate | 目标 Z 坐标 + */ + camera3DLookAt(targetX: number, targetY: number, targetZ: number): void { + if (!this.initialized) return; + this.getEngine().camera3DLookAt(targetX, targetY, targetZ); + } + + /** + * Set 3D camera near and far clip planes. + * 设置 3D 相机近远裁剪面。 + * + * @param near - Near clip plane distance | 近裁剪面距离 + * @param far - Far clip plane distance | 远裁剪面距离 + */ + setCamera3DClipPlanes(near: number, far: number): void { + if (!this.initialized) return; + this.getEngine().setCamera3DClipPlanes(near, far); + } + + /** + * Resize 3D viewport. + * 调整 3D 视口大小。 + * + * @param width - New width | 新宽度 + * @param height - New height | 新高度 + */ + resize3D(width: number, height: number): void { + if (!this.initialized) return; + this.getEngine().resize3D(width, height); + } + + /** + * Render using 3D mode. + * 使用 3D 模式渲染。 + * + * This renders all submitted 3D meshes with the current 3D camera. + * 使用当前 3D 相机渲染所有已提交的 3D 网格。 + */ + render3D(): void { + if (!this.initialized) return; + this.getEngine().render3D(); + } + /** * Dispose the bridge and release resources. * 销毁桥接并释放资源。 diff --git a/packages/ecs-engine-bindgen/src/types/index.ts b/packages/ecs-engine-bindgen/src/types/index.ts index ef0856f0..5b7e4048 100644 --- a/packages/ecs-engine-bindgen/src/types/index.ts +++ b/packages/ecs-engine-bindgen/src/types/index.ts @@ -88,8 +88,8 @@ export interface EngineStats { } /** - * Camera configuration. - * 相机配置。 + * Camera configuration (2D). + * 相机配置(2D)。 */ export interface CameraConfig { /** Camera X position. | 相机X位置。 */ @@ -101,3 +101,51 @@ export interface CameraConfig { /** Rotation in radians. | 旋转角度(弧度)。 */ rotation: number; } + +/** + * 3D Camera position. + * 3D 相机位置。 + */ +export interface Camera3DPosition { + /** X coordinate. | X 坐标。 */ + x: number; + /** Y coordinate. | Y 坐标。 */ + y: number; + /** Z coordinate. | Z 坐标。 */ + z: number; +} + +/** + * 3D Camera rotation (Euler angles in degrees). + * 3D 相机旋转(欧拉角,角度制)。 + */ +export interface Camera3DRotation { + /** Pitch angle in degrees. | 俯仰角(度)。 */ + pitch: number; + /** Yaw angle in degrees. | 偏航角(度)。 */ + yaw: number; + /** Roll angle in degrees. | 滚转角(度)。 */ + roll: number; +} + +/** + * Projection type for 3D camera. + * 3D 相机的投影类型。 + */ +export enum ProjectionType { + /** Perspective projection. | 透视投影。 */ + Perspective = 0, + /** Orthographic projection. | 正交投影。 */ + Orthographic = 1, +} + +/** + * Render mode. + * 渲染模式。 + */ +export enum RenderMode { + /** 2D rendering mode. | 2D 渲染模式。 */ + Mode2D = 0, + /** 3D rendering mode. | 3D 渲染模式。 */ + Mode3D = 1, +} diff --git a/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts b/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts index 58278545..74879ef8 100644 --- a/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts +++ b/packages/ecs-engine-bindgen/src/wasm/es_engine.d.ts @@ -5,6 +5,22 @@ * 初始化panic hook以在控制台显示更好的错误信息。 */ export function init(): void; +/** + * Render mode enumeration. + * 渲染模式枚举。 + */ +export enum RenderMode { + /** + * 2D rendering mode (orthographic camera, no depth test). + * 2D渲染模式(正交相机,无深度测试)。 + */ + Mode2D = 0, + /** + * 3D rendering mode (perspective/orthographic camera, depth test enabled). + * 3D渲染模式(透视/正交相机,启用深度测试)。 + */ + Mode3D = 1, +} /** * Game engine main interface exposed to JavaScript. * 暴露给JavaScript的游戏引擎主接口。 @@ -143,11 +159,37 @@ export class GameEngine { * The material ID for referencing this material | 用于引用此材质的ID */ createMaterial(name: string, shader_id: number, blend_mode: number): number; + /** + * Get current render mode. + * 获取当前渲染模式。 + * + * Returns 0 for 2D mode, 1 for 3D mode. + * 返回 0 表示 2D 模式,1 表示 3D 模式。 + */ + getRenderMode(): number; + /** + * Check if 3D renderer is initialized. + * 检查 3D 渲染器是否已初始化。 + */ + has3DRenderer(): boolean; /** * Remove a material. * 移除材质。 */ removeMaterial(material_id: number): boolean; + /** + * Render a 3D gizmo at the specified world position. + * 在指定的世界位置渲染 3D Gizmo。 + * + * Only works in 3D render mode. The gizmo will be rendered + * with the current transform mode (move/rotate/scale). + * 仅在 3D 渲染模式下有效。Gizmo 将使用当前的变换模式渲染。 + * + * # Arguments | 参数 + * * `x`, `y`, `z` - World position | 世界位置 + * * `scale` - Gizmo scale multiplier | Gizmo 缩放倍数 + */ + render3DGizmo(x: number, y: number, z: number, scale: number): void; /** * Resize a specific viewport. * 调整特定视口大小。 @@ -182,6 +224,18 @@ export class GameEngine { * 当为 false(运行时模式)时,编辑器专用 UI(如网格、gizmos、坐标轴指示器)会自动隐藏。 */ setEditorMode(is_editor: boolean): void; + /** + * Set render mode. + * 设置渲染模式。 + * + * # Arguments | 参数 + * * `mode` - 0 for 2D, 1 for 3D | 0 表示 2D,1 表示 3D + * + * When switching to 3D mode for the first time, the 3D renderer + * will be lazily initialized. + * 首次切换到 3D 模式时,3D 渲染器将被延迟初始化。 + */ + setRenderMode(mode: number): void; /** * Set gizmo visibility. * 设置辅助工具可见性。 @@ -241,6 +295,16 @@ export class GameEngine { * 添加胶囊Gizmo边框。 */ addGizmoCapsule(x: number, y: number, radius: number, half_height: number, rotation: number, r: number, g: number, b: number, a: number): void; + /** + * Make 3D camera look at a target position. + * 使 3D 相机朝向目标位置。 + */ + camera3DLookAt(target_x: number, target_y: number, target_z: number): void; + /** + * Get 3D camera field of view (in degrees). + * 获取 3D 相机视野角(角度制)。 + */ + getCamera3DFov(): number | undefined; /** * 获取纹理加载状态 * Get texture loading state @@ -262,6 +326,14 @@ export class GameEngine { * * `canvas_id` - HTML canvas element ID | HTML canvas元素ID */ registerViewport(id: string, canvas_id: string): void; + /** + * Set 3D camera field of view (in degrees). + * 设置 3D 相机视野角(角度制)。 + * + * Only affects perspective projection mode. + * 仅影响透视投影模式。 + */ + setCamera3DFov(fov_degrees: number): void; /** * Set a material's vec2 uniform. * 设置材质的vec2 uniform。 @@ -447,6 +519,22 @@ export class GameEngine { * 使用特定ID编译着色器。 */ compileShaderWithId(shader_id: number, vertex_source: string, fragment_source: string): void; + /** + * Get 3D camera position. + * 获取 3D 相机位置。 + * + * Returns [x, y, z] or null if 3D renderer is not initialized. + * 返回 [x, y, z],如果 3D 渲染器未初始化则返回 null。 + */ + getCamera3DPosition(): Float32Array | undefined; + /** + * Get 3D camera rotation as Euler angles (in degrees). + * 获取 3D 相机旋转的欧拉角(角度制)。 + * + * Returns [pitch, yaw, roll] or null if 3D renderer is not initialized. + * 返回 [pitch, yaw, roll],如果 3D 渲染器未初始化则返回 null。 + */ + getCamera3DRotation(): Float32Array | undefined; /** * Get texture ID by path. * 按路径获取纹理ID。 @@ -455,6 +543,21 @@ export class GameEngine { * * `path` - Image path to lookup | 要查找的图片路径 */ getTextureIdByPath(path: string): number | undefined; + /** + * Set 3D camera position. + * 设置 3D 相机位置。 + */ + setCamera3DPosition(x: number, y: number, z: number): void; + /** + * Set 3D camera rotation using Euler angles (in degrees). + * 使用欧拉角设置 3D 相机旋转(角度制)。 + * + * # Arguments | 参数 + * * `pitch` - Rotation around X axis (degrees) | X 轴旋转(角度) + * * `yaw` - Rotation around Y axis (degrees) | Y 轴旋转(角度) + * * `roll` - Rotation around Z axis (degrees) | Z 轴旋转(角度) + */ + setCamera3DRotation(pitch: number, yaw: number, roll: number): void; /** * Create a material with a specific ID. * 使用特定ID创建材质。 @@ -488,11 +591,25 @@ export class GameEngine { * * `path` - Image path to lookup | 要查找的图片路径 */ getTextureSizeByPath(path: string): Float32Array | undefined; + /** + * Set 3D camera projection type. + * 设置 3D 相机投影类型。 + * + * # Arguments | 参数 + * * `projection_type` - 0 for Perspective, 1 for Orthographic + * * `ortho_size` - Half-height of orthographic view (only used when projection_type = 1) + */ + setCamera3DProjection(projection_type: number, ortho_size: number): void; /** * 获取正在加载中的纹理数量 * Get the number of textures currently loading */ getTextureLoadingCount(): number; + /** + * Set 3D camera near and far clip planes. + * 设置 3D 相机近裁剪面和远裁剪面。 + */ + setCamera3DClipPlanes(near: number, far: number): void; /** * Create a new game engine instance. * 创建新的游戏引擎实例。 @@ -529,6 +646,19 @@ export class GameEngine { * * `height` - New viewport height | 新视口高度 */ resize(width: number, height: number): void; + /** + * Render 3D content. + * 渲染 3D 内容。 + * + * Should be called after submitting 3D meshes. + * 应在提交 3D 网格后调用。 + */ + render3D(): void; + /** + * Resize 3D renderer viewport. + * 调整 3D 渲染器视口大小。 + */ + resize3D(width: number, height: number): void; /** * Get canvas width. * 获取画布宽度。 @@ -550,6 +680,7 @@ export interface InitOutput { readonly gameengine_addGizmoCircle: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void; readonly gameengine_addGizmoLine: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void; readonly gameengine_addGizmoRect: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number) => void; + readonly gameengine_camera3DLookAt: (a: number, b: number, c: number, d: number) => void; readonly gameengine_clear: (a: number, b: number, c: number, d: number, e: number) => void; readonly gameengine_clearAllTextures: (a: number) => void; readonly gameengine_clearScissorRect: (a: number) => void; @@ -563,14 +694,19 @@ export interface InitOutput { readonly gameengine_getBackendName: (a: number) => [number, number]; readonly gameengine_getBackendVersion: (a: number) => [number, number]; readonly gameengine_getCamera: (a: number) => [number, number]; + readonly gameengine_getCamera3DFov: (a: number) => number; + readonly gameengine_getCamera3DPosition: (a: number) => [number, number]; + readonly gameengine_getCamera3DRotation: (a: number) => [number, number]; readonly gameengine_getMaxTextureSize: (a: number) => number; readonly gameengine_getOrLoadTextureByPath: (a: number, b: number, c: number) => [number, number, number]; + readonly gameengine_getRenderMode: (a: number) => number; readonly gameengine_getTextureIdByPath: (a: number, b: number, c: number) => number; readonly gameengine_getTextureLoadingCount: (a: number) => number; readonly gameengine_getTextureSizeByPath: (a: number, b: number, c: number) => any; readonly gameengine_getTextureState: (a: number, b: number) => [number, number]; readonly gameengine_getViewportCamera: (a: number, b: number, c: number) => [number, number]; readonly gameengine_getViewportIds: (a: number) => [number, number]; + readonly gameengine_has3DRenderer: (a: number) => number; readonly gameengine_hasMaterial: (a: number, b: number) => number; readonly gameengine_hasShader: (a: number, b: number) => number; readonly gameengine_height: (a: number) => number; @@ -584,13 +720,21 @@ export interface InitOutput { readonly gameengine_removeMaterial: (a: number, b: number) => number; readonly gameengine_removeShader: (a: number, b: number) => number; readonly gameengine_render: (a: number) => [number, number]; + readonly gameengine_render3D: (a: number) => [number, number]; + readonly gameengine_render3DGizmo: (a: number, b: number, c: number, d: number, e: number) => void; readonly gameengine_renderOverlay: (a: number) => [number, number]; readonly gameengine_renderToViewport: (a: number, b: number, c: number) => [number, number]; readonly gameengine_resize: (a: number, b: number, c: number) => void; + readonly gameengine_resize3D: (a: number, b: number, c: number) => void; readonly gameengine_resizeViewport: (a: number, b: number, c: number, d: number, e: number) => void; readonly gameengine_screenToWorld: (a: number, b: number, c: number) => [number, number]; readonly gameengine_setActiveViewport: (a: number, b: number, c: number) => number; readonly gameengine_setCamera: (a: number, b: number, c: number, d: number, e: number) => void; + readonly gameengine_setCamera3DClipPlanes: (a: number, b: number, c: number) => void; + readonly gameengine_setCamera3DFov: (a: number, b: number) => void; + readonly gameengine_setCamera3DPosition: (a: number, b: number, c: number, d: number) => void; + readonly gameengine_setCamera3DProjection: (a: number, b: number, c: number) => void; + readonly gameengine_setCamera3DRotation: (a: number, b: number, c: number, d: number) => void; readonly gameengine_setClearColor: (a: number, b: number, c: number, d: number, e: number) => void; readonly gameengine_setEditorMode: (a: number, b: number) => void; readonly gameengine_setMaterialBlendMode: (a: number, b: number, c: number) => number; @@ -599,6 +743,7 @@ export interface InitOutput { readonly gameengine_setMaterialVec2: (a: number, b: number, c: number, d: number, e: number, f: number) => number; readonly gameengine_setMaterialVec3: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => number; readonly gameengine_setMaterialVec4: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => number; + readonly gameengine_setRenderMode: (a: number, b: number) => [number, number]; readonly gameengine_setScissorRect: (a: number, b: number, c: number, d: number, e: number) => void; readonly gameengine_setShowGizmos: (a: number, b: number) => void; readonly gameengine_setShowGrid: (a: number, b: number) => void; @@ -614,8 +759,8 @@ export interface InitOutput { readonly gameengine_width: (a: number) => number; readonly gameengine_worldToScreen: (a: number, b: number, c: number) => [number, number]; readonly init: () => void; - readonly wasm_bindgen__convert__closures_____invoke__h0cae3d4947da04cb: (a: number, b: number) => void; - readonly wasm_bindgen__closure__destroy__h0c01365f59f73f28: (a: number, b: number) => void; + readonly wasm_bindgen__convert__closures_____invoke__h256074d77f4ce876: (a: number, b: number) => void; + readonly wasm_bindgen__closure__destroy__h14ac3db10f717d1a: (a: number, b: number) => void; readonly __wbindgen_malloc: (a: number, b: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_exn_store: (a: number) => void; diff --git a/packages/editor-app/src/components/Viewport.tsx b/packages/editor-app/src/components/Viewport.tsx index 4800ab90..11c2d086 100644 --- a/packages/editor-app/src/components/Viewport.tsx +++ b/packages/editor-app/src/components/Viewport.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useCallback } from 'react'; import { RotateCcw, Maximize2, Grid3x3, Eye, EyeOff, Activity, MousePointer2, Move, RotateCw, Scaling, Globe, QrCode, ChevronDown, - Magnet, ZoomIn, Save, X, PackageOpen + Magnet, ZoomIn, Save, X, PackageOpen, Box, Square } from 'lucide-react'; import '../styles/Viewport.css'; import { useEngine } from '../hooks/useEngine'; @@ -296,6 +296,24 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport const isDraggingCameraRef = useRef(false); const isDraggingTransformRef = useRef(false); const lastMousePosRef = useRef({ x: 0, y: 0 }); + + // 3D Camera state (orbit camera) + // 3D 相机状态(轨道相机) + const [renderMode, setRenderMode] = useState<'2D' | '3D'>('2D'); + const renderModeRef = useRef<'2D' | '3D'>('2D'); + // Orbit camera parameters: distance from target, pitch (vertical), yaw (horizontal) + // 轨道相机参数:距目标距离、俯仰角(垂直)、偏航角(水平) + const [orbitCamera, setOrbitCamera] = useState({ + distance: 10, // Distance from target | 距目标距离 + pitch: -30, // Vertical angle in degrees (-90 to 90) | 垂直角度(度) + yaw: 45, // Horizontal angle in degrees | 水平角度(度) + targetX: 0, // Target point X | 目标点 X + targetY: 0, // Target point Y | 目标点 Y + targetZ: 0 // Target point Z | 目标点 Z + }); + const orbitCameraRef = useRef(orbitCamera); + const isOrbitingRef = useRef(false); + const isPanningRef = useRef(false); const selectedEntityRef = useRef(null); const messageHubRef = useRef(null); const commandManagerRef = useRef(null); @@ -319,7 +337,9 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport gridSnapRef.current = gridSnapValue; rotationSnapRef.current = rotationSnapValue; scaleSnapRef.current = scaleSnapValue; - }, [playState, camera2DZoom, camera2DOffset, transformMode, snapEnabled, gridSnapValue, rotationSnapValue, scaleSnapValue]); + renderModeRef.current = renderMode; + orbitCameraRef.current = orbitCamera; + }, [playState, camera2DZoom, camera2DOffset, transformMode, snapEnabled, gridSnapValue, rotationSnapValue, scaleSnapValue, renderMode, orbitCamera]); // 发布 Play 状态变化事件,用于层级面板实时同步 // Publish play state change event for hierarchy panel real-time sync @@ -348,6 +368,38 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport return Math.round(value / scaleSnapRef.current) * scaleSnapRef.current; }, []); + // Calculate 3D camera position from orbit parameters + // 从轨道参数计算 3D 相机位置 + const calculateOrbitCameraPosition = useCallback((orbit: typeof orbitCamera) => { + const pitchRad = (orbit.pitch * Math.PI) / 180; + const yawRad = (orbit.yaw * Math.PI) / 180; + + // Convert spherical to Cartesian coordinates + // 将球坐标转换为笛卡尔坐标 + // Note: negative pitch means looking down, so camera should be above target + // 注意:负的 pitch 表示向下看,所以相机应该在目标上方 + const x = orbit.targetX + orbit.distance * Math.cos(pitchRad) * Math.sin(yawRad); + const y = orbit.targetY - orbit.distance * Math.sin(pitchRad); // Negate for correct direction + const z = orbit.targetZ + orbit.distance * Math.cos(pitchRad) * Math.cos(yawRad); + + return { x, y, z }; + }, []); + + // Sync orbit camera to engine + // 同步轨道相机到引擎 + const syncOrbitCameraToEngine = useCallback((orbit: typeof orbitCamera) => { + const pos = calculateOrbitCameraPosition(orbit); + const engineService = EngineService.getInstance(); + + // Set camera position + // 设置相机位置 + engineService.setCamera3DPosition(pos.x, pos.y, pos.z); + + // Make camera look at target + // 让相机看向目标 + engineService.camera3DLookAt(orbit.targetX, orbit.targetY, orbit.targetZ); + }, [calculateOrbitCameraPosition]); + // Screen to world coordinate conversion - uses refs to avoid re-registering event handlers const screenToWorld = useCallback((screenX: number, screenY: number) => { const canvas = canvasRef.current; @@ -438,6 +490,30 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport return; } + // 3D mode: orbit camera controls + // 3D 模式:轨道相机控制 + if (renderModeRef.current === '3D') { + if (e.button === 0) { + // Left button: orbit (rotate around target) + // 左键:轨道旋转(围绕目标旋转) + isOrbitingRef.current = true; + lastMousePosRef.current = { x: e.clientX, y: e.clientY }; + canvas.style.cursor = 'grabbing'; + e.preventDefault(); + } else if (e.button === 1 || e.button === 2) { + // Middle/Right button: pan + // 中键/右键:平移 + isPanningRef.current = true; + lastMousePosRef.current = { x: e.clientX, y: e.clientY }; + canvas.style.cursor = 'move'; + e.preventDefault(); + } + return; + } + + // 2D mode: original camera controls + // 2D 模式:原始相机控制 + // Middle mouse button (1) or right button (2) for camera pan if (e.button === 1 || e.button === 2) { isDraggingCameraRef.current = true; @@ -532,6 +608,54 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport const deltaX = e.clientX - lastMousePosRef.current.x; const deltaY = e.clientY - lastMousePosRef.current.y; + // 3D mode: orbit camera controls + // 3D 模式:轨道相机控制 + if (renderModeRef.current === '3D') { + if (isOrbitingRef.current) { + // Orbit: rotate around target + // 轨道:围绕目标旋转 + const orbitSensitivity = 0.3; + setOrbitCamera((prev) => { + const newYaw = prev.yaw + deltaX * orbitSensitivity; + const newPitch = Math.max(-89, Math.min(89, prev.pitch - deltaY * orbitSensitivity)); + const newOrbit = { ...prev, yaw: newYaw, pitch: newPitch }; + // Sync to engine in next tick + // 在下一帧同步到引擎 + requestAnimationFrame(() => syncOrbitCameraToEngine(newOrbit)); + return newOrbit; + }); + lastMousePosRef.current = { x: e.clientX, y: e.clientY }; + } else if (isPanningRef.current) { + // Pan: move target point + // 平移:移动目标点 + const panSensitivity = 0.01; + const orbit = orbitCameraRef.current; + const yawRad = (orbit.yaw * Math.PI) / 180; + + // Calculate pan direction based on camera orientation + // 根据相机朝向计算平移方向 + const rightX = Math.cos(yawRad); + const rightZ = -Math.sin(yawRad); + + setOrbitCamera((prev) => { + const panScale = prev.distance * panSensitivity; + const newOrbit = { + ...prev, + targetX: prev.targetX - deltaX * rightX * panScale, + targetY: prev.targetY + deltaY * panScale, + targetZ: prev.targetZ - deltaX * rightZ * panScale + }; + requestAnimationFrame(() => syncOrbitCameraToEngine(newOrbit)); + return newOrbit; + }); + lastMousePosRef.current = { x: e.clientX, y: e.clientY }; + } + return; + } + + // 2D mode: original camera controls + // 2D 模式:原始相机控制 + if (isDraggingCameraRef.current) { // Camera pan - use ref to avoid stale closure const dpr = window.devicePixelRatio || 1; @@ -625,6 +749,19 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport }; const handleMouseUp = () => { + // 3D mode: reset orbit/pan flags + // 3D 模式:重置轨道/平移标志 + if (isOrbitingRef.current) { + isOrbitingRef.current = false; + canvas.style.cursor = 'grab'; + } + if (isPanningRef.current) { + isPanningRef.current = false; + canvas.style.cursor = 'grab'; + } + + // 2D mode: original mouse up handling + // 2D 模式:原始鼠标抬起处理 if (isDraggingCameraRef.current) { isDraggingCameraRef.current = false; canvas.style.cursor = 'grab'; @@ -698,6 +835,22 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport if (playStateRef.current === 'playing') { return; } + + // 3D mode: zoom by changing distance + // 3D 模式:通过改变距离来缩放 + if (renderModeRef.current === '3D') { + const zoomFactor = e.deltaY > 0 ? 1.1 : 0.9; + setOrbitCamera((prev) => { + const newDistance = Math.max(1, Math.min(1000, prev.distance * zoomFactor)); + const newOrbit = { ...prev, distance: newDistance }; + requestAnimationFrame(() => syncOrbitCameraToEngine(newOrbit)); + return newOrbit; + }); + return; + } + + // 2D mode: original zoom handling + // 2D 模式:原始缩放处理 // Use multiplicative zoom for consistent feel across all zoom levels // 使用乘法缩放,在所有缩放级别都有一致的感觉 const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; @@ -748,13 +901,52 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport } }, [camera2DOffset, camera2DZoom, engine.state.initialized]); + // Sync render mode and 3D camera to engine + // 同步渲染模式和 3D 相机到引擎 + useEffect(() => { + if (engine.state.initialized) { + const engineService = EngineService.getInstance(); + const mode = renderMode === '3D' ? 1 : 0; + + engineService.setRenderMode(mode); + + if (renderMode === '3D') { + // Initialize 3D camera when switching to 3D mode + // 切换到 3D 模式时初始化 3D 相机 + engineService.setCamera3DProjection(0); // Perspective + engineService.setCamera3DFov(60); + engineService.setCamera3DClipPlanes(0.1, 1000); + syncOrbitCameraToEngine(orbitCamera); + + // Set a different background color for 3D mode + // 为 3D 模式设置不同的背景色 + engineService.setClearColor(0.15, 0.15, 0.2, 1.0); + } else { + // Restore 2D mode background color + // 恢复 2D 模式背景色 + engineService.setClearColor(0.1, 0.1, 0.12, 1.0); + } + } + }, [renderMode, engine.state.initialized, syncOrbitCameraToEngine, orbitCamera]); + + // Toggle render mode handler + // 切换渲染模式处理函数 + const handleToggleRenderMode = useCallback(() => { + setRenderMode((prev) => prev === '2D' ? '3D' : '2D'); + }, []); + // Sync grid and gizmo visibility + // Engine will use 2D or 3D grid/gizmo based on render mode + // 引擎会根据渲染模式使用 2D 或 3D 网格/gizmo useEffect(() => { if (engine.state.initialized) { EngineService.getInstance().setShowGrid(showGrid); - EngineService.getInstance().setShowGizmos(showGizmos); + // Gizmos are still 2D only for now + // Gizmo 目前仍然只有 2D 版本 + const is2D = renderMode === '2D'; + EngineService.getInstance().setShowGizmos(is2D && showGizmos); } - }, [showGrid, showGizmos, engine.state.initialized]); + }, [showGrid, showGizmos, renderMode, engine.state.initialized]); // Sync transform mode to engine useEffect(() => { @@ -1030,8 +1222,24 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport const handleReset = () => { // Reset camera to origin without stopping playback - setCamera2DOffset({ x: 0, y: 0 }); - setCamera2DZoom(1); + // 重置相机到原点,不停止播放 + if (renderMode === '3D') { + // Reset 3D orbit camera | 重置 3D 轨道相机 + const defaultOrbit = { + distance: 10, + pitch: -30, + yaw: 45, + targetX: 0, + targetY: 0, + targetZ: 0 + }; + setOrbitCamera(defaultOrbit); + syncOrbitCameraToEngine(defaultOrbit); + } else { + // Reset 2D camera | 重置 2D 相机 + setCamera2DOffset({ x: 0, y: 0 }); + setCamera2DZoom(1); + } }; // Store handlers in refs to avoid dependency issues @@ -1957,10 +2165,26 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport
+ {/* 2D/3D Mode Toggle | 2D/3D 模式切换 */} + + +
+ {/* Zoom display */}
- {Math.round(camera2DZoom * 100)}% + {renderMode === '2D' ? ( + {Math.round(camera2DZoom * 100)}% + ) : ( + {orbitCamera.distance.toFixed(1)}m + )}
diff --git a/packages/editor-app/src/services/EngineService.ts b/packages/editor-app/src/services/EngineService.ts index e6fe79de..ee785072 100644 --- a/packages/editor-app/src/services/EngineService.ts +++ b/packages/editor-app/src/services/EngineService.ts @@ -1376,6 +1376,17 @@ export class EngineService { return this._runtime?.getTransformMode() ?? 'select'; } + /** + * Render a 3D gizmo at the specified world position. + * 在指定的世界位置渲染 3D Gizmo。 + * + * Only works in 3D render mode. + * 仅在 3D 渲染模式下有效。 + */ + render3DGizmo(x: number, y: number, z: number, scale: number = 1.0): void { + this._runtime?.bridge?.render3DGizmo(x, y, z, scale); + } + // ===== Multi-viewport API ===== /** @@ -1449,6 +1460,122 @@ export class EngineService { return this._runtime; } + // ===== 3D Camera API ===== + + /** + * Get render mode (0 = 2D, 1 = 3D). + * 获取渲染模式(0 = 2D, 1 = 3D)。 + */ + getRenderMode(): number { + return this._runtime?.bridge?.getRenderMode() ?? 0; + } + + /** + * Set render mode (0 = 2D, 1 = 3D). + * 设置渲染模式(0 = 2D, 1 = 3D)。 + */ + setRenderMode(mode: number): void { + this._runtime?.bridge?.setRenderMode(mode); + } + + /** + * Check if 3D renderer is available. + * 检查 3D 渲染器是否可用。 + */ + has3DRenderer(): boolean { + return this._runtime?.bridge?.has3DRenderer() ?? false; + } + + /** + * Get 3D camera position. + * 获取 3D 相机位置。 + */ + getCamera3DPosition(): { x: number; y: number; z: number } | null { + return this._runtime?.bridge?.getCamera3DPosition() ?? null; + } + + /** + * Set 3D camera position. + * 设置 3D 相机位置。 + */ + setCamera3DPosition(x: number, y: number, z: number): void { + this._runtime?.bridge?.setCamera3DPosition(x, y, z); + } + + /** + * Get 3D camera rotation (Euler angles in degrees). + * 获取 3D 相机旋转(欧拉角,度数)。 + */ + getCamera3DRotation(): { pitch: number; yaw: number; roll: number } | null { + return this._runtime?.bridge?.getCamera3DRotation() ?? null; + } + + /** + * Set 3D camera rotation (Euler angles in degrees). + * 设置 3D 相机旋转(欧拉角,度数)。 + */ + setCamera3DRotation(pitch: number, yaw: number, roll: number): void { + this._runtime?.bridge?.setCamera3DRotation(pitch, yaw, roll); + } + + /** + * Get 3D camera field of view. + * 获取 3D 相机视场角。 + */ + getCamera3DFov(): number | null { + return this._runtime?.bridge?.getCamera3DFov() ?? null; + } + + /** + * Set 3D camera field of view. + * 设置 3D 相机视场角。 + */ + setCamera3DFov(fov: number): void { + this._runtime?.bridge?.setCamera3DFov(fov); + } + + /** + * Set 3D camera projection type. + * 设置 3D 相机投影类型。 + * @param type 0 = Perspective, 1 = Orthographic + * @param orthoSize Orthographic size (default 5.0) + */ + setCamera3DProjection(type: number, orthoSize: number = 5.0): void { + this._runtime?.bridge?.setCamera3DProjection(type, orthoSize); + } + + /** + * Make 3D camera look at a target point. + * 让 3D 相机看向目标点。 + */ + camera3DLookAt(targetX: number, targetY: number, targetZ: number): void { + this._runtime?.bridge?.camera3DLookAt(targetX, targetY, targetZ); + } + + /** + * Set 3D camera clip planes. + * 设置 3D 相机裁剪平面。 + */ + setCamera3DClipPlanes(near: number, far: number): void { + this._runtime?.bridge?.setCamera3DClipPlanes(near, far); + } + + /** + * Resize 3D renderer. + * 调整 3D 渲染器尺寸。 + */ + resize3D(width: number, height: number): void { + this._runtime?.bridge?.resize3D(width, height); + } + + /** + * Render 3D frame (for editor manual rendering). + * 渲染 3D 帧(用于编辑器手动渲染)。 + */ + render3D(): void { + this._runtime?.bridge?.render3D(); + } + /** * Dispose engine resources. */ diff --git a/packages/engine/src/backend/webgl2.rs b/packages/engine/src/backend/webgl2.rs index 6984ebf3..78784cad 100644 --- a/packages/engine/src/backend/webgl2.rs +++ b/packages/engine/src/backend/webgl2.rs @@ -723,14 +723,36 @@ impl GraphicsBackend for WebGL2Backend { // ==================== 渲染状态 | Render State ==================== fn apply_render_state(&mut self, state: &RenderState) { + // Blend mode | 混合模式 if self.current_render_state.blend_mode != state.blend_mode { self.set_blend_mode(state.blend_mode); } + // Scissor | 裁剪 if self.current_render_state.scissor != state.scissor { self.set_scissor(state.scissor); } + // Depth test | 深度测试 + if self.current_render_state.depth_test != state.depth_test { + self.set_depth_test(state.depth_test); + } + + // Depth write | 深度写入 + if self.current_render_state.depth_write != state.depth_write { + self.set_depth_write(state.depth_write); + } + + // Depth function | 深度比较函数 + if self.current_render_state.depth_func != state.depth_func { + self.set_depth_func(state.depth_func); + } + + // Cull mode | 裁剪模式 + if self.current_render_state.cull_mode != state.cull_mode { + self.set_cull_mode(state.cull_mode); + } + self.current_render_state = state.clone(); } @@ -932,6 +954,60 @@ impl WebGL2Backend { self.gl.active_texture(GL::TEXTURE0 + unit); self.gl.bind_texture(GL::TEXTURE_2D, texture); } + + /// Set depth test enabled state. + /// 设置深度测试启用状态。 + pub fn set_depth_test(&mut self, enabled: bool) { + if enabled { + self.gl.enable(GL::DEPTH_TEST); + } else { + self.gl.disable(GL::DEPTH_TEST); + } + self.current_render_state.depth_test = enabled; + } + + /// Set depth write enabled state. + /// 设置深度写入启用状态。 + pub fn set_depth_write(&mut self, enabled: bool) { + self.gl.depth_mask(enabled); + self.current_render_state.depth_write = enabled; + } + + /// Set depth comparison function. + /// 设置深度比较函数。 + pub fn set_depth_func(&mut self, func: CompareFunc) { + let gl_func = match func { + CompareFunc::Never => GL::NEVER, + CompareFunc::Less => GL::LESS, + CompareFunc::Equal => GL::EQUAL, + CompareFunc::LessEqual => GL::LEQUAL, + CompareFunc::Greater => GL::GREATER, + CompareFunc::NotEqual => GL::NOTEQUAL, + CompareFunc::GreaterEqual => GL::GEQUAL, + CompareFunc::Always => GL::ALWAYS, + }; + self.gl.depth_func(gl_func); + self.current_render_state.depth_func = func; + } + + /// Set face culling mode. + /// 设置面剔除模式。 + pub fn set_cull_mode(&mut self, mode: CullMode) { + match mode { + CullMode::None => { + self.gl.disable(GL::CULL_FACE); + } + CullMode::Front => { + self.gl.enable(GL::CULL_FACE); + self.gl.cull_face(GL::FRONT); + } + CullMode::Back => { + self.gl.enable(GL::CULL_FACE); + self.gl.cull_face(GL::BACK); + } + } + self.current_render_state.cull_mode = mode; + } } // ==================== 辅助函数 | Helper Functions ==================== diff --git a/packages/engine/src/core/engine.rs b/packages/engine/src/core/engine.rs index fd766677..15a3090e 100644 --- a/packages/engine/src/core/engine.rs +++ b/packages/engine/src/core/engine.rs @@ -7,10 +7,27 @@ use super::context::WebGLContext; use super::error::Result; use crate::backend::WebGL2Backend; use crate::input::InputManager; -use crate::renderer::{Renderer2D, GridRenderer, GizmoRenderer, TransformMode, ViewportManager, TextBatch, MeshBatch}; +use crate::renderer::{ + Renderer2D, Renderer3D, GridRenderer, Grid3DRenderer, GizmoRenderer, Gizmo3DRenderer, TransformMode, + ViewportManager, TextBatch, MeshBatch, Camera3D, ProjectionType, +}; use crate::resource::TextureManager; use es_engine_shared::traits::backend::GraphicsBackend; +/// Render mode enumeration. +/// 渲染模式枚举。 +#[wasm_bindgen] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum RenderMode { + /// 2D rendering mode (orthographic camera, no depth test). + /// 2D渲染模式(正交相机,无深度测试)。 + #[default] + Mode2D, + /// 3D rendering mode (perspective/orthographic camera, depth test enabled). + /// 3D渲染模式(透视/正交相机,启用深度测试)。 + Mode3D, +} + /// Engine configuration options. /// 引擎配置选项。 #[derive(Debug, Clone)] @@ -104,6 +121,22 @@ pub struct Engine { /// Mesh batch renderer for arbitrary 2D geometry. /// 任意 2D 几何体的网格批处理渲染器。 mesh_batch: MeshBatch, + + /// 3D renderer (lazily initialized). + /// 3D渲染器(延迟初始化)。 + renderer_3d: Option, + + /// 3D grid renderer (lazily initialized with renderer_3d). + /// 3D网格渲染器(与renderer_3d一起延迟初始化)。 + grid_3d_renderer: Option, + + /// 3D gizmo renderer (lazily initialized with renderer_3d). + /// 3D Gizmo渲染器(与renderer_3d一起延迟初始化)。 + gizmo_3d_renderer: Option, + + /// Current render mode. + /// 当前渲染模式。 + render_mode: RenderMode, } impl Engine { @@ -167,6 +200,10 @@ impl Engine { is_editor: true, text_batch, mesh_batch, + renderer_3d: None, // Lazily initialized when switching to 3D mode + grid_3d_renderer: None, // Lazily initialized with renderer_3d + gizmo_3d_renderer: None, // Lazily initialized with renderer_3d + render_mode: RenderMode::Mode2D, }) } @@ -230,6 +267,10 @@ impl Engine { is_editor: true, text_batch, mesh_batch, + renderer_3d: None, + grid_3d_renderer: None, + gizmo_3d_renderer: None, + render_mode: RenderMode::Mode2D, }) } @@ -400,26 +441,54 @@ impl Engine { let [r, g, b, a] = self.renderer.get_clear_color(); self.context.clear(r, g, b, a); - let camera = self.renderer.camera().clone(); + // Render based on current mode | 根据当前模式渲染 + match self.render_mode { + RenderMode::Mode2D => { + // 2D rendering | 2D 渲染 + let camera = self.renderer.camera().clone(); - if self.is_editor && self.show_grid { - self.grid_renderer.render(&mut self.backend, &camera); - self.grid_renderer.render_axes(&mut self.backend, &camera); + if self.is_editor && self.show_grid { + self.grid_renderer.render(&mut self.backend, &camera); + self.grid_renderer.render_axes(&mut self.backend, &camera); + } + + self.renderer.render(&mut self.backend, &self.texture_manager) + .map_err(|e| crate::core::error::EngineError::WebGLError(e))?; + + if self.is_editor && self.show_gizmos { + self.gizmo_renderer.render(&mut self.backend, &camera); + self.gizmo_renderer.render_axis_indicator( + &mut self.backend, + self.context.width() as f32, + self.context.height() as f32, + ); + } + self.gizmo_renderer.clear(); + } + RenderMode::Mode3D => { + // 3D rendering | 3D 渲染 + if let Some(ref mut renderer_3d) = self.renderer_3d { + let camera = renderer_3d.camera().clone(); + + // Render 3D grid if in editor mode + // 如果在编辑器模式下渲染 3D 网格 + if self.is_editor && self.show_grid { + if let Some(ref mut grid_3d) = self.grid_3d_renderer { + grid_3d.render(&mut self.backend, &camera); + grid_3d.render_axes(&mut self.backend, &camera); + } + } + + // Render 3D content + // 渲染 3D 内容 + renderer_3d.render(&mut self.backend, &self.texture_manager) + .map_err(|e| crate::core::error::EngineError::WebGLError(e))?; + } else { + log::warn!("3D mode active but renderer not initialized | 3D 模式激活但渲染器未初始化"); + } + } } - self.renderer.render(&mut self.backend, &self.texture_manager) - .map_err(|e| crate::core::error::EngineError::WebGLError(e))?; - - if self.is_editor && self.show_gizmos { - self.gizmo_renderer.render(&mut self.backend, &camera); - self.gizmo_renderer.render_axis_indicator( - &mut self.backend, - self.context.width() as f32, - self.context.height() as f32, - ); - } - self.gizmo_renderer.clear(); - Ok(()) } @@ -521,6 +590,26 @@ impl Engine { _ => TransformMode::Select, }; self.gizmo_renderer.set_transform_mode(transform_mode); + + // Also update 3D gizmo if initialized + // 如果已初始化,也更新 3D Gizmo + if let Some(ref mut gizmo_3d) = self.gizmo_3d_renderer { + gizmo_3d.set_transform_mode(transform_mode); + } + } + + /// Render a 3D gizmo at the specified world position. + /// 在指定的世界位置渲染 3D Gizmo。 + pub fn render_3d_gizmo(&mut self, x: f32, y: f32, z: f32, scale: f32) { + if self.render_mode != RenderMode::Mode3D { + return; + } + + if let (Some(ref mut gizmo_3d), Some(ref renderer_3d)) = (&mut self.gizmo_3d_renderer, &self.renderer_3d) { + let camera = renderer_3d.camera(); + let position = glam::Vec3::new(x, y, z); + gizmo_3d.render(&mut self.backend, camera, position, scale); + } } /// Load a texture from URL. @@ -810,26 +899,57 @@ impl Engine { viewport.bind(); viewport.clear(); - let renderer_camera = self.renderer.camera_mut(); - renderer_camera.position = camera.position; - renderer_camera.set_zoom(camera.zoom); - renderer_camera.rotation = camera.rotation; - renderer_camera.set_viewport(camera.viewport_width(), camera.viewport_height()); + // Render based on current mode | 根据当前模式渲染 + match self.render_mode { + RenderMode::Mode2D => { + // 2D rendering | 2D 渲染 + let renderer_camera = self.renderer.camera_mut(); + renderer_camera.position = camera.position; + renderer_camera.set_zoom(camera.zoom); + renderer_camera.rotation = camera.rotation; + renderer_camera.set_viewport(camera.viewport_width(), camera.viewport_height()); - if self.is_editor && show_grid { - self.grid_renderer.render(&mut self.backend, &camera); - self.grid_renderer.render_axes(&mut self.backend, &camera); + if self.is_editor && show_grid { + self.grid_renderer.render(&mut self.backend, &camera); + self.grid_renderer.render_axes(&mut self.backend, &camera); + } + + self.renderer.render(&mut self.backend, &self.texture_manager) + .map_err(|e| crate::core::error::EngineError::WebGLError(e))?; + + if self.is_editor && show_gizmos { + self.gizmo_renderer.render(&mut self.backend, &camera); + self.gizmo_renderer.render_axis_indicator(&mut self.backend, vp_width as f32, vp_height as f32); + } + self.gizmo_renderer.clear(); + } + RenderMode::Mode3D => { + // 3D rendering | 3D 渲染 + if let Some(ref mut renderer_3d) = self.renderer_3d { + // Update 3D camera viewport + // 更新 3D 相机视口 + renderer_3d.camera_mut().set_viewport(vp_width as f32, vp_height as f32); + let camera_3d = renderer_3d.camera().clone(); + + // Render 3D grid if in editor mode + // 如果在编辑器模式下渲染 3D 网格 + if self.is_editor && show_grid { + if let Some(ref mut grid_3d) = self.grid_3d_renderer { + grid_3d.render(&mut self.backend, &camera_3d); + grid_3d.render_axes(&mut self.backend, &camera_3d); + } + } + + // Render 3D content + // 渲染 3D 内容 + renderer_3d.render(&mut self.backend, &self.texture_manager) + .map_err(|e| crate::core::error::EngineError::WebGLError(e))?; + } else { + log::warn!("3D mode active but renderer not initialized | 3D 模式激活但渲染器未初始化"); + } + } } - self.renderer.render(&mut self.backend, &self.texture_manager) - .map_err(|e| crate::core::error::EngineError::WebGLError(e))?; - - if self.is_editor && show_gizmos { - self.gizmo_renderer.render(&mut self.backend, &camera); - self.gizmo_renderer.render_axis_indicator(&mut self.backend, vp_width as f32, vp_height as f32); - } - self.gizmo_renderer.clear(); - Ok(()) } @@ -1012,4 +1132,163 @@ impl Engine { false } } + + // ===== 3D Rendering Mode ===== + // ===== 3D 渲染模式 ===== + + /// Get current render mode. + /// 获取当前渲染模式。 + pub fn render_mode(&self) -> RenderMode { + self.render_mode + } + + /// Set render mode (2D or 3D). + /// 设置渲染模式(2D或3D)。 + /// + /// When switching to 3D mode, the 3D renderer is lazily initialized if not already created. + /// 切换到3D模式时,如果3D渲染器尚未创建,则会延迟初始化。 + pub fn set_render_mode(&mut self, mode: RenderMode) -> Result<()> { + if mode == RenderMode::Mode3D && self.renderer_3d.is_none() { + // Initialize 3D renderer on first use + // 首次使用时初始化3D渲染器 + let renderer_3d = Renderer3D::new(&mut self.backend) + .map_err(|e| crate::core::error::EngineError::WebGLError(e))?; + self.renderer_3d = Some(renderer_3d); + + // Initialize 3D grid renderer + // 初始化3D网格渲染器 + let grid_3d = Grid3DRenderer::new(&mut self.backend) + .map_err(|e| crate::core::error::EngineError::WebGLError(e))?; + self.grid_3d_renderer = Some(grid_3d); + + // Initialize 3D gizmo renderer + // 初始化3D Gizmo渲染器 + let gizmo_3d = Gizmo3DRenderer::new(&mut self.backend) + .map_err(|e| crate::core::error::EngineError::WebGLError(e))?; + self.gizmo_3d_renderer = Some(gizmo_3d); + + log::info!("3D renderer, grid and gizmo initialized | 3D渲染器、网格和Gizmo已初始化"); + } + self.render_mode = mode; + Ok(()) + } + + /// Check if 3D renderer is initialized. + /// 检查3D渲染器是否已初始化。 + pub fn has_3d_renderer(&self) -> bool { + self.renderer_3d.is_some() + } + + /// Get reference to 3D renderer (if initialized). + /// 获取3D渲染器的引用(如果已初始化)。 + pub fn renderer_3d(&self) -> Option<&Renderer3D> { + self.renderer_3d.as_ref() + } + + /// Get mutable reference to 3D renderer (if initialized). + /// 获取3D渲染器的可变引用(如果已初始化)。 + pub fn renderer_3d_mut(&mut self) -> Option<&mut Renderer3D> { + self.renderer_3d.as_mut() + } + + /// Set 3D camera position. + /// 设置3D相机位置。 + pub fn set_camera_3d_position(&mut self, x: f32, y: f32, z: f32) { + if let Some(ref mut renderer) = self.renderer_3d { + renderer.camera_mut().position = glam::Vec3::new(x, y, z); + } + } + + /// Get 3D camera position. + /// 获取3D相机位置。 + pub fn get_camera_3d_position(&self) -> Option<(f32, f32, f32)> { + self.renderer_3d.as_ref().map(|r| { + let pos = r.camera().position; + (pos.x, pos.y, pos.z) + }) + } + + /// Set 3D camera rotation using Euler angles (in degrees). + /// 使用欧拉角设置3D相机旋转(角度制)。 + pub fn set_camera_3d_rotation(&mut self, pitch: f32, yaw: f32, roll: f32) { + if let Some(ref mut renderer) = self.renderer_3d { + let pitch_rad = pitch.to_radians(); + let yaw_rad = yaw.to_radians(); + let roll_rad = roll.to_radians(); + renderer.camera_mut().set_rotation_euler(pitch_rad, yaw_rad, roll_rad); + } + } + + /// Get 3D camera rotation as Euler angles (in degrees). + /// 获取3D相机旋转的欧拉角(角度制)。 + pub fn get_camera_3d_rotation(&self) -> Option<(f32, f32, f32)> { + self.renderer_3d.as_ref().map(|r| { + let (pitch, yaw, roll) = r.camera().get_rotation_euler(); + (pitch.to_degrees(), yaw.to_degrees(), roll.to_degrees()) + }) + } + + /// Set 3D camera field of view (in degrees). + /// 设置3D相机视野角(角度制)。 + pub fn set_camera_3d_fov(&mut self, fov_degrees: f32) { + if let Some(ref mut renderer) = self.renderer_3d { + renderer.camera_mut().fov = fov_degrees.to_radians(); + } + } + + /// Get 3D camera field of view (in degrees). + /// 获取3D相机视野角(角度制)。 + pub fn get_camera_3d_fov(&self) -> Option { + self.renderer_3d.as_ref().map(|r| r.camera().fov.to_degrees()) + } + + /// Set 3D camera projection type. + /// 设置3D相机投影类型。 + /// + /// 0 = Perspective, 1 = Orthographic (with size) + pub fn set_camera_3d_projection(&mut self, projection_type: u8, ortho_size: f32) { + if let Some(ref mut renderer) = self.renderer_3d { + renderer.camera_mut().projection_type = match projection_type { + 0 => ProjectionType::Perspective, + 1 => ProjectionType::Orthographic { size: ortho_size }, + _ => ProjectionType::Perspective, + }; + } + } + + /// Make 3D camera look at a target position. + /// 使3D相机朝向目标位置。 + pub fn camera_3d_look_at(&mut self, target_x: f32, target_y: f32, target_z: f32) { + if let Some(ref mut renderer) = self.renderer_3d { + let target = glam::Vec3::new(target_x, target_y, target_z); + renderer.camera_mut().look_at(target, glam::Vec3::Y); + } + } + + /// Set 3D camera near and far clip planes. + /// 设置3D相机近裁剪面和远裁剪面。 + pub fn set_camera_3d_clip_planes(&mut self, near: f32, far: f32) { + if let Some(ref mut renderer) = self.renderer_3d { + renderer.camera_mut().near = near; + renderer.camera_mut().far = far; + } + } + + /// Resize 3D renderer viewport. + /// 调整3D渲染器视口大小。 + pub fn resize_3d(&mut self, width: f32, height: f32) { + if let Some(ref mut renderer) = self.renderer_3d { + renderer.resize(width, height); + } + } + + /// Render 3D content. + /// 渲染3D内容。 + pub fn render_3d(&mut self) -> Result<()> { + if let Some(ref mut renderer) = self.renderer_3d { + renderer.render(&mut self.backend, &self.texture_manager) + .map_err(|e| crate::core::error::EngineError::WebGLError(e))?; + } + Ok(()) + } } diff --git a/packages/engine/src/core/mod.rs b/packages/engine/src/core/mod.rs index 937e1826..9a1a3b7b 100644 --- a/packages/engine/src/core/mod.rs +++ b/packages/engine/src/core/mod.rs @@ -5,6 +5,6 @@ pub mod error; pub mod context; mod engine; -pub use engine::{Engine, EngineConfig}; +pub use engine::{Engine, EngineConfig, RenderMode}; pub use context::WebGLContext; pub use error::{EngineError, Result}; diff --git a/packages/engine/src/lib.rs b/packages/engine/src/lib.rs index 2d37e4c3..cf9051d4 100644 --- a/packages/engine/src/lib.rs +++ b/packages/engine/src/lib.rs @@ -556,6 +556,21 @@ impl GameEngine { self.engine.set_transform_mode(mode); } + /// Render a 3D gizmo at the specified world position. + /// 在指定的世界位置渲染 3D Gizmo。 + /// + /// Only works in 3D render mode. The gizmo will be rendered + /// with the current transform mode (move/rotate/scale). + /// 仅在 3D 渲染模式下有效。Gizmo 将使用当前的变换模式渲染。 + /// + /// # Arguments | 参数 + /// * `x`, `y`, `z` - World position | 世界位置 + /// * `scale` - Gizmo scale multiplier | Gizmo 缩放倍数 + #[wasm_bindgen(js_name = render3DGizmo)] + pub fn render_3d_gizmo(&mut self, x: f32, y: f32, z: f32, scale: f32) { + self.engine.render_3d_gizmo(x, y, z, scale); + } + /// Set gizmo visibility. /// 设置辅助工具可见性。 #[wasm_bindgen(js_name = setShowGizmos)] @@ -909,4 +924,152 @@ impl GameEngine { pub fn get_max_texture_size(&self) -> u32 { self.engine.max_texture_size() } + + // ===== 3D Rendering API ===== + // ===== 3D 渲染 API ===== + + /// Get current render mode. + /// 获取当前渲染模式。 + /// + /// Returns 0 for 2D mode, 1 for 3D mode. + /// 返回 0 表示 2D 模式,1 表示 3D 模式。 + #[wasm_bindgen(js_name = getRenderMode)] + pub fn get_render_mode(&self) -> u8 { + match self.engine.render_mode() { + crate::core::RenderMode::Mode2D => 0, + crate::core::RenderMode::Mode3D => 1, + } + } + + /// Set render mode. + /// 设置渲染模式。 + /// + /// # Arguments | 参数 + /// * `mode` - 0 for 2D, 1 for 3D | 0 表示 2D,1 表示 3D + /// + /// When switching to 3D mode for the first time, the 3D renderer + /// will be lazily initialized. + /// 首次切换到 3D 模式时,3D 渲染器将被延迟初始化。 + #[wasm_bindgen(js_name = setRenderMode)] + pub fn set_render_mode(&mut self, mode: u8) -> std::result::Result<(), JsValue> { + let render_mode = match mode { + 0 => crate::core::RenderMode::Mode2D, + 1 => crate::core::RenderMode::Mode3D, + _ => return Err(JsValue::from_str("Invalid render mode. Use 0 for 2D, 1 for 3D.")), + }; + self.engine + .set_render_mode(render_mode) + .map_err(|e| JsValue::from_str(&e.to_string())) + } + + /// Check if 3D renderer is initialized. + /// 检查 3D 渲染器是否已初始化。 + #[wasm_bindgen(js_name = has3DRenderer)] + pub fn has_3d_renderer(&self) -> bool { + self.engine.has_3d_renderer() + } + + /// Set 3D camera position. + /// 设置 3D 相机位置。 + #[wasm_bindgen(js_name = setCamera3DPosition)] + pub fn set_camera_3d_position(&mut self, x: f32, y: f32, z: f32) { + self.engine.set_camera_3d_position(x, y, z); + } + + /// Get 3D camera position. + /// 获取 3D 相机位置。 + /// + /// Returns [x, y, z] or null if 3D renderer is not initialized. + /// 返回 [x, y, z],如果 3D 渲染器未初始化则返回 null。 + #[wasm_bindgen(js_name = getCamera3DPosition)] + pub fn get_camera_3d_position(&self) -> Option> { + self.engine + .get_camera_3d_position() + .map(|(x, y, z)| vec![x, y, z]) + } + + /// Set 3D camera rotation using Euler angles (in degrees). + /// 使用欧拉角设置 3D 相机旋转(角度制)。 + /// + /// # Arguments | 参数 + /// * `pitch` - Rotation around X axis (degrees) | X 轴旋转(角度) + /// * `yaw` - Rotation around Y axis (degrees) | Y 轴旋转(角度) + /// * `roll` - Rotation around Z axis (degrees) | Z 轴旋转(角度) + #[wasm_bindgen(js_name = setCamera3DRotation)] + pub fn set_camera_3d_rotation(&mut self, pitch: f32, yaw: f32, roll: f32) { + self.engine.set_camera_3d_rotation(pitch, yaw, roll); + } + + /// Get 3D camera rotation as Euler angles (in degrees). + /// 获取 3D 相机旋转的欧拉角(角度制)。 + /// + /// Returns [pitch, yaw, roll] or null if 3D renderer is not initialized. + /// 返回 [pitch, yaw, roll],如果 3D 渲染器未初始化则返回 null。 + #[wasm_bindgen(js_name = getCamera3DRotation)] + pub fn get_camera_3d_rotation(&self) -> Option> { + self.engine + .get_camera_3d_rotation() + .map(|(pitch, yaw, roll)| vec![pitch, yaw, roll]) + } + + /// Set 3D camera field of view (in degrees). + /// 设置 3D 相机视野角(角度制)。 + /// + /// Only affects perspective projection mode. + /// 仅影响透视投影模式。 + #[wasm_bindgen(js_name = setCamera3DFov)] + pub fn set_camera_3d_fov(&mut self, fov_degrees: f32) { + self.engine.set_camera_3d_fov(fov_degrees); + } + + /// Get 3D camera field of view (in degrees). + /// 获取 3D 相机视野角(角度制)。 + #[wasm_bindgen(js_name = getCamera3DFov)] + pub fn get_camera_3d_fov(&self) -> Option { + self.engine.get_camera_3d_fov() + } + + /// Set 3D camera projection type. + /// 设置 3D 相机投影类型。 + /// + /// # Arguments | 参数 + /// * `projection_type` - 0 for Perspective, 1 for Orthographic + /// * `ortho_size` - Half-height of orthographic view (only used when projection_type = 1) + #[wasm_bindgen(js_name = setCamera3DProjection)] + pub fn set_camera_3d_projection(&mut self, projection_type: u8, ortho_size: f32) { + self.engine.set_camera_3d_projection(projection_type, ortho_size); + } + + /// Make 3D camera look at a target position. + /// 使 3D 相机朝向目标位置。 + #[wasm_bindgen(js_name = camera3DLookAt)] + pub fn camera_3d_look_at(&mut self, target_x: f32, target_y: f32, target_z: f32) { + self.engine.camera_3d_look_at(target_x, target_y, target_z); + } + + /// Set 3D camera near and far clip planes. + /// 设置 3D 相机近裁剪面和远裁剪面。 + #[wasm_bindgen(js_name = setCamera3DClipPlanes)] + pub fn set_camera_3d_clip_planes(&mut self, near: f32, far: f32) { + self.engine.set_camera_3d_clip_planes(near, far); + } + + /// Resize 3D renderer viewport. + /// 调整 3D 渲染器视口大小。 + #[wasm_bindgen(js_name = resize3D)] + pub fn resize_3d(&mut self, width: f32, height: f32) { + self.engine.resize_3d(width, height); + } + + /// Render 3D content. + /// 渲染 3D 内容。 + /// + /// Should be called after submitting 3D meshes. + /// 应在提交 3D 网格后调用。 + #[wasm_bindgen(js_name = render3D)] + pub fn render_3d(&mut self) -> std::result::Result<(), JsValue> { + self.engine + .render_3d() + .map_err(|e| JsValue::from_str(&e.to_string())) + } } diff --git a/packages/engine/src/renderer/batch/mod.rs b/packages/engine/src/renderer/batch/mod.rs index 182f8dde..d1bf743f 100644 --- a/packages/engine/src/renderer/batch/mod.rs +++ b/packages/engine/src/renderer/batch/mod.rs @@ -5,8 +5,14 @@ mod sprite_batch; mod text_batch; mod mesh_batch; mod vertex; +mod vertex3d; pub use sprite_batch::{BatchKey, SpriteBatch}; pub use text_batch::TextBatch; pub use mesh_batch::MeshBatch; pub use vertex::{SpriteVertex, VERTEX_SIZE}; +pub use vertex3d::{ + Vertex3D, SimpleVertex3D, + VERTEX3D_SIZE, SIMPLE_VERTEX3D_SIZE, + FLOATS_PER_VERTEX_3D, FLOATS_PER_SIMPLE_VERTEX_3D, +}; diff --git a/packages/engine/src/renderer/batch/vertex3d.rs b/packages/engine/src/renderer/batch/vertex3d.rs new file mode 100644 index 00000000..a3ec0d9c --- /dev/null +++ b/packages/engine/src/renderer/batch/vertex3d.rs @@ -0,0 +1,147 @@ +//! Vertex data structures for 3D rendering. +//! 用于3D渲染的顶点数据结构。 + +use bytemuck::{Pod, Zeroable}; + +/// Size of a single 3D vertex in bytes. +/// 单个3D顶点的字节大小。 +pub const VERTEX3D_SIZE: usize = std::mem::size_of::(); + +/// Number of floats per 3D vertex. +/// 每个3D顶点的浮点数数量。 +/// +/// Layout: position(3) + tex_coord(2) + color(4) + normal(3) = 12 +/// 布局: 位置(3) + 纹理坐标(2) + 颜色(4) + 法线(3) = 12 +pub const FLOATS_PER_VERTEX_3D: usize = 12; + +/// 3D vertex data. +/// 3D顶点数据。 +/// +/// Used for mesh rendering with optional lighting support. +/// 用于带可选光照支持的网格渲染。 +#[derive(Debug, Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct Vertex3D { + /// Position (x, y, z) in world or local space. + /// 位置(世界或局部空间)。 + pub position: [f32; 3], + + /// Texture coordinates (u, v). + /// 纹理坐标。 + pub tex_coord: [f32; 2], + + /// Color (r, g, b, a). + /// 颜色。 + pub color: [f32; 4], + + /// Normal vector (nx, ny, nz) for lighting. + /// 用于光照的法线向量。 + pub normal: [f32; 3], +} + +impl Vertex3D { + /// Create a new 3D vertex. + /// 创建新的3D顶点。 + #[inline] + pub const fn new( + position: [f32; 3], + tex_coord: [f32; 2], + color: [f32; 4], + normal: [f32; 3], + ) -> Self { + Self { + position, + tex_coord, + color, + normal, + } + } + + /// Create a simple vertex without normal (for unlit rendering). + /// 创建不带法线的简单顶点(用于无光照渲染)。 + #[inline] + pub const fn simple(position: [f32; 3], tex_coord: [f32; 2], color: [f32; 4]) -> Self { + Self { + position, + tex_coord, + color, + normal: [0.0, 0.0, 1.0], // Default facing +Z + } + } +} + +impl Default for Vertex3D { + fn default() -> Self { + Self { + position: [0.0, 0.0, 0.0], + tex_coord: [0.0, 0.0], + color: [1.0, 1.0, 1.0, 1.0], + normal: [0.0, 0.0, 1.0], + } + } +} + +/// Simplified 3D vertex without normal (for unlit/billboard rendering). +/// 简化的3D顶点,不带法线(用于无光照/公告板渲染)。 +/// +/// Layout: position(3) + tex_coord(2) + color(4) = 9 +/// 布局: 位置(3) + 纹理坐标(2) + 颜色(4) = 9 +#[derive(Debug, Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct SimpleVertex3D { + /// Position (x, y, z). + /// 位置。 + pub position: [f32; 3], + + /// Texture coordinates (u, v). + /// 纹理坐标。 + pub tex_coord: [f32; 2], + + /// Color (r, g, b, a). + /// 颜色。 + pub color: [f32; 4], +} + +/// Size of a simple 3D vertex in bytes. +/// 简单3D顶点的字节大小。 +pub const SIMPLE_VERTEX3D_SIZE: usize = std::mem::size_of::(); + +/// Number of floats per simple 3D vertex. +/// 每个简单3D顶点的浮点数数量。 +pub const FLOATS_PER_SIMPLE_VERTEX_3D: usize = 9; + +impl SimpleVertex3D { + /// Create a new simple 3D vertex. + /// 创建新的简单3D顶点。 + #[inline] + pub const fn new(position: [f32; 3], tex_coord: [f32; 2], color: [f32; 4]) -> Self { + Self { + position, + tex_coord, + color, + } + } +} + +impl Default for SimpleVertex3D { + fn default() -> Self { + Self { + position: [0.0, 0.0, 0.0], + tex_coord: [0.0, 0.0], + color: [1.0, 1.0, 1.0, 1.0], + } + } +} + +/// Convert SimpleVertex3D to Vertex3D with default normal. +/// 将SimpleVertex3D转换为带默认法线的Vertex3D。 +impl From for Vertex3D { + fn from(v: SimpleVertex3D) -> Self { + Self { + position: v.position, + tex_coord: v.tex_coord, + color: v.color, + normal: [0.0, 0.0, 1.0], + } + } +} diff --git a/packages/engine/src/renderer/camera3d.rs b/packages/engine/src/renderer/camera3d.rs new file mode 100644 index 00000000..2531cb16 --- /dev/null +++ b/packages/engine/src/renderer/camera3d.rs @@ -0,0 +1,414 @@ +//! 3D camera implementation. +//! 3D相机实现。 +//! +//! Uses left-hand coordinate system convention (consistent with Camera2D): +//! 使用左手坐标系约定(与Camera2D一致): +//! - X axis: positive to the right / X 轴:正方向向右 +//! - Y axis: positive upward / Y 轴:正方向向上 +//! - Z axis: positive into the screen / Z 轴:正方向指向屏幕内 +//! +//! Supports both perspective and orthographic projection. +//! 支持透视和正交两种投影模式。 + +use glam::{Mat4, Quat, Vec2, Vec3}; + +/// Projection type for 3D camera. +/// 3D相机的投影类型。 +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ProjectionType { + /// Perspective projection with field of view. + /// 带视野角的透视投影。 + Perspective, + /// Orthographic projection with fixed size. + /// 固定大小的正交投影。 + Orthographic { + /// Half-height of the view in world units. + /// 视图半高度(世界单位)。 + size: f32, + }, +} + +impl Default for ProjectionType { + fn default() -> Self { + ProjectionType::Perspective + } +} + +/// 3D ray for raycasting. +/// 用于射线检测的3D射线。 +#[derive(Debug, Clone, Copy)] +pub struct Ray3D { + /// Ray origin in world space. + /// 射线在世界空间中的起点。 + pub origin: Vec3, + /// Ray direction (normalized). + /// 射线方向(已归一化)。 + pub direction: Vec3, +} + +impl Ray3D { + /// Create a new ray. + /// 创建新射线。 + pub fn new(origin: Vec3, direction: Vec3) -> Self { + Self { + origin, + direction: direction.normalize(), + } + } + + /// Get point along the ray at distance t. + /// 获取射线上距离为t的点。 + #[inline] + pub fn point_at(&self, t: f32) -> Vec3 { + self.origin + self.direction * t + } +} + +/// 3D camera supporting perspective and orthographic projection. +/// 支持透视和正交投影的3D相机。 +/// +/// Provides view, projection, and combined matrices for 3D rendering. +/// 提供用于3D渲染的视图、投影和组合矩阵。 +#[derive(Debug, Clone)] +pub struct Camera3D { + /// Camera position in world space. + /// 相机在世界空间中的位置。 + pub position: Vec3, + + /// Camera rotation as quaternion. + /// 相机旋转(四元数)。 + pub rotation: Quat, + + /// Field of view in radians (for perspective projection). + /// 视野角(弧度,用于透视投影)。 + pub fov: f32, + + /// Near clipping plane distance. + /// 近裁剪面距离。 + pub near: f32, + + /// Far clipping plane distance. + /// 远裁剪面距离。 + pub far: f32, + + /// Aspect ratio (width / height). + /// 宽高比(宽度 / 高度)。 + pub aspect: f32, + + /// Projection type (perspective or orthographic). + /// 投影类型(透视或正交)。 + pub projection_type: ProjectionType, + + /// Viewport width in pixels. + /// 视口宽度(像素)。 + viewport_width: f32, + + /// Viewport height in pixels. + /// 视口高度(像素)。 + viewport_height: f32, +} + +impl Camera3D { + /// Create a new 3D perspective camera. + /// 创建新的3D透视相机。 + /// + /// # Arguments | 参数 + /// * `width` - Viewport width | 视口宽度 + /// * `height` - Viewport height | 视口高度 + /// * `fov` - Field of view in radians | 视野角(弧度) + pub fn new(width: f32, height: f32, fov: f32) -> Self { + Self { + position: Vec3::new(0.0, 0.0, -10.0), + rotation: Quat::IDENTITY, + fov, + near: 0.1, + far: 1000.0, + aspect: width / height, + projection_type: ProjectionType::Perspective, + viewport_width: width, + viewport_height: height, + } + } + + /// Create a new orthographic camera. + /// 创建新的正交相机。 + /// + /// # Arguments | 参数 + /// * `width` - Viewport width | 视口宽度 + /// * `height` - Viewport height | 视口高度 + /// * `size` - Orthographic half-height | 正交视图半高度 + pub fn new_orthographic(width: f32, height: f32, size: f32) -> Self { + Self { + position: Vec3::new(0.0, 0.0, -10.0), + rotation: Quat::IDENTITY, + fov: std::f32::consts::FRAC_PI_4, + near: 0.1, + far: 1000.0, + aspect: width / height, + projection_type: ProjectionType::Orthographic { size }, + viewport_width: width, + viewport_height: height, + } + } + + /// Update viewport size. + /// 更新视口大小。 + pub fn set_viewport(&mut self, width: f32, height: f32) { + self.viewport_width = width; + self.viewport_height = height; + self.aspect = width / height; + } + + /// Get the view matrix. + /// 获取视图矩阵。 + /// + /// Transforms world coordinates to camera/view space. + /// 将世界坐标转换为相机/视图空间。 + pub fn view_matrix(&self) -> Mat4 { + // Camera forward is +Z in left-hand system + // 左手系中相机前方是 +Z + let forward = self.rotation * Vec3::Z; + let up = self.rotation * Vec3::Y; + let target = self.position + forward; + + Mat4::look_at_lh(self.position, target, up) + } + + /// Get the projection matrix. + /// 获取投影矩阵。 + /// + /// Transforms view space coordinates to clip space. + /// 将视图空间坐标转换为裁剪空间。 + pub fn projection_matrix(&self) -> Mat4 { + match self.projection_type { + ProjectionType::Perspective => { + Mat4::perspective_lh(self.fov, self.aspect, self.near, self.far) + } + ProjectionType::Orthographic { size } => { + let half_width = size * self.aspect; + let half_height = size; + Mat4::orthographic_lh( + -half_width, + half_width, + -half_height, + half_height, + self.near, + self.far, + ) + } + } + } + + /// Get the combined view-projection matrix. + /// 获取组合的视图-投影矩阵。 + /// + /// Transforms world coordinates directly to clip space. + /// 将世界坐标直接转换为裁剪空间。 + #[inline] + pub fn view_projection_matrix(&self) -> Mat4 { + self.projection_matrix() * self.view_matrix() + } + + /// Convert screen coordinates to a world-space ray. + /// 将屏幕坐标转换为世界空间射线。 + /// + /// Screen: (0,0) at top-left, Y-down | 屏幕:(0,0)在左上角,Y向下 + /// Returns a ray from the camera through the screen point. + /// 返回从相机穿过屏幕点的射线。 + pub fn screen_to_world_ray(&self, screen: Vec2) -> Ray3D { + // Convert screen to NDC [-1, 1] + // 将屏幕坐标转换为NDC [-1, 1] + let ndc_x = (2.0 * screen.x / self.viewport_width) - 1.0; + let ndc_y = 1.0 - (2.0 * screen.y / self.viewport_height); // Flip Y + + // Get inverse matrices + // 获取逆矩阵 + let inv_proj = self.projection_matrix().inverse(); + let inv_view = self.view_matrix().inverse(); + + match self.projection_type { + ProjectionType::Perspective => { + // For perspective: ray from camera through near plane point + // 透视模式:从相机穿过近平面点的射线 + let ray_clip = glam::Vec4::new(ndc_x, ndc_y, 0.0, 1.0); + let ray_eye = inv_proj * ray_clip; + let ray_eye = glam::Vec4::new(ray_eye.x, ray_eye.y, 1.0, 0.0); // Forward direction + let ray_world = inv_view * ray_eye; + let direction = Vec3::new(ray_world.x, ray_world.y, ray_world.z).normalize(); + + Ray3D::new(self.position, direction) + } + ProjectionType::Orthographic { size } => { + // For orthographic: parallel rays, origin varies + // 正交模式:平行射线,起点变化 + let half_width = size * self.aspect; + let half_height = size; + let local_x = ndc_x * half_width; + let local_y = ndc_y * half_height; + + // Ray origin in world space + // 世界空间中的射线起点 + let right = self.rotation * Vec3::X; + let up = self.rotation * Vec3::Y; + let forward = self.rotation * Vec3::Z; + let origin = self.position + right * local_x + up * local_y; + + Ray3D::new(origin, forward) + } + } + } + + /// Convert world coordinates to screen coordinates. + /// 将世界坐标转换为屏幕坐标。 + /// + /// Returns None if the point is behind the camera. + /// 如果点在相机后面则返回None。 + pub fn world_to_screen(&self, world: Vec3) -> Option { + let clip = self.view_projection_matrix() * world.extend(1.0); + + // Check if behind camera (for perspective) + // 检查是否在相机后面(透视模式) + if clip.w <= 0.0 { + return None; + } + + // Perspective divide + // 透视除法 + let ndc = clip.truncate() / clip.w; + + // Check if outside frustum + // 检查是否在视锥外 + if ndc.x < -1.0 || ndc.x > 1.0 || ndc.y < -1.0 || ndc.y > 1.0 { + return None; + } + + // Convert NDC to screen coordinates + // 将NDC转换为屏幕坐标 + let screen_x = (ndc.x + 1.0) * 0.5 * self.viewport_width; + let screen_y = (1.0 - ndc.y) * 0.5 * self.viewport_height; // Flip Y + + Some(Vec2::new(screen_x, screen_y)) + } + + /// Set position from Euler angles (in radians). + /// 从欧拉角设置旋转(弧度)。 + /// + /// Uses XYZ rotation order. + /// 使用XYZ旋转顺序。 + pub fn set_rotation_euler(&mut self, pitch: f32, yaw: f32, roll: f32) { + self.rotation = Quat::from_euler(glam::EulerRot::XYZ, pitch, yaw, roll); + } + + /// Get Euler angles from current rotation (in radians). + /// 从当前旋转获取欧拉角(弧度)。 + /// + /// Returns (pitch, yaw, roll) in XYZ order. + /// 返回 (pitch, yaw, roll) 以XYZ顺序。 + pub fn get_rotation_euler(&self) -> (f32, f32, f32) { + self.rotation.to_euler(glam::EulerRot::XYZ) + } + + /// Move camera by delta in local space. + /// 在局部空间中按增量移动相机。 + pub fn translate_local(&mut self, delta: Vec3) { + let world_delta = self.rotation * delta; + self.position += world_delta; + } + + /// Move camera by delta in world space. + /// 在世界空间中按增量移动相机。 + #[inline] + pub fn translate(&mut self, delta: Vec3) { + self.position += delta; + } + + /// Get the forward direction vector. + /// 获取前方方向向量。 + #[inline] + pub fn forward(&self) -> Vec3 { + self.rotation * Vec3::Z + } + + /// Get the right direction vector. + /// 获取右方方向向量。 + #[inline] + pub fn right(&self) -> Vec3 { + self.rotation * Vec3::X + } + + /// Get the up direction vector. + /// 获取上方方向向量。 + #[inline] + pub fn up(&self) -> Vec3 { + self.rotation * Vec3::Y + } + + /// Look at a target position. + /// 朝向目标位置。 + pub fn look_at(&mut self, target: Vec3, up: Vec3) { + let forward = (target - self.position).normalize(); + let right = up.cross(forward).normalize(); + let actual_up = forward.cross(right); + + // Build rotation matrix and convert to quaternion + // 构建旋转矩阵并转换为四元数 + let rotation_matrix = Mat4::from_cols( + right.extend(0.0), + actual_up.extend(0.0), + forward.extend(0.0), + glam::Vec4::W, + ); + self.rotation = Quat::from_mat4(&rotation_matrix); + } + + #[inline] + pub fn viewport_width(&self) -> f32 { + self.viewport_width + } + + #[inline] + pub fn viewport_height(&self) -> f32 { + self.viewport_height + } +} + +impl Default for Camera3D { + fn default() -> Self { + Self::new(800.0, 600.0, std::f32::consts::FRAC_PI_4) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_camera_creation() { + let camera = Camera3D::new(800.0, 600.0, std::f32::consts::FRAC_PI_4); + assert_eq!(camera.position, Vec3::new(0.0, 0.0, -10.0)); + assert!((camera.aspect - 800.0 / 600.0).abs() < 0.001); + } + + #[test] + fn test_view_projection() { + let camera = Camera3D::new(800.0, 600.0, std::f32::consts::FRAC_PI_4); + let vp = camera.view_projection_matrix(); + // Basic sanity check: matrix should not be identity + assert_ne!(vp, Mat4::IDENTITY); + } + + #[test] + fn test_world_to_screen_center() { + let mut camera = Camera3D::new(800.0, 600.0, std::f32::consts::FRAC_PI_4); + camera.position = Vec3::new(0.0, 0.0, -10.0); + camera.rotation = Quat::IDENTITY; + + // Point directly in front of camera should map to center + // 相机正前方的点应该映射到中心 + let screen = camera.world_to_screen(Vec3::new(0.0, 0.0, 0.0)); + if let Some(s) = screen { + assert!((s.x - 400.0).abs() < 1.0); + assert!((s.y - 300.0).abs() < 1.0); + } + } +} diff --git a/packages/engine/src/renderer/gizmo3d.rs b/packages/engine/src/renderer/gizmo3d.rs new file mode 100644 index 00000000..ae7a174f --- /dev/null +++ b/packages/engine/src/renderer/gizmo3d.rs @@ -0,0 +1,577 @@ +//! 3D Gizmo renderer for editor overlays. +//! 编辑器 3D Gizmo 渲染器。 +//! +//! Provides transform handles for 3D editing: +//! - Translation: XYZ axis arrows with plane handles +//! - Rotation: XYZ rotation circles +//! - Scale: XYZ axis with cube handles +//! +//! 提供 3D 编辑的变换手柄: +//! - 平移:XYZ 轴箭头和平面手柄 +//! - 旋转:XYZ 旋转圆环 +//! - 缩放:XYZ 轴和立方体手柄 + +use es_engine_shared::{ + traits::backend::{GraphicsBackend, BufferUsage}, + types::{ + handle::{ShaderHandle, BufferHandle, VertexArrayHandle}, + vertex::{VertexLayout, VertexAttribute, VertexAttributeType}, + blend::BlendMode, + }, + Vec3, Mat4, +}; +use super::camera3d::Camera3D; +use super::gizmo::TransformMode; +use std::f32::consts::PI; + +const GIZMO3D_VERTEX_SHADER: &str = r#"#version 300 es +precision highp float; + +layout(location = 0) in vec3 a_position; +layout(location = 1) in vec4 a_color; + +uniform mat4 u_viewProjection; +uniform mat4 u_model; + +out vec4 v_color; + +void main() { + gl_Position = u_viewProjection * u_model * vec4(a_position, 1.0); + v_color = a_color; +} +"#; + +const GIZMO3D_FRAGMENT_SHADER: &str = r#"#version 300 es +precision highp float; + +in vec4 v_color; +out vec4 fragColor; + +void main() { + fragColor = v_color; +} +"#; + +// Axis colors | 轴颜色 +const X_AXIS_COLOR: [f32; 4] = [1.0, 0.3, 0.3, 1.0]; // Red +const Y_AXIS_COLOR: [f32; 4] = [0.3, 1.0, 0.3, 1.0]; // Green +const Z_AXIS_COLOR: [f32; 4] = [0.3, 0.3, 1.0, 1.0]; // Blue +const XY_PLANE_COLOR: [f32; 4] = [1.0, 1.0, 0.3, 0.5]; // Yellow (XY plane) +const XZ_PLANE_COLOR: [f32; 4] = [1.0, 0.3, 1.0, 0.5]; // Magenta (XZ plane) +const YZ_PLANE_COLOR: [f32; 4] = [0.3, 1.0, 1.0, 0.5]; // Cyan (YZ plane) +const CENTER_COLOR: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; // White center + +/// 3D Gizmo renderer for transform handles. +/// 用于变换手柄的 3D Gizmo 渲染器。 +pub struct Gizmo3DRenderer { + shader: ShaderHandle, + + // Translation gizmo + translate_vbo: BufferHandle, + translate_vao: VertexArrayHandle, + translate_line_count: u32, + translate_arrow_vbo: BufferHandle, + translate_arrow_vao: VertexArrayHandle, + translate_arrow_count: u32, + + // Rotation gizmo + rotate_vbo: BufferHandle, + rotate_vao: VertexArrayHandle, + rotate_vertex_count: u32, + + // Scale gizmo + scale_vbo: BufferHandle, + scale_vao: VertexArrayHandle, + scale_line_count: u32, + scale_cube_vbo: BufferHandle, + scale_cube_vao: VertexArrayHandle, + scale_cube_count: u32, + + // Center sphere + center_vbo: BufferHandle, + center_vao: VertexArrayHandle, + center_vertex_count: u32, + + // Plane handles for translation + plane_vbo: BufferHandle, + plane_vao: VertexArrayHandle, + plane_vertex_count: u32, + + /// Current transform mode + transform_mode: TransformMode, + + /// Gizmo size (in world units at distance 1) + gizmo_size: f32, +} + +impl Gizmo3DRenderer { + /// Create a new 3D gizmo renderer. + /// 创建新的 3D Gizmo 渲染器。 + pub fn new(backend: &mut impl GraphicsBackend) -> Result { + let shader = backend.compile_shader(GIZMO3D_VERTEX_SHADER, GIZMO3D_FRAGMENT_SHADER) + .map_err(|e| format!("3D Gizmo shader: {:?}", e))?; + + let layout = VertexLayout { + attributes: vec![ + VertexAttribute { + name: "a_position".into(), + attr_type: VertexAttributeType::Float3, + offset: 0, + normalized: false, + }, + VertexAttribute { + name: "a_color".into(), + attr_type: VertexAttributeType::Float4, + offset: 12, + normalized: false, + }, + ], + stride: 28, + }; + + let gizmo_size = 1.0; + + // Translation axis lines + let translate_verts = Self::generate_axis_lines(gizmo_size); + let translate_line_count = (translate_verts.len() / 7) as u32; + let translate_vbo = backend.create_vertex_buffer( + bytemuck::cast_slice(&translate_verts), + BufferUsage::Static, + ).map_err(|e| format!("Translate VBO: {:?}", e))?; + let translate_vao = backend.create_vertex_array(translate_vbo, None, &layout) + .map_err(|e| format!("Translate VAO: {:?}", e))?; + + // Translation arrow cones + let arrow_verts = Self::generate_arrow_cones(gizmo_size, 16); + let translate_arrow_count = (arrow_verts.len() / 7) as u32; + let translate_arrow_vbo = backend.create_vertex_buffer( + bytemuck::cast_slice(&arrow_verts), + BufferUsage::Static, + ).map_err(|e| format!("Arrow VBO: {:?}", e))?; + let translate_arrow_vao = backend.create_vertex_array(translate_arrow_vbo, None, &layout) + .map_err(|e| format!("Arrow VAO: {:?}", e))?; + + // Rotation circles + let rotate_verts = Self::generate_rotation_circles(gizmo_size * 0.8, 48); + let rotate_vertex_count = (rotate_verts.len() / 7) as u32; + let rotate_vbo = backend.create_vertex_buffer( + bytemuck::cast_slice(&rotate_verts), + BufferUsage::Static, + ).map_err(|e| format!("Rotate VBO: {:?}", e))?; + let rotate_vao = backend.create_vertex_array(rotate_vbo, None, &layout) + .map_err(|e| format!("Rotate VAO: {:?}", e))?; + + // Scale axis lines (same as translate for now) + let scale_verts = Self::generate_axis_lines(gizmo_size); + let scale_line_count = (scale_verts.len() / 7) as u32; + let scale_vbo = backend.create_vertex_buffer( + bytemuck::cast_slice(&scale_verts), + BufferUsage::Static, + ).map_err(|e| format!("Scale VBO: {:?}", e))?; + let scale_vao = backend.create_vertex_array(scale_vbo, None, &layout) + .map_err(|e| format!("Scale VAO: {:?}", e))?; + + // Scale cubes at end of axes + let cube_verts = Self::generate_scale_cubes(gizmo_size, 0.08); + let scale_cube_count = (cube_verts.len() / 7) as u32; + let scale_cube_vbo = backend.create_vertex_buffer( + bytemuck::cast_slice(&cube_verts), + BufferUsage::Static, + ).map_err(|e| format!("Scale cube VBO: {:?}", e))?; + let scale_cube_vao = backend.create_vertex_array(scale_cube_vbo, None, &layout) + .map_err(|e| format!("Scale cube VAO: {:?}", e))?; + + // Center sphere + let center_verts = Self::generate_center_sphere(0.08, 12); + let center_vertex_count = (center_verts.len() / 7) as u32; + let center_vbo = backend.create_vertex_buffer( + bytemuck::cast_slice(¢er_verts), + BufferUsage::Static, + ).map_err(|e| format!("Center VBO: {:?}", e))?; + let center_vao = backend.create_vertex_array(center_vbo, None, &layout) + .map_err(|e| format!("Center VAO: {:?}", e))?; + + // Plane handles + let plane_verts = Self::generate_plane_handles(gizmo_size * 0.3); + let plane_vertex_count = (plane_verts.len() / 7) as u32; + let plane_vbo = backend.create_vertex_buffer( + bytemuck::cast_slice(&plane_verts), + BufferUsage::Static, + ).map_err(|e| format!("Plane VBO: {:?}", e))?; + let plane_vao = backend.create_vertex_array(plane_vbo, None, &layout) + .map_err(|e| format!("Plane VAO: {:?}", e))?; + + Ok(Self { + shader, + translate_vbo, + translate_vao, + translate_line_count, + translate_arrow_vbo, + translate_arrow_vao, + translate_arrow_count, + rotate_vbo, + rotate_vao, + rotate_vertex_count, + scale_vbo, + scale_vao, + scale_line_count, + scale_cube_vbo, + scale_cube_vao, + scale_cube_count, + center_vbo, + center_vao, + center_vertex_count, + plane_vbo, + plane_vao, + plane_vertex_count, + transform_mode: TransformMode::Move, + gizmo_size, + }) + } + + /// Generate XYZ axis lines. + fn generate_axis_lines(length: f32) -> Vec { + let mut verts = Vec::new(); + + // X axis + verts.extend_from_slice(&[0.0, 0.0, 0.0]); + verts.extend_from_slice(&X_AXIS_COLOR); + verts.extend_from_slice(&[length, 0.0, 0.0]); + verts.extend_from_slice(&X_AXIS_COLOR); + + // Y axis + verts.extend_from_slice(&[0.0, 0.0, 0.0]); + verts.extend_from_slice(&Y_AXIS_COLOR); + verts.extend_from_slice(&[0.0, length, 0.0]); + verts.extend_from_slice(&Y_AXIS_COLOR); + + // Z axis + verts.extend_from_slice(&[0.0, 0.0, 0.0]); + verts.extend_from_slice(&Z_AXIS_COLOR); + verts.extend_from_slice(&[0.0, 0.0, length]); + verts.extend_from_slice(&Z_AXIS_COLOR); + + verts + } + + /// Generate arrow cones for translation gizmo. + fn generate_arrow_cones(axis_length: f32, segments: u32) -> Vec { + let mut verts = Vec::new(); + let cone_length = 0.15; + let cone_radius = 0.05; + + // Generate cone for each axis + for axis in 0..3 { + let color = match axis { + 0 => X_AXIS_COLOR, + 1 => Y_AXIS_COLOR, + _ => Z_AXIS_COLOR, + }; + + let tip = match axis { + 0 => [axis_length + cone_length, 0.0, 0.0], + 1 => [0.0, axis_length + cone_length, 0.0], + _ => [0.0, 0.0, axis_length + cone_length], + }; + + let base_center = match axis { + 0 => [axis_length, 0.0, 0.0], + 1 => [0.0, axis_length, 0.0], + _ => [0.0, 0.0, axis_length], + }; + + // Generate cone triangles (as lines for wireframe) + for i in 0..segments { + let angle1 = (i as f32) * 2.0 * PI / (segments as f32); + let angle2 = ((i + 1) as f32) * 2.0 * PI / (segments as f32); + + let (p1, p2) = match axis { + 0 => ( + [base_center[0], cone_radius * angle1.cos(), cone_radius * angle1.sin()], + [base_center[0], cone_radius * angle2.cos(), cone_radius * angle2.sin()], + ), + 1 => ( + [cone_radius * angle1.cos(), base_center[1], cone_radius * angle1.sin()], + [cone_radius * angle2.cos(), base_center[1], cone_radius * angle2.sin()], + ), + _ => ( + [cone_radius * angle1.cos(), cone_radius * angle1.sin(), base_center[2]], + [cone_radius * angle2.cos(), cone_radius * angle2.sin(), base_center[2]], + ), + }; + + // Line from tip to base edge + verts.extend_from_slice(&tip); + verts.extend_from_slice(&color); + verts.extend_from_slice(&p1); + verts.extend_from_slice(&color); + + // Base circle segment + verts.extend_from_slice(&p1); + verts.extend_from_slice(&color); + verts.extend_from_slice(&p2); + verts.extend_from_slice(&color); + } + } + + verts + } + + /// Generate rotation circles for each axis. + fn generate_rotation_circles(radius: f32, segments: u32) -> Vec { + let mut verts = Vec::new(); + + // X axis rotation (YZ plane) - Red + for i in 0..segments { + let angle1 = (i as f32) * 2.0 * PI / (segments as f32); + let angle2 = ((i + 1) as f32) * 2.0 * PI / (segments as f32); + + verts.extend_from_slice(&[0.0, radius * angle1.cos(), radius * angle1.sin()]); + verts.extend_from_slice(&X_AXIS_COLOR); + verts.extend_from_slice(&[0.0, radius * angle2.cos(), radius * angle2.sin()]); + verts.extend_from_slice(&X_AXIS_COLOR); + } + + // Y axis rotation (XZ plane) - Green + for i in 0..segments { + let angle1 = (i as f32) * 2.0 * PI / (segments as f32); + let angle2 = ((i + 1) as f32) * 2.0 * PI / (segments as f32); + + verts.extend_from_slice(&[radius * angle1.cos(), 0.0, radius * angle1.sin()]); + verts.extend_from_slice(&Y_AXIS_COLOR); + verts.extend_from_slice(&[radius * angle2.cos(), 0.0, radius * angle2.sin()]); + verts.extend_from_slice(&Y_AXIS_COLOR); + } + + // Z axis rotation (XY plane) - Blue + for i in 0..segments { + let angle1 = (i as f32) * 2.0 * PI / (segments as f32); + let angle2 = ((i + 1) as f32) * 2.0 * PI / (segments as f32); + + verts.extend_from_slice(&[radius * angle1.cos(), radius * angle1.sin(), 0.0]); + verts.extend_from_slice(&Z_AXIS_COLOR); + verts.extend_from_slice(&[radius * angle2.cos(), radius * angle2.sin(), 0.0]); + verts.extend_from_slice(&Z_AXIS_COLOR); + } + + verts + } + + /// Generate scale cubes at end of axes. + fn generate_scale_cubes(axis_length: f32, cube_size: f32) -> Vec { + let mut verts = Vec::new(); + let half = cube_size / 2.0; + + // Generate cube wireframe for each axis + for axis in 0..3 { + let color = match axis { + 0 => X_AXIS_COLOR, + 1 => Y_AXIS_COLOR, + _ => Z_AXIS_COLOR, + }; + + let center = match axis { + 0 => [axis_length, 0.0, 0.0], + 1 => [0.0, axis_length, 0.0], + _ => [0.0, 0.0, axis_length], + }; + + // 12 edges of cube + let edges = [ + // Bottom face + ([-half, -half, -half], [half, -half, -half]), + ([half, -half, -half], [half, -half, half]), + ([half, -half, half], [-half, -half, half]), + ([-half, -half, half], [-half, -half, -half]), + // Top face + ([-half, half, -half], [half, half, -half]), + ([half, half, -half], [half, half, half]), + ([half, half, half], [-half, half, half]), + ([-half, half, half], [-half, half, -half]), + // Vertical edges + ([-half, -half, -half], [-half, half, -half]), + ([half, -half, -half], [half, half, -half]), + ([half, -half, half], [half, half, half]), + ([-half, -half, half], [-half, half, half]), + ]; + + for (p1, p2) in edges { + verts.extend_from_slice(&[center[0] + p1[0], center[1] + p1[1], center[2] + p1[2]]); + verts.extend_from_slice(&color); + verts.extend_from_slice(&[center[0] + p2[0], center[1] + p2[1], center[2] + p2[2]]); + verts.extend_from_slice(&color); + } + } + + verts + } + + /// Generate center sphere wireframe. + fn generate_center_sphere(radius: f32, segments: u32) -> Vec { + let mut verts = Vec::new(); + + // Three circles for sphere wireframe + // XY plane + for i in 0..segments { + let angle1 = (i as f32) * 2.0 * PI / (segments as f32); + let angle2 = ((i + 1) as f32) * 2.0 * PI / (segments as f32); + + verts.extend_from_slice(&[radius * angle1.cos(), radius * angle1.sin(), 0.0]); + verts.extend_from_slice(&CENTER_COLOR); + verts.extend_from_slice(&[radius * angle2.cos(), radius * angle2.sin(), 0.0]); + verts.extend_from_slice(&CENTER_COLOR); + } + + // XZ plane + for i in 0..segments { + let angle1 = (i as f32) * 2.0 * PI / (segments as f32); + let angle2 = ((i + 1) as f32) * 2.0 * PI / (segments as f32); + + verts.extend_from_slice(&[radius * angle1.cos(), 0.0, radius * angle1.sin()]); + verts.extend_from_slice(&CENTER_COLOR); + verts.extend_from_slice(&[radius * angle2.cos(), 0.0, radius * angle2.sin()]); + verts.extend_from_slice(&CENTER_COLOR); + } + + // YZ plane + for i in 0..segments { + let angle1 = (i as f32) * 2.0 * PI / (segments as f32); + let angle2 = ((i + 1) as f32) * 2.0 * PI / (segments as f32); + + verts.extend_from_slice(&[0.0, radius * angle1.cos(), radius * angle1.sin()]); + verts.extend_from_slice(&CENTER_COLOR); + verts.extend_from_slice(&[0.0, radius * angle2.cos(), radius * angle2.sin()]); + verts.extend_from_slice(&CENTER_COLOR); + } + + verts + } + + /// Generate plane handles for translation. + fn generate_plane_handles(size: f32) -> Vec { + let mut verts = Vec::new(); + let offset = size * 0.3; + + // XY plane handle (Blue) + let xy = [ + [offset, offset, 0.0], + [offset + size, offset, 0.0], + [offset + size, offset + size, 0.0], + [offset, offset + size, 0.0], + ]; + for i in 0..4 { + verts.extend_from_slice(&xy[i]); + verts.extend_from_slice(&XY_PLANE_COLOR); + verts.extend_from_slice(&xy[(i + 1) % 4]); + verts.extend_from_slice(&XY_PLANE_COLOR); + } + + // XZ plane handle (Magenta) + let xz = [ + [offset, 0.0, offset], + [offset + size, 0.0, offset], + [offset + size, 0.0, offset + size], + [offset, 0.0, offset + size], + ]; + for i in 0..4 { + verts.extend_from_slice(&xz[i]); + verts.extend_from_slice(&XZ_PLANE_COLOR); + verts.extend_from_slice(&xz[(i + 1) % 4]); + verts.extend_from_slice(&XZ_PLANE_COLOR); + } + + // YZ plane handle (Cyan) + let yz = [ + [0.0, offset, offset], + [0.0, offset + size, offset], + [0.0, offset + size, offset + size], + [0.0, offset, offset + size], + ]; + for i in 0..4 { + verts.extend_from_slice(&yz[i]); + verts.extend_from_slice(&YZ_PLANE_COLOR); + verts.extend_from_slice(&yz[(i + 1) % 4]); + verts.extend_from_slice(&YZ_PLANE_COLOR); + } + + verts + } + + /// Set the current transform mode. + pub fn set_transform_mode(&mut self, mode: TransformMode) { + self.transform_mode = mode; + } + + /// Get the current transform mode. + pub fn get_transform_mode(&self) -> TransformMode { + self.transform_mode + } + + /// Render the gizmo at a specific position. + /// 在指定位置渲染 Gizmo。 + pub fn render( + &mut self, + backend: &mut impl GraphicsBackend, + camera: &Camera3D, + position: Vec3, + scale: f32, + ) { + let vp = camera.view_projection_matrix(); + + // Calculate distance-based scale to keep gizmo constant screen size + let cam_pos = camera.position; + let distance = (position - cam_pos).length().max(0.1); + let screen_scale = distance * 0.15 * scale; + + // Create model matrix (translation + uniform scale) + let model = Mat4::from_translation(position) * Mat4::from_scale(Vec3::splat(screen_scale)); + + backend.bind_shader(self.shader).ok(); + backend.set_uniform_mat4("u_viewProjection", &vp).ok(); + backend.set_uniform_mat4("u_model", &model).ok(); + backend.set_blend_mode(BlendMode::Alpha); + + // Render center + backend.draw_lines(self.center_vao, self.center_vertex_count, 0).ok(); + + match self.transform_mode { + TransformMode::Select => { + // Just render center + } + TransformMode::Move => { + // Render translation handles + backend.draw_lines(self.translate_vao, self.translate_line_count, 0).ok(); + backend.draw_lines(self.translate_arrow_vao, self.translate_arrow_count, 0).ok(); + backend.draw_lines(self.plane_vao, self.plane_vertex_count, 0).ok(); + } + TransformMode::Rotate => { + // Render rotation handles + backend.draw_lines(self.rotate_vao, self.rotate_vertex_count, 0).ok(); + } + TransformMode::Scale => { + // Render scale handles + backend.draw_lines(self.scale_vao, self.scale_line_count, 0).ok(); + backend.draw_lines(self.scale_cube_vao, self.scale_cube_count, 0).ok(); + } + } + } + + /// Destroy renderer resources. + pub fn destroy(self, backend: &mut impl GraphicsBackend) { + backend.destroy_vertex_array(self.translate_vao); + backend.destroy_buffer(self.translate_vbo); + backend.destroy_vertex_array(self.translate_arrow_vao); + backend.destroy_buffer(self.translate_arrow_vbo); + backend.destroy_vertex_array(self.rotate_vao); + backend.destroy_buffer(self.rotate_vbo); + backend.destroy_vertex_array(self.scale_vao); + backend.destroy_buffer(self.scale_vbo); + backend.destroy_vertex_array(self.scale_cube_vao); + backend.destroy_buffer(self.scale_cube_vbo); + backend.destroy_vertex_array(self.center_vao); + backend.destroy_buffer(self.center_vbo); + backend.destroy_vertex_array(self.plane_vao); + backend.destroy_buffer(self.plane_vbo); + backend.destroy_shader(self.shader); + } +} diff --git a/packages/engine/src/renderer/grid3d.rs b/packages/engine/src/renderer/grid3d.rs new file mode 100644 index 00000000..f6b2ddf7 --- /dev/null +++ b/packages/engine/src/renderer/grid3d.rs @@ -0,0 +1,450 @@ +//! 3D Grid renderer for editor. +//! 编辑器 3D 网格渲染器。 +//! +//! Features: +//! - Multi-level grid (major lines every 10 units, minor lines every 1 unit) +//! - Distance-based fade out for infinite grid effect +//! - RGB colored coordinate axes +//! - Origin marker +//! +//! 特性: +//! - 多层级网格(主网格每10单位,次网格每1单位) +//! - 基于距离的淡出效果,实现无限网格效果 +//! - RGB 彩色坐标轴 +//! - 原点标记 + +use es_engine_shared::{ + traits::backend::{GraphicsBackend, BufferUsage}, + types::{ + handle::{ShaderHandle, BufferHandle, VertexArrayHandle}, + vertex::{VertexLayout, VertexAttribute, VertexAttributeType}, + blend::BlendMode, + }, + Vec3, +}; +use super::camera3d::Camera3D; +use super::shader::{GRID3D_VERTEX_SHADER, GRID3D_FRAGMENT_SHADER}; + +/// Grid configuration. +/// 网格配置。 +#[derive(Debug, Clone)] +pub struct GridConfig { + /// Size of the grid (extends from -size/2 to +size/2). + /// 网格大小(从 -size/2 延伸到 +size/2)。 + pub size: f32, + + /// Major grid spacing (typically 10 units). + /// 主网格间距(通常为10单位)。 + pub major_spacing: f32, + + /// Minor grid spacing (typically 1 unit). + /// 次网格间距(通常为1单位)。 + pub minor_spacing: f32, + + /// Major grid line alpha. + /// 主网格线透明度。 + pub major_alpha: f32, + + /// Minor grid line alpha. + /// 次网格线透明度。 + pub minor_alpha: f32, + + /// Distance at which fade starts. + /// 开始淡出的距离。 + pub fade_start: f32, + + /// Distance at which grid is fully transparent. + /// 完全透明的距离。 + pub fade_end: f32, + + /// Axis line length. + /// 坐标轴线长度。 + pub axis_length: f32, +} + +impl Default for GridConfig { + fn default() -> Self { + Self { + size: 100.0, + major_spacing: 10.0, + minor_spacing: 1.0, + major_alpha: 0.5, + minor_alpha: 0.15, + fade_start: 30.0, + fade_end: 50.0, + axis_length: 1000.0, + } + } +} + +/// 3D grid renderer for displaying ground plane and axes. +/// 用于显示地面平面和坐标轴的 3D 网格渲染器。 +pub struct Grid3DRenderer { + shader: ShaderHandle, + + // Major grid (every 10 units) + major_grid_vbo: BufferHandle, + major_grid_vao: VertexArrayHandle, + major_grid_vertex_count: u32, + + // Minor grid (every 1 unit) + minor_grid_vbo: BufferHandle, + minor_grid_vao: VertexArrayHandle, + minor_grid_vertex_count: u32, + + // Coordinate axes + axis_vbo: BufferHandle, + axis_vao: VertexArrayHandle, + axis_vertex_count: u32, + + // Origin marker + origin_vbo: BufferHandle, + origin_vao: VertexArrayHandle, + origin_vertex_count: u32, + + config: GridConfig, +} + +impl Grid3DRenderer { + /// Create a new 3D grid renderer with default configuration. + /// 使用默认配置创建新的 3D 网格渲染器。 + pub fn new(backend: &mut impl GraphicsBackend) -> Result { + Self::with_config(backend, GridConfig::default()) + } + + /// Create a new 3D grid renderer with custom configuration. + /// 使用自定义配置创建新的 3D 网格渲染器。 + pub fn with_config(backend: &mut impl GraphicsBackend, config: GridConfig) -> Result { + // Compile shader + let shader = backend.compile_shader(GRID3D_VERTEX_SHADER, GRID3D_FRAGMENT_SHADER) + .map_err(|e| format!("3D Grid shader: {:?}", e))?; + + // Create vertex layout for 3D lines (position + color) + let layout = VertexLayout { + attributes: vec![ + VertexAttribute { + name: "a_position".into(), + attr_type: VertexAttributeType::Float3, + offset: 0, + normalized: false, + }, + VertexAttribute { + name: "a_color".into(), + attr_type: VertexAttributeType::Float4, + offset: 12, + normalized: false, + }, + ], + stride: 28, // 3 floats position + 4 floats color = 7 * 4 = 28 bytes + }; + + // Generate major grid vertices (every 10 units) + let major_vertices = Self::generate_grid_vertices( + config.size, + config.major_spacing, + [0.5, 0.5, 0.5, config.major_alpha], + true, // Skip center lines (will be axes) + ); + let major_grid_vertex_count = (major_vertices.len() / 7) as u32; + + let major_grid_vbo = backend.create_vertex_buffer( + bytemuck::cast_slice(&major_vertices), + BufferUsage::Static, + ).map_err(|e| format!("3D Major Grid VBO: {:?}", e))?; + + let major_grid_vao = backend.create_vertex_array(major_grid_vbo, None, &layout) + .map_err(|e| format!("3D Major Grid VAO: {:?}", e))?; + + // Generate minor grid vertices (every 1 unit, skip major lines) + let minor_vertices = Self::generate_minor_grid_vertices( + config.size, + config.minor_spacing, + config.major_spacing, + [0.4, 0.4, 0.4, config.minor_alpha], + ); + let minor_grid_vertex_count = (minor_vertices.len() / 7) as u32; + + let minor_grid_vbo = backend.create_vertex_buffer( + bytemuck::cast_slice(&minor_vertices), + BufferUsage::Static, + ).map_err(|e| format!("3D Minor Grid VBO: {:?}", e))?; + + let minor_grid_vao = backend.create_vertex_array(minor_grid_vbo, None, &layout) + .map_err(|e| format!("3D Minor Grid VAO: {:?}", e))?; + + // Generate axis vertices + let axis_vertices = Self::generate_axis_vertices(config.axis_length); + let axis_vertex_count = (axis_vertices.len() / 7) as u32; + + let axis_vbo = backend.create_vertex_buffer( + bytemuck::cast_slice(&axis_vertices), + BufferUsage::Static, + ).map_err(|e| format!("3D Axis VBO: {:?}", e))?; + + let axis_vao = backend.create_vertex_array(axis_vbo, None, &layout) + .map_err(|e| format!("3D Axis VAO: {:?}", e))?; + + // Generate origin marker + let origin_vertices = Self::generate_origin_marker(0.5); + let origin_vertex_count = (origin_vertices.len() / 7) as u32; + + let origin_vbo = backend.create_vertex_buffer( + bytemuck::cast_slice(&origin_vertices), + BufferUsage::Static, + ).map_err(|e| format!("3D Origin VBO: {:?}", e))?; + + let origin_vao = backend.create_vertex_array(origin_vbo, None, &layout) + .map_err(|e| format!("3D Origin VAO: {:?}", e))?; + + Ok(Self { + shader, + major_grid_vbo, + major_grid_vao, + major_grid_vertex_count, + minor_grid_vbo, + minor_grid_vao, + minor_grid_vertex_count, + axis_vbo, + axis_vao, + axis_vertex_count, + origin_vbo, + origin_vao, + origin_vertex_count, + config, + }) + } + + /// Generate grid vertices on XZ plane (Y = 0). + /// 在 XZ 平面上生成网格顶点(Y = 0)。 + fn generate_grid_vertices(size: f32, spacing: f32, color: [f32; 4], skip_center: bool) -> Vec { + let mut vertices = Vec::new(); + let half_size = size / 2.0; + let line_count = (size / spacing) as i32; + + // Generate lines along X axis (varying Z) + for i in -line_count/2..=line_count/2 { + let z = i as f32 * spacing; + + // Skip center line if requested (will be drawn as axis) + if skip_center && i == 0 { + continue; + } + + // Start point + vertices.extend_from_slice(&[-half_size, 0.0, z]); + vertices.extend_from_slice(&color); + // End point + vertices.extend_from_slice(&[half_size, 0.0, z]); + vertices.extend_from_slice(&color); + } + + // Generate lines along Z axis (varying X) + for i in -line_count/2..=line_count/2 { + let x = i as f32 * spacing; + + // Skip center line if requested (will be drawn as axis) + if skip_center && i == 0 { + continue; + } + + // Start point + vertices.extend_from_slice(&[x, 0.0, -half_size]); + vertices.extend_from_slice(&color); + // End point + vertices.extend_from_slice(&[x, 0.0, half_size]); + vertices.extend_from_slice(&color); + } + + vertices + } + + /// Generate minor grid vertices, skipping major grid lines. + /// 生成次网格顶点,跳过主网格线。 + fn generate_minor_grid_vertices( + size: f32, + minor_spacing: f32, + major_spacing: f32, + color: [f32; 4] + ) -> Vec { + let mut vertices = Vec::new(); + let half_size = size / 2.0; + let line_count = (size / minor_spacing) as i32; + let epsilon = minor_spacing * 0.01; // Small tolerance for float comparison + + // Generate lines along X axis (varying Z) + for i in -line_count/2..=line_count/2 { + let z = i as f32 * minor_spacing; + + // Skip if this is a major line or center line + let is_major = (z.abs() % major_spacing).abs() < epsilon + || (z.abs() % major_spacing - major_spacing).abs() < epsilon; + if is_major || z.abs() < epsilon { + continue; + } + + // Start point + vertices.extend_from_slice(&[-half_size, 0.0, z]); + vertices.extend_from_slice(&color); + // End point + vertices.extend_from_slice(&[half_size, 0.0, z]); + vertices.extend_from_slice(&color); + } + + // Generate lines along Z axis (varying X) + for i in -line_count/2..=line_count/2 { + let x = i as f32 * minor_spacing; + + // Skip if this is a major line or center line + let is_major = (x.abs() % major_spacing).abs() < epsilon + || (x.abs() % major_spacing - major_spacing).abs() < epsilon; + if is_major || x.abs() < epsilon { + continue; + } + + // Start point + vertices.extend_from_slice(&[x, 0.0, -half_size]); + vertices.extend_from_slice(&color); + // End point + vertices.extend_from_slice(&[x, 0.0, half_size]); + vertices.extend_from_slice(&color); + } + + vertices + } + + /// Generate axis vertices (X = red, Y = green, Z = blue). + /// 生成坐标轴顶点(X = 红色,Y = 绿色,Z = 蓝色)。 + fn generate_axis_vertices(length: f32) -> Vec { + let mut vertices = Vec::new(); + + // X axis (red) - extends in both directions + // X 轴(红色)- 双向延伸 + vertices.extend_from_slice(&[-length, 0.0, 0.0]); + vertices.extend_from_slice(&[0.6, 0.2, 0.2, 0.6]); // Negative side dimmer + vertices.extend_from_slice(&[0.0, 0.0, 0.0]); + vertices.extend_from_slice(&[0.6, 0.2, 0.2, 0.6]); + + vertices.extend_from_slice(&[0.0, 0.0, 0.0]); + vertices.extend_from_slice(&[1.0, 0.3, 0.3, 1.0]); // Positive side brighter + vertices.extend_from_slice(&[length, 0.0, 0.0]); + vertices.extend_from_slice(&[1.0, 0.3, 0.3, 1.0]); + + // Y axis (green) - extends upward and downward + // Y 轴(绿色)- 向上和向下延伸 + vertices.extend_from_slice(&[0.0, -length, 0.0]); + vertices.extend_from_slice(&[0.2, 0.6, 0.2, 0.6]); // Negative side dimmer + vertices.extend_from_slice(&[0.0, 0.0, 0.0]); + vertices.extend_from_slice(&[0.2, 0.6, 0.2, 0.6]); + + vertices.extend_from_slice(&[0.0, 0.0, 0.0]); + vertices.extend_from_slice(&[0.3, 1.0, 0.3, 1.0]); // Positive side brighter + vertices.extend_from_slice(&[0.0, length, 0.0]); + vertices.extend_from_slice(&[0.3, 1.0, 0.3, 1.0]); + + // Z axis (blue) - extends in both directions + // Z 轴(蓝色)- 双向延伸 + vertices.extend_from_slice(&[0.0, 0.0, -length]); + vertices.extend_from_slice(&[0.2, 0.2, 0.6, 0.6]); // Negative side dimmer + vertices.extend_from_slice(&[0.0, 0.0, 0.0]); + vertices.extend_from_slice(&[0.2, 0.2, 0.6, 0.6]); + + vertices.extend_from_slice(&[0.0, 0.0, 0.0]); + vertices.extend_from_slice(&[0.3, 0.3, 1.0, 1.0]); // Positive side brighter + vertices.extend_from_slice(&[0.0, 0.0, length]); + vertices.extend_from_slice(&[0.3, 0.3, 1.0, 1.0]); + + vertices + } + + /// Generate origin marker (small cross at origin). + /// 生成原点标记(原点处的小十字)。 + fn generate_origin_marker(size: f32) -> Vec { + let mut vertices = Vec::new(); + let color = [1.0, 1.0, 1.0, 0.8]; // White + + // Small cross on XZ plane + vertices.extend_from_slice(&[-size, 0.0, 0.0]); + vertices.extend_from_slice(&color); + vertices.extend_from_slice(&[size, 0.0, 0.0]); + vertices.extend_from_slice(&color); + + vertices.extend_from_slice(&[0.0, 0.0, -size]); + vertices.extend_from_slice(&color); + vertices.extend_from_slice(&[0.0, 0.0, size]); + vertices.extend_from_slice(&color); + + // Vertical line + vertices.extend_from_slice(&[0.0, -size, 0.0]); + vertices.extend_from_slice(&color); + vertices.extend_from_slice(&[0.0, size, 0.0]); + vertices.extend_from_slice(&color); + + vertices + } + + /// Render the 3D grid (both major and minor lines). + /// 渲染 3D 网格(主网格线和次网格线)。 + pub fn render(&mut self, backend: &mut impl GraphicsBackend, camera: &Camera3D) { + let vp = camera.view_projection_matrix(); + let cam_pos = camera.position; + + // Calculate fade distances based on camera height for dynamic density + // 根据相机高度计算淡出距离以实现动态密度 + let cam_height = cam_pos.y.abs().max(1.0); + let minor_fade_start = (cam_height * 1.5).min(self.config.fade_start * 0.5); + let minor_fade_end = (cam_height * 3.0).min(self.config.fade_start); + + // Bind shader and set common uniforms + backend.bind_shader(self.shader).ok(); + backend.set_uniform_mat4("u_viewProjection", &vp).ok(); + backend.set_uniform_vec3("u_cameraPos", Vec3::new(cam_pos.x, cam_pos.y, cam_pos.z)).ok(); + backend.set_blend_mode(BlendMode::Alpha); + + // Render minor grid (fades out faster when camera is close) + backend.set_uniform_f32("u_fadeStart", minor_fade_start).ok(); + backend.set_uniform_f32("u_fadeEnd", minor_fade_end).ok(); + backend.draw_lines(self.minor_grid_vao, self.minor_grid_vertex_count, 0).ok(); + + // Render major grid + backend.set_uniform_f32("u_fadeStart", self.config.fade_start).ok(); + backend.set_uniform_f32("u_fadeEnd", self.config.fade_end).ok(); + backend.draw_lines(self.major_grid_vao, self.major_grid_vertex_count, 0).ok(); + + // Render origin marker (always visible, no fade) + backend.set_uniform_f32("u_fadeStart", 1000.0).ok(); + backend.set_uniform_f32("u_fadeEnd", 2000.0).ok(); + backend.draw_lines(self.origin_vao, self.origin_vertex_count, 0).ok(); + } + + /// Render the coordinate axes. + /// 渲染坐标轴。 + pub fn render_axes(&mut self, backend: &mut impl GraphicsBackend, camera: &Camera3D) { + let vp = camera.view_projection_matrix(); + let cam_pos = camera.position; + + backend.bind_shader(self.shader).ok(); + backend.set_uniform_mat4("u_viewProjection", &vp).ok(); + backend.set_uniform_vec3("u_cameraPos", Vec3::new(cam_pos.x, cam_pos.y, cam_pos.z)).ok(); + + // Axes fade slower than grid + backend.set_uniform_f32("u_fadeStart", self.config.fade_end * 2.0).ok(); + backend.set_uniform_f32("u_fadeEnd", self.config.fade_end * 4.0).ok(); + + backend.set_blend_mode(BlendMode::Alpha); + backend.draw_lines(self.axis_vao, self.axis_vertex_count, 0).ok(); + } + + /// Destroy renderer resources. + /// 销毁渲染器资源。 + pub fn destroy(self, backend: &mut impl GraphicsBackend) { + backend.destroy_vertex_array(self.major_grid_vao); + backend.destroy_buffer(self.major_grid_vbo); + backend.destroy_vertex_array(self.minor_grid_vao); + backend.destroy_buffer(self.minor_grid_vbo); + backend.destroy_vertex_array(self.axis_vao); + backend.destroy_buffer(self.axis_vbo); + backend.destroy_vertex_array(self.origin_vao); + backend.destroy_buffer(self.origin_vbo); + backend.destroy_shader(self.shader); + } +} diff --git a/packages/engine/src/renderer/mod.rs b/packages/engine/src/renderer/mod.rs index 402e951c..be29ca77 100644 --- a/packages/engine/src/renderer/mod.rs +++ b/packages/engine/src/renderer/mod.rs @@ -1,5 +1,5 @@ -//! 2D rendering system with batch optimization. -//! 带批处理优化的2D渲染系统。 +//! 2D and 3D rendering system with batch optimization. +//! 带批处理优化的2D和3D渲染系统。 pub mod batch; pub mod shader; @@ -7,17 +7,31 @@ pub mod texture; pub mod material; mod renderer2d; +mod renderer3d; mod camera; +mod camera3d; mod grid; +mod grid3d; mod gizmo; +mod gizmo3d; mod viewport; pub use renderer2d::Renderer2D; +pub use renderer3d::{Renderer3D, MeshSubmission}; pub use camera::Camera2D; +pub use camera3d::{Camera3D, ProjectionType, Ray3D}; pub use batch::{SpriteBatch, TextBatch, MeshBatch}; +pub use batch::{Vertex3D, SimpleVertex3D, VERTEX3D_SIZE, FLOATS_PER_VERTEX_3D}; pub use texture::{Texture, TextureManager}; pub use grid::GridRenderer; +pub use grid3d::Grid3DRenderer; pub use gizmo::{GizmoRenderer, TransformMode}; +pub use gizmo3d::Gizmo3DRenderer; pub use viewport::{RenderTarget, ViewportManager, ViewportConfig}; pub use shader::{ShaderManager, ShaderProgram, SHADER_ID_DEFAULT_SPRITE}; +pub use shader::{ + MESH3D_VERTEX_SHADER, MESH3D_FRAGMENT_SHADER_UNLIT, MESH3D_FRAGMENT_SHADER_LIT, + SIMPLE3D_VERTEX_SHADER, SIMPLE3D_FRAGMENT_SHADER, + GRID3D_VERTEX_SHADER, GRID3D_FRAGMENT_SHADER, +}; pub use material::{Material, MaterialManager, BlendMode, CullMode, UniformValue, MaterialUniforms}; diff --git a/packages/engine/src/renderer/renderer3d.rs b/packages/engine/src/renderer/renderer3d.rs new file mode 100644 index 00000000..c6fef51d --- /dev/null +++ b/packages/engine/src/renderer/renderer3d.rs @@ -0,0 +1,481 @@ +//! Main 3D renderer implementation. +//! 主3D渲染器实现。 +//! +//! Provides perspective and orthographic 3D rendering with depth testing. +//! 提供带深度测试的透视和正交3D渲染。 + +use es_engine_shared::{ + traits::backend::GraphicsBackend, + types::{ + handle::ShaderHandle, + blend::{RenderState, CompareFunc, CullMode, BlendMode as SharedBlendMode}, + }, + Mat4, +}; +use std::collections::HashMap; +use crate::backend::WebGL2Backend; +use super::camera3d::Camera3D; +use super::batch::{SimpleVertex3D, FLOATS_PER_SIMPLE_VERTEX_3D}; +use super::texture::TextureManager; +use super::material::{Material, BlendMode, UniformValue}; +use super::shader::{SIMPLE3D_VERTEX_SHADER, SIMPLE3D_FRAGMENT_SHADER}; + +/// Convert local BlendMode to shared BlendMode. +/// 将本地 BlendMode 转换为共享 BlendMode。 +fn to_shared_blend_mode(mode: BlendMode) -> SharedBlendMode { + match mode { + BlendMode::None => SharedBlendMode::None, + BlendMode::Alpha => SharedBlendMode::Alpha, + BlendMode::Additive => SharedBlendMode::Additive, + BlendMode::Multiply => SharedBlendMode::Multiply, + BlendMode::Screen => SharedBlendMode::Screen, + BlendMode::PremultipliedAlpha => SharedBlendMode::PremultipliedAlpha, + } +} + +/// Mesh submission data for batched 3D rendering. +/// 用于批处理3D渲染的网格提交数据。 +#[derive(Debug, Clone)] +pub struct MeshSubmission { + /// Vertex data (position, uv, color). + /// 顶点数据(位置、UV、颜色)。 + pub vertices: Vec, + /// Index data. + /// 索引数据。 + pub indices: Vec, + /// Model transformation matrix. + /// 模型变换矩阵。 + pub transform: Mat4, + /// Material ID. + /// 材质 ID。 + pub material_id: u32, + /// Texture ID. + /// 纹理 ID。 + pub texture_id: u32, +} + +/// 3D Renderer with perspective/orthographic camera support. +/// 支持透视/正交相机的3D渲染器。 +pub struct Renderer3D { + /// 3D camera. + /// 3D相机。 + camera: Camera3D, + + /// Default 3D shader. + /// 默认3D着色器。 + default_shader: ShaderHandle, + + /// Custom shaders by ID. + /// 按ID存储的自定义着色器。 + custom_shaders: HashMap, + + /// Next shader ID for auto-assignment. + /// 自动分配的下一个着色器ID。 + next_shader_id: u32, + + /// Materials by ID. + /// 按ID存储的材质。 + materials: HashMap, + + /// Pending mesh submissions for this frame. + /// 本帧待渲染的网格提交。 + mesh_queue: Vec, + + /// Clear color. + /// 清除颜色。 + clear_color: [f32; 4], + + /// Whether depth test is enabled. + /// 是否启用深度测试。 + depth_test_enabled: bool, + + /// Whether depth write is enabled. + /// 是否启用深度写入。 + depth_write_enabled: bool, +} + +impl Renderer3D { + /// Create a new 3D renderer. + /// 创建新的3D渲染器。 + pub fn new(backend: &mut WebGL2Backend) -> Result { + // Compile default 3D shader + // 编译默认3D着色器 + let default_shader = backend + .compile_shader(SIMPLE3D_VERTEX_SHADER, SIMPLE3D_FRAGMENT_SHADER) + .map_err(|e| format!("Failed to compile 3D shader: {:?}", e))?; + + let (width, height) = (backend.width() as f32, backend.height() as f32); + let camera = Camera3D::new(width, height, std::f32::consts::FRAC_PI_4); + + let mut materials = HashMap::new(); + materials.insert(0, Material::default()); + + Ok(Self { + camera, + default_shader, + custom_shaders: HashMap::new(), + next_shader_id: 100, + materials, + mesh_queue: Vec::new(), + clear_color: [0.1, 0.1, 0.12, 1.0], + depth_test_enabled: true, + depth_write_enabled: true, + }) + } + + /// Submit a mesh for rendering. + /// 提交网格进行渲染。 + pub fn submit_mesh(&mut self, submission: MeshSubmission) { + self.mesh_queue.push(submission); + } + + /// Submit a simple textured quad at position. + /// 在指定位置提交一个简单的纹理四边形。 + pub fn submit_quad( + &mut self, + position: [f32; 3], + size: [f32; 2], + texture_id: u32, + color: [f32; 4], + material_id: u32, + ) { + let half_w = size[0] / 2.0; + let half_h = size[1] / 2.0; + + let vertices = vec![ + SimpleVertex3D::new([-half_w, -half_h, 0.0], [0.0, 1.0], color), + SimpleVertex3D::new([half_w, -half_h, 0.0], [1.0, 1.0], color), + SimpleVertex3D::new([half_w, half_h, 0.0], [1.0, 0.0], color), + SimpleVertex3D::new([-half_w, half_h, 0.0], [0.0, 0.0], color), + ]; + + let indices = vec![0, 1, 2, 2, 3, 0]; + + let transform = Mat4::from_translation(glam::Vec3::new( + position[0], + position[1], + position[2], + )); + + self.mesh_queue.push(MeshSubmission { + vertices, + indices, + transform, + material_id, + texture_id, + }); + } + + /// Render all submitted meshes. + /// 渲染所有已提交的网格。 + pub fn render( + &mut self, + backend: &mut WebGL2Backend, + texture_manager: &TextureManager, + ) -> Result<(), String> { + if self.mesh_queue.is_empty() { + return Ok(()); + } + + // Apply 3D render state (depth test enabled) + // 应用3D渲染状态(启用深度测试) + let render_state = RenderState { + blend_mode: SharedBlendMode::Alpha, + cull_mode: CullMode::Back, + depth_test: self.depth_test_enabled, + depth_write: self.depth_write_enabled, + depth_func: CompareFunc::LessEqual, + scissor: None, + }; + backend.apply_render_state(&render_state); + + // Get view-projection matrix + // 获取视图-投影矩阵 + let view_projection = self.camera.view_projection_matrix(); + + let mut current_material_id = u32::MAX; + let mut current_texture_id = u32::MAX; + + for submission in &self.mesh_queue { + // Bind material/shader if changed + // 如果材质/着色器变化则绑定 + if submission.material_id != current_material_id { + current_material_id = submission.material_id; + + let material = self + .materials + .get(&submission.material_id) + .cloned() + .unwrap_or_default(); + + let shader = if material.shader_id == 0 { + self.default_shader + } else { + self.custom_shaders + .get(&material.shader_id) + .copied() + .unwrap_or(self.default_shader) + }; + + backend.bind_shader(shader).ok(); + backend.set_blend_mode(to_shared_blend_mode(material.blend_mode)); + + // Set view-projection matrix + // 设置视图-投影矩阵 + backend + .set_uniform_mat4("u_viewProjection", &view_projection) + .ok(); + backend.set_uniform_i32("u_texture", 0).ok(); + + // Apply custom uniforms + // 应用自定义 uniforms + for name in material.uniforms.names() { + if let Some(value) = material.uniforms.get(name) { + match value { + UniformValue::Float(v) => { + backend.set_uniform_f32(name, *v).ok(); + } + UniformValue::Vec2(v) => { + backend + .set_uniform_vec2( + name, + es_engine_shared::Vec2::new(v[0], v[1]), + ) + .ok(); + } + UniformValue::Vec3(v) => { + backend + .set_uniform_vec3( + name, + es_engine_shared::Vec3::new(v[0], v[1], v[2]), + ) + .ok(); + } + UniformValue::Vec4(v) => { + backend + .set_uniform_vec4( + name, + es_engine_shared::Vec4::new(v[0], v[1], v[2], v[3]), + ) + .ok(); + } + UniformValue::Int(v) => { + backend.set_uniform_i32(name, *v).ok(); + } + UniformValue::Mat3(v) => { + backend + .set_uniform_mat3(name, &es_engine_shared::Mat3::from_cols_array(v)) + .ok(); + } + UniformValue::Mat4(v) => { + backend + .set_uniform_mat4(name, &es_engine_shared::Mat4::from_cols_array(v)) + .ok(); + } + UniformValue::Sampler(v) => { + backend.set_uniform_i32(name, *v).ok(); + } + } + } + } + } + + // Bind texture if changed + // 如果纹理变化则绑定 + if submission.texture_id != current_texture_id { + current_texture_id = submission.texture_id; + texture_manager.bind_texture_via_backend(backend, submission.texture_id, 0); + } + + // Set model matrix for this mesh + // 设置此网格的模型矩阵 + backend + .set_uniform_mat4("u_model", &submission.transform) + .ok(); + + // TODO: For now, we'll render each mesh individually + // In the future, implement proper mesh batching + // 目前我们逐个渲染网格,未来实现正确的网格批处理 + + // Create temporary vertex buffer and VAO for this mesh + // 为此网格创建临时顶点缓冲区和VAO + self.render_mesh_immediate(backend, &submission.vertices, &submission.indices)?; + } + + // Reset to 2D render state + // 重置为2D渲染状态 + let default_state = RenderState::default(); + backend.apply_render_state(&default_state); + + self.mesh_queue.clear(); + Ok(()) + } + + /// Render a mesh immediately (no batching). + /// 立即渲染网格(无批处理)。 + fn render_mesh_immediate( + &self, + backend: &mut WebGL2Backend, + vertices: &[SimpleVertex3D], + indices: &[u32], + ) -> Result<(), String> { + use es_engine_shared::types::vertex::{VertexLayout, VertexAttribute, VertexAttributeType}; + use es_engine_shared::BufferUsage; + + // Create vertex layout for SimpleVertex3D + // 为SimpleVertex3D创建顶点布局 + let layout = VertexLayout { + attributes: vec![ + VertexAttribute { + name: "a_position", + attr_type: VertexAttributeType::Float3, + offset: 0, + normalized: false, + }, + VertexAttribute { + name: "a_texCoord", + attr_type: VertexAttributeType::Float2, + offset: 12, // 3 * 4 bytes + normalized: false, + }, + VertexAttribute { + name: "a_color", + attr_type: VertexAttributeType::Float4, + offset: 20, // 3 * 4 + 2 * 4 bytes + normalized: false, + }, + ], + stride: FLOATS_PER_SIMPLE_VERTEX_3D * 4, // 9 * 4 = 36 bytes + }; + + // Convert vertices to bytes + // 将顶点转换为字节 + let vertex_data: &[u8] = bytemuck::cast_slice(vertices); + + // Create buffers + // 创建缓冲区 + let vertex_buffer = backend + .create_vertex_buffer(vertex_data, BufferUsage::Dynamic) + .map_err(|e| format!("Failed to create vertex buffer: {:?}", e))?; + + let index_buffer = backend + .create_index_buffer_u32(indices, BufferUsage::Dynamic) + .map_err(|e| format!("Failed to create index buffer: {:?}", e))?; + + // Create VAO + // 创建VAO + let vao = backend + .create_vertex_array(vertex_buffer, Some(index_buffer), &layout) + .map_err(|e| format!("Failed to create VAO: {:?}", e))?; + + // Draw + // 绘制 + backend + .draw_indexed_u32(vao, indices.len() as u32, 0) + .map_err(|e| format!("Failed to draw: {:?}", e))?; + + // Cleanup + // 清理 + backend.destroy_vertex_array(vao); + backend.destroy_buffer(vertex_buffer); + backend.destroy_buffer(index_buffer); + + Ok(()) + } + + /// Get mutable reference to camera. + /// 获取相机的可变引用。 + #[inline] + pub fn camera_mut(&mut self) -> &mut Camera3D { + &mut self.camera + } + + /// Get reference to camera. + /// 获取相机的引用。 + #[inline] + pub fn camera(&self) -> &Camera3D { + &self.camera + } + + /// Set clear color. + /// 设置清除颜色。 + pub fn set_clear_color(&mut self, r: f32, g: f32, b: f32, a: f32) { + self.clear_color = [r, g, b, a]; + } + + /// Get clear color. + /// 获取清除颜色。 + pub fn get_clear_color(&self) -> [f32; 4] { + self.clear_color + } + + /// Resize viewport. + /// 调整视口大小。 + pub fn resize(&mut self, width: f32, height: f32) { + self.camera.set_viewport(width, height); + } + + /// Enable or disable depth testing. + /// 启用或禁用深度测试。 + pub fn set_depth_test(&mut self, enabled: bool) { + self.depth_test_enabled = enabled; + } + + /// Enable or disable depth writing. + /// 启用或禁用深度写入。 + pub fn set_depth_write(&mut self, enabled: bool) { + self.depth_write_enabled = enabled; + } + + /// Compile a custom shader. + /// 编译自定义着色器。 + pub fn compile_shader( + &mut self, + backend: &mut WebGL2Backend, + vertex: &str, + fragment: &str, + ) -> Result { + let handle = backend + .compile_shader(vertex, fragment) + .map_err(|e| format!("{:?}", e))?; + let id = self.next_shader_id; + self.next_shader_id += 1; + self.custom_shaders.insert(id, handle); + Ok(id) + } + + /// Register a material. + /// 注册材质。 + pub fn register_material(&mut self, material: Material) -> u32 { + let id = self.materials.keys().max().unwrap_or(&0) + 1; + self.materials.insert(id, material); + id + } + + /// Register material with specific ID. + /// 使用特定ID注册材质。 + pub fn register_material_with_id(&mut self, id: u32, material: Material) { + self.materials.insert(id, material); + } + + /// Get material by ID. + /// 按ID获取材质。 + pub fn get_material(&self, id: u32) -> Option<&Material> { + self.materials.get(&id) + } + + /// Get mutable material by ID. + /// 按ID获取可变材质。 + pub fn get_material_mut(&mut self, id: u32) -> Option<&mut Material> { + self.materials.get_mut(&id) + } + + /// Clean up resources. + /// 清理资源。 + pub fn destroy(self, backend: &mut WebGL2Backend) { + backend.destroy_shader(self.default_shader); + for (_, handle) in self.custom_shaders { + backend.destroy_shader(handle); + } + } +} diff --git a/packages/engine/src/renderer/shader/builtin.rs b/packages/engine/src/renderer/shader/builtin.rs index 7c510cf0..8c0a6f53 100644 --- a/packages/engine/src/renderer/shader/builtin.rs +++ b/packages/engine/src/renderer/shader/builtin.rs @@ -145,3 +145,236 @@ void main() { } } "#; + +// ============================================================================= +// 3D Shaders +// 3D着色器 +// ============================================================================= + +/// 3D mesh vertex shader source. +/// 3D网格顶点着色器源代码。 +/// +/// Handles 3D transformation with position, UV, color, and normal attributes. +/// 处理带有位置、UV、颜色和法线属性的3D变换。 +pub const MESH3D_VERTEX_SHADER: &str = r#"#version 300 es +precision highp float; + +// Vertex attributes | 顶点属性 +layout(location = 0) in vec3 a_position; +layout(location = 1) in vec2 a_texCoord; +layout(location = 2) in vec4 a_color; +layout(location = 3) in vec3 a_normal; + +// Uniforms | 统一变量 +uniform mat4 u_viewProjection; +uniform mat4 u_model; + +// Outputs to fragment shader | 输出到片段着色器 +out vec2 v_texCoord; +out vec4 v_color; +out vec3 v_normal; +out vec3 v_worldPos; + +void main() { + // Transform position to world space | 将位置变换到世界空间 + vec4 worldPos = u_model * vec4(a_position, 1.0); + v_worldPos = worldPos.xyz; + + // Apply view-projection matrix | 应用视图-投影矩阵 + gl_Position = u_viewProjection * worldPos; + + // Transform normal to world space | 将法线变换到世界空间 + // Using mat3 to ignore translation, should use inverse-transpose for non-uniform scaling + // 使用 mat3 忽略平移,非均匀缩放时应使用逆转置矩阵 + v_normal = mat3(u_model) * a_normal; + + // Pass through to fragment shader | 传递到片段着色器 + v_texCoord = a_texCoord; + v_color = a_color; +} +"#; + +/// 3D mesh fragment shader source (unlit). +/// 3D网格片段着色器源代码(无光照)。 +/// +/// Samples texture and applies vertex color, without lighting calculations. +/// 采样纹理并应用顶点颜色,不进行光照计算。 +pub const MESH3D_FRAGMENT_SHADER_UNLIT: &str = r#"#version 300 es +precision highp float; + +// Inputs from vertex shader | 来自顶点着色器的输入 +in vec2 v_texCoord; +in vec4 v_color; +in vec3 v_normal; +in vec3 v_worldPos; + +// Texture sampler | 纹理采样器 +uniform sampler2D u_texture; + +// Output color | 输出颜色 +out vec4 fragColor; + +void main() { + // Sample texture and multiply by vertex color | 采样纹理并乘以顶点颜色 + vec4 texColor = texture(u_texture, v_texCoord); + fragColor = texColor * v_color; + + // Discard fully transparent pixels | 丢弃完全透明的像素 + if (fragColor.a < 0.01) { + discard; + } +} +"#; + +/// 3D mesh fragment shader source (basic directional lighting). +/// 3D网格片段着色器源代码(基础方向光照)。 +/// +/// Applies simple directional lighting with ambient term. +/// 应用带环境光的简单方向光照。 +pub const MESH3D_FRAGMENT_SHADER_LIT: &str = r#"#version 300 es +precision highp float; + +// Inputs from vertex shader | 来自顶点着色器的输入 +in vec2 v_texCoord; +in vec4 v_color; +in vec3 v_normal; +in vec3 v_worldPos; + +// Texture sampler | 纹理采样器 +uniform sampler2D u_texture; + +// Lighting uniforms | 光照统一变量 +uniform vec3 u_lightDirection; // Normalized direction TO light | 指向光源的归一化方向 +uniform vec3 u_lightColor; // Light color and intensity | 光源颜色和强度 +uniform vec3 u_ambientColor; // Ambient light color | 环境光颜色 + +// Output color | 输出颜色 +out vec4 fragColor; + +void main() { + // Sample texture | 采样纹理 + vec4 texColor = texture(u_texture, v_texCoord); + vec4 baseColor = texColor * v_color; + + // Normalize interpolated normal | 归一化插值后的法线 + vec3 normal = normalize(v_normal); + + // Lambertian diffuse lighting | 兰伯特漫反射光照 + float diffuse = max(dot(normal, u_lightDirection), 0.0); + + // Combine ambient and diffuse | 组合环境光和漫反射 + vec3 lighting = u_ambientColor + u_lightColor * diffuse; + + // Apply lighting to base color | 将光照应用到基础颜色 + fragColor = vec4(baseColor.rgb * lighting, baseColor.a); + + // Discard fully transparent pixels | 丢弃完全透明的像素 + if (fragColor.a < 0.01) { + discard; + } +} +"#; + +/// Simple 3D vertex shader (no normal, for unlit rendering). +/// 简单3D顶点着色器(无法线,用于无光照渲染)。 +pub const SIMPLE3D_VERTEX_SHADER: &str = r#"#version 300 es +precision highp float; + +// Vertex attributes | 顶点属性 +layout(location = 0) in vec3 a_position; +layout(location = 1) in vec2 a_texCoord; +layout(location = 2) in vec4 a_color; + +// Uniforms | 统一变量 +uniform mat4 u_viewProjection; +uniform mat4 u_model; + +// Outputs to fragment shader | 输出到片段着色器 +out vec2 v_texCoord; +out vec4 v_color; + +void main() { + // Apply model and view-projection matrices | 应用模型和视图-投影矩阵 + gl_Position = u_viewProjection * u_model * vec4(a_position, 1.0); + + // Pass through to fragment shader | 传递到片段着色器 + v_texCoord = a_texCoord; + v_color = a_color; +} +"#; + +/// Simple 3D fragment shader (shared with unlit mesh shader). +/// 简单3D片段着色器(与无光照网格着色器共用)。 +pub const SIMPLE3D_FRAGMENT_SHADER: &str = r#"#version 300 es +precision highp float; + +// Inputs from vertex shader | 来自顶点着色器的输入 +in vec2 v_texCoord; +in vec4 v_color; + +// Texture sampler | 纹理采样器 +uniform sampler2D u_texture; + +// Output color | 输出颜色 +out vec4 fragColor; + +void main() { + // Sample texture and multiply by vertex color | 采样纹理并乘以顶点颜色 + vec4 texColor = texture(u_texture, v_texCoord); + fragColor = texColor * v_color; + + // Discard fully transparent pixels | 丢弃完全透明的像素 + if (fragColor.a < 0.01) { + discard; + } +} +"#; + +/// 3D Grid vertex shader for editor floor grid. +/// 用于编辑器地面网格的3D网格顶点着色器。 +pub const GRID3D_VERTEX_SHADER: &str = r#"#version 300 es +precision highp float; + +layout(location = 0) in vec3 a_position; +layout(location = 1) in vec4 a_color; + +uniform mat4 u_viewProjection; + +out vec4 v_color; +out vec3 v_worldPos; + +void main() { + gl_Position = u_viewProjection * vec4(a_position, 1.0); + v_color = a_color; + v_worldPos = a_position; +} +"#; + +/// 3D Grid fragment shader with distance fade. +/// 带距离淡出的3D网格片段着色器。 +pub const GRID3D_FRAGMENT_SHADER: &str = r#"#version 300 es +precision highp float; + +in vec4 v_color; +in vec3 v_worldPos; + +uniform vec3 u_cameraPos; +uniform float u_fadeStart; // Distance at which fade starts | 开始淡出的距离 +uniform float u_fadeEnd; // Distance at which fully transparent | 完全透明的距离 + +out vec4 fragColor; + +void main() { + // Calculate distance from camera | 计算到相机的距离 + float dist = length(v_worldPos - u_cameraPos); + + // Apply distance-based fade | 应用基于距离的淡出 + float fade = 1.0 - smoothstep(u_fadeStart, u_fadeEnd, dist); + + fragColor = vec4(v_color.rgb, v_color.a * fade); + + if (fragColor.a < 0.01) { + discard; + } +} +"#; diff --git a/packages/engine/src/renderer/shader/mod.rs b/packages/engine/src/renderer/shader/mod.rs index 90caae44..7c19ecdd 100644 --- a/packages/engine/src/renderer/shader/mod.rs +++ b/packages/engine/src/renderer/shader/mod.rs @@ -7,7 +7,12 @@ mod manager; pub use program::ShaderProgram; pub use builtin::{ + // 2D shaders | 2D着色器 SPRITE_VERTEX_SHADER, SPRITE_FRAGMENT_SHADER, - MSDF_TEXT_VERTEX_SHADER, MSDF_TEXT_FRAGMENT_SHADER + MSDF_TEXT_VERTEX_SHADER, MSDF_TEXT_FRAGMENT_SHADER, + // 3D shaders | 3D着色器 + MESH3D_VERTEX_SHADER, MESH3D_FRAGMENT_SHADER_UNLIT, MESH3D_FRAGMENT_SHADER_LIT, + SIMPLE3D_VERTEX_SHADER, SIMPLE3D_FRAGMENT_SHADER, + GRID3D_VERTEX_SHADER, GRID3D_FRAGMENT_SHADER, }; pub use manager::{ShaderManager, SHADER_ID_DEFAULT_SPRITE};