feat: 3D 编辑器支持 - 网格、相机、Gizmo

This commit is contained in:
yhh
2025-12-22 12:40:43 +08:00
parent a1e1189f9d
commit 66d9f428b3
18 changed files with 3646 additions and 50 deletions

View File

@@ -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 表示 2D1 表示 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.
* 销毁桥接并释放资源。

View File

@@ -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,
}

View File

@@ -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 表示 2D1 表示 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;

View File

@@ -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<Entity | null>(null);
const messageHubRef = useRef<MessageHub | null>(null);
const commandManagerRef = useRef<CommandManager | null>(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
<div className="viewport-divider" />
{/* 2D/3D Mode Toggle | 2D/3D 模式切换 */}
<button
className={`viewport-btn ${renderMode === '3D' ? 'active' : ''}`}
onClick={handleToggleRenderMode}
title={renderMode === '2D' ? t('viewport.view.switchTo3D') || 'Switch to 3D' : t('viewport.view.switchTo2D') || 'Switch to 2D'}
>
{renderMode === '2D' ? <Square size={14} /> : <Box size={14} />}
<span style={{ marginLeft: 4, fontSize: 10 }}>{renderMode}</span>
</button>
<div className="viewport-divider" />
{/* Zoom display */}
<div className="viewport-zoom-display">
<ZoomIn size={12} />
<span>{Math.round(camera2DZoom * 100)}%</span>
{renderMode === '2D' ? (
<span>{Math.round(camera2DZoom * 100)}%</span>
) : (
<span>{orbitCamera.distance.toFixed(1)}m</span>
)}
</div>
<div className="viewport-divider" />

View File

@@ -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.
*/

View File

@@ -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 ====================

View File

@@ -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<Renderer3D>,
/// 3D grid renderer (lazily initialized with renderer_3d).
/// 3D网格渲染器与renderer_3d一起延迟初始化
grid_3d_renderer: Option<Grid3DRenderer>,
/// 3D gizmo renderer (lazily initialized with renderer_3d).
/// 3D Gizmo渲染器与renderer_3d一起延迟初始化
gizmo_3d_renderer: Option<Gizmo3DRenderer>,
/// 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<f32> {
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(())
}
}

View File

@@ -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};

View File

@@ -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 表示 2D1 表示 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<Vec<f32>> {
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<Vec<f32>> {
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<f32> {
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()))
}
}

View File

@@ -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,
};

View File

@@ -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::<Vertex3D>();
/// 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::<SimpleVertex3D>();
/// 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<SimpleVertex3D> 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],
}
}
}

View File

@@ -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<Vec2> {
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);
}
}
}

View File

@@ -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<Self, String> {
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(&center_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<f32> {
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<f32> {
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<f32> {
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<f32> {
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<f32> {
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<f32> {
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);
}
}

View File

@@ -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, String> {
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<Self, String> {
// 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<f32> {
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<f32> {
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<f32> {
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<f32> {
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);
}
}

View File

@@ -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};

View File

@@ -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<SimpleVertex3D>,
/// Index data.
/// 索引数据。
pub indices: Vec<u32>,
/// 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<u32, ShaderHandle>,
/// Next shader ID for auto-assignment.
/// 自动分配的下一个着色器ID。
next_shader_id: u32,
/// Materials by ID.
/// 按ID存储的材质。
materials: HashMap<u32, Material>,
/// Pending mesh submissions for this frame.
/// 本帧待渲染的网格提交。
mesh_queue: Vec<MeshSubmission>,
/// 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<Self, String> {
// 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<u32, String> {
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);
}
}
}

View File

@@ -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;
}
}
"#;

View File

@@ -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};