refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
10
packages/rust/engine/src/backend/mod.rs
Normal file
10
packages/rust/engine/src/backend/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! 图形后端实现
|
||||
//!
|
||||
//! Graphics backend implementations.
|
||||
//!
|
||||
//! 本模块提供 `GraphicsBackend` trait 的具体实现。
|
||||
//! This module provides concrete implementations of the `GraphicsBackend` trait.
|
||||
|
||||
mod webgl2;
|
||||
|
||||
pub use webgl2::WebGL2Backend;
|
||||
1038
packages/rust/engine/src/backend/webgl2.rs
Normal file
1038
packages/rust/engine/src/backend/webgl2.rs
Normal file
File diff suppressed because it is too large
Load Diff
161
packages/rust/engine/src/core/context.rs
Normal file
161
packages/rust/engine/src/core/context.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
//! WebGL context management.
|
||||
//! WebGL上下文管理。
|
||||
|
||||
use web_sys::{HtmlCanvasElement, WebGl2RenderingContext};
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use super::error::{EngineError, Result};
|
||||
|
||||
/// WebGL2 rendering context wrapper.
|
||||
/// WebGL2渲染上下文包装器。
|
||||
///
|
||||
/// Manages the WebGL2 context and provides helper methods for common operations.
|
||||
/// 管理WebGL2上下文并提供常用操作的辅助方法。
|
||||
pub struct WebGLContext {
|
||||
/// The WebGL2 rendering context.
|
||||
/// WebGL2渲染上下文。
|
||||
gl: WebGl2RenderingContext,
|
||||
|
||||
/// The canvas element.
|
||||
/// Canvas元素。
|
||||
canvas: HtmlCanvasElement,
|
||||
}
|
||||
|
||||
impl WebGLContext {
|
||||
/// Create a new WebGL context from a canvas ID.
|
||||
/// 从canvas ID创建新的WebGL上下文。
|
||||
///
|
||||
/// # Arguments | 参数
|
||||
/// * `canvas_id` - The ID of the canvas element | canvas元素的ID
|
||||
///
|
||||
/// # Returns | 返回
|
||||
/// A new WebGLContext or an error | 新的WebGLContext或错误
|
||||
pub fn new(canvas_id: &str) -> Result<Self> {
|
||||
// Get document and canvas | 获取document和canvas
|
||||
let window = web_sys::window().expect("No window found | 未找到window");
|
||||
let document = window.document().expect("No document found | 未找到document");
|
||||
|
||||
let canvas = document
|
||||
.get_element_by_id(canvas_id)
|
||||
.ok_or_else(|| EngineError::CanvasNotFound(canvas_id.to_string()))?
|
||||
.dyn_into::<HtmlCanvasElement>()
|
||||
.map_err(|_| EngineError::CanvasNotFound(canvas_id.to_string()))?;
|
||||
|
||||
// Create WebGL2 context | 创建WebGL2上下文
|
||||
let gl = canvas
|
||||
.get_context("webgl2")
|
||||
.map_err(|_| EngineError::ContextCreationFailed)?
|
||||
.ok_or(EngineError::ContextCreationFailed)?
|
||||
.dyn_into::<WebGl2RenderingContext>()
|
||||
.map_err(|_| EngineError::ContextCreationFailed)?;
|
||||
|
||||
log::info!(
|
||||
"WebGL2 context created | WebGL2上下文已创建: {}x{}",
|
||||
canvas.width(),
|
||||
canvas.height()
|
||||
);
|
||||
|
||||
Ok(Self { gl, canvas })
|
||||
}
|
||||
|
||||
/// Create a new WebGL context from external JavaScript objects.
|
||||
/// 从外部 JavaScript 对象创建 WebGL 上下文。
|
||||
///
|
||||
/// This method is designed for environments like WeChat MiniGame
|
||||
/// where the canvas is not a standard HTML element.
|
||||
/// 此方法适用于微信小游戏等环境,其中 canvas 不是标准 HTML 元素。
|
||||
pub fn from_external(
|
||||
gl_context: JsValue,
|
||||
canvas_width: u32,
|
||||
canvas_height: u32,
|
||||
) -> Result<Self> {
|
||||
// Convert JsValue to WebGl2RenderingContext
|
||||
let gl = gl_context
|
||||
.dyn_into::<WebGl2RenderingContext>()
|
||||
.map_err(|_| EngineError::ContextCreationFailed)?;
|
||||
|
||||
// Create a dummy canvas for compatibility
|
||||
// In MiniGame environment, we don't have HtmlCanvasElement
|
||||
let window = web_sys::window().ok_or(EngineError::ContextCreationFailed)?;
|
||||
let document = window.document().ok_or(EngineError::ContextCreationFailed)?;
|
||||
let canvas = document
|
||||
.create_element("canvas")
|
||||
.map_err(|_| EngineError::ContextCreationFailed)?
|
||||
.dyn_into::<HtmlCanvasElement>()
|
||||
.map_err(|_| EngineError::ContextCreationFailed)?;
|
||||
|
||||
canvas.set_width(canvas_width);
|
||||
canvas.set_height(canvas_height);
|
||||
|
||||
log::info!(
|
||||
"WebGL2 context created from external | 从外部创建WebGL2上下文: {}x{}",
|
||||
canvas_width,
|
||||
canvas_height
|
||||
);
|
||||
|
||||
Ok(Self { gl, canvas })
|
||||
}
|
||||
|
||||
/// Get a reference to the WebGL2 context.
|
||||
/// 获取WebGL2上下文的引用。
|
||||
#[inline]
|
||||
pub fn gl(&self) -> &WebGl2RenderingContext {
|
||||
&self.gl
|
||||
}
|
||||
|
||||
/// Get a reference to the canvas element.
|
||||
/// 获取canvas元素的引用。
|
||||
#[inline]
|
||||
pub fn canvas(&self) -> &HtmlCanvasElement {
|
||||
&self.canvas
|
||||
}
|
||||
|
||||
/// Get canvas width.
|
||||
/// 获取canvas宽度。
|
||||
#[inline]
|
||||
pub fn width(&self) -> u32 {
|
||||
self.canvas.width()
|
||||
}
|
||||
|
||||
/// Get canvas height.
|
||||
/// 获取canvas高度。
|
||||
#[inline]
|
||||
pub fn height(&self) -> u32 {
|
||||
self.canvas.height()
|
||||
}
|
||||
|
||||
/// Clear the canvas with specified color.
|
||||
/// 使用指定颜色清除canvas。
|
||||
pub fn clear(&self, r: f32, g: f32, b: f32, a: f32) {
|
||||
self.gl.clear_color(r, g, b, a);
|
||||
self.gl.clear(
|
||||
WebGl2RenderingContext::COLOR_BUFFER_BIT | WebGl2RenderingContext::DEPTH_BUFFER_BIT,
|
||||
);
|
||||
}
|
||||
|
||||
/// Set the viewport to match canvas size.
|
||||
/// 设置视口以匹配canvas大小。
|
||||
pub fn set_viewport(&self) {
|
||||
self.gl
|
||||
.viewport(0, 0, self.width() as i32, self.height() as i32);
|
||||
}
|
||||
|
||||
/// Resize the canvas and viewport.
|
||||
/// 调整canvas和视口大小。
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
self.canvas.set_width(width);
|
||||
self.canvas.set_height(height);
|
||||
self.gl.viewport(0, 0, width as i32, height as i32);
|
||||
}
|
||||
|
||||
/// Enable alpha blending for transparency.
|
||||
/// 启用透明度的alpha混合。
|
||||
pub fn enable_blend(&self) {
|
||||
self.gl.enable(WebGl2RenderingContext::BLEND);
|
||||
self.gl.blend_func(
|
||||
WebGl2RenderingContext::SRC_ALPHA,
|
||||
WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA,
|
||||
);
|
||||
}
|
||||
}
|
||||
1434
packages/rust/engine/src/core/engine.rs
Normal file
1434
packages/rust/engine/src/core/engine.rs
Normal file
File diff suppressed because it is too large
Load Diff
58
packages/rust/engine/src/core/error.rs
Normal file
58
packages/rust/engine/src/core/error.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
//! Error types for the engine.
|
||||
//! 引擎的错误类型定义。
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Engine error types.
|
||||
/// 引擎错误类型。
|
||||
#[derive(Error, Debug)]
|
||||
pub enum EngineError {
|
||||
/// Canvas element not found.
|
||||
/// 未找到Canvas元素。
|
||||
#[error("Canvas element not found: {0} | 未找到Canvas元素: {0}")]
|
||||
CanvasNotFound(String),
|
||||
|
||||
/// WebGL context creation failed.
|
||||
/// WebGL上下文创建失败。
|
||||
#[error("WebGL2 context creation failed | WebGL2上下文创建失败")]
|
||||
ContextCreationFailed,
|
||||
|
||||
/// Shader compilation failed.
|
||||
/// Shader编译失败。
|
||||
#[error("Shader compilation failed: {0} | Shader编译失败: {0}")]
|
||||
ShaderCompileFailed(String),
|
||||
|
||||
/// Shader program linking failed.
|
||||
/// Shader程序链接失败。
|
||||
#[error("Shader program linking failed: {0} | Shader程序链接失败: {0}")]
|
||||
ProgramLinkFailed(String),
|
||||
|
||||
/// Texture loading failed.
|
||||
/// 纹理加载失败。
|
||||
#[error("Texture loading failed: {0} | 纹理加载失败: {0}")]
|
||||
TextureLoadFailed(String),
|
||||
|
||||
/// Texture not found.
|
||||
/// 未找到纹理。
|
||||
#[error("Texture not found: {0} | 未找到纹理: {0}")]
|
||||
TextureNotFound(u32),
|
||||
|
||||
/// Invalid batch data.
|
||||
/// 无效的批处理数据。
|
||||
#[error("Invalid batch data: {0} | 无效的批处理数据: {0}")]
|
||||
InvalidBatchData(String),
|
||||
|
||||
/// Buffer creation failed.
|
||||
/// 缓冲区创建失败。
|
||||
#[error("Buffer creation failed | 缓冲区创建失败")]
|
||||
BufferCreationFailed,
|
||||
|
||||
/// WebGL operation failed.
|
||||
/// WebGL操作失败。
|
||||
#[error("WebGL operation failed: {0} | WebGL操作失败: {0}")]
|
||||
WebGLError(String),
|
||||
}
|
||||
|
||||
/// Result type alias for engine operations.
|
||||
/// 引擎操作的Result类型别名。
|
||||
pub type Result<T> = std::result::Result<T, EngineError>;
|
||||
10
packages/rust/engine/src/core/mod.rs
Normal file
10
packages/rust/engine/src/core/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! Core engine module containing lifecycle management and context.
|
||||
//! 核心引擎模块,包含生命周期管理和上下文。
|
||||
|
||||
pub mod error;
|
||||
pub mod context;
|
||||
mod engine;
|
||||
|
||||
pub use engine::{Engine, EngineConfig, RenderMode};
|
||||
pub use context::WebGLContext;
|
||||
pub use error::{EngineError, Result};
|
||||
61
packages/rust/engine/src/input/input_manager.rs
Normal file
61
packages/rust/engine/src/input/input_manager.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
//! Unified input manager.
|
||||
//! 统一输入管理器。
|
||||
|
||||
use super::{KeyboardState, MouseState, TouchState};
|
||||
|
||||
/// Unified input manager handling keyboard, mouse, and touch.
|
||||
/// 处理键盘、鼠标和触摸的统一输入管理器。
|
||||
///
|
||||
/// Provides a single interface for all input types.
|
||||
/// 为所有输入类型提供单一接口。
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InputManager {
|
||||
/// Keyboard state.
|
||||
/// 键盘状态。
|
||||
pub keyboard: KeyboardState,
|
||||
|
||||
/// Mouse state.
|
||||
/// 鼠标状态。
|
||||
pub mouse: MouseState,
|
||||
|
||||
/// Touch state.
|
||||
/// 触摸状态。
|
||||
pub touch: TouchState,
|
||||
}
|
||||
|
||||
impl InputManager {
|
||||
/// Create a new input manager.
|
||||
/// 创建新的输入管理器。
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Update all input states for a new frame.
|
||||
/// 为新帧更新所有输入状态。
|
||||
pub fn update(&mut self) {
|
||||
self.keyboard.update();
|
||||
self.mouse.update();
|
||||
self.touch.update();
|
||||
}
|
||||
|
||||
/// Check if a key is currently pressed.
|
||||
/// 检查某个键是否当前被按下。
|
||||
#[inline]
|
||||
pub fn is_key_down(&self, key: &str) -> bool {
|
||||
self.keyboard.is_key_down(key)
|
||||
}
|
||||
|
||||
/// Check if a key was just pressed this frame.
|
||||
/// 检查某个键是否在本帧刚被按下。
|
||||
#[inline]
|
||||
pub fn is_key_just_pressed(&self, key: &str) -> bool {
|
||||
self.keyboard.is_key_just_pressed(key)
|
||||
}
|
||||
|
||||
/// Clear all input states.
|
||||
/// 清除所有输入状态。
|
||||
pub fn clear(&mut self) {
|
||||
self.keyboard.clear();
|
||||
self.touch.clear();
|
||||
}
|
||||
}
|
||||
82
packages/rust/engine/src/input/keyboard.rs
Normal file
82
packages/rust/engine/src/input/keyboard.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
//! Keyboard input handling.
|
||||
//! 键盘输入处理。
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Keyboard input state.
|
||||
/// 键盘输入状态。
|
||||
#[derive(Debug, Default)]
|
||||
pub struct KeyboardState {
|
||||
/// Currently pressed keys.
|
||||
/// 当前按下的键。
|
||||
pressed: HashSet<String>,
|
||||
|
||||
/// Keys pressed this frame.
|
||||
/// 本帧按下的键。
|
||||
just_pressed: HashSet<String>,
|
||||
|
||||
/// Keys released this frame.
|
||||
/// 本帧释放的键。
|
||||
just_released: HashSet<String>,
|
||||
}
|
||||
|
||||
impl KeyboardState {
|
||||
/// Create new keyboard state.
|
||||
/// 创建新的键盘状态。
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Handle key down event.
|
||||
/// 处理按键按下事件。
|
||||
pub fn key_down(&mut self, key: String) {
|
||||
if !self.pressed.contains(&key) {
|
||||
self.just_pressed.insert(key.clone());
|
||||
}
|
||||
self.pressed.insert(key);
|
||||
}
|
||||
|
||||
/// Handle key up event.
|
||||
/// 处理按键释放事件。
|
||||
pub fn key_up(&mut self, key: String) {
|
||||
if self.pressed.remove(&key) {
|
||||
self.just_released.insert(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a key is currently pressed.
|
||||
/// 检查某个键是否当前被按下。
|
||||
#[inline]
|
||||
pub fn is_key_down(&self, key: &str) -> bool {
|
||||
self.pressed.contains(key)
|
||||
}
|
||||
|
||||
/// Check if a key was just pressed this frame.
|
||||
/// 检查某个键是否在本帧刚被按下。
|
||||
#[inline]
|
||||
pub fn is_key_just_pressed(&self, key: &str) -> bool {
|
||||
self.just_pressed.contains(key)
|
||||
}
|
||||
|
||||
/// Check if a key was just released this frame.
|
||||
/// 检查某个键是否在本帧刚被释放。
|
||||
#[inline]
|
||||
pub fn is_key_just_released(&self, key: &str) -> bool {
|
||||
self.just_released.contains(key)
|
||||
}
|
||||
|
||||
/// Update state for new frame.
|
||||
/// 为新帧更新状态。
|
||||
pub fn update(&mut self) {
|
||||
self.just_pressed.clear();
|
||||
self.just_released.clear();
|
||||
}
|
||||
|
||||
/// Clear all input state.
|
||||
/// 清除所有输入状态。
|
||||
pub fn clear(&mut self) {
|
||||
self.pressed.clear();
|
||||
self.just_pressed.clear();
|
||||
self.just_released.clear();
|
||||
}
|
||||
}
|
||||
12
packages/rust/engine/src/input/mod.rs
Normal file
12
packages/rust/engine/src/input/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
//! Input handling system.
|
||||
//! 输入处理系统。
|
||||
|
||||
mod keyboard;
|
||||
mod mouse;
|
||||
mod touch;
|
||||
mod input_manager;
|
||||
|
||||
pub use input_manager::InputManager;
|
||||
pub use keyboard::KeyboardState;
|
||||
pub use mouse::{MouseState, MouseButton};
|
||||
pub use touch::{TouchState, TouchPoint};
|
||||
136
packages/rust/engine/src/input/mouse.rs
Normal file
136
packages/rust/engine/src/input/mouse.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
//! Mouse input handling.
|
||||
//! 鼠标输入处理。
|
||||
|
||||
use crate::math::Vec2;
|
||||
|
||||
/// Mouse button identifiers.
|
||||
/// 鼠标按钮标识符。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum MouseButton {
|
||||
/// Left mouse button.
|
||||
/// 鼠标左键。
|
||||
Left,
|
||||
/// Middle mouse button (scroll wheel).
|
||||
/// 鼠标中键(滚轮)。
|
||||
Middle,
|
||||
/// Right mouse button.
|
||||
/// 鼠标右键。
|
||||
Right,
|
||||
}
|
||||
|
||||
impl MouseButton {
|
||||
/// Convert from button index.
|
||||
/// 从按钮索引转换。
|
||||
pub fn from_index(index: i16) -> Option<Self> {
|
||||
match index {
|
||||
0 => Some(MouseButton::Left),
|
||||
1 => Some(MouseButton::Middle),
|
||||
2 => Some(MouseButton::Right),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mouse input state.
|
||||
/// 鼠标输入状态。
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MouseState {
|
||||
/// Current mouse position.
|
||||
/// 当前鼠标位置。
|
||||
pub position: Vec2,
|
||||
|
||||
/// Mouse movement delta since last frame.
|
||||
/// 自上一帧以来的鼠标移动增量。
|
||||
pub delta: Vec2,
|
||||
|
||||
/// Scroll wheel delta.
|
||||
/// 滚轮增量。
|
||||
pub scroll_delta: f32,
|
||||
|
||||
/// Button states (left, middle, right).
|
||||
/// 按钮状态(左、中、右)。
|
||||
buttons: [bool; 3],
|
||||
|
||||
/// Buttons just pressed this frame.
|
||||
/// 本帧刚按下的按钮。
|
||||
just_pressed: [bool; 3],
|
||||
|
||||
/// Buttons just released this frame.
|
||||
/// 本帧刚释放的按钮。
|
||||
just_released: [bool; 3],
|
||||
|
||||
/// Previous position for delta calculation.
|
||||
/// 用于计算增量的上一位置。
|
||||
prev_position: Vec2,
|
||||
}
|
||||
|
||||
impl MouseState {
|
||||
/// Create new mouse state.
|
||||
/// 创建新的鼠标状态。
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Handle mouse move event.
|
||||
/// 处理鼠标移动事件。
|
||||
pub fn mouse_move(&mut self, x: f32, y: f32) {
|
||||
self.position = Vec2::new(x, y);
|
||||
}
|
||||
|
||||
/// Handle mouse button down event.
|
||||
/// 处理鼠标按钮按下事件。
|
||||
pub fn button_down(&mut self, button: MouseButton) {
|
||||
let index = button as usize;
|
||||
if !self.buttons[index] {
|
||||
self.just_pressed[index] = true;
|
||||
}
|
||||
self.buttons[index] = true;
|
||||
}
|
||||
|
||||
/// Handle mouse button up event.
|
||||
/// 处理鼠标按钮释放事件。
|
||||
pub fn button_up(&mut self, button: MouseButton) {
|
||||
let index = button as usize;
|
||||
if self.buttons[index] {
|
||||
self.just_released[index] = true;
|
||||
}
|
||||
self.buttons[index] = false;
|
||||
}
|
||||
|
||||
/// Handle scroll wheel event.
|
||||
/// 处理滚轮事件。
|
||||
pub fn scroll(&mut self, delta: f32) {
|
||||
self.scroll_delta = delta;
|
||||
}
|
||||
|
||||
/// Check if a button is currently pressed.
|
||||
/// 检查某个按钮是否当前被按下。
|
||||
#[inline]
|
||||
pub fn is_button_down(&self, button: MouseButton) -> bool {
|
||||
self.buttons[button as usize]
|
||||
}
|
||||
|
||||
/// Check if a button was just pressed this frame.
|
||||
/// 检查某个按钮是否在本帧刚被按下。
|
||||
#[inline]
|
||||
pub fn is_button_just_pressed(&self, button: MouseButton) -> bool {
|
||||
self.just_pressed[button as usize]
|
||||
}
|
||||
|
||||
/// Check if a button was just released this frame.
|
||||
/// 检查某个按钮是否在本帧刚被释放。
|
||||
#[inline]
|
||||
pub fn is_button_just_released(&self, button: MouseButton) -> bool {
|
||||
self.just_released[button as usize]
|
||||
}
|
||||
|
||||
/// Update state for new frame.
|
||||
/// 为新帧更新状态。
|
||||
pub fn update(&mut self) {
|
||||
self.delta = self.position - self.prev_position;
|
||||
self.prev_position = self.position;
|
||||
self.scroll_delta = 0.0;
|
||||
self.just_pressed = [false; 3];
|
||||
self.just_released = [false; 3];
|
||||
}
|
||||
}
|
||||
164
packages/rust/engine/src/input/touch.rs
Normal file
164
packages/rust/engine/src/input/touch.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
//! Touch input handling.
|
||||
//! 触摸输入处理。
|
||||
|
||||
use crate::math::Vec2;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Single touch point.
|
||||
/// 单个触摸点。
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct TouchPoint {
|
||||
/// Touch identifier.
|
||||
/// 触摸标识符。
|
||||
pub id: i32,
|
||||
|
||||
/// Current position.
|
||||
/// 当前位置。
|
||||
pub position: Vec2,
|
||||
|
||||
/// Starting position.
|
||||
/// 起始位置。
|
||||
pub start_position: Vec2,
|
||||
|
||||
/// Movement delta since last frame.
|
||||
/// 自上一帧以来的移动增量。
|
||||
pub delta: Vec2,
|
||||
|
||||
/// Previous position.
|
||||
/// 上一位置。
|
||||
prev_position: Vec2,
|
||||
}
|
||||
|
||||
impl TouchPoint {
|
||||
/// Create a new touch point.
|
||||
/// 创建新的触摸点。
|
||||
pub fn new(id: i32, x: f32, y: f32) -> Self {
|
||||
let pos = Vec2::new(x, y);
|
||||
Self {
|
||||
id,
|
||||
position: pos,
|
||||
start_position: pos,
|
||||
delta: Vec2::ZERO,
|
||||
prev_position: pos,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update touch position.
|
||||
/// 更新触摸位置。
|
||||
pub fn update_position(&mut self, x: f32, y: f32) {
|
||||
self.prev_position = self.position;
|
||||
self.position = Vec2::new(x, y);
|
||||
self.delta = self.position - self.prev_position;
|
||||
}
|
||||
}
|
||||
|
||||
/// Touch input state.
|
||||
/// 触摸输入状态。
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TouchState {
|
||||
/// Active touch points.
|
||||
/// 活动的触摸点。
|
||||
touches: HashMap<i32, TouchPoint>,
|
||||
|
||||
/// Touch IDs that started this frame.
|
||||
/// 本帧开始的触摸ID。
|
||||
just_started: Vec<i32>,
|
||||
|
||||
/// Touch IDs that ended this frame.
|
||||
/// 本帧结束的触摸ID。
|
||||
just_ended: Vec<i32>,
|
||||
}
|
||||
|
||||
impl TouchState {
|
||||
/// Create new touch state.
|
||||
/// 创建新的触摸状态。
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Handle touch start event.
|
||||
/// 处理触摸开始事件。
|
||||
pub fn touch_start(&mut self, id: i32, x: f32, y: f32) {
|
||||
let touch = TouchPoint::new(id, x, y);
|
||||
self.touches.insert(id, touch);
|
||||
self.just_started.push(id);
|
||||
}
|
||||
|
||||
/// Handle touch move event.
|
||||
/// 处理触摸移动事件。
|
||||
pub fn touch_move(&mut self, id: i32, x: f32, y: f32) {
|
||||
if let Some(touch) = self.touches.get_mut(&id) {
|
||||
touch.update_position(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle touch end event.
|
||||
/// 处理触摸结束事件。
|
||||
pub fn touch_end(&mut self, id: i32) {
|
||||
if self.touches.remove(&id).is_some() {
|
||||
self.just_ended.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a touch point by ID.
|
||||
/// 按ID获取触摸点。
|
||||
#[inline]
|
||||
pub fn get_touch(&self, id: i32) -> Option<&TouchPoint> {
|
||||
self.touches.get(&id)
|
||||
}
|
||||
|
||||
/// Get all active touch points.
|
||||
/// 获取所有活动的触摸点。
|
||||
#[inline]
|
||||
pub fn get_touches(&self) -> impl Iterator<Item = &TouchPoint> {
|
||||
self.touches.values()
|
||||
}
|
||||
|
||||
/// Get number of active touches.
|
||||
/// 获取活动触摸数量。
|
||||
#[inline]
|
||||
pub fn touch_count(&self) -> usize {
|
||||
self.touches.len()
|
||||
}
|
||||
|
||||
/// Check if any touch is active.
|
||||
/// 检查是否有任何触摸活动。
|
||||
#[inline]
|
||||
pub fn is_touching(&self) -> bool {
|
||||
!self.touches.is_empty()
|
||||
}
|
||||
|
||||
/// Get touches that started this frame.
|
||||
/// 获取本帧开始的触摸。
|
||||
#[inline]
|
||||
pub fn just_started(&self) -> &[i32] {
|
||||
&self.just_started
|
||||
}
|
||||
|
||||
/// Get touches that ended this frame.
|
||||
/// 获取本帧结束的触摸。
|
||||
#[inline]
|
||||
pub fn just_ended(&self) -> &[i32] {
|
||||
&self.just_ended
|
||||
}
|
||||
|
||||
/// Update state for new frame.
|
||||
/// 为新帧更新状态。
|
||||
pub fn update(&mut self) {
|
||||
self.just_started.clear();
|
||||
self.just_ended.clear();
|
||||
|
||||
// Reset deltas | 重置增量
|
||||
for touch in self.touches.values_mut() {
|
||||
touch.delta = Vec2::ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all touch state.
|
||||
/// 清除所有触摸状态。
|
||||
pub fn clear(&mut self) {
|
||||
self.touches.clear();
|
||||
self.just_started.clear();
|
||||
self.just_ended.clear();
|
||||
}
|
||||
}
|
||||
1127
packages/rust/engine/src/lib.rs
Normal file
1127
packages/rust/engine/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
197
packages/rust/engine/src/math/color.rs
Normal file
197
packages/rust/engine/src/math/color.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
//! Color utilities.
|
||||
//! 颜色工具。
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
/// RGBA color representation.
|
||||
/// RGBA颜色表示。
|
||||
///
|
||||
/// Colors are stored as normalized floats (0.0-1.0) and can be converted
|
||||
/// to packed u32 format for efficient GPU transfer.
|
||||
/// 颜色以归一化浮点数(0.0-1.0)存储,可转换为打包的u32格式以高效传输到GPU。
|
||||
///
|
||||
/// # Examples | 示例
|
||||
/// ```rust
|
||||
/// use es_engine::math::Color;
|
||||
/// let red = Color::RED;
|
||||
/// let custom = Color::new(0.5, 0.7, 0.3, 1.0);
|
||||
/// let packed = custom.to_packed(); // For GPU
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Pod, Zeroable)]
|
||||
#[repr(C)]
|
||||
pub struct Color {
|
||||
/// Red component (0.0-1.0).
|
||||
/// 红色分量。
|
||||
pub r: f32,
|
||||
/// Green component (0.0-1.0).
|
||||
/// 绿色分量。
|
||||
pub g: f32,
|
||||
/// Blue component (0.0-1.0).
|
||||
/// 蓝色分量。
|
||||
pub b: f32,
|
||||
/// Alpha component (0.0-1.0).
|
||||
/// 透明度分量。
|
||||
pub a: f32,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
/// White (1, 1, 1, 1).
|
||||
/// 白色。
|
||||
pub const WHITE: Self = Self { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
|
||||
|
||||
/// Black (0, 0, 0, 1).
|
||||
/// 黑色。
|
||||
pub const BLACK: Self = Self { r: 0.0, g: 0.0, b: 0.0, a: 1.0 };
|
||||
|
||||
/// Red (1, 0, 0, 1).
|
||||
/// 红色。
|
||||
pub const RED: Self = Self { r: 1.0, g: 0.0, b: 0.0, a: 1.0 };
|
||||
|
||||
/// Green (0, 1, 0, 1).
|
||||
/// 绿色。
|
||||
pub const GREEN: Self = Self { r: 0.0, g: 1.0, b: 0.0, a: 1.0 };
|
||||
|
||||
/// Blue (0, 0, 1, 1).
|
||||
/// 蓝色。
|
||||
pub const BLUE: Self = Self { r: 0.0, g: 0.0, b: 1.0, a: 1.0 };
|
||||
|
||||
/// Transparent (0, 0, 0, 0).
|
||||
/// 透明。
|
||||
pub const TRANSPARENT: Self = Self { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
|
||||
|
||||
/// Create a new color.
|
||||
/// 创建新颜色。
|
||||
#[inline]
|
||||
pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
|
||||
Self { r, g, b, a }
|
||||
}
|
||||
|
||||
/// Create a color from RGB values (alpha = 1.0).
|
||||
/// 从RGB值创建颜色(alpha = 1.0)。
|
||||
#[inline]
|
||||
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
|
||||
Self { r, g, b, a: 1.0 }
|
||||
}
|
||||
|
||||
/// Create from u8 values (0-255).
|
||||
/// 从u8值创建(0-255)。
|
||||
#[inline]
|
||||
pub fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
Self {
|
||||
r: r as f32 / 255.0,
|
||||
g: g as f32 / 255.0,
|
||||
b: b as f32 / 255.0,
|
||||
a: a as f32 / 255.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from hex value (0xRRGGBB or 0xRRGGBBAA).
|
||||
/// 从十六进制值创建。
|
||||
#[inline]
|
||||
pub fn from_hex(hex: u32) -> Self {
|
||||
if hex > 0xFFFFFF {
|
||||
// 0xRRGGBBAA format
|
||||
Self::from_rgba8(
|
||||
((hex >> 24) & 0xFF) as u8,
|
||||
((hex >> 16) & 0xFF) as u8,
|
||||
((hex >> 8) & 0xFF) as u8,
|
||||
(hex & 0xFF) as u8,
|
||||
)
|
||||
} else {
|
||||
// 0xRRGGBB format
|
||||
Self::from_rgba8(
|
||||
((hex >> 16) & 0xFF) as u8,
|
||||
((hex >> 8) & 0xFF) as u8,
|
||||
(hex & 0xFF) as u8,
|
||||
255,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to packed u32 (0xRRGGBBAA format, industry standard).
|
||||
/// 转换为打包的 u32(0xRRGGBBAA 格式,行业标准)。
|
||||
#[inline]
|
||||
pub fn to_packed(&self) -> u32 {
|
||||
let r = (self.r.clamp(0.0, 1.0) * 255.0) as u32;
|
||||
let g = (self.g.clamp(0.0, 1.0) * 255.0) as u32;
|
||||
let b = (self.b.clamp(0.0, 1.0) * 255.0) as u32;
|
||||
let a = (self.a.clamp(0.0, 1.0) * 255.0) as u32;
|
||||
|
||||
(r << 24) | (g << 16) | (b << 8) | a
|
||||
}
|
||||
|
||||
/// Create from packed u32 (0xRRGGBBAA format, industry standard).
|
||||
/// 从打包的 u32 创建(0xRRGGBBAA 格式,行业标准)。
|
||||
#[inline]
|
||||
pub fn from_packed(packed: u32) -> Self {
|
||||
Self::from_rgba8(
|
||||
((packed >> 24) & 0xFF) as u8,
|
||||
((packed >> 16) & 0xFF) as u8,
|
||||
((packed >> 8) & 0xFF) as u8,
|
||||
(packed & 0xFF) as u8,
|
||||
)
|
||||
}
|
||||
|
||||
/// Convert to GPU vertex format (ABGR for WebGL little-endian).
|
||||
/// 转换为 GPU 顶点格式(WebGL 小端序 ABGR)。
|
||||
#[inline]
|
||||
pub fn to_vertex_u32(&self) -> u32 {
|
||||
let r = (self.r.clamp(0.0, 1.0) * 255.0) as u32;
|
||||
let g = (self.g.clamp(0.0, 1.0) * 255.0) as u32;
|
||||
let b = (self.b.clamp(0.0, 1.0) * 255.0) as u32;
|
||||
let a = (self.a.clamp(0.0, 1.0) * 255.0) as u32;
|
||||
|
||||
(a << 24) | (b << 16) | (g << 8) | r
|
||||
}
|
||||
|
||||
/// Linear interpolation between two colors.
|
||||
/// 两个颜色之间的线性插值。
|
||||
#[inline]
|
||||
pub fn lerp(&self, other: &Self, t: f32) -> Self {
|
||||
Self {
|
||||
r: self.r + (other.r - self.r) * t,
|
||||
g: self.g + (other.g - self.g) * t,
|
||||
b: self.b + (other.b - self.b) * t,
|
||||
a: self.a + (other.a - self.a) * t,
|
||||
}
|
||||
}
|
||||
|
||||
/// Multiply color by alpha (premultiplied alpha).
|
||||
/// 颜色乘以alpha(预乘alpha)。
|
||||
#[inline]
|
||||
pub fn premultiply(&self) -> Self {
|
||||
Self {
|
||||
r: self.r * self.a,
|
||||
g: self.g * self.a,
|
||||
b: self.b * self.a,
|
||||
a: self.a,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the alpha value.
|
||||
/// 设置alpha值。
|
||||
#[inline]
|
||||
pub fn with_alpha(self, a: f32) -> Self {
|
||||
Self { a, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Color {
|
||||
fn default() -> Self {
|
||||
Self::WHITE
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[f32; 4]> for Color {
|
||||
#[inline]
|
||||
fn from([r, g, b, a]: [f32; 4]) -> Self {
|
||||
Self { r, g, b, a }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for [f32; 4] {
|
||||
#[inline]
|
||||
fn from(c: Color) -> Self {
|
||||
[c.r, c.g, c.b, c.a]
|
||||
}
|
||||
}
|
||||
19
packages/rust/engine/src/math/mod.rs
Normal file
19
packages/rust/engine/src/math/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
//! Mathematical primitives for 2D game development.
|
||||
//! 用于2D游戏开发的数学基元。
|
||||
//!
|
||||
//! This module provides wrappers around `glam` types with additional
|
||||
//! game-specific functionality.
|
||||
//! 此模块提供对`glam`类型的封装,并添加游戏特定的功能。
|
||||
|
||||
mod vec2;
|
||||
mod transform;
|
||||
mod rect;
|
||||
mod color;
|
||||
|
||||
pub use vec2::Vec2;
|
||||
pub use transform::Transform2D;
|
||||
pub use rect::Rect;
|
||||
pub use color::Color;
|
||||
|
||||
// Re-export glam types for internal use | 重新导出glam类型供内部使用
|
||||
pub use glam::{Mat3, Mat4, Vec3, Vec4};
|
||||
149
packages/rust/engine/src/math/rect.rs
Normal file
149
packages/rust/engine/src/math/rect.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
//! Rectangle implementation.
|
||||
//! 矩形实现。
|
||||
|
||||
use super::Vec2;
|
||||
|
||||
/// Axis-aligned rectangle.
|
||||
/// 轴对齐矩形。
|
||||
///
|
||||
/// # Examples | 示例
|
||||
/// ```rust
|
||||
/// use es_engine::math::{Rect, Vec2};
|
||||
/// let rect = Rect::new(10.0, 20.0, 100.0, 50.0);
|
||||
/// let point = Vec2::new(50.0, 40.0);
|
||||
/// assert!(rect.contains_point(point));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct Rect {
|
||||
/// X position (left edge).
|
||||
/// X位置(左边缘)。
|
||||
pub x: f32,
|
||||
/// Y position (top edge).
|
||||
/// Y位置(上边缘)。
|
||||
pub y: f32,
|
||||
/// Width.
|
||||
/// 宽度。
|
||||
pub width: f32,
|
||||
/// Height.
|
||||
/// 高度。
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// Create a new rectangle.
|
||||
/// 创建新矩形。
|
||||
#[inline]
|
||||
pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
|
||||
Self { x, y, width, height }
|
||||
}
|
||||
|
||||
/// Create a rectangle from two corner points.
|
||||
/// 从两个角点创建矩形。
|
||||
#[inline]
|
||||
pub fn from_corners(min: Vec2, max: Vec2) -> Self {
|
||||
Self {
|
||||
x: min.x,
|
||||
y: min.y,
|
||||
width: max.x - min.x,
|
||||
height: max.y - min.y,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a rectangle centered at a point.
|
||||
/// 创建以某点为中心的矩形。
|
||||
#[inline]
|
||||
pub fn from_center(center: Vec2, width: f32, height: f32) -> Self {
|
||||
Self {
|
||||
x: center.x - width * 0.5,
|
||||
y: center.y - height * 0.5,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the minimum (top-left) corner.
|
||||
/// 获取最小(左上)角点。
|
||||
#[inline]
|
||||
pub fn min(&self) -> Vec2 {
|
||||
Vec2::new(self.x, self.y)
|
||||
}
|
||||
|
||||
/// Get the maximum (bottom-right) corner.
|
||||
/// 获取最大(右下)角点。
|
||||
#[inline]
|
||||
pub fn max(&self) -> Vec2 {
|
||||
Vec2::new(self.x + self.width, self.y + self.height)
|
||||
}
|
||||
|
||||
/// Get the center point.
|
||||
/// 获取中心点。
|
||||
#[inline]
|
||||
pub fn center(&self) -> Vec2 {
|
||||
Vec2::new(self.x + self.width * 0.5, self.y + self.height * 0.5)
|
||||
}
|
||||
|
||||
/// Get the size as a vector.
|
||||
/// 获取尺寸向量。
|
||||
#[inline]
|
||||
pub fn size(&self) -> Vec2 {
|
||||
Vec2::new(self.width, self.height)
|
||||
}
|
||||
|
||||
/// Check if the rectangle contains a point.
|
||||
/// 检查矩形是否包含某点。
|
||||
#[inline]
|
||||
pub fn contains_point(&self, point: Vec2) -> bool {
|
||||
point.x >= self.x
|
||||
&& point.x <= self.x + self.width
|
||||
&& point.y >= self.y
|
||||
&& point.y <= self.y + self.height
|
||||
}
|
||||
|
||||
/// Check if this rectangle intersects with another.
|
||||
/// 检查此矩形是否与另一个相交。
|
||||
#[inline]
|
||||
pub fn intersects(&self, other: &Rect) -> bool {
|
||||
self.x < other.x + other.width
|
||||
&& self.x + self.width > other.x
|
||||
&& self.y < other.y + other.height
|
||||
&& self.y + self.height > other.y
|
||||
}
|
||||
|
||||
/// Get the intersection of two rectangles.
|
||||
/// 获取两个矩形的交集。
|
||||
pub fn intersection(&self, other: &Rect) -> Option<Rect> {
|
||||
let x = self.x.max(other.x);
|
||||
let y = self.y.max(other.y);
|
||||
let right = (self.x + self.width).min(other.x + other.width);
|
||||
let bottom = (self.y + self.height).min(other.y + other.height);
|
||||
|
||||
if right > x && bottom > y {
|
||||
Some(Rect::new(x, y, right - x, bottom - y))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the union of two rectangles (bounding box).
|
||||
/// 获取两个矩形的并集(包围盒)。
|
||||
pub fn union(&self, other: &Rect) -> Rect {
|
||||
let x = self.x.min(other.x);
|
||||
let y = self.y.min(other.y);
|
||||
let right = (self.x + self.width).max(other.x + other.width);
|
||||
let bottom = (self.y + self.height).max(other.y + other.height);
|
||||
|
||||
Rect::new(x, y, right - x, bottom - y)
|
||||
}
|
||||
|
||||
/// Expand the rectangle by a margin.
|
||||
/// 按边距扩展矩形。
|
||||
#[inline]
|
||||
pub fn expand(&self, margin: f32) -> Rect {
|
||||
Rect::new(
|
||||
self.x - margin,
|
||||
self.y - margin,
|
||||
self.width + margin * 2.0,
|
||||
self.height + margin * 2.0,
|
||||
)
|
||||
}
|
||||
}
|
||||
177
packages/rust/engine/src/math/transform.rs
Normal file
177
packages/rust/engine/src/math/transform.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
//! 2D transform implementation.
|
||||
//! 2D变换实现。
|
||||
|
||||
use super::Vec2;
|
||||
use glam::Mat3;
|
||||
|
||||
/// 2D transformation combining position, rotation, and scale.
|
||||
/// 组合位置、旋转和缩放的2D变换。
|
||||
///
|
||||
/// # Examples | 示例
|
||||
/// ```rust
|
||||
/// use es_engine::math::{Transform2D, Vec2};
|
||||
/// let mut transform = Transform2D::new();
|
||||
/// transform.position = Vec2::new(100.0, 200.0);
|
||||
/// transform.rotation = std::f32::consts::PI / 4.0; // 45 degrees
|
||||
/// transform.scale = Vec2::new(2.0, 2.0);
|
||||
///
|
||||
/// let matrix = transform.to_matrix();
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Transform2D {
|
||||
/// Position in world space.
|
||||
/// 世界空间中的位置。
|
||||
pub position: Vec2,
|
||||
|
||||
/// Rotation in radians.
|
||||
/// 旋转角度(弧度)。
|
||||
pub rotation: f32,
|
||||
|
||||
/// Scale factor.
|
||||
/// 缩放因子。
|
||||
pub scale: Vec2,
|
||||
|
||||
/// Origin point for rotation and scaling (0-1 range, relative to size).
|
||||
/// 旋转和缩放的原点(0-1范围,相对于尺寸)。
|
||||
pub origin: Vec2,
|
||||
}
|
||||
|
||||
impl Default for Transform2D {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: Vec2::ZERO,
|
||||
rotation: 0.0,
|
||||
scale: Vec2::new(1.0, 1.0),
|
||||
origin: Vec2::new(0.5, 0.5), // Center by default | 默认居中
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Transform2D {
|
||||
/// Create a new transform with default values.
|
||||
/// 使用默认值创建新变换。
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Create a transform with specified position.
|
||||
/// 使用指定位置创建变换。
|
||||
#[inline]
|
||||
pub fn from_position(x: f32, y: f32) -> Self {
|
||||
Self {
|
||||
position: Vec2::new(x, y),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a transform with position, rotation, and scale.
|
||||
/// 使用位置、旋转和缩放创建变换。
|
||||
#[inline]
|
||||
pub fn from_pos_rot_scale(position: Vec2, rotation: f32, scale: Vec2) -> Self {
|
||||
Self {
|
||||
position,
|
||||
rotation,
|
||||
scale,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to a 3x3 transformation matrix.
|
||||
/// 转换为3x3变换矩阵。
|
||||
///
|
||||
/// The matrix is constructed as: T * R * S (translate, rotate, scale).
|
||||
/// 矩阵构造顺序为:T * R * S(平移、旋转、缩放)。
|
||||
///
|
||||
/// Uses left-hand coordinate system convention:
|
||||
/// 使用左手坐标系约定:
|
||||
/// - Positive rotation = clockwise (when viewed from +Z)
|
||||
/// - 正旋转 = 顺时针(从 +Z 方向观察时)
|
||||
pub fn to_matrix(&self) -> Mat3 {
|
||||
let cos = self.rotation.cos();
|
||||
let sin = self.rotation.sin();
|
||||
|
||||
// Construct TRS matrix directly for performance
|
||||
// 直接构造TRS矩阵以提高性能
|
||||
// Clockwise rotation: [cos, -sin; sin, cos] (column-major)
|
||||
// 顺时针旋转矩阵
|
||||
Mat3::from_cols(
|
||||
glam::Vec3::new(cos * self.scale.x, -sin * self.scale.x, 0.0),
|
||||
glam::Vec3::new(sin * self.scale.y, cos * self.scale.y, 0.0),
|
||||
glam::Vec3::new(self.position.x, self.position.y, 1.0),
|
||||
)
|
||||
}
|
||||
|
||||
/// Convert to a 3x3 matrix with origin offset applied.
|
||||
/// 转换为应用原点偏移的3x3矩阵。
|
||||
///
|
||||
/// # Arguments | 参数
|
||||
/// * `width` - Sprite width | 精灵宽度
|
||||
/// * `height` - Sprite height | 精灵高度
|
||||
///
|
||||
/// Uses left-hand coordinate system (clockwise positive rotation).
|
||||
/// 使用左手坐标系(顺时针正旋转)。
|
||||
pub fn to_matrix_with_origin(&self, width: f32, height: f32) -> Mat3 {
|
||||
let ox = -self.origin.x * width * self.scale.x;
|
||||
let oy = -self.origin.y * height * self.scale.y;
|
||||
|
||||
let cos = self.rotation.cos();
|
||||
let sin = self.rotation.sin();
|
||||
|
||||
// Apply origin offset after rotation (clockwise rotation)
|
||||
// 在旋转后应用原点偏移(顺时针旋转)
|
||||
let tx = self.position.x + ox * cos + oy * sin;
|
||||
let ty = self.position.y - ox * sin + oy * cos;
|
||||
|
||||
// Clockwise rotation matrix
|
||||
// 顺时针旋转矩阵
|
||||
Mat3::from_cols(
|
||||
glam::Vec3::new(cos * self.scale.x, -sin * self.scale.x, 0.0),
|
||||
glam::Vec3::new(sin * self.scale.y, cos * self.scale.y, 0.0),
|
||||
glam::Vec3::new(tx, ty, 1.0),
|
||||
)
|
||||
}
|
||||
|
||||
/// Transform a local point to world space.
|
||||
/// 将局部点变换到世界空间。
|
||||
#[inline]
|
||||
pub fn transform_point(&self, point: Vec2) -> Vec2 {
|
||||
let rotated = point.rotate(self.rotation);
|
||||
Vec2::new(
|
||||
rotated.x * self.scale.x + self.position.x,
|
||||
rotated.y * self.scale.y + self.position.y,
|
||||
)
|
||||
}
|
||||
|
||||
/// Inverse transform a world point to local space.
|
||||
/// 将世界点反变换到局部空间。
|
||||
#[inline]
|
||||
pub fn inverse_transform_point(&self, point: Vec2) -> Vec2 {
|
||||
let local = Vec2::new(
|
||||
(point.x - self.position.x) / self.scale.x,
|
||||
(point.y - self.position.y) / self.scale.y,
|
||||
);
|
||||
local.rotate(-self.rotation)
|
||||
}
|
||||
|
||||
/// Translate the transform by a delta.
|
||||
/// 按增量平移变换。
|
||||
#[inline]
|
||||
pub fn translate(&mut self, delta: Vec2) {
|
||||
self.position = self.position + delta;
|
||||
}
|
||||
|
||||
/// Rotate the transform by an angle (in radians).
|
||||
/// 按角度旋转变换(弧度)。
|
||||
#[inline]
|
||||
pub fn rotate(&mut self, angle: f32) {
|
||||
self.rotation += angle;
|
||||
}
|
||||
|
||||
/// Scale the transform by a factor.
|
||||
/// 按因子缩放变换。
|
||||
#[inline]
|
||||
pub fn scale_by(&mut self, factor: Vec2) {
|
||||
self.scale = Vec2::new(self.scale.x * factor.x, self.scale.y * factor.y);
|
||||
}
|
||||
}
|
||||
222
packages/rust/engine/src/math/vec2.rs
Normal file
222
packages/rust/engine/src/math/vec2.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
//! 2D vector implementation.
|
||||
//! 2D向量实现。
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
/// 2D vector for positions, velocities, and directions.
|
||||
/// 用于位置、速度和方向的2D向量。
|
||||
///
|
||||
/// # Examples | 示例
|
||||
/// ```rust
|
||||
/// use es_engine::math::Vec2;
|
||||
/// let pos = Vec2::new(100.0, 200.0);
|
||||
/// let velocity = Vec2::new(1.0, 0.0);
|
||||
/// let new_pos = pos + velocity * 16.0;
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Pod, Zeroable)]
|
||||
#[repr(C)]
|
||||
pub struct Vec2 {
|
||||
/// X component.
|
||||
/// X分量。
|
||||
pub x: f32,
|
||||
/// Y component.
|
||||
/// Y分量。
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
impl Vec2 {
|
||||
/// Zero vector (0, 0).
|
||||
/// 零向量。
|
||||
pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
|
||||
|
||||
/// Unit vector pointing right (1, 0).
|
||||
/// 指向右的单位向量。
|
||||
pub const RIGHT: Self = Self { x: 1.0, y: 0.0 };
|
||||
|
||||
/// Unit vector pointing up (0, 1).
|
||||
/// 指向上的单位向量。
|
||||
pub const UP: Self = Self { x: 0.0, y: 1.0 };
|
||||
|
||||
/// Create a new 2D vector.
|
||||
/// 创建新的2D向量。
|
||||
#[inline]
|
||||
pub const fn new(x: f32, y: f32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
/// Create a vector with both components set to the same value.
|
||||
/// 创建两个分量相同的向量。
|
||||
#[inline]
|
||||
pub const fn splat(v: f32) -> Self {
|
||||
Self { x: v, y: v }
|
||||
}
|
||||
|
||||
/// Calculate the length (magnitude) of the vector.
|
||||
/// 计算向量的长度(模)。
|
||||
#[inline]
|
||||
pub fn length(&self) -> f32 {
|
||||
(self.x * self.x + self.y * self.y).sqrt()
|
||||
}
|
||||
|
||||
/// Calculate the squared length (avoids sqrt).
|
||||
/// 计算长度的平方(避免开方运算)。
|
||||
#[inline]
|
||||
pub fn length_squared(&self) -> f32 {
|
||||
self.x * self.x + self.y * self.y
|
||||
}
|
||||
|
||||
/// Normalize the vector (make it unit length).
|
||||
/// 归一化向量(使其成为单位长度)。
|
||||
#[inline]
|
||||
pub fn normalize(&self) -> Self {
|
||||
let len = self.length();
|
||||
if len > 0.0 {
|
||||
Self {
|
||||
x: self.x / len,
|
||||
y: self.y / len,
|
||||
}
|
||||
} else {
|
||||
Self::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate dot product with another vector.
|
||||
/// 计算与另一个向量的点积。
|
||||
#[inline]
|
||||
pub fn dot(&self, other: &Self) -> f32 {
|
||||
self.x * other.x + self.y * other.y
|
||||
}
|
||||
|
||||
/// Calculate cross product (returns scalar for 2D).
|
||||
/// 计算叉积(2D返回标量)。
|
||||
#[inline]
|
||||
pub fn cross(&self, other: &Self) -> f32 {
|
||||
self.x * other.y - self.y * other.x
|
||||
}
|
||||
|
||||
/// Calculate distance to another point.
|
||||
/// 计算到另一点的距离。
|
||||
#[inline]
|
||||
pub fn distance(&self, other: &Self) -> f32 {
|
||||
(*self - *other).length()
|
||||
}
|
||||
|
||||
/// Linear interpolation between two vectors.
|
||||
/// 两个向量之间的线性插值。
|
||||
#[inline]
|
||||
pub fn lerp(&self, other: &Self, t: f32) -> Self {
|
||||
Self {
|
||||
x: self.x + (other.x - self.x) * t,
|
||||
y: self.y + (other.y - self.y) * t,
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotate the vector by an angle (in radians).
|
||||
/// 按角度旋转向量(弧度)。
|
||||
///
|
||||
/// Uses left-hand coordinate system convention:
|
||||
/// 使用左手坐标系约定:
|
||||
/// - Positive angle = clockwise rotation (when viewed from +Z)
|
||||
/// - 正角度 = 顺时针旋转(从 +Z 方向观察时)
|
||||
#[inline]
|
||||
pub fn rotate(&self, angle: f32) -> Self {
|
||||
let cos = angle.cos();
|
||||
let sin = angle.sin();
|
||||
// Clockwise rotation matrix: [cos, sin; -sin, cos]
|
||||
// 顺时针旋转矩阵
|
||||
Self {
|
||||
x: self.x * cos + self.y * sin,
|
||||
y: -self.x * sin + self.y * cos,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to glam Vec2.
|
||||
/// 转换为glam Vec2。
|
||||
#[inline]
|
||||
pub fn to_glam(&self) -> glam::Vec2 {
|
||||
glam::Vec2::new(self.x, self.y)
|
||||
}
|
||||
|
||||
/// Create from glam Vec2.
|
||||
/// 从glam Vec2创建。
|
||||
#[inline]
|
||||
pub fn from_glam(v: glam::Vec2) -> Self {
|
||||
Self { x: v.x, y: v.y }
|
||||
}
|
||||
}
|
||||
|
||||
// Operator implementations | 运算符实现
|
||||
|
||||
impl std::ops::Add for Vec2 {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
x: self.x + rhs.x,
|
||||
y: self.y + rhs.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub for Vec2 {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
x: self.x - rhs.x,
|
||||
y: self.y - rhs.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<f32> for Vec2 {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn mul(self, rhs: f32) -> Self::Output {
|
||||
Self {
|
||||
x: self.x * rhs,
|
||||
y: self.y * rhs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Div<f32> for Vec2 {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn div(self, rhs: f32) -> Self::Output {
|
||||
Self {
|
||||
x: self.x / rhs,
|
||||
y: self.y / rhs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Neg for Vec2 {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
fn neg(self) -> Self::Output {
|
||||
Self {
|
||||
x: -self.x,
|
||||
y: -self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(f32, f32)> for Vec2 {
|
||||
#[inline]
|
||||
fn from((x, y): (f32, f32)) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[f32; 2]> for Vec2 {
|
||||
#[inline]
|
||||
fn from([x, y]: [f32; 2]) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
51
packages/rust/engine/src/platform/mod.rs
Normal file
51
packages/rust/engine/src/platform/mod.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
//! Platform abstraction layer.
|
||||
//! 平台抽象层。
|
||||
//!
|
||||
//! Provides abstractions for platform-specific functionality.
|
||||
//! 提供平台特定功能的抽象。
|
||||
|
||||
mod web;
|
||||
|
||||
pub use web::WebPlatform;
|
||||
|
||||
/// Platform capabilities and information.
|
||||
/// 平台能力和信息。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlatformInfo {
|
||||
/// Platform name.
|
||||
/// 平台名称。
|
||||
pub name: String,
|
||||
|
||||
/// Whether WebGL2 is supported.
|
||||
/// 是否支持WebGL2。
|
||||
pub webgl2_supported: bool,
|
||||
|
||||
/// Whether touch input is supported.
|
||||
/// 是否支持触摸输入。
|
||||
pub touch_supported: bool,
|
||||
|
||||
/// Device pixel ratio.
|
||||
/// 设备像素比。
|
||||
pub pixel_ratio: f32,
|
||||
|
||||
/// Screen width.
|
||||
/// 屏幕宽度。
|
||||
pub screen_width: u32,
|
||||
|
||||
/// Screen height.
|
||||
/// 屏幕高度。
|
||||
pub screen_height: u32,
|
||||
}
|
||||
|
||||
impl Default for PlatformInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "Unknown".to_string(),
|
||||
webgl2_supported: false,
|
||||
touch_supported: false,
|
||||
pixel_ratio: 1.0,
|
||||
screen_width: 0,
|
||||
screen_height: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
146
packages/rust/engine/src/platform/web.rs
Normal file
146
packages/rust/engine/src/platform/web.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
//! Web platform implementation.
|
||||
//! Web平台实现。
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::Window;
|
||||
|
||||
use super::PlatformInfo;
|
||||
|
||||
/// Web platform utilities.
|
||||
/// Web平台工具。
|
||||
pub struct WebPlatform;
|
||||
|
||||
impl WebPlatform {
|
||||
/// Get platform information.
|
||||
/// 获取平台信息。
|
||||
pub fn get_info() -> PlatformInfo {
|
||||
let window = match web_sys::window() {
|
||||
Some(w) => w,
|
||||
None => return PlatformInfo::default(),
|
||||
};
|
||||
|
||||
let navigator = window.navigator();
|
||||
let user_agent = navigator.user_agent().unwrap_or_default();
|
||||
|
||||
// Detect platform name | 检测平台名称
|
||||
let name = Self::detect_platform_name(&user_agent);
|
||||
|
||||
// Check WebGL2 support | 检查WebGL2支持
|
||||
let webgl2_supported = Self::check_webgl2_support(&window);
|
||||
|
||||
// Check touch support | 检查触摸支持
|
||||
let touch_supported = Self::check_touch_support(&window);
|
||||
|
||||
// Get device pixel ratio | 获取设备像素比
|
||||
let pixel_ratio = window.device_pixel_ratio() as f32;
|
||||
|
||||
// Get screen size | 获取屏幕尺寸
|
||||
let screen = window.screen().ok();
|
||||
let (screen_width, screen_height) = screen
|
||||
.map(|s| {
|
||||
(
|
||||
s.width().unwrap_or(0) as u32,
|
||||
s.height().unwrap_or(0) as u32,
|
||||
)
|
||||
})
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
PlatformInfo {
|
||||
name,
|
||||
webgl2_supported,
|
||||
touch_supported,
|
||||
pixel_ratio,
|
||||
screen_width,
|
||||
screen_height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect platform name from user agent.
|
||||
/// 从用户代理检测平台名称。
|
||||
fn detect_platform_name(user_agent: &str) -> String {
|
||||
let ua = user_agent.to_lowercase();
|
||||
|
||||
if ua.contains("micromessenger") {
|
||||
"WeChat MiniGame".to_string()
|
||||
} else if ua.contains("bytedance") || ua.contains("toutiao") {
|
||||
"ByteDance MiniGame".to_string()
|
||||
} else if ua.contains("alipay") {
|
||||
"Alipay MiniGame".to_string()
|
||||
} else if ua.contains("iphone") || ua.contains("ipad") {
|
||||
"iOS Web".to_string()
|
||||
} else if ua.contains("android") {
|
||||
"Android Web".to_string()
|
||||
} else if ua.contains("windows") {
|
||||
"Windows Web".to_string()
|
||||
} else if ua.contains("macintosh") {
|
||||
"macOS Web".to_string()
|
||||
} else {
|
||||
"Web".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if WebGL2 is supported.
|
||||
/// 检查是否支持WebGL2。
|
||||
fn check_webgl2_support(window: &Window) -> bool {
|
||||
let document = match window.document() {
|
||||
Some(d) => d,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let canvas = match document.create_element("canvas") {
|
||||
Ok(c) => c,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let canvas = match canvas.dyn_into::<web_sys::HtmlCanvasElement>() {
|
||||
Ok(c) => c,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
canvas.get_context("webgl2").ok().flatten().is_some()
|
||||
}
|
||||
|
||||
/// Check if touch input is supported.
|
||||
/// 检查是否支持触摸输入。
|
||||
fn check_touch_support(window: &Window) -> bool {
|
||||
// Check for touch events | 检查触摸事件
|
||||
let has_touch_event = js_sys::Reflect::has(
|
||||
window,
|
||||
&wasm_bindgen::JsValue::from_str("ontouchstart"),
|
||||
)
|
||||
.unwrap_or(false);
|
||||
|
||||
if has_touch_event {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check navigator.maxTouchPoints | 检查navigator.maxTouchPoints
|
||||
let navigator = window.navigator();
|
||||
navigator.max_touch_points() > 0
|
||||
}
|
||||
|
||||
/// Request animation frame.
|
||||
/// 请求动画帧。
|
||||
pub fn request_animation_frame(callback: &wasm_bindgen::closure::Closure<dyn FnMut()>) -> i32 {
|
||||
let window = web_sys::window().expect("No window found");
|
||||
window
|
||||
.request_animation_frame(callback.as_ref().unchecked_ref())
|
||||
.expect("Failed to request animation frame")
|
||||
}
|
||||
|
||||
/// Get current timestamp in milliseconds.
|
||||
/// 获取当前时间戳(毫秒)。
|
||||
pub fn now() -> f64 {
|
||||
let window = web_sys::window().expect("No window found");
|
||||
window
|
||||
.performance()
|
||||
.expect("No performance object")
|
||||
.now()
|
||||
}
|
||||
|
||||
/// Log a message to the console.
|
||||
/// 向控制台输出消息。
|
||||
pub fn console_log(message: &str) {
|
||||
web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(message));
|
||||
}
|
||||
}
|
||||
243
packages/rust/engine/src/renderer/batch/mesh_batch.rs
Normal file
243
packages/rust/engine/src/renderer/batch/mesh_batch.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
//! Mesh batch renderer for arbitrary 2D geometry.
|
||||
//! 用于任意 2D 几何体的网格批处理渲染器。
|
||||
//!
|
||||
//! Unlike SpriteBatch which only supports quads, MeshBatch can render
|
||||
//! arbitrary triangulated meshes (ellipses, polygons, rounded rectangles, etc.).
|
||||
//!
|
||||
//! 与仅支持四边形的 SpriteBatch 不同,MeshBatch 可以渲染
|
||||
//! 任意三角化的网格(椭圆、多边形、圆角矩形等)。
|
||||
|
||||
use es_engine_shared::{
|
||||
traits::backend::{GraphicsBackend, BufferUsage},
|
||||
types::{
|
||||
handle::{BufferHandle, VertexArrayHandle},
|
||||
vertex::{VertexLayout, VertexAttribute, VertexAttributeType},
|
||||
},
|
||||
};
|
||||
|
||||
/// Floats per mesh vertex: position(2) + texCoord(2) + color(4) = 8
|
||||
/// 每个网格顶点的浮点数:位置(2) + 纹理坐标(2) + 颜色(4) = 8
|
||||
const FLOATS_PER_VERTEX: usize = 8;
|
||||
|
||||
/// Mesh batch for rendering arbitrary 2D geometry.
|
||||
/// 用于渲染任意 2D 几何体的网格批处理。
|
||||
pub struct MeshBatch {
|
||||
vbo: BufferHandle,
|
||||
ibo: BufferHandle,
|
||||
vao: VertexArrayHandle,
|
||||
max_vertices: usize,
|
||||
max_indices: usize,
|
||||
vertex_data: Vec<f32>,
|
||||
index_data: Vec<u16>,
|
||||
vertex_count: usize,
|
||||
index_count: usize,
|
||||
}
|
||||
|
||||
impl MeshBatch {
|
||||
/// Create a new mesh batch.
|
||||
/// 创建新的网格批处理。
|
||||
///
|
||||
/// # Arguments | 参数
|
||||
/// * `backend` - Graphics backend
|
||||
/// * `max_vertices` - Maximum number of vertices
|
||||
/// * `max_indices` - Maximum number of indices
|
||||
pub fn new(
|
||||
backend: &mut impl GraphicsBackend,
|
||||
max_vertices: usize,
|
||||
max_indices: usize,
|
||||
) -> Result<Self, String> {
|
||||
let vertex_buffer_size = max_vertices * FLOATS_PER_VERTEX * 4;
|
||||
let vbo = backend.create_vertex_buffer(
|
||||
&vec![0u8; vertex_buffer_size],
|
||||
BufferUsage::Dynamic,
|
||||
).map_err(|e| format!("Mesh VBO: {:?}", e))?;
|
||||
|
||||
let ibo = backend.create_index_buffer(
|
||||
bytemuck::cast_slice(&vec![0u16; max_indices]),
|
||||
BufferUsage::Dynamic,
|
||||
).map_err(|e| format!("Mesh IBO: {:?}", e))?;
|
||||
|
||||
// Mesh vertex layout:
|
||||
// a_position: vec2 (location 0)
|
||||
// a_texCoord: vec2 (location 1)
|
||||
// a_color: vec4 (location 2)
|
||||
let layout = VertexLayout {
|
||||
attributes: vec![
|
||||
VertexAttribute {
|
||||
name: "a_position".into(),
|
||||
attr_type: VertexAttributeType::Float2,
|
||||
offset: 0,
|
||||
normalized: false,
|
||||
},
|
||||
VertexAttribute {
|
||||
name: "a_texcoord".into(),
|
||||
attr_type: VertexAttributeType::Float2,
|
||||
offset: 8,
|
||||
normalized: false,
|
||||
},
|
||||
VertexAttribute {
|
||||
name: "a_color".into(),
|
||||
attr_type: VertexAttributeType::Float4,
|
||||
offset: 16,
|
||||
normalized: false,
|
||||
},
|
||||
],
|
||||
stride: FLOATS_PER_VERTEX * 4,
|
||||
};
|
||||
|
||||
let vao = backend.create_vertex_array(vbo, Some(ibo), &layout)
|
||||
.map_err(|e| format!("Mesh VAO: {:?}", e))?;
|
||||
|
||||
Ok(Self {
|
||||
vbo,
|
||||
ibo,
|
||||
vao,
|
||||
max_vertices,
|
||||
max_indices,
|
||||
vertex_data: Vec::with_capacity(max_vertices * FLOATS_PER_VERTEX),
|
||||
index_data: Vec::with_capacity(max_indices),
|
||||
vertex_count: 0,
|
||||
index_count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Clear the batch.
|
||||
/// 清除批处理。
|
||||
pub fn clear(&mut self) {
|
||||
self.vertex_data.clear();
|
||||
self.index_data.clear();
|
||||
self.vertex_count = 0;
|
||||
self.index_count = 0;
|
||||
}
|
||||
|
||||
/// Add a mesh to the batch.
|
||||
/// 将网格添加到批处理。
|
||||
///
|
||||
/// # Arguments | 参数
|
||||
/// * `positions` - Float array [x, y, ...] for each vertex
|
||||
/// * `uvs` - Float array [u, v, ...] for each vertex
|
||||
/// * `colors` - Packed RGBA colors (one per vertex)
|
||||
/// * `indices` - Triangle indices
|
||||
/// * `offset_x` - X offset to apply to all positions
|
||||
/// * `offset_y` - Y offset to apply to all positions
|
||||
pub fn add_mesh(
|
||||
&mut self,
|
||||
positions: &[f32],
|
||||
uvs: &[f32],
|
||||
colors: &[u32],
|
||||
indices: &[u16],
|
||||
offset_x: f32,
|
||||
offset_y: f32,
|
||||
) -> Result<(), String> {
|
||||
let vertex_count = positions.len() / 2;
|
||||
|
||||
if self.vertex_count + vertex_count > self.max_vertices {
|
||||
return Err(format!(
|
||||
"Mesh batch vertex overflow: {} + {} > {}",
|
||||
self.vertex_count, vertex_count, self.max_vertices
|
||||
));
|
||||
}
|
||||
|
||||
if self.index_count + indices.len() > self.max_indices {
|
||||
return Err(format!(
|
||||
"Mesh batch index overflow: {} + {} > {}",
|
||||
self.index_count, indices.len(), self.max_indices
|
||||
));
|
||||
}
|
||||
|
||||
// Validate input sizes
|
||||
if uvs.len() != positions.len() {
|
||||
return Err(format!(
|
||||
"UV size mismatch: {} vs {}",
|
||||
uvs.len(), positions.len()
|
||||
));
|
||||
}
|
||||
if colors.len() != vertex_count {
|
||||
return Err(format!(
|
||||
"Color count mismatch: {} vs {}",
|
||||
colors.len(), vertex_count
|
||||
));
|
||||
}
|
||||
|
||||
// Build vertex data
|
||||
let base_index = self.vertex_count as u16;
|
||||
for v in 0..vertex_count {
|
||||
let pos_idx = v * 2;
|
||||
|
||||
// Position with offset (2 floats)
|
||||
self.vertex_data.push(positions[pos_idx] + offset_x);
|
||||
self.vertex_data.push(positions[pos_idx + 1] + offset_y);
|
||||
|
||||
// TexCoord (2 floats)
|
||||
self.vertex_data.push(uvs[pos_idx]);
|
||||
self.vertex_data.push(uvs[pos_idx + 1]);
|
||||
|
||||
// Color (4 floats from packed RGBA)
|
||||
let color = colors[v];
|
||||
let r = ((color >> 24) & 0xFF) as f32 / 255.0;
|
||||
let g = ((color >> 16) & 0xFF) as f32 / 255.0;
|
||||
let b = ((color >> 8) & 0xFF) as f32 / 255.0;
|
||||
let a = (color & 0xFF) as f32 / 255.0;
|
||||
self.vertex_data.push(r);
|
||||
self.vertex_data.push(g);
|
||||
self.vertex_data.push(b);
|
||||
self.vertex_data.push(a);
|
||||
}
|
||||
|
||||
// Add indices with base offset
|
||||
for &idx in indices {
|
||||
self.index_data.push(base_index + idx);
|
||||
}
|
||||
|
||||
self.vertex_count += vertex_count;
|
||||
self.index_count += indices.len();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the vertex count.
|
||||
/// 获取顶点数量。
|
||||
#[inline]
|
||||
pub fn vertex_count(&self) -> usize {
|
||||
self.vertex_count
|
||||
}
|
||||
|
||||
/// Get the index count.
|
||||
/// 获取索引数量。
|
||||
#[inline]
|
||||
pub fn index_count(&self) -> usize {
|
||||
self.index_count
|
||||
}
|
||||
|
||||
/// Get the VAO handle.
|
||||
/// 获取 VAO 句柄。
|
||||
#[inline]
|
||||
pub fn vao(&self) -> VertexArrayHandle {
|
||||
self.vao
|
||||
}
|
||||
|
||||
/// Flush and render the batch.
|
||||
/// 刷新并渲染批处理。
|
||||
pub fn flush(&self, backend: &mut impl GraphicsBackend) {
|
||||
if self.vertex_data.is_empty() || self.index_data.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload vertex data
|
||||
backend.update_buffer(self.vbo, 0, bytemuck::cast_slice(&self.vertex_data)).ok();
|
||||
|
||||
// Upload index data
|
||||
backend.update_buffer(self.ibo, 0, bytemuck::cast_slice(&self.index_data)).ok();
|
||||
|
||||
// Draw indexed
|
||||
backend.draw_indexed(self.vao, self.index_count as u32, 0).ok();
|
||||
}
|
||||
|
||||
/// Destroy the batch resources.
|
||||
/// 销毁批处理资源。
|
||||
pub fn destroy(self, backend: &mut impl GraphicsBackend) {
|
||||
backend.destroy_vertex_array(self.vao);
|
||||
backend.destroy_buffer(self.vbo);
|
||||
backend.destroy_buffer(self.ibo);
|
||||
}
|
||||
}
|
||||
18
packages/rust/engine/src/renderer/batch/mod.rs
Normal file
18
packages/rust/engine/src/renderer/batch/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
//! Batch rendering system.
|
||||
//! 批处理渲染系统。
|
||||
|
||||
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,
|
||||
};
|
||||
192
packages/rust/engine/src/renderer/batch/sprite_batch.rs
Normal file
192
packages/rust/engine/src/renderer/batch/sprite_batch.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
//! Sprite batch renderer for efficient 2D rendering.
|
||||
|
||||
use es_engine_shared::{
|
||||
traits::backend::{GraphicsBackend, BufferUsage},
|
||||
types::{
|
||||
handle::{BufferHandle, VertexArrayHandle},
|
||||
vertex::{VertexLayout, VertexAttribute, VertexAttributeType},
|
||||
},
|
||||
};
|
||||
use crate::math::Color;
|
||||
|
||||
const VERTICES_PER_SPRITE: usize = 4;
|
||||
const INDICES_PER_SPRITE: usize = 6;
|
||||
const FLOATS_PER_VERTEX: usize = 9;
|
||||
const TRANSFORM_STRIDE: usize = 7;
|
||||
const UV_STRIDE: usize = 4;
|
||||
|
||||
#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)]
|
||||
pub struct BatchKey {
|
||||
pub material_id: u32,
|
||||
pub texture_id: u32,
|
||||
}
|
||||
|
||||
pub struct SpriteBatch {
|
||||
vbo: BufferHandle,
|
||||
ibo: BufferHandle,
|
||||
vao: VertexArrayHandle,
|
||||
max_sprites: usize,
|
||||
batches: Vec<(BatchKey, Vec<f32>)>,
|
||||
sprite_count: usize,
|
||||
last_batch_key: Option<BatchKey>,
|
||||
}
|
||||
|
||||
impl SpriteBatch {
|
||||
pub fn new(backend: &mut impl GraphicsBackend, max_sprites: usize) -> Result<Self, String> {
|
||||
let vertex_buffer_size = max_sprites * VERTICES_PER_SPRITE * FLOATS_PER_VERTEX * 4;
|
||||
let vbo = backend.create_vertex_buffer(
|
||||
&vec![0u8; vertex_buffer_size],
|
||||
BufferUsage::Dynamic,
|
||||
).map_err(|e| format!("VBO: {:?}", e))?;
|
||||
|
||||
let indices = Self::generate_indices(max_sprites);
|
||||
let ibo = backend.create_index_buffer(
|
||||
bytemuck::cast_slice(&indices),
|
||||
BufferUsage::Static,
|
||||
).map_err(|e| format!("IBO: {:?}", e))?;
|
||||
|
||||
let layout = VertexLayout {
|
||||
attributes: vec![
|
||||
VertexAttribute { name: "a_position".into(), attr_type: VertexAttributeType::Float2, offset: 0, normalized: false },
|
||||
VertexAttribute { name: "a_texcoord".into(), attr_type: VertexAttributeType::Float2, offset: 8, normalized: false },
|
||||
VertexAttribute { name: "a_color".into(), attr_type: VertexAttributeType::Float4, offset: 16, normalized: false },
|
||||
VertexAttribute { name: "a_aspect".into(), attr_type: VertexAttributeType::Float, offset: 32, normalized: false },
|
||||
],
|
||||
stride: FLOATS_PER_VERTEX * 4,
|
||||
};
|
||||
|
||||
let vao = backend.create_vertex_array(vbo, Some(ibo), &layout)
|
||||
.map_err(|e| format!("VAO: {:?}", e))?;
|
||||
|
||||
Ok(Self {
|
||||
vbo, ibo, vao,
|
||||
max_sprites,
|
||||
batches: Vec::new(),
|
||||
sprite_count: 0,
|
||||
last_batch_key: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_indices(max_sprites: usize) -> Vec<u16> {
|
||||
(0..max_sprites).flat_map(|i| {
|
||||
let base = (i * VERTICES_PER_SPRITE) as u16;
|
||||
[base, base + 1, base + 2, base + 2, base + 3, base]
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.batches.clear();
|
||||
self.sprite_count = 0;
|
||||
self.last_batch_key = None;
|
||||
}
|
||||
|
||||
pub fn add_sprites(
|
||||
&mut self,
|
||||
transforms: &[f32],
|
||||
texture_ids: &[u32],
|
||||
uvs: &[f32],
|
||||
colors: &[u32],
|
||||
material_ids: &[u32],
|
||||
) -> Result<(), String> {
|
||||
let count = texture_ids.len();
|
||||
|
||||
if transforms.len() != count * TRANSFORM_STRIDE {
|
||||
return Err(format!("Transform mismatch: {} vs {}", transforms.len(), count * TRANSFORM_STRIDE));
|
||||
}
|
||||
if uvs.len() != count * UV_STRIDE {
|
||||
return Err(format!("UV mismatch: {} vs {}", uvs.len(), count * UV_STRIDE));
|
||||
}
|
||||
if colors.len() != count || material_ids.len() != count {
|
||||
return Err("Color/material count mismatch".into());
|
||||
}
|
||||
if self.sprite_count + count > self.max_sprites {
|
||||
return Err(format!("Batch overflow: {} + {} > {}", self.sprite_count, count, self.max_sprites));
|
||||
}
|
||||
|
||||
for i in 0..count {
|
||||
let t = i * TRANSFORM_STRIDE;
|
||||
let uv = i * UV_STRIDE;
|
||||
|
||||
let (x, y) = (transforms[t], transforms[t + 1]);
|
||||
let rotation = transforms[t + 2];
|
||||
let (width, height) = (transforms[t + 3], transforms[t + 4]);
|
||||
let (origin_x, origin_y) = (transforms[t + 5], transforms[t + 6]);
|
||||
|
||||
let (u0, v0, u1, v1) = (uvs[uv], uvs[uv + 1], uvs[uv + 2], uvs[uv + 3]);
|
||||
let color = Color::from_packed(colors[i]);
|
||||
let color_arr = [color.r, color.g, color.b, color.a];
|
||||
let aspect = if height.abs() > 0.001 { width / height } else { 1.0 };
|
||||
|
||||
let key = BatchKey { material_id: material_ids[i], texture_id: texture_ids[i] };
|
||||
|
||||
if self.last_batch_key != Some(key) {
|
||||
self.batches.push((key, Vec::new()));
|
||||
self.last_batch_key = Some(key);
|
||||
}
|
||||
|
||||
let batch = &mut self.batches.last_mut().unwrap().1;
|
||||
Self::add_sprite_vertices(batch, x, y, width, height, rotation, origin_x, origin_y,
|
||||
u0, v0, u1, v1, color_arr, aspect);
|
||||
}
|
||||
|
||||
self.sprite_count += count;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn add_sprite_vertices(
|
||||
batch: &mut Vec<f32>,
|
||||
x: f32, y: f32, width: f32, height: f32, rotation: f32,
|
||||
origin_x: f32, origin_y: f32,
|
||||
u0: f32, v0: f32, u1: f32, v1: f32,
|
||||
color: [f32; 4], aspect: f32,
|
||||
) {
|
||||
let (cos, sin) = (rotation.cos(), rotation.sin());
|
||||
let (ox, oy) = (origin_x * width, origin_y * height);
|
||||
|
||||
let corners = [(-ox, height - oy), (width - ox, height - oy), (width - ox, -oy), (-ox, -oy)];
|
||||
let tex_coords = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
|
||||
|
||||
for i in 0..4 {
|
||||
let (lx, ly) = corners[i];
|
||||
let (rx, ry) = (lx * cos - ly * sin, lx * sin + ly * cos);
|
||||
batch.extend_from_slice(&[rx + x, ry + y]);
|
||||
batch.extend_from_slice(&tex_coords[i]);
|
||||
batch.extend_from_slice(&color);
|
||||
batch.push(aspect);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn batches(&self) -> &[(BatchKey, Vec<f32>)] {
|
||||
&self.batches
|
||||
}
|
||||
|
||||
pub fn flush_batch(&self, backend: &mut impl GraphicsBackend, vertices: &[f32]) {
|
||||
if vertices.is_empty() { return; }
|
||||
|
||||
let sprite_count = vertices.len() / (VERTICES_PER_SPRITE * FLOATS_PER_VERTEX);
|
||||
backend.update_buffer(self.vbo, 0, bytemuck::cast_slice(vertices)).ok();
|
||||
backend.draw_indexed(self.vao, (sprite_count * INDICES_PER_SPRITE) as u32, 0).ok();
|
||||
}
|
||||
|
||||
pub fn flush_batch_at(&self, backend: &mut impl GraphicsBackend, index: usize) {
|
||||
if let Some((_, vertices)) = self.batches.get(index) {
|
||||
self.flush_batch(backend, vertices);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn sprite_count(&self) -> usize {
|
||||
self.sprite_count
|
||||
}
|
||||
|
||||
pub fn vao(&self) -> VertexArrayHandle {
|
||||
self.vao
|
||||
}
|
||||
|
||||
pub fn destroy(self, backend: &mut impl GraphicsBackend) {
|
||||
backend.destroy_vertex_array(self.vao);
|
||||
backend.destroy_buffer(self.vbo);
|
||||
backend.destroy_buffer(self.ibo);
|
||||
}
|
||||
}
|
||||
262
packages/rust/engine/src/renderer/batch/text_batch.rs
Normal file
262
packages/rust/engine/src/renderer/batch/text_batch.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
//! Text batch renderer for MSDF text rendering.
|
||||
//! MSDF 文本批处理渲染器。
|
||||
|
||||
use es_engine_shared::{
|
||||
traits::backend::{GraphicsBackend, BufferUsage},
|
||||
types::{
|
||||
handle::{BufferHandle, VertexArrayHandle, ShaderHandle},
|
||||
vertex::{VertexLayout, VertexAttribute, VertexAttributeType},
|
||||
},
|
||||
};
|
||||
|
||||
/// Number of vertices per glyph (quad).
|
||||
/// 每个字形的顶点数(四边形)。
|
||||
const VERTICES_PER_GLYPH: usize = 4;
|
||||
|
||||
/// Number of indices per glyph (2 triangles).
|
||||
/// 每个字形的索引数(2 个三角形)。
|
||||
const INDICES_PER_GLYPH: usize = 6;
|
||||
|
||||
/// Floats per text vertex: position(2) + texCoord(2) + color(4) + outlineColor(4) + outlineWidth(1) = 13
|
||||
/// 每个文本顶点的浮点数:位置(2) + 纹理坐标(2) + 颜色(4) + 描边颜色(4) + 描边宽度(1) = 13
|
||||
const FLOATS_PER_VERTEX: usize = 13;
|
||||
|
||||
/// Text batch for MSDF text rendering.
|
||||
/// MSDF 文本批处理。
|
||||
pub struct TextBatch {
|
||||
vbo: BufferHandle,
|
||||
ibo: BufferHandle,
|
||||
vao: VertexArrayHandle,
|
||||
shader: ShaderHandle,
|
||||
max_glyphs: usize,
|
||||
vertex_data: Vec<f32>,
|
||||
glyph_count: usize,
|
||||
}
|
||||
|
||||
impl TextBatch {
|
||||
/// Create a new text batch.
|
||||
/// 创建新的文本批处理。
|
||||
pub fn new(backend: &mut impl GraphicsBackend, max_glyphs: usize) -> Result<Self, String> {
|
||||
let vertex_buffer_size = max_glyphs * VERTICES_PER_GLYPH * FLOATS_PER_VERTEX * 4;
|
||||
let vbo = backend.create_vertex_buffer(
|
||||
&vec![0u8; vertex_buffer_size],
|
||||
BufferUsage::Dynamic,
|
||||
).map_err(|e| format!("Text VBO: {:?}", e))?;
|
||||
|
||||
let indices = Self::generate_indices(max_glyphs);
|
||||
let ibo = backend.create_index_buffer(
|
||||
bytemuck::cast_slice(&indices),
|
||||
BufferUsage::Static,
|
||||
).map_err(|e| format!("Text IBO: {:?}", e))?;
|
||||
|
||||
// MSDF text vertex layout:
|
||||
// a_position: vec2 (location 0)
|
||||
// a_texCoord: vec2 (location 1)
|
||||
// a_color: vec4 (location 2)
|
||||
// a_outlineColor: vec4 (location 3)
|
||||
// a_outlineWidth: float (location 4)
|
||||
let layout = VertexLayout {
|
||||
attributes: vec![
|
||||
VertexAttribute {
|
||||
name: "a_position".into(),
|
||||
attr_type: VertexAttributeType::Float2,
|
||||
offset: 0,
|
||||
normalized: false
|
||||
},
|
||||
VertexAttribute {
|
||||
name: "a_texCoord".into(),
|
||||
attr_type: VertexAttributeType::Float2,
|
||||
offset: 8,
|
||||
normalized: false
|
||||
},
|
||||
VertexAttribute {
|
||||
name: "a_color".into(),
|
||||
attr_type: VertexAttributeType::Float4,
|
||||
offset: 16,
|
||||
normalized: false
|
||||
},
|
||||
VertexAttribute {
|
||||
name: "a_outlineColor".into(),
|
||||
attr_type: VertexAttributeType::Float4,
|
||||
offset: 32,
|
||||
normalized: false
|
||||
},
|
||||
VertexAttribute {
|
||||
name: "a_outlineWidth".into(),
|
||||
attr_type: VertexAttributeType::Float,
|
||||
offset: 48,
|
||||
normalized: false
|
||||
},
|
||||
],
|
||||
stride: FLOATS_PER_VERTEX * 4,
|
||||
};
|
||||
|
||||
let vao = backend.create_vertex_array(vbo, Some(ibo), &layout)
|
||||
.map_err(|e| format!("Text VAO: {:?}", e))?;
|
||||
|
||||
// Compile MSDF text shader
|
||||
let shader = backend.compile_shader(
|
||||
crate::renderer::shader::MSDF_TEXT_VERTEX_SHADER,
|
||||
crate::renderer::shader::MSDF_TEXT_FRAGMENT_SHADER,
|
||||
).map_err(|e| format!("MSDF shader: {:?}", e))?;
|
||||
|
||||
Ok(Self {
|
||||
vbo,
|
||||
ibo,
|
||||
vao,
|
||||
shader,
|
||||
max_glyphs,
|
||||
vertex_data: Vec::with_capacity(max_glyphs * VERTICES_PER_GLYPH * FLOATS_PER_VERTEX),
|
||||
glyph_count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate indices for all glyphs.
|
||||
/// 为所有字形生成索引。
|
||||
fn generate_indices(max_glyphs: usize) -> Vec<u16> {
|
||||
(0..max_glyphs).flat_map(|i| {
|
||||
let base = (i * VERTICES_PER_GLYPH) as u16;
|
||||
// Two triangles: 0-1-2, 2-3-0
|
||||
[base, base + 1, base + 2, base + 2, base + 3, base]
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// Clear the batch.
|
||||
/// 清除批处理。
|
||||
pub fn clear(&mut self) {
|
||||
self.vertex_data.clear();
|
||||
self.glyph_count = 0;
|
||||
}
|
||||
|
||||
/// Add text glyphs to the batch.
|
||||
/// 将文本字形添加到批处理。
|
||||
///
|
||||
/// # Arguments | 参数
|
||||
/// * `positions` - Float32Array [x, y, ...] for each vertex (4 per glyph)
|
||||
/// * `tex_coords` - Float32Array [u, v, ...] for each vertex (4 per glyph)
|
||||
/// * `colors` - Float32Array [r, g, b, a, ...] for each vertex (4 per glyph)
|
||||
/// * `outline_colors` - Float32Array [r, g, b, a, ...] for each vertex
|
||||
/// * `outline_widths` - Float32Array [width, ...] for each vertex
|
||||
pub fn add_glyphs(
|
||||
&mut self,
|
||||
positions: &[f32],
|
||||
tex_coords: &[f32],
|
||||
colors: &[f32],
|
||||
outline_colors: &[f32],
|
||||
outline_widths: &[f32],
|
||||
) -> Result<(), String> {
|
||||
// Calculate glyph count from positions (2 floats per vertex, 4 vertices per glyph)
|
||||
let vertex_count = positions.len() / 2;
|
||||
let glyph_count = vertex_count / VERTICES_PER_GLYPH;
|
||||
|
||||
if self.glyph_count + glyph_count > self.max_glyphs {
|
||||
return Err(format!(
|
||||
"Text batch overflow: {} + {} > {}",
|
||||
self.glyph_count, glyph_count, self.max_glyphs
|
||||
));
|
||||
}
|
||||
|
||||
// Validate input sizes
|
||||
if tex_coords.len() != positions.len() {
|
||||
return Err(format!(
|
||||
"TexCoord size mismatch: {} vs {}",
|
||||
tex_coords.len(), positions.len()
|
||||
));
|
||||
}
|
||||
if colors.len() != vertex_count * 4 {
|
||||
return Err(format!(
|
||||
"Colors size mismatch: {} vs {}",
|
||||
colors.len(), vertex_count * 4
|
||||
));
|
||||
}
|
||||
if outline_colors.len() != vertex_count * 4 {
|
||||
return Err(format!(
|
||||
"OutlineColors size mismatch: {} vs {}",
|
||||
outline_colors.len(), vertex_count * 4
|
||||
));
|
||||
}
|
||||
if outline_widths.len() != vertex_count {
|
||||
return Err(format!(
|
||||
"OutlineWidths size mismatch: {} vs {}",
|
||||
outline_widths.len(), vertex_count
|
||||
));
|
||||
}
|
||||
|
||||
// Build vertex data
|
||||
for v in 0..vertex_count {
|
||||
let pos_idx = v * 2;
|
||||
let col_idx = v * 4;
|
||||
|
||||
// Position (2 floats)
|
||||
self.vertex_data.push(positions[pos_idx]);
|
||||
self.vertex_data.push(positions[pos_idx + 1]);
|
||||
|
||||
// TexCoord (2 floats)
|
||||
self.vertex_data.push(tex_coords[pos_idx]);
|
||||
self.vertex_data.push(tex_coords[pos_idx + 1]);
|
||||
|
||||
// Color (4 floats)
|
||||
self.vertex_data.push(colors[col_idx]);
|
||||
self.vertex_data.push(colors[col_idx + 1]);
|
||||
self.vertex_data.push(colors[col_idx + 2]);
|
||||
self.vertex_data.push(colors[col_idx + 3]);
|
||||
|
||||
// Outline color (4 floats)
|
||||
self.vertex_data.push(outline_colors[col_idx]);
|
||||
self.vertex_data.push(outline_colors[col_idx + 1]);
|
||||
self.vertex_data.push(outline_colors[col_idx + 2]);
|
||||
self.vertex_data.push(outline_colors[col_idx + 3]);
|
||||
|
||||
// Outline width (1 float)
|
||||
self.vertex_data.push(outline_widths[v]);
|
||||
}
|
||||
|
||||
self.glyph_count += glyph_count;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the glyph count.
|
||||
/// 获取字形数量。
|
||||
#[inline]
|
||||
pub fn glyph_count(&self) -> usize {
|
||||
self.glyph_count
|
||||
}
|
||||
|
||||
/// Get the shader handle.
|
||||
/// 获取着色器句柄。
|
||||
#[inline]
|
||||
pub fn shader(&self) -> ShaderHandle {
|
||||
self.shader
|
||||
}
|
||||
|
||||
/// Get the VAO handle.
|
||||
/// 获取 VAO 句柄。
|
||||
#[inline]
|
||||
pub fn vao(&self) -> VertexArrayHandle {
|
||||
self.vao
|
||||
}
|
||||
|
||||
/// Flush and render the batch.
|
||||
/// 刷新并渲染批处理。
|
||||
pub fn flush(&self, backend: &mut impl GraphicsBackend) {
|
||||
if self.vertex_data.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload vertex data
|
||||
backend.update_buffer(self.vbo, 0, bytemuck::cast_slice(&self.vertex_data)).ok();
|
||||
|
||||
// Draw indexed
|
||||
let index_count = (self.glyph_count * INDICES_PER_GLYPH) as u32;
|
||||
backend.draw_indexed(self.vao, index_count, 0).ok();
|
||||
}
|
||||
|
||||
/// Destroy the batch resources.
|
||||
/// 销毁批处理资源。
|
||||
pub fn destroy(self, backend: &mut impl GraphicsBackend) {
|
||||
backend.destroy_vertex_array(self.vao);
|
||||
backend.destroy_buffer(self.vbo);
|
||||
backend.destroy_buffer(self.ibo);
|
||||
backend.destroy_shader(self.shader);
|
||||
}
|
||||
}
|
||||
75
packages/rust/engine/src/renderer/batch/vertex.rs
Normal file
75
packages/rust/engine/src/renderer/batch/vertex.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
//! Vertex data structures for sprite rendering.
|
||||
//! 用于精灵渲染的顶点数据结构。
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
/// Size of a single sprite vertex in bytes.
|
||||
/// 单个精灵顶点的字节大小。
|
||||
pub const VERTEX_SIZE: usize = std::mem::size_of::<SpriteVertex>();
|
||||
|
||||
/// Number of floats per vertex.
|
||||
/// 每个顶点的浮点数数量。
|
||||
///
|
||||
/// Layout: position(2) + tex_coord(2) + color(4) + aspect_ratio(1) = 9
|
||||
/// 布局: 位置(2) + 纹理坐标(2) + 颜色(4) + 宽高比(1) = 9
|
||||
pub const FLOATS_PER_VERTEX: usize = 9;
|
||||
|
||||
/// Sprite vertex data.
|
||||
/// 精灵顶点数据。
|
||||
///
|
||||
/// Each sprite requires 4 vertices (quad), each with position, UV, color, and aspect ratio.
|
||||
/// 每个精灵需要4个顶点(四边形),每个顶点包含位置、UV、颜色和宽高比。
|
||||
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
|
||||
#[repr(C)]
|
||||
pub struct SpriteVertex {
|
||||
/// Position (x, y).
|
||||
/// 位置。
|
||||
pub position: [f32; 2],
|
||||
|
||||
/// Texture coordinates (u, v).
|
||||
/// 纹理坐标。
|
||||
pub tex_coord: [f32; 2],
|
||||
|
||||
/// Color (r, g, b, a).
|
||||
/// 颜色。
|
||||
pub color: [f32; 4],
|
||||
|
||||
/// Aspect ratio (width / height) for shader effects.
|
||||
/// 宽高比(宽度/高度),用于着色器效果。
|
||||
///
|
||||
/// This allows shaders to apply aspect-ratio-aware transformations
|
||||
/// (e.g., rotation in shiny effects) without per-instance uniforms.
|
||||
/// 这允许着色器应用宽高比感知的变换(如闪光效果中的旋转),
|
||||
/// 无需每实例 uniform。
|
||||
pub aspect_ratio: f32,
|
||||
}
|
||||
|
||||
impl SpriteVertex {
|
||||
/// Create a new sprite vertex.
|
||||
/// 创建新的精灵顶点。
|
||||
#[inline]
|
||||
pub const fn new(
|
||||
position: [f32; 2],
|
||||
tex_coord: [f32; 2],
|
||||
color: [f32; 4],
|
||||
aspect_ratio: f32,
|
||||
) -> Self {
|
||||
Self {
|
||||
position,
|
||||
tex_coord,
|
||||
color,
|
||||
aspect_ratio,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SpriteVertex {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: [0.0, 0.0],
|
||||
tex_coord: [0.0, 0.0],
|
||||
color: [1.0, 1.0, 1.0, 1.0],
|
||||
aspect_ratio: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
147
packages/rust/engine/src/renderer/batch/vertex3d.rs
Normal file
147
packages/rust/engine/src/renderer/batch/vertex3d.rs
Normal 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],
|
||||
}
|
||||
}
|
||||
}
|
||||
229
packages/rust/engine/src/renderer/camera.rs
Normal file
229
packages/rust/engine/src/renderer/camera.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
//! 2D camera implementation.
|
||||
//! 2D相机实现。
|
||||
//!
|
||||
//! Uses left-hand coordinate system convention:
|
||||
//! 使用左手坐标系约定:
|
||||
//! - X axis: positive to the right / X 轴:正方向向右
|
||||
//! - Y axis: positive upward (in world space) / Y 轴:正方向向上(世界空间)
|
||||
//! - Z axis: positive into the screen / Z 轴:正方向指向屏幕内
|
||||
//! - Positive rotation: clockwise (when viewed from +Z) / 正旋转:顺时针(从 +Z 观察)
|
||||
|
||||
use crate::math::Vec2;
|
||||
use glam::Mat3;
|
||||
|
||||
/// 2D orthographic camera.
|
||||
/// 2D正交相机。
|
||||
///
|
||||
/// Provides view and projection matrices for 2D rendering.
|
||||
/// 提供用于2D渲染的视图和投影矩阵。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Camera2D {
|
||||
/// Camera position in world space.
|
||||
/// 相机在世界空间中的位置。
|
||||
pub position: Vec2,
|
||||
|
||||
/// Rotation in radians.
|
||||
/// 旋转角度(弧度)。
|
||||
pub rotation: f32,
|
||||
|
||||
/// Zoom level (1.0 = normal).
|
||||
/// 缩放级别(1.0 = 正常)。
|
||||
pub zoom: f32,
|
||||
|
||||
/// Viewport width.
|
||||
/// 视口宽度。
|
||||
width: f32,
|
||||
|
||||
/// Viewport height.
|
||||
/// 视口高度。
|
||||
height: f32,
|
||||
}
|
||||
|
||||
impl Camera2D {
|
||||
/// Create a new 2D camera.
|
||||
/// 创建新的2D相机。
|
||||
///
|
||||
/// # Arguments | 参数
|
||||
/// * `width` - Viewport width | 视口宽度
|
||||
/// * `height` - Viewport height | 视口高度
|
||||
pub fn new(width: f32, height: f32) -> Self {
|
||||
Self {
|
||||
position: Vec2::ZERO,
|
||||
rotation: 0.0,
|
||||
zoom: 1.0,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update viewport size.
|
||||
/// 更新视口大小。
|
||||
pub fn set_viewport(&mut self, width: f32, height: f32) {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
|
||||
/// Get the projection matrix.
|
||||
/// 获取投影矩阵。
|
||||
///
|
||||
/// Creates an orthographic projection that maps world coordinates
|
||||
/// to normalized device coordinates [-1, 1].
|
||||
/// 创建将世界坐标映射到标准化设备坐标[-1, 1]的正交投影。
|
||||
///
|
||||
/// Coordinate system | 坐标系统:
|
||||
/// - World: Y-up, origin at camera position | 世界坐标:Y向上,原点在相机位置
|
||||
/// - Screen: Y-down, origin at top-left | 屏幕坐标:Y向下,原点在左上角
|
||||
/// - NDC: Y-up, origin at center [-1, 1] | NDC:Y向上,原点在中心
|
||||
/// - Rotation: positive = clockwise | 旋转:正 = 顺时针
|
||||
///
|
||||
/// When zoom=1, 1 world unit = 1 screen pixel.
|
||||
/// 当zoom=1时,1个世界单位 = 1个屏幕像素。
|
||||
pub fn projection_matrix(&self) -> Mat3 {
|
||||
// Standard orthographic projection
|
||||
// 标准正交投影
|
||||
// Maps world coordinates to NDC [-1, 1]
|
||||
// 将世界坐标映射到NDC [-1, 1]
|
||||
|
||||
// Scale factors: world units to NDC
|
||||
// 缩放因子:世界单位到NDC
|
||||
let sx = 2.0 / self.width * self.zoom;
|
||||
let sy = 2.0 / self.height * self.zoom;
|
||||
|
||||
// Handle rotation (clockwise positive)
|
||||
// 处理旋转(顺时针为正)
|
||||
let cos = self.rotation.cos();
|
||||
let sin = self.rotation.sin();
|
||||
|
||||
// Translation: camera position to NDC
|
||||
// 平移:相机位置到NDC
|
||||
// We negate position because moving camera right should move world left
|
||||
// 取反位置,因为相机向右移动应该使世界向左移动
|
||||
let tx = -self.position.x * sx;
|
||||
let ty = -self.position.y * sy;
|
||||
|
||||
// Combine scale, rotation, and translation
|
||||
// 组合缩放、旋转和平移
|
||||
// Matrix = Scale * Rotation * Translation (applied right to left)
|
||||
// 矩阵 = 缩放 * 旋转 * 平移(从右到左应用)
|
||||
// Clockwise rotation: [cos, -sin; sin, cos]
|
||||
// 顺时针旋转矩阵
|
||||
if self.rotation != 0.0 {
|
||||
// With rotation: need to rotate the translation as well (clockwise)
|
||||
// 有旋转时:平移也需要旋转(顺时针)
|
||||
let rtx = tx * cos + ty * sin;
|
||||
let rty = -tx * sin + ty * cos;
|
||||
|
||||
Mat3::from_cols(
|
||||
glam::Vec3::new(sx * cos, -sx * sin, 0.0),
|
||||
glam::Vec3::new(sy * sin, sy * cos, 0.0),
|
||||
glam::Vec3::new(rtx, rty, 1.0),
|
||||
)
|
||||
} else {
|
||||
// No rotation: simplified matrix
|
||||
// 无旋转:简化矩阵
|
||||
Mat3::from_cols(
|
||||
glam::Vec3::new(sx, 0.0, 0.0),
|
||||
glam::Vec3::new(0.0, sy, 0.0),
|
||||
glam::Vec3::new(tx, ty, 1.0),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert screen coordinates to world coordinates.
|
||||
/// 将屏幕坐标转换为世界坐标。
|
||||
///
|
||||
/// Screen: (0,0) at top-left, Y-down | 屏幕:(0,0)在左上角,Y向下
|
||||
/// World: Y-up, camera at center | 世界:Y向上,相机在中心
|
||||
/// Rotation: positive = clockwise | 旋转:正 = 顺时针
|
||||
pub fn screen_to_world(&self, screen: Vec2) -> Vec2 {
|
||||
// Convert screen to NDC-like coordinates (centered, Y-up)
|
||||
// 将屏幕坐标转换为类NDC坐标(居中,Y向上)
|
||||
let centered_x = screen.x - self.width / 2.0;
|
||||
let centered_y = self.height / 2.0 - screen.y; // Flip Y
|
||||
|
||||
// Apply inverse zoom and add camera position
|
||||
// 应用反向缩放并加上相机位置
|
||||
let world_x = centered_x / self.zoom + self.position.x;
|
||||
let world_y = centered_y / self.zoom + self.position.y;
|
||||
|
||||
if self.rotation != 0.0 {
|
||||
// Apply inverse rotation around camera position
|
||||
// 围绕相机位置应用反向旋转
|
||||
// Inverse of clockwise θ is clockwise -θ
|
||||
// 顺时针 θ 的逆变换是顺时针 -θ
|
||||
let dx = world_x - self.position.x;
|
||||
let dy = world_y - self.position.y;
|
||||
let cos = self.rotation.cos(); // cos(-θ) = cos(θ)
|
||||
let sin = self.rotation.sin(); // for clockwise -θ: use -sin(θ)
|
||||
|
||||
// Clockwise rotation with -θ: x' = x*cos + y*(-sin), y' = -x*(-sin) + y*cos
|
||||
// 用 -θ 做顺时针旋转
|
||||
Vec2::new(
|
||||
dx * cos - dy * sin + self.position.x,
|
||||
dx * sin + dy * cos + self.position.y,
|
||||
)
|
||||
} else {
|
||||
Vec2::new(world_x, world_y)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert world coordinates to screen coordinates.
|
||||
/// 将世界坐标转换为屏幕坐标。
|
||||
///
|
||||
/// World: Y-up | 世界:Y向上
|
||||
/// Screen: (0,0) at top-left, Y-down | 屏幕:(0,0)在左上角,Y向下
|
||||
/// Rotation: positive = clockwise | 旋转:正 = 顺时针
|
||||
pub fn world_to_screen(&self, world: Vec2) -> Vec2 {
|
||||
let dx = world.x - self.position.x;
|
||||
let dy = world.y - self.position.y;
|
||||
|
||||
// Apply clockwise rotation
|
||||
// 应用顺时针旋转
|
||||
let (rx, ry) = if self.rotation != 0.0 {
|
||||
let cos = self.rotation.cos();
|
||||
let sin = self.rotation.sin();
|
||||
// Clockwise: x' = x*cos + y*sin, y' = -x*sin + y*cos
|
||||
// 顺时针旋转公式
|
||||
(dx * cos + dy * sin, -dx * sin + dy * cos)
|
||||
} else {
|
||||
(dx, dy)
|
||||
};
|
||||
|
||||
// Apply zoom and convert to screen coordinates
|
||||
// 应用缩放并转换为屏幕坐标
|
||||
let screen_x = rx * self.zoom + self.width / 2.0;
|
||||
let screen_y = self.height / 2.0 - ry * self.zoom; // Flip Y
|
||||
|
||||
Vec2::new(screen_x, screen_y)
|
||||
}
|
||||
|
||||
/// Move camera by delta.
|
||||
/// 按增量移动相机。
|
||||
#[inline]
|
||||
pub fn translate(&mut self, delta: Vec2) {
|
||||
self.position = self.position + delta;
|
||||
}
|
||||
|
||||
/// Set zoom level with clamping.
|
||||
/// 设置缩放级别并限制范围。
|
||||
#[inline]
|
||||
pub fn set_zoom(&mut self, zoom: f32) {
|
||||
self.zoom = zoom.clamp(0.01, 100.0);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn viewport_width(&self) -> f32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn viewport_height(&self) -> f32 {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Camera2D {
|
||||
fn default() -> Self {
|
||||
Self::new(800.0, 600.0)
|
||||
}
|
||||
}
|
||||
414
packages/rust/engine/src/renderer/camera3d.rs
Normal file
414
packages/rust/engine/src/renderer/camera3d.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
348
packages/rust/engine/src/renderer/gizmo.rs
Normal file
348
packages/rust/engine/src/renderer/gizmo.rs
Normal file
@@ -0,0 +1,348 @@
|
||||
//! Gizmo renderer for editor overlays.
|
||||
|
||||
use es_engine_shared::{
|
||||
traits::backend::{GraphicsBackend, BufferUsage},
|
||||
types::{
|
||||
handle::{ShaderHandle, BufferHandle, VertexArrayHandle},
|
||||
vertex::{VertexLayout, VertexAttribute, VertexAttributeType},
|
||||
blend::BlendMode,
|
||||
},
|
||||
Vec4, Mat3,
|
||||
};
|
||||
use super::camera::Camera2D;
|
||||
use std::f32::consts::PI;
|
||||
|
||||
const VERTEX_SHADER: &str = r#"#version 300 es
|
||||
precision highp float;
|
||||
layout(location = 0) in vec2 a_position;
|
||||
uniform mat3 u_projection;
|
||||
void main() {
|
||||
vec3 pos = u_projection * vec3(a_position, 1.0);
|
||||
gl_Position = vec4(pos.xy, 0.0, 1.0);
|
||||
}
|
||||
"#;
|
||||
|
||||
const FRAGMENT_SHADER: &str = r#"#version 300 es
|
||||
precision highp float;
|
||||
uniform vec4 u_color;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = u_color;
|
||||
}
|
||||
"#;
|
||||
|
||||
const X_AXIS_COLOR: Vec4 = Vec4::new(1.0, 0.3, 0.3, 1.0);
|
||||
const Y_AXIS_COLOR: Vec4 = Vec4::new(0.3, 1.0, 0.3, 1.0);
|
||||
const ROTATE_COLOR: Vec4 = Vec4::new(0.3, 0.6, 1.0, 1.0);
|
||||
const SCALE_COLOR: Vec4 = Vec4::new(1.0, 0.8, 0.2, 1.0);
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
||||
pub enum TransformMode {
|
||||
#[default]
|
||||
Select,
|
||||
Move,
|
||||
Rotate,
|
||||
Scale,
|
||||
}
|
||||
|
||||
pub struct GizmoRenderer {
|
||||
shader: ShaderHandle,
|
||||
vbo: BufferHandle,
|
||||
vao: VertexArrayHandle,
|
||||
rects: Vec<RectGizmo>,
|
||||
circles: Vec<CircleGizmo>,
|
||||
lines: Vec<LineGizmo>,
|
||||
capsules: Vec<CapsuleGizmo>,
|
||||
transform_mode: TransformMode,
|
||||
}
|
||||
|
||||
struct RectGizmo {
|
||||
x: f32, y: f32, width: f32, height: f32,
|
||||
rotation: f32, origin_x: f32, origin_y: f32,
|
||||
color: Vec4, show_handles: bool,
|
||||
}
|
||||
|
||||
struct CircleGizmo {
|
||||
x: f32, y: f32, radius: f32, color: Vec4, segments: u32,
|
||||
}
|
||||
|
||||
struct LineGizmo {
|
||||
points: Vec<f32>, color: Vec4, closed: bool,
|
||||
}
|
||||
|
||||
struct CapsuleGizmo {
|
||||
x: f32, y: f32, radius: f32, half_height: f32, rotation: f32, color: Vec4,
|
||||
}
|
||||
|
||||
const MAX_GIZMO_VERTICES: usize = 4000;
|
||||
|
||||
impl GizmoRenderer {
|
||||
pub fn new(backend: &mut impl GraphicsBackend) -> Result<Self, String> {
|
||||
let shader = backend.compile_shader(VERTEX_SHADER, FRAGMENT_SHADER)
|
||||
.map_err(|e| format!("Gizmo shader: {:?}", e))?;
|
||||
|
||||
let layout = VertexLayout {
|
||||
attributes: vec![VertexAttribute {
|
||||
name: "a_position".into(),
|
||||
attr_type: VertexAttributeType::Float2,
|
||||
offset: 0,
|
||||
normalized: false,
|
||||
}],
|
||||
stride: 8,
|
||||
};
|
||||
|
||||
let buffer_size = MAX_GIZMO_VERTICES * 2 * 4;
|
||||
let vbo = backend.create_vertex_buffer_sized(buffer_size, BufferUsage::Dynamic)
|
||||
.map_err(|e| format!("Gizmo VBO: {:?}", e))?;
|
||||
let vao = backend.create_vertex_array(vbo, None, &layout)
|
||||
.map_err(|e| format!("Gizmo VAO: {:?}", e))?;
|
||||
|
||||
Ok(Self {
|
||||
shader, vbo, vao,
|
||||
rects: Vec::new(),
|
||||
circles: Vec::new(),
|
||||
lines: Vec::new(),
|
||||
capsules: Vec::new(),
|
||||
transform_mode: TransformMode::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.rects.clear();
|
||||
self.circles.clear();
|
||||
self.lines.clear();
|
||||
self.capsules.clear();
|
||||
}
|
||||
|
||||
pub fn add_rect(&mut self, x: f32, y: f32, width: f32, height: f32, rotation: f32,
|
||||
origin_x: f32, origin_y: f32, r: f32, g: f32, b: f32, a: f32, show_handles: bool) {
|
||||
self.rects.push(RectGizmo {
|
||||
x, y, width, height, rotation, origin_x, origin_y,
|
||||
color: Vec4::new(r, g, b, a), show_handles,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn add_circle(&mut self, x: f32, y: f32, radius: f32, r: f32, g: f32, b: f32, a: f32) {
|
||||
self.circles.push(CircleGizmo { x, y, radius, color: Vec4::new(r, g, b, a), segments: 32 });
|
||||
}
|
||||
|
||||
pub fn add_line(&mut self, points: Vec<f32>, r: f32, g: f32, b: f32, a: f32, closed: bool) {
|
||||
self.lines.push(LineGizmo { points, color: Vec4::new(r, g, b, a), closed });
|
||||
}
|
||||
|
||||
pub fn add_capsule(&mut self, x: f32, y: f32, radius: f32, half_height: f32, rotation: f32,
|
||||
r: f32, g: f32, b: f32, a: f32) {
|
||||
self.capsules.push(CapsuleGizmo { x, y, radius, half_height, rotation, color: Vec4::new(r, g, b, a) });
|
||||
}
|
||||
|
||||
pub fn set_transform_mode(&mut self, mode: TransformMode) {
|
||||
self.transform_mode = mode;
|
||||
}
|
||||
|
||||
pub fn get_transform_mode(&self) -> TransformMode {
|
||||
self.transform_mode
|
||||
}
|
||||
|
||||
pub fn render(&mut self, backend: &mut impl GraphicsBackend, camera: &Camera2D) {
|
||||
if self.rects.is_empty() && self.circles.is_empty() && self.lines.is_empty() && self.capsules.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
backend.bind_shader(self.shader).ok();
|
||||
backend.set_uniform_mat3("u_projection", &camera.projection_matrix()).ok();
|
||||
backend.set_blend_mode(BlendMode::Alpha);
|
||||
|
||||
self.render_rects(backend, camera);
|
||||
self.render_circles(backend);
|
||||
self.render_lines(backend);
|
||||
self.render_capsules(backend);
|
||||
}
|
||||
|
||||
pub fn render_axis_indicator(&mut self, backend: &mut impl GraphicsBackend, width: f32, height: f32) {
|
||||
if width < 100.0 || height < 100.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
backend.bind_shader(self.shader).ok();
|
||||
|
||||
let half_w = width / 2.0;
|
||||
let half_h = height / 2.0;
|
||||
let projection = Mat3::from_cols_array(&[
|
||||
1.0 / half_w, 0.0, 0.0,
|
||||
0.0, 1.0 / half_h, 0.0,
|
||||
0.0, 0.0, 1.0,
|
||||
]);
|
||||
backend.set_uniform_mat3("u_projection", &projection).ok();
|
||||
backend.set_blend_mode(BlendMode::Alpha);
|
||||
|
||||
let padding = 35.0;
|
||||
let cx = -half_w + padding;
|
||||
let cy = -half_h + padding;
|
||||
let axis_len = 25.0;
|
||||
let arrow = 6.0;
|
||||
let label_off = 8.0;
|
||||
let label_sz = 3.5;
|
||||
|
||||
// X axis
|
||||
let x_end = cx + axis_len;
|
||||
self.upload_and_draw_lines(backend, &[cx, cy, x_end - arrow * 0.3, cy], X_AXIS_COLOR);
|
||||
self.upload_and_draw(backend, &[x_end, cy, x_end - arrow, cy + arrow * 0.35, x_end - arrow, cy - arrow * 0.35], X_AXIS_COLOR);
|
||||
let lx = x_end + label_off;
|
||||
self.upload_and_draw_lines(backend, &[lx - label_sz, cy + label_sz, lx + label_sz, cy - label_sz,
|
||||
lx - label_sz, cy - label_sz, lx + label_sz, cy + label_sz], X_AXIS_COLOR);
|
||||
|
||||
// Y axis
|
||||
let y_end = cy + axis_len;
|
||||
self.upload_and_draw_lines(backend, &[cx, cy, cx, y_end - arrow * 0.3], Y_AXIS_COLOR);
|
||||
self.upload_and_draw(backend, &[cx, y_end, cx - arrow * 0.35, y_end - arrow, cx + arrow * 0.35, y_end - arrow], Y_AXIS_COLOR);
|
||||
let ly = y_end + label_off;
|
||||
self.upload_and_draw_lines(backend, &[cx - label_sz, ly + label_sz, cx, ly, cx + label_sz, ly + label_sz, cx, ly,
|
||||
cx, ly, cx, ly - label_sz * 0.8], Y_AXIS_COLOR);
|
||||
}
|
||||
|
||||
fn render_rects(&mut self, backend: &mut impl GraphicsBackend, camera: &Camera2D) {
|
||||
let rects: Vec<_> = std::mem::take(&mut self.rects);
|
||||
for rect in &rects {
|
||||
let verts = Self::calc_rect_vertices(rect.x, rect.y, rect.width, rect.height,
|
||||
rect.rotation, rect.origin_x, rect.origin_y);
|
||||
self.upload_and_draw_line_loop(backend, &verts, rect.color);
|
||||
|
||||
if rect.show_handles {
|
||||
match self.transform_mode {
|
||||
TransformMode::Select => {}
|
||||
TransformMode::Move => self.draw_move_handles(backend, rect.x, rect.y, rect.rotation, camera),
|
||||
TransformMode::Rotate => self.draw_rotate_handles(backend, rect.x, rect.y, rect.width.max(rect.height) * 0.6),
|
||||
TransformMode::Scale => self.draw_scale_handles(backend, &verts, camera),
|
||||
}
|
||||
}
|
||||
}
|
||||
self.rects = rects;
|
||||
}
|
||||
|
||||
fn render_circles(&mut self, backend: &mut impl GraphicsBackend) {
|
||||
let circles: Vec<_> = std::mem::take(&mut self.circles);
|
||||
for circle in &circles {
|
||||
let verts = Self::build_circle(circle.x, circle.y, circle.radius, circle.segments);
|
||||
self.upload_and_draw_line_loop(backend, &verts, circle.color);
|
||||
}
|
||||
self.circles = circles;
|
||||
}
|
||||
|
||||
fn render_lines(&mut self, backend: &mut impl GraphicsBackend) {
|
||||
let lines: Vec<_> = std::mem::take(&mut self.lines);
|
||||
for line in &lines {
|
||||
if line.points.len() < 4 { continue; }
|
||||
if line.closed {
|
||||
self.upload_and_draw_line_loop(backend, &line.points, line.color);
|
||||
} else {
|
||||
self.upload_and_draw_line_strip(backend, &line.points, line.color);
|
||||
}
|
||||
}
|
||||
self.lines = lines;
|
||||
}
|
||||
|
||||
fn render_capsules(&mut self, backend: &mut impl GraphicsBackend) {
|
||||
const SEGMENTS: usize = 16;
|
||||
let capsules: Vec<_> = std::mem::take(&mut self.capsules);
|
||||
for cap in &capsules {
|
||||
let (cos_r, sin_r) = (cap.rotation.cos(), cap.rotation.sin());
|
||||
let mut verts = Vec::with_capacity((SEGMENTS * 2 + 2) * 2);
|
||||
|
||||
for j in 0..=SEGMENTS {
|
||||
let angle = (j as f32 / SEGMENTS as f32) * PI;
|
||||
let (lx, ly) = (cap.radius * angle.cos(), cap.half_height + cap.radius * angle.sin());
|
||||
verts.push(cap.x + lx * cos_r - ly * sin_r);
|
||||
verts.push(cap.y + lx * sin_r + ly * cos_r);
|
||||
}
|
||||
for j in 0..=SEGMENTS {
|
||||
let angle = (j as f32 / SEGMENTS as f32) * PI;
|
||||
let (lx, ly) = (-cap.radius * angle.cos(), -cap.half_height - cap.radius * angle.sin());
|
||||
verts.push(cap.x + lx * cos_r - ly * sin_r);
|
||||
verts.push(cap.y + lx * sin_r + ly * cos_r);
|
||||
}
|
||||
self.upload_and_draw_line_loop(backend, &verts, cap.color);
|
||||
}
|
||||
self.capsules = capsules;
|
||||
}
|
||||
|
||||
fn draw_move_handles(&mut self, backend: &mut impl GraphicsBackend, x: f32, y: f32, rotation: f32, camera: &Camera2D) {
|
||||
let len = 50.0 / camera.zoom;
|
||||
let head = 10.0 / camera.zoom;
|
||||
let (cos, sin) = (rotation.cos(), rotation.sin());
|
||||
|
||||
let (xe, ye) = (x + len * cos, y + len * sin);
|
||||
let x_arrow = [x, y, xe, ye,
|
||||
xe - head * cos + head * 0.3 * sin, ye - head * sin - head * 0.3 * cos, xe, ye,
|
||||
xe - head * cos - head * 0.3 * sin, ye - head * sin + head * 0.3 * cos];
|
||||
self.upload_and_draw_line_strip(backend, &x_arrow, X_AXIS_COLOR);
|
||||
|
||||
let (xe2, ye2) = (x - len * sin, y + len * cos);
|
||||
let y_arrow = [x, y, xe2, ye2,
|
||||
xe2 + head * sin + head * 0.3 * cos, ye2 - head * cos + head * 0.3 * sin, xe2, ye2,
|
||||
xe2 + head * sin - head * 0.3 * cos, ye2 - head * cos - head * 0.3 * sin];
|
||||
self.upload_and_draw_line_strip(backend, &y_arrow, Y_AXIS_COLOR);
|
||||
}
|
||||
|
||||
fn draw_rotate_handles(&mut self, backend: &mut impl GraphicsBackend, x: f32, y: f32, radius: f32) {
|
||||
let verts = Self::build_circle(x, y, radius, 32);
|
||||
self.upload_and_draw_line_loop(backend, &verts, ROTATE_COLOR);
|
||||
}
|
||||
|
||||
fn draw_scale_handles(&mut self, backend: &mut impl GraphicsBackend, corners: &[f32], camera: &Camera2D) {
|
||||
let sz = 6.0 / camera.zoom;
|
||||
for i in 0..4 {
|
||||
let (cx, cy) = (corners[i * 2], corners[i * 2 + 1]);
|
||||
let sq = [cx - sz, cy - sz, cx + sz, cy - sz, cx + sz, cy + sz, cx - sz, cy + sz];
|
||||
self.upload_and_draw_line_loop(backend, &sq, SCALE_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_and_draw(&mut self, backend: &mut impl GraphicsBackend, verts: &[f32], color: Vec4) {
|
||||
backend.update_buffer(self.vbo, 0, bytemuck::cast_slice(verts)).ok();
|
||||
backend.set_uniform_vec4("u_color", color).ok();
|
||||
backend.draw(self.vao, (verts.len() / 2) as u32, 0).ok();
|
||||
}
|
||||
|
||||
fn upload_and_draw_lines(&mut self, backend: &mut impl GraphicsBackend, verts: &[f32], color: Vec4) {
|
||||
backend.update_buffer(self.vbo, 0, bytemuck::cast_slice(verts)).ok();
|
||||
backend.set_uniform_vec4("u_color", color).ok();
|
||||
backend.draw_lines(self.vao, (verts.len() / 2) as u32, 0).ok();
|
||||
}
|
||||
|
||||
fn upload_and_draw_line_loop(&mut self, backend: &mut impl GraphicsBackend, verts: &[f32], color: Vec4) {
|
||||
backend.update_buffer(self.vbo, 0, bytemuck::cast_slice(verts)).ok();
|
||||
backend.set_uniform_vec4("u_color", color).ok();
|
||||
backend.draw_line_loop(self.vao, (verts.len() / 2) as u32, 0).ok();
|
||||
}
|
||||
|
||||
fn upload_and_draw_line_strip(&mut self, backend: &mut impl GraphicsBackend, verts: &[f32], color: Vec4) {
|
||||
backend.update_buffer(self.vbo, 0, bytemuck::cast_slice(verts)).ok();
|
||||
backend.set_uniform_vec4("u_color", color).ok();
|
||||
backend.draw_line_strip(self.vao, (verts.len() / 2) as u32, 0).ok();
|
||||
}
|
||||
|
||||
fn calc_rect_vertices(x: f32, y: f32, w: f32, h: f32, rot: f32, ox: f32, oy: f32) -> [f32; 8] {
|
||||
let (cos, sin) = (rot.cos(), rot.sin());
|
||||
let (oxx, oyy) = (ox * w, oy * h);
|
||||
let corners = [(-oxx, h - oyy), (w - oxx, h - oyy), (w - oxx, -oyy), (-oxx, -oyy)];
|
||||
let mut out = [0.0f32; 8];
|
||||
for (i, (lx, ly)) in corners.iter().enumerate() {
|
||||
out[i * 2] = lx * cos - ly * sin + x;
|
||||
out[i * 2 + 1] = lx * sin + ly * cos + y;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn build_circle(x: f32, y: f32, r: f32, segments: u32) -> Vec<f32> {
|
||||
(0..segments).flat_map(|i| {
|
||||
let angle = (i as f32 / segments as f32) * PI * 2.0;
|
||||
[x + r * angle.cos(), y + r * angle.sin()]
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn destroy(self, backend: &mut impl GraphicsBackend) {
|
||||
backend.destroy_vertex_array(self.vao);
|
||||
backend.destroy_buffer(self.vbo);
|
||||
backend.destroy_shader(self.shader);
|
||||
}
|
||||
}
|
||||
577
packages/rust/engine/src/renderer/gizmo3d.rs
Normal file
577
packages/rust/engine/src/renderer/gizmo3d.rs
Normal 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(¢er_verts),
|
||||
BufferUsage::Static,
|
||||
).map_err(|e| format!("Center VBO: {:?}", e))?;
|
||||
let center_vao = backend.create_vertex_array(center_vbo, None, &layout)
|
||||
.map_err(|e| format!("Center VAO: {:?}", e))?;
|
||||
|
||||
// Plane handles
|
||||
let plane_verts = Self::generate_plane_handles(gizmo_size * 0.3);
|
||||
let plane_vertex_count = (plane_verts.len() / 7) as u32;
|
||||
let plane_vbo = backend.create_vertex_buffer(
|
||||
bytemuck::cast_slice(&plane_verts),
|
||||
BufferUsage::Static,
|
||||
).map_err(|e| format!("Plane VBO: {:?}", e))?;
|
||||
let plane_vao = backend.create_vertex_array(plane_vbo, None, &layout)
|
||||
.map_err(|e| format!("Plane VAO: {:?}", e))?;
|
||||
|
||||
Ok(Self {
|
||||
shader,
|
||||
translate_vbo,
|
||||
translate_vao,
|
||||
translate_line_count,
|
||||
translate_arrow_vbo,
|
||||
translate_arrow_vao,
|
||||
translate_arrow_count,
|
||||
rotate_vbo,
|
||||
rotate_vao,
|
||||
rotate_vertex_count,
|
||||
scale_vbo,
|
||||
scale_vao,
|
||||
scale_line_count,
|
||||
scale_cube_vbo,
|
||||
scale_cube_vao,
|
||||
scale_cube_count,
|
||||
center_vbo,
|
||||
center_vao,
|
||||
center_vertex_count,
|
||||
plane_vbo,
|
||||
plane_vao,
|
||||
plane_vertex_count,
|
||||
transform_mode: TransformMode::Move,
|
||||
gizmo_size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate XYZ axis lines.
|
||||
fn generate_axis_lines(length: f32) -> Vec<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);
|
||||
}
|
||||
}
|
||||
210
packages/rust/engine/src/renderer/grid.rs
Normal file
210
packages/rust/engine/src/renderer/grid.rs
Normal file
@@ -0,0 +1,210 @@
|
||||
//! Grid renderer for editor viewport.
|
||||
|
||||
use es_engine_shared::{
|
||||
traits::backend::{GraphicsBackend, BufferUsage},
|
||||
types::{
|
||||
handle::{ShaderHandle, BufferHandle, VertexArrayHandle},
|
||||
vertex::{VertexLayout, VertexAttribute, VertexAttributeType},
|
||||
blend::BlendMode,
|
||||
},
|
||||
Vec4,
|
||||
};
|
||||
use super::camera::Camera2D;
|
||||
|
||||
const VERTEX_SHADER: &str = r#"#version 300 es
|
||||
precision highp float;
|
||||
layout(location = 0) in vec2 a_position;
|
||||
uniform mat3 u_projection;
|
||||
void main() {
|
||||
vec3 pos = u_projection * vec3(a_position, 1.0);
|
||||
gl_Position = vec4(pos.xy, 0.0, 1.0);
|
||||
}
|
||||
"#;
|
||||
|
||||
const FRAGMENT_SHADER: &str = r#"#version 300 es
|
||||
precision highp float;
|
||||
uniform vec4 u_color;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = u_color;
|
||||
}
|
||||
"#;
|
||||
|
||||
const GRID_COLOR: Vec4 = Vec4::new(0.3, 0.3, 0.35, 1.0);
|
||||
const X_AXIS_COLOR: Vec4 = Vec4::new(1.0, 0.3, 0.3, 1.0);
|
||||
const Y_AXIS_COLOR: Vec4 = Vec4::new(0.3, 1.0, 0.3, 1.0);
|
||||
|
||||
pub struct GridRenderer {
|
||||
shader: ShaderHandle,
|
||||
grid_vbo: BufferHandle,
|
||||
grid_vao: VertexArrayHandle,
|
||||
axis_vbo: BufferHandle,
|
||||
axis_vao: VertexArrayHandle,
|
||||
grid_vertex_count: u32,
|
||||
cache: GridCache,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GridCache {
|
||||
zoom: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
}
|
||||
|
||||
impl GridCache {
|
||||
fn is_dirty(&self, camera: &Camera2D) -> bool {
|
||||
(camera.zoom - self.zoom).abs() > 0.001
|
||||
|| (camera.viewport_width() - self.width).abs() > 1.0
|
||||
|| (camera.viewport_height() - self.height).abs() > 1.0
|
||||
}
|
||||
|
||||
fn update(&mut self, camera: &Camera2D) {
|
||||
self.zoom = camera.zoom;
|
||||
self.width = camera.viewport_width();
|
||||
self.height = camera.viewport_height();
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_GRID_VERTICES: usize = 8000;
|
||||
|
||||
impl GridRenderer {
|
||||
pub fn new(backend: &mut impl GraphicsBackend) -> Result<Self, String> {
|
||||
let shader = backend.compile_shader(VERTEX_SHADER, FRAGMENT_SHADER)
|
||||
.map_err(|e| format!("Grid shader: {:?}", e))?;
|
||||
|
||||
let layout = VertexLayout {
|
||||
attributes: vec![
|
||||
VertexAttribute {
|
||||
name: "a_position".into(),
|
||||
attr_type: VertexAttributeType::Float2,
|
||||
offset: 0,
|
||||
normalized: false,
|
||||
},
|
||||
],
|
||||
stride: 8,
|
||||
};
|
||||
|
||||
let grid_buffer_size = MAX_GRID_VERTICES * 2 * 4;
|
||||
let grid_vbo = backend.create_vertex_buffer_sized(grid_buffer_size, BufferUsage::Dynamic)
|
||||
.map_err(|e| format!("Grid VBO: {:?}", e))?;
|
||||
let grid_vao = backend.create_vertex_array(grid_vbo, None, &layout)
|
||||
.map_err(|e| format!("Grid VAO: {:?}", e))?;
|
||||
|
||||
let axis_data = Self::build_axis_vertices(1000.0);
|
||||
let axis_vbo = backend.create_vertex_buffer(
|
||||
bytemuck::cast_slice(&axis_data),
|
||||
BufferUsage::Dynamic,
|
||||
).map_err(|e| format!("Axis VBO: {:?}", e))?;
|
||||
let axis_vao = backend.create_vertex_array(axis_vbo, None, &layout)
|
||||
.map_err(|e| format!("Axis VAO: {:?}", e))?;
|
||||
|
||||
Ok(Self {
|
||||
shader,
|
||||
grid_vbo,
|
||||
grid_vao,
|
||||
axis_vbo,
|
||||
axis_vao,
|
||||
grid_vertex_count: 0,
|
||||
cache: GridCache::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render(&mut self, backend: &mut impl GraphicsBackend, camera: &Camera2D) {
|
||||
self.update_grid_if_needed(backend, camera);
|
||||
|
||||
if self.grid_vertex_count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
backend.bind_shader(self.shader).ok();
|
||||
backend.set_uniform_mat3("u_projection", &camera.projection_matrix()).ok();
|
||||
backend.set_uniform_vec4("u_color", GRID_COLOR).ok();
|
||||
backend.set_blend_mode(BlendMode::Alpha);
|
||||
backend.draw_lines(self.grid_vao, self.grid_vertex_count, 0).ok();
|
||||
}
|
||||
|
||||
pub fn render_axes(&mut self, backend: &mut impl GraphicsBackend, camera: &Camera2D) {
|
||||
let axis_length = self.calculate_axis_length(camera);
|
||||
self.update_axis_buffer(backend, axis_length);
|
||||
|
||||
backend.bind_shader(self.shader).ok();
|
||||
backend.set_uniform_mat3("u_projection", &camera.projection_matrix()).ok();
|
||||
backend.set_blend_mode(BlendMode::Alpha);
|
||||
|
||||
backend.set_uniform_vec4("u_color", X_AXIS_COLOR).ok();
|
||||
backend.draw_lines(self.axis_vao, 2, 0).ok();
|
||||
|
||||
backend.set_uniform_vec4("u_color", Y_AXIS_COLOR).ok();
|
||||
backend.draw_lines(self.axis_vao, 2, 2).ok();
|
||||
}
|
||||
|
||||
fn update_grid_if_needed(&mut self, backend: &mut impl GraphicsBackend, camera: &Camera2D) {
|
||||
if !self.cache.is_dirty(camera) {
|
||||
return;
|
||||
}
|
||||
self.cache.update(camera);
|
||||
|
||||
let vertices = self.build_grid_vertices(camera);
|
||||
self.grid_vertex_count = (vertices.len() / 2) as u32;
|
||||
|
||||
backend.update_buffer(self.grid_vbo, 0, bytemuck::cast_slice(&vertices)).ok();
|
||||
}
|
||||
|
||||
fn build_grid_vertices(&self, camera: &Camera2D) -> Vec<f32> {
|
||||
let half_w = camera.viewport_width() / (2.0 * camera.zoom);
|
||||
let half_h = camera.viewport_height() / (2.0 * camera.zoom);
|
||||
let max_size = half_w.max(half_h) * 2.0;
|
||||
|
||||
let step = Self::calculate_grid_step(max_size);
|
||||
let range = max_size * 1.5;
|
||||
|
||||
let mut vertices = Vec::new();
|
||||
let start = (-range / step).floor() * step;
|
||||
let end = (range / step).ceil() * step;
|
||||
|
||||
let mut pos = start;
|
||||
while pos <= end {
|
||||
vertices.extend_from_slice(&[pos, -range, pos, range]);
|
||||
vertices.extend_from_slice(&[-range, pos, range, pos]);
|
||||
pos += step;
|
||||
}
|
||||
|
||||
vertices
|
||||
}
|
||||
|
||||
fn calculate_grid_step(max_size: f32) -> f32 {
|
||||
match max_size {
|
||||
s if s > 10000.0 => 1000.0,
|
||||
s if s > 1000.0 => 100.0,
|
||||
s if s > 100.0 => 10.0,
|
||||
s if s > 10.0 => 1.0,
|
||||
_ => 0.1,
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_axis_length(&self, camera: &Camera2D) -> f32 {
|
||||
let half_w = camera.viewport_width() / (2.0 * camera.zoom);
|
||||
let half_h = camera.viewport_height() / (2.0 * camera.zoom);
|
||||
half_w.max(half_h) * 2.0
|
||||
}
|
||||
|
||||
fn build_axis_vertices(length: f32) -> Vec<f32> {
|
||||
vec![
|
||||
-length, 0.0, length, 0.0,
|
||||
0.0, -length, 0.0, length,
|
||||
]
|
||||
}
|
||||
|
||||
fn update_axis_buffer(&mut self, backend: &mut impl GraphicsBackend, length: f32) {
|
||||
let data = Self::build_axis_vertices(length);
|
||||
backend.update_buffer(self.axis_vbo, 0, bytemuck::cast_slice(&data)).ok();
|
||||
}
|
||||
|
||||
pub fn destroy(self, backend: &mut impl GraphicsBackend) {
|
||||
backend.destroy_vertex_array(self.grid_vao);
|
||||
backend.destroy_vertex_array(self.axis_vao);
|
||||
backend.destroy_buffer(self.grid_vbo);
|
||||
backend.destroy_buffer(self.axis_vbo);
|
||||
backend.destroy_shader(self.shader);
|
||||
}
|
||||
}
|
||||
450
packages/rust/engine/src/renderer/grid3d.rs
Normal file
450
packages/rust/engine/src/renderer/grid3d.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
207
packages/rust/engine/src/renderer/material/manager.rs
Normal file
207
packages/rust/engine/src/renderer/material/manager.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
//! Material manager for storing and retrieving materials.
|
||||
//! 材质管理器,用于存储和检索材质。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use web_sys::WebGl2RenderingContext;
|
||||
|
||||
use super::material::{Material, BlendMode};
|
||||
|
||||
/// Reserved material IDs for built-in materials.
|
||||
/// 内置材质的保留ID。
|
||||
pub const MATERIAL_ID_DEFAULT: u32 = 0;
|
||||
pub const MATERIAL_ID_ADDITIVE: u32 = 1;
|
||||
pub const MATERIAL_ID_MULTIPLY: u32 = 2;
|
||||
pub const MATERIAL_ID_UNLIT: u32 = 3;
|
||||
|
||||
/// Material manager for creating and caching materials.
|
||||
/// 材质管理器,用于创建和缓存材质。
|
||||
pub struct MaterialManager {
|
||||
/// Stored materials indexed by ID.
|
||||
/// 按ID索引的存储材质。
|
||||
materials: HashMap<u32, Material>,
|
||||
|
||||
/// Next available material ID for custom materials.
|
||||
/// 下一个可用的自定义材质ID。
|
||||
next_material_id: u32,
|
||||
}
|
||||
|
||||
impl MaterialManager {
|
||||
/// Create a new material manager with built-in materials.
|
||||
/// 创建带有内置材质的新材质管理器。
|
||||
pub fn new() -> Self {
|
||||
let mut manager = Self {
|
||||
materials: HashMap::new(),
|
||||
next_material_id: 100, // Reserve 0-99 for built-in materials
|
||||
};
|
||||
|
||||
// Register built-in materials | 注册内置材质
|
||||
manager.materials.insert(MATERIAL_ID_DEFAULT, Material::sprite());
|
||||
manager.materials.insert(MATERIAL_ID_ADDITIVE, Material::additive());
|
||||
manager.materials.insert(MATERIAL_ID_MULTIPLY, Material::multiply());
|
||||
manager.materials.insert(MATERIAL_ID_UNLIT, Material::unlit());
|
||||
|
||||
log::info!("MaterialManager initialized with {} built-in materials | 材质管理器初始化完成,内置材质数量: {}",
|
||||
manager.materials.len(), manager.materials.len());
|
||||
|
||||
manager
|
||||
}
|
||||
|
||||
/// Register a custom material.
|
||||
/// 注册自定义材质。
|
||||
///
|
||||
/// # Returns | 返回
|
||||
/// The material ID for referencing this material | 用于引用此材质的ID
|
||||
pub fn register_material(&mut self, material: Material) -> u32 {
|
||||
let material_id = self.next_material_id;
|
||||
self.next_material_id += 1;
|
||||
|
||||
log::debug!("Registered material '{}' with ID: {} | 注册材质 '{}' ID: {}",
|
||||
material.name, material_id, material.name, material_id);
|
||||
|
||||
self.materials.insert(material_id, material);
|
||||
material_id
|
||||
}
|
||||
|
||||
/// Register a material with a specific ID.
|
||||
/// 使用特定ID注册材质。
|
||||
pub fn register_material_with_id(&mut self, material_id: u32, material: Material) {
|
||||
log::debug!("Registered material '{}' with ID: {} | 注册材质 '{}' ID: {}",
|
||||
material.name, material_id, material.name, material_id);
|
||||
|
||||
self.materials.insert(material_id, material);
|
||||
|
||||
// Update next_material_id if necessary
|
||||
if material_id >= self.next_material_id {
|
||||
self.next_material_id = material_id + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a material by ID.
|
||||
/// 按ID获取材质。
|
||||
#[inline]
|
||||
pub fn get_material(&self, material_id: u32) -> Option<&Material> {
|
||||
self.materials.get(&material_id)
|
||||
}
|
||||
|
||||
/// Get a mutable material by ID.
|
||||
/// 按ID获取可变材质。
|
||||
#[inline]
|
||||
pub fn get_material_mut(&mut self, material_id: u32) -> Option<&mut Material> {
|
||||
self.materials.get_mut(&material_id)
|
||||
}
|
||||
|
||||
/// Get the default material.
|
||||
/// 获取默认材质。
|
||||
#[inline]
|
||||
pub fn get_default_material(&self) -> &Material {
|
||||
self.materials.get(&MATERIAL_ID_DEFAULT)
|
||||
.expect("Default material should always exist | 默认材质应该始终存在")
|
||||
}
|
||||
|
||||
/// Check if a material exists.
|
||||
/// 检查材质是否存在。
|
||||
#[inline]
|
||||
pub fn has_material(&self, material_id: u32) -> bool {
|
||||
self.materials.contains_key(&material_id)
|
||||
}
|
||||
|
||||
/// Remove a material.
|
||||
/// 移除材质。
|
||||
///
|
||||
/// Note: Cannot remove built-in materials (ID < 100).
|
||||
/// 注意:无法移除内置材质(ID < 100)。
|
||||
pub fn remove_material(&mut self, material_id: u32) -> bool {
|
||||
if material_id < 100 {
|
||||
log::warn!("Cannot remove built-in material: {} | 无法移除内置材质: {}", material_id, material_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
self.materials.remove(&material_id).is_some()
|
||||
}
|
||||
|
||||
/// Update a material's uniform value.
|
||||
/// 更新材质的uniform值。
|
||||
pub fn set_material_float(&mut self, material_id: u32, name: &str, value: f32) -> bool {
|
||||
if let Some(material) = self.materials.get_mut(&material_id) {
|
||||
material.uniforms.set_float(name, value);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a material's vec4 uniform.
|
||||
/// 更新材质的vec4 uniform。
|
||||
pub fn set_material_vec4(&mut self, material_id: u32, name: &str, x: f32, y: f32, z: f32, w: f32) -> bool {
|
||||
if let Some(material) = self.materials.get_mut(&material_id) {
|
||||
material.uniforms.set_vec4(name, x, y, z, w);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply blend mode to WebGL context.
|
||||
/// 将混合模式应用到WebGL上下文。
|
||||
pub fn apply_blend_mode(gl: &WebGl2RenderingContext, blend_mode: BlendMode) {
|
||||
match blend_mode {
|
||||
BlendMode::None => {
|
||||
gl.disable(WebGl2RenderingContext::BLEND);
|
||||
}
|
||||
BlendMode::Alpha => {
|
||||
gl.enable(WebGl2RenderingContext::BLEND);
|
||||
gl.blend_func(
|
||||
WebGl2RenderingContext::SRC_ALPHA,
|
||||
WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA,
|
||||
);
|
||||
}
|
||||
BlendMode::Additive => {
|
||||
gl.enable(WebGl2RenderingContext::BLEND);
|
||||
gl.blend_func(
|
||||
WebGl2RenderingContext::SRC_ALPHA,
|
||||
WebGl2RenderingContext::ONE,
|
||||
);
|
||||
}
|
||||
BlendMode::Multiply => {
|
||||
gl.enable(WebGl2RenderingContext::BLEND);
|
||||
gl.blend_func(
|
||||
WebGl2RenderingContext::DST_COLOR,
|
||||
WebGl2RenderingContext::ZERO,
|
||||
);
|
||||
}
|
||||
BlendMode::Screen => {
|
||||
gl.enable(WebGl2RenderingContext::BLEND);
|
||||
gl.blend_func(
|
||||
WebGl2RenderingContext::ONE,
|
||||
WebGl2RenderingContext::ONE_MINUS_SRC_COLOR,
|
||||
);
|
||||
}
|
||||
BlendMode::PremultipliedAlpha => {
|
||||
gl.enable(WebGl2RenderingContext::BLEND);
|
||||
gl.blend_func(
|
||||
WebGl2RenderingContext::ONE,
|
||||
WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all material IDs.
|
||||
/// 获取所有材质ID。
|
||||
pub fn material_ids(&self) -> Vec<u32> {
|
||||
self.materials.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// Get material count.
|
||||
/// 获取材质数量。
|
||||
#[inline]
|
||||
pub fn material_count(&self) -> usize {
|
||||
self.materials.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MaterialManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
177
packages/rust/engine/src/renderer/material/material.rs
Normal file
177
packages/rust/engine/src/renderer/material/material.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
//! Material definition and properties.
|
||||
//! 材质定义和属性。
|
||||
|
||||
use super::uniform::MaterialUniforms;
|
||||
|
||||
/// Blend modes for material rendering.
|
||||
/// 材质渲染的混合模式。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum BlendMode {
|
||||
/// No blending, fully opaque | 无混合,完全不透明
|
||||
None,
|
||||
/// Standard alpha blending | 标准透明度混合
|
||||
#[default]
|
||||
Alpha,
|
||||
/// Additive blending (good for glow effects) | 加法混合(适用于发光效果)
|
||||
Additive,
|
||||
/// Multiplicative blending (good for shadows) | 乘法混合(适用于阴影)
|
||||
Multiply,
|
||||
/// Screen blending (opposite of multiply) | 滤色混合(与乘法相反)
|
||||
Screen,
|
||||
/// Premultiplied alpha | 预乘透明度
|
||||
PremultipliedAlpha,
|
||||
}
|
||||
|
||||
/// Cull modes for material rendering.
|
||||
/// 材质渲染的剔除模式。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum CullMode {
|
||||
/// No face culling | 不剔除
|
||||
#[default]
|
||||
None,
|
||||
/// Cull front faces | 剔除正面
|
||||
Front,
|
||||
/// Cull back faces | 剔除背面
|
||||
Back,
|
||||
}
|
||||
|
||||
/// Material definition for 2D rendering.
|
||||
/// 2D渲染的材质定义。
|
||||
///
|
||||
/// A material combines a shader program with uniform parameters and render states.
|
||||
/// 材质将着色器程序与uniform参数和渲染状态组合在一起。
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Material {
|
||||
/// Shader program ID | 着色器程序ID
|
||||
pub shader_id: u32,
|
||||
|
||||
/// Material uniform parameters | 材质uniform参数
|
||||
pub uniforms: MaterialUniforms,
|
||||
|
||||
/// Blend mode | 混合模式
|
||||
pub blend_mode: BlendMode,
|
||||
|
||||
/// Cull mode | 剔除模式
|
||||
pub cull_mode: CullMode,
|
||||
|
||||
/// Depth test enabled | 是否启用深度测试
|
||||
pub depth_test: bool,
|
||||
|
||||
/// Depth write enabled | 是否启用深度写入
|
||||
pub depth_write: bool,
|
||||
|
||||
/// Material name (for debugging) | 材质名称(用于调试)
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Default for Material {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
shader_id: 0, // Default sprite shader
|
||||
uniforms: MaterialUniforms::new(),
|
||||
blend_mode: BlendMode::Alpha,
|
||||
cull_mode: CullMode::None,
|
||||
depth_test: false,
|
||||
depth_write: false,
|
||||
name: "Default".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Material {
|
||||
/// Create a new material with default settings.
|
||||
/// 使用默认设置创建新材质。
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a material with a specific shader.
|
||||
/// 使用特定着色器创建材质。
|
||||
pub fn with_shader(name: &str, shader_id: u32) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
shader_id,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the blend mode.
|
||||
/// 设置混合模式。
|
||||
pub fn set_blend_mode(&mut self, mode: BlendMode) -> &mut Self {
|
||||
self.blend_mode = mode;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a float uniform.
|
||||
/// 设置浮点uniform。
|
||||
pub fn set_float(&mut self, name: &str, value: f32) -> &mut Self {
|
||||
self.uniforms.set_float(name, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a vec2 uniform.
|
||||
/// 设置vec2 uniform。
|
||||
pub fn set_vec2(&mut self, name: &str, x: f32, y: f32) -> &mut Self {
|
||||
self.uniforms.set_vec2(name, x, y);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a vec3 uniform.
|
||||
/// 设置vec3 uniform。
|
||||
pub fn set_vec3(&mut self, name: &str, x: f32, y: f32, z: f32) -> &mut Self {
|
||||
self.uniforms.set_vec3(name, x, y, z);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a vec4 uniform.
|
||||
/// 设置vec4 uniform。
|
||||
pub fn set_vec4(&mut self, name: &str, x: f32, y: f32, z: f32, w: f32) -> &mut Self {
|
||||
self.uniforms.set_vec4(name, x, y, z, w);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a color uniform (RGBA, 0.0-1.0).
|
||||
/// 设置颜色uniform(RGBA,0.0-1.0)。
|
||||
pub fn set_color(&mut self, name: &str, r: f32, g: f32, b: f32, a: f32) -> &mut Self {
|
||||
self.uniforms.set_color(name, r, g, b, a);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// ============= Built-in material presets =============
|
||||
// ============= 内置材质预设 =============
|
||||
|
||||
impl Material {
|
||||
/// Create a standard sprite material.
|
||||
/// 创建标准精灵材质。
|
||||
pub fn sprite() -> Self {
|
||||
Self::new("Sprite")
|
||||
}
|
||||
|
||||
/// Create an additive (glow) material.
|
||||
/// 创建加法(发光)材质。
|
||||
pub fn additive() -> Self {
|
||||
let mut mat = Self::new("Additive");
|
||||
mat.blend_mode = BlendMode::Additive;
|
||||
mat
|
||||
}
|
||||
|
||||
/// Create a multiply (shadow) material.
|
||||
/// 创建乘法(阴影)材质。
|
||||
pub fn multiply() -> Self {
|
||||
let mut mat = Self::new("Multiply");
|
||||
mat.blend_mode = BlendMode::Multiply;
|
||||
mat
|
||||
}
|
||||
|
||||
/// Create an unlit/opaque material.
|
||||
/// 创建无光照/不透明材质。
|
||||
pub fn unlit() -> Self {
|
||||
let mut mat = Self::new("Unlit");
|
||||
mat.blend_mode = BlendMode::None;
|
||||
mat
|
||||
}
|
||||
}
|
||||
10
packages/rust/engine/src/renderer/material/mod.rs
Normal file
10
packages/rust/engine/src/renderer/material/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! Material system for 2D rendering.
|
||||
//! 2D渲染的材质系统。
|
||||
|
||||
mod material;
|
||||
mod manager;
|
||||
mod uniform;
|
||||
|
||||
pub use material::{Material, BlendMode, CullMode};
|
||||
pub use manager::MaterialManager;
|
||||
pub use uniform::{UniformValue, MaterialUniforms};
|
||||
185
packages/rust/engine/src/renderer/material/uniform.rs
Normal file
185
packages/rust/engine/src/renderer/material/uniform.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
//! Material uniform values and types.
|
||||
//! 材质uniform值和类型。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use web_sys::{WebGl2RenderingContext, WebGlUniformLocation};
|
||||
|
||||
use crate::renderer::shader::ShaderProgram;
|
||||
|
||||
/// Uniform value types supported by the material system.
|
||||
/// 材质系统支持的uniform值类型。
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum UniformValue {
|
||||
/// Single float value | 单精度浮点值
|
||||
Float(f32),
|
||||
/// Two component vector | 二维向量
|
||||
Vec2([f32; 2]),
|
||||
/// Three component vector | 三维向量
|
||||
Vec3([f32; 3]),
|
||||
/// Four component vector (also used for colors) | 四维向量(也用于颜色)
|
||||
Vec4([f32; 4]),
|
||||
/// Single integer value | 整数值
|
||||
Int(i32),
|
||||
/// 3x3 matrix | 3x3矩阵
|
||||
Mat3([f32; 9]),
|
||||
/// 4x4 matrix | 4x4矩阵
|
||||
Mat4([f32; 16]),
|
||||
/// Texture sampler slot | 纹理采样器槽位
|
||||
Sampler(i32),
|
||||
}
|
||||
|
||||
impl UniformValue {
|
||||
/// Apply this uniform value to a shader.
|
||||
/// 将此uniform值应用到着色器。
|
||||
pub fn apply(&self, gl: &WebGl2RenderingContext, location: &WebGlUniformLocation) {
|
||||
match self {
|
||||
UniformValue::Float(v) => {
|
||||
gl.uniform1f(Some(location), *v);
|
||||
}
|
||||
UniformValue::Vec2(v) => {
|
||||
gl.uniform2f(Some(location), v[0], v[1]);
|
||||
}
|
||||
UniformValue::Vec3(v) => {
|
||||
gl.uniform3f(Some(location), v[0], v[1], v[2]);
|
||||
}
|
||||
UniformValue::Vec4(v) => {
|
||||
gl.uniform4f(Some(location), v[0], v[1], v[2], v[3]);
|
||||
}
|
||||
UniformValue::Int(v) => {
|
||||
gl.uniform1i(Some(location), *v);
|
||||
}
|
||||
UniformValue::Mat3(v) => {
|
||||
gl.uniform_matrix3fv_with_f32_array(Some(location), false, v);
|
||||
}
|
||||
UniformValue::Mat4(v) => {
|
||||
gl.uniform_matrix4fv_with_f32_array(Some(location), false, v);
|
||||
}
|
||||
UniformValue::Sampler(slot) => {
|
||||
gl.uniform1i(Some(location), *slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collection of material uniform values.
|
||||
/// 材质uniform值集合。
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MaterialUniforms {
|
||||
/// Named uniform values | 命名的uniform值
|
||||
values: HashMap<String, UniformValue>,
|
||||
}
|
||||
|
||||
impl MaterialUniforms {
|
||||
/// Create empty uniforms collection.
|
||||
/// 创建空的uniform集合。
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
values: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a uniform value.
|
||||
/// 设置uniform值。
|
||||
pub fn set(&mut self, name: &str, value: UniformValue) {
|
||||
self.values.insert(name.to_string(), value);
|
||||
}
|
||||
|
||||
/// Get a uniform value.
|
||||
/// 获取uniform值。
|
||||
pub fn get(&self, name: &str) -> Option<&UniformValue> {
|
||||
self.values.get(name)
|
||||
}
|
||||
|
||||
/// Remove a uniform value.
|
||||
/// 移除uniform值。
|
||||
pub fn remove(&mut self, name: &str) -> Option<UniformValue> {
|
||||
self.values.remove(name)
|
||||
}
|
||||
|
||||
/// Check if a uniform exists.
|
||||
/// 检查uniform是否存在。
|
||||
pub fn has(&self, name: &str) -> bool {
|
||||
self.values.contains_key(name)
|
||||
}
|
||||
|
||||
/// Apply all uniforms to a shader program.
|
||||
/// 将所有uniform应用到着色器程序。
|
||||
pub fn apply_to_shader(&self, gl: &WebGl2RenderingContext, shader: &ShaderProgram) {
|
||||
for (name, value) in &self.values {
|
||||
if let Some(location) = shader.get_uniform_location(gl, name) {
|
||||
value.apply(gl, &location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all uniform names.
|
||||
/// 获取所有uniform名称。
|
||||
pub fn names(&self) -> Vec<&String> {
|
||||
self.values.keys().collect()
|
||||
}
|
||||
|
||||
/// Clear all uniforms.
|
||||
/// 清除所有uniform。
|
||||
pub fn clear(&mut self) {
|
||||
self.values.clear();
|
||||
}
|
||||
|
||||
/// Get uniform count.
|
||||
/// 获取uniform数量。
|
||||
pub fn len(&self) -> usize {
|
||||
self.values.len()
|
||||
}
|
||||
|
||||
/// Check if empty.
|
||||
/// 检查是否为空。
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.values.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
// ============= Convenience setters =============
|
||||
// ============= 便捷设置方法 =============
|
||||
|
||||
impl MaterialUniforms {
|
||||
/// Set a float uniform.
|
||||
/// 设置浮点uniform。
|
||||
pub fn set_float(&mut self, name: &str, value: f32) {
|
||||
self.set(name, UniformValue::Float(value));
|
||||
}
|
||||
|
||||
/// Set a vec2 uniform.
|
||||
/// 设置vec2 uniform。
|
||||
pub fn set_vec2(&mut self, name: &str, x: f32, y: f32) {
|
||||
self.set(name, UniformValue::Vec2([x, y]));
|
||||
}
|
||||
|
||||
/// Set a vec3 uniform.
|
||||
/// 设置vec3 uniform。
|
||||
pub fn set_vec3(&mut self, name: &str, x: f32, y: f32, z: f32) {
|
||||
self.set(name, UniformValue::Vec3([x, y, z]));
|
||||
}
|
||||
|
||||
/// Set a vec4 uniform (also used for colors).
|
||||
/// 设置vec4 uniform(也用于颜色)。
|
||||
pub fn set_vec4(&mut self, name: &str, x: f32, y: f32, z: f32, w: f32) {
|
||||
self.set(name, UniformValue::Vec4([x, y, z, w]));
|
||||
}
|
||||
|
||||
/// Set a color uniform (RGBA, 0.0-1.0).
|
||||
/// 设置颜色uniform(RGBA,0.0-1.0)。
|
||||
pub fn set_color(&mut self, name: &str, r: f32, g: f32, b: f32, a: f32) {
|
||||
self.set(name, UniformValue::Vec4([r, g, b, a]));
|
||||
}
|
||||
|
||||
/// Set an integer uniform.
|
||||
/// 设置整数uniform。
|
||||
pub fn set_int(&mut self, name: &str, value: i32) {
|
||||
self.set(name, UniformValue::Int(value));
|
||||
}
|
||||
|
||||
/// Set a texture sampler uniform.
|
||||
/// 设置纹理采样器uniform。
|
||||
pub fn set_sampler(&mut self, name: &str, slot: i32) {
|
||||
self.set(name, UniformValue::Sampler(slot));
|
||||
}
|
||||
}
|
||||
37
packages/rust/engine/src/renderer/mod.rs
Normal file
37
packages/rust/engine/src/renderer/mod.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
//! 2D and 3D rendering system with batch optimization.
|
||||
//! 带批处理优化的2D和3D渲染系统。
|
||||
|
||||
pub mod batch;
|
||||
pub mod shader;
|
||||
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};
|
||||
281
packages/rust/engine/src/renderer/renderer2d.rs
Normal file
281
packages/rust/engine/src/renderer/renderer2d.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
//! Main 2D renderer implementation.
|
||||
|
||||
use es_engine_shared::{
|
||||
traits::backend::GraphicsBackend,
|
||||
types::{
|
||||
handle::ShaderHandle,
|
||||
blend::ScissorRect,
|
||||
},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use crate::backend::WebGL2Backend;
|
||||
use super::batch::SpriteBatch;
|
||||
use super::camera::Camera2D;
|
||||
use super::texture::TextureManager;
|
||||
use super::material::{Material, BlendMode, UniformValue};
|
||||
|
||||
fn to_shared_blend_mode(mode: BlendMode) -> es_engine_shared::types::blend::BlendMode {
|
||||
match mode {
|
||||
BlendMode::None => es_engine_shared::types::blend::BlendMode::None,
|
||||
BlendMode::Alpha => es_engine_shared::types::blend::BlendMode::Alpha,
|
||||
BlendMode::Additive => es_engine_shared::types::blend::BlendMode::Additive,
|
||||
BlendMode::Multiply => es_engine_shared::types::blend::BlendMode::Multiply,
|
||||
BlendMode::Screen => es_engine_shared::types::blend::BlendMode::Screen,
|
||||
BlendMode::PremultipliedAlpha => es_engine_shared::types::blend::BlendMode::PremultipliedAlpha,
|
||||
}
|
||||
}
|
||||
|
||||
const SPRITE_VERTEX_SHADER: &str = r#"#version 300 es
|
||||
precision highp float;
|
||||
layout(location = 0) in vec2 a_position;
|
||||
layout(location = 1) in vec2 a_texCoord;
|
||||
layout(location = 2) in vec4 a_color;
|
||||
uniform mat3 u_projection;
|
||||
out vec2 v_texCoord;
|
||||
out vec4 v_color;
|
||||
void main() {
|
||||
vec3 pos = u_projection * vec3(a_position, 1.0);
|
||||
gl_Position = vec4(pos.xy, 0.0, 1.0);
|
||||
v_texCoord = a_texCoord;
|
||||
v_color = a_color;
|
||||
}
|
||||
"#;
|
||||
|
||||
const SPRITE_FRAGMENT_SHADER: &str = r#"#version 300 es
|
||||
precision highp float;
|
||||
in vec2 v_texCoord;
|
||||
in vec4 v_color;
|
||||
uniform sampler2D u_texture;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 texColor = texture(u_texture, v_texCoord);
|
||||
fragColor = texColor * v_color;
|
||||
if (fragColor.a < 0.01) discard;
|
||||
}
|
||||
"#;
|
||||
|
||||
pub struct Renderer2D {
|
||||
sprite_batch: SpriteBatch,
|
||||
default_shader: ShaderHandle,
|
||||
custom_shaders: HashMap<u32, ShaderHandle>,
|
||||
next_shader_id: u32,
|
||||
materials: HashMap<u32, Material>,
|
||||
camera: Camera2D,
|
||||
clear_color: [f32; 4],
|
||||
scissor_rect: Option<ScissorRect>,
|
||||
viewport_height: f32,
|
||||
}
|
||||
|
||||
impl Renderer2D {
|
||||
pub fn new(backend: &mut WebGL2Backend, max_sprites: usize) -> Result<Self, String> {
|
||||
let sprite_batch = SpriteBatch::new(backend, max_sprites)?;
|
||||
let default_shader = backend.compile_shader(SPRITE_VERTEX_SHADER, SPRITE_FRAGMENT_SHADER)
|
||||
.map_err(|e| format!("Default shader: {:?}", e))?;
|
||||
|
||||
let (width, height) = (backend.width() as f32, backend.height() as f32);
|
||||
let camera = Camera2D::new(width, height);
|
||||
|
||||
let mut materials = HashMap::new();
|
||||
materials.insert(0, Material::default());
|
||||
|
||||
Ok(Self {
|
||||
sprite_batch,
|
||||
default_shader,
|
||||
custom_shaders: HashMap::new(),
|
||||
next_shader_id: 100,
|
||||
materials,
|
||||
camera,
|
||||
clear_color: [0.1, 0.1, 0.12, 1.0],
|
||||
scissor_rect: None,
|
||||
viewport_height: height,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn submit_batch(
|
||||
&mut self,
|
||||
transforms: &[f32],
|
||||
texture_ids: &[u32],
|
||||
uvs: &[f32],
|
||||
colors: &[u32],
|
||||
material_ids: &[u32],
|
||||
) -> Result<(), String> {
|
||||
self.sprite_batch.add_sprites(transforms, texture_ids, uvs, colors, material_ids)
|
||||
}
|
||||
|
||||
pub fn render(&mut self, backend: &mut WebGL2Backend, texture_manager: &TextureManager) -> Result<(), String> {
|
||||
if self.sprite_batch.sprite_count() == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.apply_scissor(backend);
|
||||
|
||||
let projection = self.camera.projection_matrix();
|
||||
let mut current_material_id = u32::MAX;
|
||||
let mut current_texture_id = u32::MAX;
|
||||
|
||||
for batch_idx in 0..self.sprite_batch.batches().len() {
|
||||
let (batch_key, vertices) = &self.sprite_batch.batches()[batch_idx];
|
||||
if vertices.is_empty() { continue; }
|
||||
|
||||
if batch_key.material_id != current_material_id {
|
||||
current_material_id = batch_key.material_id;
|
||||
|
||||
let material = self.materials.get(&batch_key.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));
|
||||
backend.set_uniform_mat3("u_projection", &projection).ok();
|
||||
backend.set_uniform_i32("u_texture", 0).ok();
|
||||
|
||||
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(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if batch_key.texture_id != current_texture_id {
|
||||
current_texture_id = batch_key.texture_id;
|
||||
texture_manager.bind_texture_via_backend(backend, batch_key.texture_id, 0);
|
||||
}
|
||||
|
||||
self.sprite_batch.flush_batch_at(backend, batch_idx);
|
||||
}
|
||||
|
||||
self.sprite_batch.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_scissor(&self, backend: &mut WebGL2Backend) {
|
||||
if let Some(rect) = &self.scissor_rect {
|
||||
backend.set_scissor(Some(ScissorRect {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
}));
|
||||
} else {
|
||||
backend.set_scissor(None);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn camera_mut(&mut self) -> &mut Camera2D { &mut self.camera }
|
||||
|
||||
#[inline]
|
||||
pub fn camera(&self) -> &Camera2D { &self.camera }
|
||||
|
||||
pub fn set_clear_color(&mut self, r: f32, g: f32, b: f32, a: f32) {
|
||||
self.clear_color = [r, g, b, a];
|
||||
}
|
||||
|
||||
pub fn get_clear_color(&self) -> [f32; 4] { self.clear_color }
|
||||
|
||||
pub fn resize(&mut self, width: f32, height: f32) {
|
||||
self.camera.set_viewport(width, height);
|
||||
self.viewport_height = height;
|
||||
}
|
||||
|
||||
pub fn set_scissor_rect(&mut self, x: f32, y: f32, width: f32, height: f32) {
|
||||
self.scissor_rect = Some(ScissorRect {
|
||||
x: x as i32, y: y as i32,
|
||||
width: width as u32, height: height as u32,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn clear_scissor_rect(&mut self) { self.scissor_rect = None; }
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn compile_shader_with_id(&mut self, backend: &mut WebGL2Backend, id: u32, vertex: &str, fragment: &str) -> Result<(), String> {
|
||||
let handle = backend.compile_shader(vertex, fragment)
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
self.custom_shaders.insert(id, handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_shader(&self, id: u32) -> bool {
|
||||
id == 0 || self.custom_shaders.contains_key(&id)
|
||||
}
|
||||
|
||||
/// Get shader handle by ID.
|
||||
/// 按 ID 获取着色器句柄。
|
||||
///
|
||||
/// Returns the default shader for ID 0, or custom shader for other IDs.
|
||||
/// ID 0 返回默认着色器,其他 ID 返回自定义着色器。
|
||||
pub fn get_shader_handle(&self, id: u32) -> Option<ShaderHandle> {
|
||||
if id == 0 || id == crate::renderer::shader::SHADER_ID_DEFAULT_SPRITE {
|
||||
Some(self.default_shader)
|
||||
} else {
|
||||
self.custom_shaders.get(&id).copied()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_shader(&mut self, id: u32) -> bool {
|
||||
if id < 100 { return false; }
|
||||
self.custom_shaders.remove(&id).is_some()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
pub fn register_material_with_id(&mut self, id: u32, material: Material) {
|
||||
self.materials.insert(id, material);
|
||||
}
|
||||
|
||||
pub fn get_material(&self, id: u32) -> Option<&Material> { self.materials.get(&id) }
|
||||
pub fn get_material_mut(&mut self, id: u32) -> Option<&mut Material> { self.materials.get_mut(&id) }
|
||||
pub fn has_material(&self, id: u32) -> bool { self.materials.contains_key(&id) }
|
||||
pub fn remove_material(&mut self, id: u32) -> bool { self.materials.remove(&id).is_some() }
|
||||
|
||||
pub fn set_material_float(&mut self, id: u32, name: &str, value: f32) -> bool {
|
||||
if let Some(mat) = self.materials.get_mut(&id) {
|
||||
mat.uniforms.set_float(name, value);
|
||||
true
|
||||
} else { false }
|
||||
}
|
||||
|
||||
pub fn set_material_vec4(&mut self, id: u32, name: &str, x: f32, y: f32, z: f32, w: f32) -> bool {
|
||||
if let Some(mat) = self.materials.get_mut(&id) {
|
||||
mat.uniforms.set_vec4(name, x, y, z, w);
|
||||
true
|
||||
} else { false }
|
||||
}
|
||||
|
||||
pub fn destroy(self, backend: &mut WebGL2Backend) {
|
||||
self.sprite_batch.destroy(backend);
|
||||
backend.destroy_shader(self.default_shader);
|
||||
for (_, handle) in self.custom_shaders {
|
||||
backend.destroy_shader(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
481
packages/rust/engine/src/renderer/renderer3d.rs
Normal file
481
packages/rust/engine/src/renderer/renderer3d.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
380
packages/rust/engine/src/renderer/shader/builtin.rs
Normal file
380
packages/rust/engine/src/renderer/shader/builtin.rs
Normal file
@@ -0,0 +1,380 @@
|
||||
//! Built-in shader source code.
|
||||
//! 内置Shader源代码。
|
||||
|
||||
// =============================================================================
|
||||
// MSDF Text Shaders
|
||||
// MSDF 文本着色器
|
||||
// =============================================================================
|
||||
|
||||
/// MSDF text vertex shader source.
|
||||
/// MSDF 文本顶点着色器源代码。
|
||||
pub const MSDF_TEXT_VERTEX_SHADER: &str = r#"#version 300 es
|
||||
precision highp float;
|
||||
|
||||
layout(location = 0) in vec2 a_position;
|
||||
layout(location = 1) in vec2 a_texCoord;
|
||||
layout(location = 2) in vec4 a_color;
|
||||
layout(location = 3) in vec4 a_outlineColor;
|
||||
layout(location = 4) in float a_outlineWidth;
|
||||
|
||||
uniform mat3 u_projection;
|
||||
|
||||
out vec2 v_texCoord;
|
||||
out vec4 v_color;
|
||||
out vec4 v_outlineColor;
|
||||
out float v_outlineWidth;
|
||||
|
||||
void main() {
|
||||
vec3 pos = u_projection * vec3(a_position, 1.0);
|
||||
gl_Position = vec4(pos.xy, 0.0, 1.0);
|
||||
v_texCoord = a_texCoord;
|
||||
v_color = a_color;
|
||||
v_outlineColor = a_outlineColor;
|
||||
v_outlineWidth = a_outlineWidth;
|
||||
}
|
||||
"#;
|
||||
|
||||
/// MSDF text fragment shader source.
|
||||
/// MSDF 文本片段着色器源代码。
|
||||
pub const MSDF_TEXT_FRAGMENT_SHADER: &str = r#"#version 300 es
|
||||
precision highp float;
|
||||
|
||||
in vec2 v_texCoord;
|
||||
in vec4 v_color;
|
||||
in vec4 v_outlineColor;
|
||||
in float v_outlineWidth;
|
||||
|
||||
uniform sampler2D u_msdfTexture;
|
||||
uniform float u_pxRange;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
float median(float r, float g, float b) {
|
||||
return max(min(r, g), min(max(r, g), b));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 msdf = texture(u_msdfTexture, v_texCoord).rgb;
|
||||
float sd = median(msdf.r, msdf.g, msdf.b);
|
||||
|
||||
vec2 unitRange = vec2(u_pxRange) / vec2(textureSize(u_msdfTexture, 0));
|
||||
vec2 screenTexSize = vec2(1.0) / fwidth(v_texCoord);
|
||||
float screenPxRange = max(0.5 * dot(unitRange, screenTexSize), 1.0);
|
||||
|
||||
float screenPxDistance = screenPxRange * (sd - 0.5);
|
||||
float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0);
|
||||
|
||||
if (v_outlineWidth > 0.0) {
|
||||
float outlineDistance = screenPxRange * (sd - 0.5 + v_outlineWidth);
|
||||
float outlineOpacity = clamp(outlineDistance + 0.5, 0.0, 1.0);
|
||||
vec4 outlineCol = vec4(v_outlineColor.rgb, v_outlineColor.a * outlineOpacity);
|
||||
vec4 fillCol = vec4(v_color.rgb, v_color.a * opacity);
|
||||
fragColor = mix(outlineCol, fillCol, opacity);
|
||||
} else {
|
||||
fragColor = vec4(v_color.rgb, v_color.a * opacity);
|
||||
}
|
||||
|
||||
if (fragColor.a < 0.01) {
|
||||
discard;
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
// =============================================================================
|
||||
// Sprite Shaders
|
||||
// 精灵着色器
|
||||
// =============================================================================
|
||||
|
||||
/// Sprite vertex shader source.
|
||||
/// 精灵顶点着色器源代码。
|
||||
///
|
||||
/// Handles sprite transformation with position, UV, and color attributes.
|
||||
/// 处理带有位置、UV和颜色属性的精灵变换。
|
||||
pub const SPRITE_VERTEX_SHADER: &str = r#"#version 300 es
|
||||
precision highp float;
|
||||
|
||||
// Vertex attributes | 顶点属性
|
||||
layout(location = 0) in vec2 a_position;
|
||||
layout(location = 1) in vec2 a_texCoord;
|
||||
layout(location = 2) in vec4 a_color;
|
||||
|
||||
// Uniforms | 统一变量
|
||||
uniform mat3 u_projection;
|
||||
|
||||
// Outputs to fragment shader | 输出到片段着色器
|
||||
out vec2 v_texCoord;
|
||||
out vec4 v_color;
|
||||
|
||||
void main() {
|
||||
// Apply projection matrix | 应用投影矩阵
|
||||
vec3 pos = u_projection * vec3(a_position, 1.0);
|
||||
gl_Position = vec4(pos.xy, 0.0, 1.0);
|
||||
|
||||
// Pass through to fragment shader | 传递到片段着色器
|
||||
v_texCoord = a_texCoord;
|
||||
v_color = a_color;
|
||||
}
|
||||
"#;
|
||||
|
||||
/// Sprite fragment shader source.
|
||||
/// 精灵片段着色器源代码。
|
||||
///
|
||||
/// Samples texture and applies vertex color tinting.
|
||||
/// 采样纹理并应用顶点颜色着色。
|
||||
pub const SPRITE_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 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;
|
||||
}
|
||||
}
|
||||
"#;
|
||||
172
packages/rust/engine/src/renderer/shader/manager.rs
Normal file
172
packages/rust/engine/src/renderer/shader/manager.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
//! Shader manager for runtime shader compilation and caching.
|
||||
//! 着色器管理器,用于运行时着色器编译和缓存。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use web_sys::WebGl2RenderingContext;
|
||||
|
||||
use crate::core::error::Result;
|
||||
use super::program::ShaderProgram;
|
||||
use super::builtin::{SPRITE_VERTEX_SHADER, SPRITE_FRAGMENT_SHADER};
|
||||
|
||||
/// Reserved shader IDs for built-in shaders.
|
||||
/// 内置着色器的保留ID。
|
||||
pub const SHADER_ID_DEFAULT_SPRITE: u32 = 0;
|
||||
|
||||
/// Shader manager for compiling and caching shader programs.
|
||||
/// 着色器管理器,用于编译和缓存着色器程序。
|
||||
///
|
||||
/// Manages multiple shader programs, allowing runtime compilation of custom shaders.
|
||||
/// 管理多个着色器程序,允许运行时编译自定义着色器。
|
||||
pub struct ShaderManager {
|
||||
/// Compiled shader programs indexed by ID.
|
||||
/// 按ID索引的已编译着色器程序。
|
||||
shaders: HashMap<u32, ShaderProgram>,
|
||||
|
||||
/// Next available shader ID for custom shaders.
|
||||
/// 下一个可用的自定义着色器ID。
|
||||
next_shader_id: u32,
|
||||
|
||||
/// Shader source cache for hot-reloading (optional).
|
||||
/// 着色器源代码缓存,用于热重载(可选)。
|
||||
shader_sources: HashMap<u32, (String, String)>,
|
||||
}
|
||||
|
||||
impl ShaderManager {
|
||||
/// Create a new shader manager with built-in shaders.
|
||||
/// 创建带有内置着色器的新着色器管理器。
|
||||
///
|
||||
/// # Arguments | 参数
|
||||
/// * `gl` - WebGL2 context | WebGL2上下文
|
||||
pub fn new(gl: &WebGl2RenderingContext) -> Result<Self> {
|
||||
let mut manager = Self {
|
||||
shaders: HashMap::new(),
|
||||
next_shader_id: 100, // Reserve 0-99 for built-in shaders
|
||||
shader_sources: HashMap::new(),
|
||||
};
|
||||
|
||||
// Compile built-in sprite shader | 编译内置精灵着色器
|
||||
let default_shader = ShaderProgram::new(gl, SPRITE_VERTEX_SHADER, SPRITE_FRAGMENT_SHADER)?;
|
||||
manager.shaders.insert(SHADER_ID_DEFAULT_SPRITE, default_shader);
|
||||
manager.shader_sources.insert(
|
||||
SHADER_ID_DEFAULT_SPRITE,
|
||||
(SPRITE_VERTEX_SHADER.to_string(), SPRITE_FRAGMENT_SHADER.to_string()),
|
||||
);
|
||||
|
||||
log::info!("ShaderManager initialized with {} built-in shaders | 着色器管理器初始化完成,内置着色器数量: {}",
|
||||
manager.shaders.len(), manager.shaders.len());
|
||||
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
/// Compile and register a custom shader program.
|
||||
/// 编译并注册自定义着色器程序。
|
||||
///
|
||||
/// # Arguments | 参数
|
||||
/// * `gl` - WebGL2 context | WebGL2上下文
|
||||
/// * `vertex_source` - Vertex shader GLSL source | 顶点着色器GLSL源代码
|
||||
/// * `fragment_source` - Fragment shader GLSL source | 片段着色器GLSL源代码
|
||||
///
|
||||
/// # Returns | 返回
|
||||
/// The shader ID for referencing this shader | 用于引用此着色器的ID
|
||||
pub fn compile_shader(
|
||||
&mut self,
|
||||
gl: &WebGl2RenderingContext,
|
||||
vertex_source: &str,
|
||||
fragment_source: &str,
|
||||
) -> Result<u32> {
|
||||
let shader = ShaderProgram::new(gl, vertex_source, fragment_source)?;
|
||||
let shader_id = self.next_shader_id;
|
||||
self.next_shader_id += 1;
|
||||
|
||||
self.shaders.insert(shader_id, shader);
|
||||
self.shader_sources.insert(
|
||||
shader_id,
|
||||
(vertex_source.to_string(), fragment_source.to_string()),
|
||||
);
|
||||
|
||||
log::debug!("Custom shader compiled with ID: {} | 自定义着色器编译完成,ID: {}", shader_id, shader_id);
|
||||
|
||||
Ok(shader_id)
|
||||
}
|
||||
|
||||
/// Compile and register a shader with a specific ID.
|
||||
/// 使用特定ID编译并注册着色器。
|
||||
///
|
||||
/// # Arguments | 参数
|
||||
/// * `gl` - WebGL2 context | WebGL2上下文
|
||||
/// * `shader_id` - Desired shader ID | 期望的着色器ID
|
||||
/// * `vertex_source` - Vertex shader GLSL source | 顶点着色器GLSL源代码
|
||||
/// * `fragment_source` - Fragment shader GLSL source | 片段着色器GLSL源代码
|
||||
pub fn compile_shader_with_id(
|
||||
&mut self,
|
||||
gl: &WebGl2RenderingContext,
|
||||
shader_id: u32,
|
||||
vertex_source: &str,
|
||||
fragment_source: &str,
|
||||
) -> Result<()> {
|
||||
let shader = ShaderProgram::new(gl, vertex_source, fragment_source)?;
|
||||
|
||||
// Remove old shader if exists | 如果存在则移除旧着色器
|
||||
self.shaders.remove(&shader_id);
|
||||
|
||||
self.shaders.insert(shader_id, shader);
|
||||
self.shader_sources.insert(
|
||||
shader_id,
|
||||
(vertex_source.to_string(), fragment_source.to_string()),
|
||||
);
|
||||
|
||||
log::debug!("Shader compiled with ID: {} | 着色器编译完成,ID: {}", shader_id, shader_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a shader program by ID.
|
||||
/// 按ID获取着色器程序。
|
||||
#[inline]
|
||||
pub fn get_shader(&self, shader_id: u32) -> Option<&ShaderProgram> {
|
||||
self.shaders.get(&shader_id)
|
||||
}
|
||||
|
||||
/// Get the default sprite shader.
|
||||
/// 获取默认精灵着色器。
|
||||
#[inline]
|
||||
pub fn get_default_shader(&self) -> &ShaderProgram {
|
||||
self.shaders.get(&SHADER_ID_DEFAULT_SPRITE)
|
||||
.expect("Default shader should always exist | 默认着色器应该始终存在")
|
||||
}
|
||||
|
||||
/// Check if a shader exists.
|
||||
/// 检查着色器是否存在。
|
||||
#[inline]
|
||||
pub fn has_shader(&self, shader_id: u32) -> bool {
|
||||
self.shaders.contains_key(&shader_id)
|
||||
}
|
||||
|
||||
/// Remove a shader program.
|
||||
/// 移除着色器程序。
|
||||
///
|
||||
/// Note: Cannot remove built-in shaders (ID < 100).
|
||||
/// 注意:无法移除内置着色器(ID < 100)。
|
||||
pub fn remove_shader(&mut self, shader_id: u32) -> bool {
|
||||
if shader_id < 100 {
|
||||
log::warn!("Cannot remove built-in shader: {} | 无法移除内置着色器: {}", shader_id, shader_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
self.shader_sources.remove(&shader_id);
|
||||
self.shaders.remove(&shader_id).is_some()
|
||||
}
|
||||
|
||||
/// Get all shader IDs.
|
||||
/// 获取所有着色器ID。
|
||||
pub fn shader_ids(&self) -> Vec<u32> {
|
||||
self.shaders.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// Get shader count.
|
||||
/// 获取着色器数量。
|
||||
#[inline]
|
||||
pub fn shader_count(&self) -> usize {
|
||||
self.shaders.len()
|
||||
}
|
||||
}
|
||||
18
packages/rust/engine/src/renderer/shader/mod.rs
Normal file
18
packages/rust/engine/src/renderer/shader/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
//! Shader management system.
|
||||
//! Shader管理系统。
|
||||
|
||||
mod program;
|
||||
mod builtin;
|
||||
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,
|
||||
// 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};
|
||||
154
packages/rust/engine/src/renderer/shader/program.rs
Normal file
154
packages/rust/engine/src/renderer/shader/program.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
//! Shader program compilation and management.
|
||||
//! Shader程序编译和管理。
|
||||
|
||||
use web_sys::{WebGl2RenderingContext, WebGlProgram, WebGlShader, WebGlUniformLocation};
|
||||
use crate::core::error::{EngineError, Result};
|
||||
|
||||
/// Compiled shader program.
|
||||
/// 已编译的Shader程序。
|
||||
///
|
||||
/// Manages vertex and fragment shaders, including compilation and linking.
|
||||
/// 管理顶点和片段着色器,包括编译和链接。
|
||||
pub struct ShaderProgram {
|
||||
program: WebGlProgram,
|
||||
}
|
||||
|
||||
impl ShaderProgram {
|
||||
/// Create and compile a new shader program.
|
||||
/// 创建并编译新的Shader程序。
|
||||
///
|
||||
/// # Arguments | 参数
|
||||
/// * `gl` - WebGL2 context | WebGL2上下文
|
||||
/// * `vertex_source` - Vertex shader source code | 顶点着色器源代码
|
||||
/// * `fragment_source` - Fragment shader source code | 片段着色器源代码
|
||||
///
|
||||
/// # Returns | 返回
|
||||
/// A compiled shader program or an error | 已编译的Shader程序或错误
|
||||
pub fn new(
|
||||
gl: &WebGl2RenderingContext,
|
||||
vertex_source: &str,
|
||||
fragment_source: &str,
|
||||
) -> Result<Self> {
|
||||
// Compile shaders | 编译着色器
|
||||
let vertex_shader = Self::compile_shader(
|
||||
gl,
|
||||
WebGl2RenderingContext::VERTEX_SHADER,
|
||||
vertex_source,
|
||||
)?;
|
||||
|
||||
let fragment_shader = Self::compile_shader(
|
||||
gl,
|
||||
WebGl2RenderingContext::FRAGMENT_SHADER,
|
||||
fragment_source,
|
||||
)?;
|
||||
|
||||
// Create and link program | 创建并链接程序
|
||||
let program = gl
|
||||
.create_program()
|
||||
.ok_or_else(|| EngineError::ProgramLinkFailed("Failed to create program".into()))?;
|
||||
|
||||
gl.attach_shader(&program, &vertex_shader);
|
||||
gl.attach_shader(&program, &fragment_shader);
|
||||
gl.link_program(&program);
|
||||
|
||||
// Check for linking errors | 检查链接错误
|
||||
let success = gl
|
||||
.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
|
||||
if !success {
|
||||
let log = gl.get_program_info_log(&program).unwrap_or_default();
|
||||
return Err(EngineError::ProgramLinkFailed(log));
|
||||
}
|
||||
|
||||
// Clean up shaders (they're linked to the program now)
|
||||
// 清理着色器(它们现在已链接到程序)
|
||||
gl.delete_shader(Some(&vertex_shader));
|
||||
gl.delete_shader(Some(&fragment_shader));
|
||||
|
||||
log::debug!("Shader program compiled successfully | Shader程序编译成功");
|
||||
|
||||
Ok(Self { program })
|
||||
}
|
||||
|
||||
/// Compile a single shader.
|
||||
/// 编译单个着色器。
|
||||
fn compile_shader(
|
||||
gl: &WebGl2RenderingContext,
|
||||
shader_type: u32,
|
||||
source: &str,
|
||||
) -> Result<WebGlShader> {
|
||||
let shader = gl
|
||||
.create_shader(shader_type)
|
||||
.ok_or_else(|| EngineError::ShaderCompileFailed("Failed to create shader".into()))?;
|
||||
|
||||
gl.shader_source(&shader, source);
|
||||
gl.compile_shader(&shader);
|
||||
|
||||
// Check for compilation errors | 检查编译错误
|
||||
let success = gl
|
||||
.get_shader_parameter(&shader, WebGl2RenderingContext::COMPILE_STATUS)
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
|
||||
if !success {
|
||||
let log = gl.get_shader_info_log(&shader).unwrap_or_default();
|
||||
let shader_type_name = if shader_type == WebGl2RenderingContext::VERTEX_SHADER {
|
||||
"Vertex"
|
||||
} else {
|
||||
"Fragment"
|
||||
};
|
||||
return Err(EngineError::ShaderCompileFailed(format!(
|
||||
"{} shader: {}",
|
||||
shader_type_name, log
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(shader)
|
||||
}
|
||||
|
||||
/// Use this shader program for rendering.
|
||||
/// 使用此Shader程序进行渲染。
|
||||
#[inline]
|
||||
pub fn bind(&self, gl: &WebGl2RenderingContext) {
|
||||
gl.use_program(Some(&self.program));
|
||||
}
|
||||
|
||||
/// Get uniform location by name.
|
||||
/// 按名称获取uniform位置。
|
||||
#[inline]
|
||||
pub fn get_uniform_location(
|
||||
&self,
|
||||
gl: &WebGl2RenderingContext,
|
||||
name: &str,
|
||||
) -> Option<WebGlUniformLocation> {
|
||||
gl.get_uniform_location(&self.program, name)
|
||||
}
|
||||
|
||||
/// Set a mat3 uniform.
|
||||
/// 设置mat3 uniform。
|
||||
pub fn set_uniform_mat3(
|
||||
&self,
|
||||
gl: &WebGl2RenderingContext,
|
||||
name: &str,
|
||||
value: &[f32; 9],
|
||||
) {
|
||||
if let Some(location) = self.get_uniform_location(gl, name) {
|
||||
gl.uniform_matrix3fv_with_f32_array(Some(&location), false, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set an i32 uniform (for texture samplers).
|
||||
/// 设置i32 uniform(用于纹理采样器)。
|
||||
pub fn set_uniform_i32(
|
||||
&self,
|
||||
gl: &WebGl2RenderingContext,
|
||||
name: &str,
|
||||
value: i32,
|
||||
) {
|
||||
if let Some(location) = self.get_uniform_location(gl, name) {
|
||||
gl.uniform1i(Some(&location), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
packages/rust/engine/src/renderer/texture/mod.rs
Normal file
8
packages/rust/engine/src/renderer/texture/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
//! Texture management system.
|
||||
//! 纹理管理系统。
|
||||
|
||||
mod texture;
|
||||
mod texture_manager;
|
||||
|
||||
pub use texture::Texture;
|
||||
pub use texture_manager::{TextureManager, TextureState};
|
||||
39
packages/rust/engine/src/renderer/texture/texture.rs
Normal file
39
packages/rust/engine/src/renderer/texture/texture.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
//! Texture representation.
|
||||
//! 纹理表示。
|
||||
|
||||
use web_sys::WebGlTexture;
|
||||
|
||||
/// 2D texture.
|
||||
/// 2D纹理。
|
||||
pub struct Texture {
|
||||
/// WebGL texture handle.
|
||||
/// WebGL纹理句柄。
|
||||
pub(crate) handle: WebGlTexture,
|
||||
|
||||
/// Texture width in pixels.
|
||||
/// 纹理宽度(像素)。
|
||||
pub width: u32,
|
||||
|
||||
/// Texture height in pixels.
|
||||
/// 纹理高度(像素)。
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl Texture {
|
||||
/// Create a new texture.
|
||||
/// 创建新纹理。
|
||||
pub fn new(handle: WebGlTexture, width: u32, height: u32) -> Self {
|
||||
Self {
|
||||
handle,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the WebGL texture handle.
|
||||
/// 获取WebGL纹理句柄。
|
||||
#[inline]
|
||||
pub fn handle(&self) -> &WebGlTexture {
|
||||
&self.handle
|
||||
}
|
||||
}
|
||||
595
packages/rust/engine/src/renderer/texture/texture_manager.rs
Normal file
595
packages/rust/engine/src/renderer/texture/texture_manager.rs
Normal file
@@ -0,0 +1,595 @@
|
||||
//! Texture loading and management.
|
||||
//! 纹理加载和管理。
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{HtmlImageElement, WebGl2RenderingContext, WebGlTexture};
|
||||
|
||||
use crate::core::error::{EngineError, Result};
|
||||
use crate::backend::WebGL2Backend;
|
||||
use super::Texture;
|
||||
|
||||
/// 纹理加载状态
|
||||
/// Texture loading state
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum TextureState {
|
||||
/// 正在加载中
|
||||
/// Loading in progress
|
||||
Loading,
|
||||
/// 加载完成,可以使用
|
||||
/// Loaded and ready to use
|
||||
Ready,
|
||||
/// 加载失败
|
||||
/// Load failed
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
/// Texture manager for loading and caching textures.
|
||||
/// 用于加载和缓存纹理的纹理管理器。
|
||||
pub struct TextureManager {
|
||||
/// WebGL context.
|
||||
/// WebGL上下文。
|
||||
gl: WebGl2RenderingContext,
|
||||
|
||||
/// Loaded textures.
|
||||
/// 已加载的纹理。
|
||||
textures: HashMap<u32, Texture>,
|
||||
|
||||
/// Path to texture ID mapping.
|
||||
/// 路径到纹理ID的映射。
|
||||
path_to_id: HashMap<String, u32>,
|
||||
|
||||
/// Next texture ID for auto-assignment.
|
||||
/// 下一个自动分配的纹理ID。
|
||||
next_id: u32,
|
||||
|
||||
/// Default white texture for untextured rendering.
|
||||
/// 用于无纹理渲染的默认白色纹理。
|
||||
default_texture: Option<WebGlTexture>,
|
||||
|
||||
/// 纹理加载状态(使用 Rc<RefCell<>> 以便闭包可以修改)
|
||||
/// Texture loading states (using Rc<RefCell<>> so closures can modify)
|
||||
texture_states: Rc<RefCell<HashMap<u32, TextureState>>>,
|
||||
|
||||
/// 纹理尺寸缓存(使用 Rc<RefCell<>> 以便闭包可以修改)
|
||||
/// Texture dimensions cache (using Rc<RefCell<>> so closures can modify)
|
||||
/// Key: texture ID, Value: (width, height)
|
||||
texture_dimensions: Rc<RefCell<HashMap<u32, (u32, u32)>>>,
|
||||
}
|
||||
|
||||
impl TextureManager {
|
||||
/// Create a new texture manager.
|
||||
/// 创建新的纹理管理器。
|
||||
pub fn new(gl: WebGl2RenderingContext) -> Self {
|
||||
let mut manager = Self {
|
||||
gl,
|
||||
textures: HashMap::new(),
|
||||
path_to_id: HashMap::new(),
|
||||
next_id: 1, // Start from 1, 0 is reserved for default
|
||||
default_texture: None,
|
||||
texture_states: Rc::new(RefCell::new(HashMap::new())),
|
||||
texture_dimensions: Rc::new(RefCell::new(HashMap::new())),
|
||||
};
|
||||
|
||||
// Create default white texture | 创建默认白色纹理
|
||||
manager.create_default_texture();
|
||||
|
||||
manager
|
||||
}
|
||||
|
||||
/// Create a 1x1 white texture as default.
|
||||
/// 创建1x1白色纹理作为默认纹理。
|
||||
fn create_default_texture(&mut self) {
|
||||
let texture = self.gl.create_texture();
|
||||
if let Some(tex) = &texture {
|
||||
self.gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(tex));
|
||||
|
||||
let white_pixel: [u8; 4] = [255, 255, 255, 255];
|
||||
let _ = self.gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
|
||||
WebGl2RenderingContext::TEXTURE_2D,
|
||||
0,
|
||||
WebGl2RenderingContext::RGBA as i32,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
WebGl2RenderingContext::RGBA,
|
||||
WebGl2RenderingContext::UNSIGNED_BYTE,
|
||||
Some(&white_pixel),
|
||||
);
|
||||
|
||||
self.gl.tex_parameteri(
|
||||
WebGl2RenderingContext::TEXTURE_2D,
|
||||
WebGl2RenderingContext::TEXTURE_MIN_FILTER,
|
||||
WebGl2RenderingContext::NEAREST as i32,
|
||||
);
|
||||
self.gl.tex_parameteri(
|
||||
WebGl2RenderingContext::TEXTURE_2D,
|
||||
WebGl2RenderingContext::TEXTURE_MAG_FILTER,
|
||||
WebGl2RenderingContext::NEAREST as i32,
|
||||
);
|
||||
}
|
||||
|
||||
self.default_texture = texture;
|
||||
}
|
||||
|
||||
/// Load a texture from URL.
|
||||
/// 从URL加载纹理。
|
||||
///
|
||||
/// Note: This is an async operation. The texture will be available
|
||||
/// after the image loads. Use `get_texture_state` to check loading status.
|
||||
/// 注意:这是一个异步操作。纹理在图片加载后可用。使用 `get_texture_state` 检查加载状态。
|
||||
pub fn load_texture(&mut self, id: u32, url: &str) -> Result<()> {
|
||||
// 设置初始状态为 Loading | Set initial state to Loading
|
||||
self.texture_states.borrow_mut().insert(id, TextureState::Loading);
|
||||
|
||||
// Create placeholder texture | 创建占位纹理
|
||||
let texture = self.gl
|
||||
.create_texture()
|
||||
.ok_or_else(|| EngineError::TextureLoadFailed("Failed to create texture".into()))?;
|
||||
|
||||
// Set up temporary 1x1 transparent texture | 设置临时1x1透明纹理
|
||||
// 使用透明而非灰色,这样未加载完成时不会显示奇怪的颜色
|
||||
// Use transparent instead of gray, so incomplete textures don't show strange colors
|
||||
self.gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture));
|
||||
let placeholder: [u8; 4] = [0, 0, 0, 0];
|
||||
let _ = self.gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
|
||||
WebGl2RenderingContext::TEXTURE_2D,
|
||||
0,
|
||||
WebGl2RenderingContext::RGBA as i32,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
WebGl2RenderingContext::RGBA,
|
||||
WebGl2RenderingContext::UNSIGNED_BYTE,
|
||||
Some(&placeholder),
|
||||
);
|
||||
|
||||
// Clone texture handle for async loading before storing | 在存储前克隆纹理句柄用于异步加载
|
||||
let texture_for_closure = texture.clone();
|
||||
|
||||
// Store texture with placeholder size | 存储带占位符尺寸的纹理
|
||||
self.textures.insert(id, Texture::new(texture, 1, 1));
|
||||
|
||||
// Clone state map for closures | 克隆状态映射用于闭包
|
||||
let states_for_onload = Rc::clone(&self.texture_states);
|
||||
let states_for_onerror = Rc::clone(&self.texture_states);
|
||||
|
||||
// Clone dimensions map for closure | 克隆尺寸映射用于闭包
|
||||
let dimensions_for_onload = Rc::clone(&self.texture_dimensions);
|
||||
|
||||
// Load actual image asynchronously | 异步加载实际图片
|
||||
let gl = self.gl.clone();
|
||||
|
||||
let image = HtmlImageElement::new()
|
||||
.map_err(|_| EngineError::TextureLoadFailed("Failed to create image element".into()))?;
|
||||
|
||||
// Set crossOrigin for CORS support | 设置crossOrigin以支持CORS
|
||||
image.set_cross_origin(Some("anonymous"));
|
||||
|
||||
// Clone image for use in closure | 克隆图片用于闭包
|
||||
let image_clone = image.clone();
|
||||
let texture_id = id;
|
||||
|
||||
// Set up load callback | 设置加载回调
|
||||
let onload = Closure::wrap(Box::new(move || {
|
||||
gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture_for_closure));
|
||||
|
||||
// Use the captured image element | 使用捕获的图片元素
|
||||
let result = gl.tex_image_2d_with_u32_and_u32_and_html_image_element(
|
||||
WebGl2RenderingContext::TEXTURE_2D,
|
||||
0,
|
||||
WebGl2RenderingContext::RGBA as i32,
|
||||
WebGl2RenderingContext::RGBA,
|
||||
WebGl2RenderingContext::UNSIGNED_BYTE,
|
||||
&image_clone,
|
||||
);
|
||||
|
||||
if let Err(e) = result {
|
||||
log::error!("Failed to upload texture {}: {:?} | 纹理 {} 上传失败: {:?}", texture_id, e, texture_id, e);
|
||||
states_for_onload.borrow_mut().insert(texture_id, TextureState::Failed(format!("{:?}", e)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set texture parameters | 设置纹理参数
|
||||
gl.tex_parameteri(
|
||||
WebGl2RenderingContext::TEXTURE_2D,
|
||||
WebGl2RenderingContext::TEXTURE_WRAP_S,
|
||||
WebGl2RenderingContext::CLAMP_TO_EDGE as i32,
|
||||
);
|
||||
gl.tex_parameteri(
|
||||
WebGl2RenderingContext::TEXTURE_2D,
|
||||
WebGl2RenderingContext::TEXTURE_WRAP_T,
|
||||
WebGl2RenderingContext::CLAMP_TO_EDGE as i32,
|
||||
);
|
||||
gl.tex_parameteri(
|
||||
WebGl2RenderingContext::TEXTURE_2D,
|
||||
WebGl2RenderingContext::TEXTURE_MIN_FILTER,
|
||||
WebGl2RenderingContext::LINEAR as i32,
|
||||
);
|
||||
gl.tex_parameteri(
|
||||
WebGl2RenderingContext::TEXTURE_2D,
|
||||
WebGl2RenderingContext::TEXTURE_MAG_FILTER,
|
||||
WebGl2RenderingContext::LINEAR as i32,
|
||||
);
|
||||
|
||||
// 存储纹理尺寸(从加载的图片获取)
|
||||
// Store texture dimensions (from loaded image)
|
||||
let width = image_clone.width();
|
||||
let height = image_clone.height();
|
||||
dimensions_for_onload.borrow_mut().insert(texture_id, (width, height));
|
||||
|
||||
// 标记为就绪 | Mark as ready
|
||||
states_for_onload.borrow_mut().insert(texture_id, TextureState::Ready);
|
||||
|
||||
}) as Box<dyn Fn()>);
|
||||
|
||||
// Set up error callback | 设置错误回调
|
||||
let url_for_error = url.to_string();
|
||||
let onerror = Closure::wrap(Box::new(move || {
|
||||
let error_msg = format!("Failed to load image: {}", url_for_error);
|
||||
states_for_onerror.borrow_mut().insert(texture_id, TextureState::Failed(error_msg));
|
||||
}) as Box<dyn Fn()>);
|
||||
|
||||
image.set_onload(Some(onload.as_ref().unchecked_ref()));
|
||||
image.set_onerror(Some(onerror.as_ref().unchecked_ref()));
|
||||
onload.forget(); // Prevent closure from being dropped | 防止闭包被销毁
|
||||
onerror.forget();
|
||||
|
||||
image.set_src(url);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get texture by ID.
|
||||
/// 按ID获取纹理。
|
||||
#[inline]
|
||||
pub fn get_texture(&self, id: u32) -> Option<&Texture> {
|
||||
self.textures.get(&id)
|
||||
}
|
||||
|
||||
/// Get texture size by ID.
|
||||
/// 按ID获取纹理尺寸。
|
||||
///
|
||||
/// First checks the dimensions cache (updated when texture loads),
|
||||
/// then falls back to the Texture struct.
|
||||
/// 首先检查尺寸缓存(在纹理加载时更新),
|
||||
/// 然后回退到 Texture 结构体。
|
||||
#[inline]
|
||||
pub fn get_texture_size(&self, id: u32) -> Option<(f32, f32)> {
|
||||
// Check dimensions cache first (has actual loaded dimensions)
|
||||
// 首先检查尺寸缓存(有实际加载的尺寸)
|
||||
if let Some(&(w, h)) = self.texture_dimensions.borrow().get(&id) {
|
||||
return Some((w as f32, h as f32));
|
||||
}
|
||||
|
||||
// Fall back to texture struct (may have placeholder dimensions)
|
||||
// 回退到纹理结构体(可能是占位符尺寸)
|
||||
self.textures
|
||||
.get(&id)
|
||||
.map(|t| (t.width as f32, t.height as f32))
|
||||
}
|
||||
|
||||
/// Bind texture for rendering.
|
||||
/// 绑定纹理用于渲染。
|
||||
pub fn bind_texture(&self, id: u32, slot: u32) {
|
||||
self.gl.active_texture(WebGl2RenderingContext::TEXTURE0 + slot);
|
||||
|
||||
if let Some(texture) = self.textures.get(&id) {
|
||||
self.gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture.handle));
|
||||
} else if let Some(default) = &self.default_texture {
|
||||
if id != 0 {
|
||||
log::warn!("Texture {} not found, using default | 未找到纹理 {},使用默认纹理", id, id);
|
||||
}
|
||||
self.gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(default));
|
||||
} else {
|
||||
log::error!("Texture {} not found and no default texture! | 未找到纹理 {} 且没有默认纹理!", id, id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bind texture via backend.
|
||||
/// 通过后端绑定纹理。
|
||||
pub fn bind_texture_via_backend(&self, backend: &WebGL2Backend, id: u32, slot: u32) {
|
||||
let texture = if let Some(tex) = self.textures.get(&id) {
|
||||
Some(&tex.handle)
|
||||
} else {
|
||||
if id != 0 {
|
||||
log::warn!("Texture {} not found, using default | 未找到纹理 {},使用默认纹理", id, id);
|
||||
}
|
||||
self.default_texture.as_ref()
|
||||
};
|
||||
backend.bind_texture_raw(texture, slot);
|
||||
}
|
||||
|
||||
/// Check if texture is loaded.
|
||||
/// 检查纹理是否已加载。
|
||||
#[inline]
|
||||
pub fn has_texture(&self, id: u32) -> bool {
|
||||
self.textures.contains_key(&id)
|
||||
}
|
||||
|
||||
/// 获取纹理加载状态
|
||||
/// Get texture loading state
|
||||
///
|
||||
/// 返回纹理的当前加载状态:Loading、Ready 或 Failed。
|
||||
/// Returns the current loading state of the texture: Loading, Ready, or Failed.
|
||||
#[inline]
|
||||
pub fn get_texture_state(&self, id: u32) -> TextureState {
|
||||
// ID 0 是默认纹理,始终就绪
|
||||
// ID 0 is default texture, always ready
|
||||
if id == 0 {
|
||||
return TextureState::Ready;
|
||||
}
|
||||
|
||||
self.texture_states
|
||||
.borrow()
|
||||
.get(&id)
|
||||
.cloned()
|
||||
.unwrap_or(TextureState::Failed("Texture not found".to_string()))
|
||||
}
|
||||
|
||||
/// 检查纹理是否已就绪可用
|
||||
/// Check if texture is ready to use
|
||||
///
|
||||
/// 这是 `get_texture_state() == TextureState::Ready` 的便捷方法。
|
||||
/// This is a convenience method for `get_texture_state() == TextureState::Ready`.
|
||||
#[inline]
|
||||
pub fn is_texture_ready(&self, id: u32) -> bool {
|
||||
// ID 0 是默认纹理,始终就绪
|
||||
// ID 0 is default texture, always ready
|
||||
if id == 0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
matches!(
|
||||
self.texture_states.borrow().get(&id),
|
||||
Some(TextureState::Ready)
|
||||
)
|
||||
}
|
||||
|
||||
/// 获取正在加载中的纹理数量
|
||||
/// Get the number of textures currently loading
|
||||
#[inline]
|
||||
pub fn get_loading_count(&self) -> u32 {
|
||||
self.texture_states
|
||||
.borrow()
|
||||
.values()
|
||||
.filter(|s| matches!(s, TextureState::Loading))
|
||||
.count() as u32
|
||||
}
|
||||
|
||||
/// Remove texture.
|
||||
/// 移除纹理。
|
||||
pub fn remove_texture(&mut self, id: u32) {
|
||||
if let Some(texture) = self.textures.remove(&id) {
|
||||
self.gl.delete_texture(Some(&texture.handle));
|
||||
}
|
||||
// Also remove from path mapping | 同时从路径映射中移除
|
||||
self.path_to_id.retain(|_, &mut v| v != id);
|
||||
// Remove state | 移除状态
|
||||
self.texture_states.borrow_mut().remove(&id);
|
||||
// Remove dimensions | 移除尺寸
|
||||
self.texture_dimensions.borrow_mut().remove(&id);
|
||||
}
|
||||
|
||||
/// Load texture by path, returning texture ID.
|
||||
/// 按路径加载纹理,返回纹理ID。
|
||||
///
|
||||
/// If the texture is already loaded, returns existing ID.
|
||||
/// 如果纹理已加载,返回现有ID。
|
||||
pub fn load_texture_by_path(&mut self, path: &str) -> Result<u32> {
|
||||
// Check if already loaded | 检查是否已加载
|
||||
if let Some(&id) = self.path_to_id.get(path) {
|
||||
return Ok(id);
|
||||
}
|
||||
|
||||
// Assign new ID and load | 分配新ID并加载
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
|
||||
// Store path mapping first | 先存储路径映射
|
||||
self.path_to_id.insert(path.to_string(), id);
|
||||
|
||||
// Load texture with assigned ID | 用分配的ID加载纹理
|
||||
self.load_texture(id, path)?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Get texture ID by path.
|
||||
/// 按路径获取纹理ID。
|
||||
///
|
||||
/// Returns None if texture is not loaded.
|
||||
/// 如果纹理未加载,返回None。
|
||||
#[inline]
|
||||
pub fn get_texture_id_by_path(&self, path: &str) -> Option<u32> {
|
||||
self.path_to_id.get(path).copied()
|
||||
}
|
||||
|
||||
/// Get or load texture by path.
|
||||
/// 按路径获取或加载纹理。
|
||||
///
|
||||
/// If texture is already loaded, returns existing ID.
|
||||
/// If not loaded, loads it and returns new ID.
|
||||
/// 如果纹理已加载,返回现有ID。
|
||||
/// 如果未加载,加载它并返回新ID。
|
||||
pub fn get_or_load_by_path(&mut self, path: &str) -> Result<u32> {
|
||||
// Empty path means default texture | 空路径表示默认纹理
|
||||
if path.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
self.load_texture_by_path(path)
|
||||
}
|
||||
|
||||
/// Clear the path-to-ID cache.
|
||||
/// 清除路径到ID的缓存映射。
|
||||
///
|
||||
/// This should be called when restoring scene snapshots to ensure
|
||||
/// textures are reloaded with correct IDs.
|
||||
/// 在恢复场景快照时应调用此方法,以确保纹理使用正确的ID重新加载。
|
||||
pub fn clear_path_cache(&mut self) {
|
||||
self.path_to_id.clear();
|
||||
}
|
||||
|
||||
/// Clear all textures and reset state.
|
||||
/// 清除所有纹理并重置状态。
|
||||
///
|
||||
/// This removes all loaded textures from GPU memory and resets
|
||||
/// the ID counter. The default texture is preserved.
|
||||
/// 这会从GPU内存中移除所有已加载的纹理并重置ID计数器。默认纹理会被保留。
|
||||
pub fn clear_all(&mut self) {
|
||||
// Delete all textures from GPU | 从GPU删除所有纹理
|
||||
for (_, texture) in self.textures.drain() {
|
||||
self.gl.delete_texture(Some(&texture.handle));
|
||||
}
|
||||
|
||||
// Clear path mapping | 清除路径映射
|
||||
self.path_to_id.clear();
|
||||
|
||||
// Clear texture states | 清除纹理状态
|
||||
self.texture_states.borrow_mut().clear();
|
||||
|
||||
// Clear texture dimensions | 清除纹理尺寸
|
||||
self.texture_dimensions.borrow_mut().clear();
|
||||
|
||||
// Reset ID counter (1 is reserved for first texture, 0 for default)
|
||||
// 重置ID计数器(1保留给第一个纹理,0给默认纹理)
|
||||
self.next_id = 1;
|
||||
}
|
||||
|
||||
/// Create a blank texture with specified dimensions.
|
||||
/// 创建具有指定尺寸的空白纹理。
|
||||
///
|
||||
/// This is used for dynamic atlas creation where textures
|
||||
/// are later filled with content using `update_texture_region`.
|
||||
/// 用于动态图集创建,之后使用 `update_texture_region` 填充内容。
|
||||
///
|
||||
/// # Arguments | 参数
|
||||
/// * `width` - Texture width in pixels | 纹理宽度(像素)
|
||||
/// * `height` - Texture height in pixels | 纹理高度(像素)
|
||||
///
|
||||
/// # Returns | 返回
|
||||
/// The texture ID for the created texture | 创建的纹理ID
|
||||
pub fn create_blank_texture(&mut self, width: u32, height: u32) -> Result<u32> {
|
||||
let texture = self.gl
|
||||
.create_texture()
|
||||
.ok_or_else(|| EngineError::TextureLoadFailed("Failed to create blank texture".into()))?;
|
||||
|
||||
self.gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture));
|
||||
|
||||
// Initialize with transparent pixels
|
||||
// 使用透明像素初始化
|
||||
let _ = self.gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
|
||||
WebGl2RenderingContext::TEXTURE_2D,
|
||||
0,
|
||||
WebGl2RenderingContext::RGBA as i32,
|
||||
width as i32,
|
||||
height as i32,
|
||||
0,
|
||||
WebGl2RenderingContext::RGBA,
|
||||
WebGl2RenderingContext::UNSIGNED_BYTE,
|
||||
None, // NULL data - allocate but don't fill
|
||||
);
|
||||
|
||||
// Set texture parameters for atlas use
|
||||
// 设置图集使用的纹理参数
|
||||
self.gl.tex_parameteri(
|
||||
WebGl2RenderingContext::TEXTURE_2D,
|
||||
WebGl2RenderingContext::TEXTURE_WRAP_S,
|
||||
WebGl2RenderingContext::CLAMP_TO_EDGE as i32,
|
||||
);
|
||||
self.gl.tex_parameteri(
|
||||
WebGl2RenderingContext::TEXTURE_2D,
|
||||
WebGl2RenderingContext::TEXTURE_WRAP_T,
|
||||
WebGl2RenderingContext::CLAMP_TO_EDGE as i32,
|
||||
);
|
||||
self.gl.tex_parameteri(
|
||||
WebGl2RenderingContext::TEXTURE_2D,
|
||||
WebGl2RenderingContext::TEXTURE_MIN_FILTER,
|
||||
WebGl2RenderingContext::LINEAR as i32,
|
||||
);
|
||||
self.gl.tex_parameteri(
|
||||
WebGl2RenderingContext::TEXTURE_2D,
|
||||
WebGl2RenderingContext::TEXTURE_MAG_FILTER,
|
||||
WebGl2RenderingContext::LINEAR as i32,
|
||||
);
|
||||
|
||||
// Assign ID and store
|
||||
// 分配ID并存储
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
|
||||
self.textures.insert(id, Texture::new(texture, width, height));
|
||||
self.texture_states.borrow_mut().insert(id, TextureState::Ready);
|
||||
self.texture_dimensions.borrow_mut().insert(id, (width, height));
|
||||
|
||||
log::debug!("Created blank texture {} ({}x{}) | 创建空白纹理 {} ({}x{})", id, width, height, id, width, height);
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Update a region of an existing texture with pixel data.
|
||||
/// 使用像素数据更新现有纹理的区域。
|
||||
///
|
||||
/// This is used for dynamic atlas to copy individual textures
|
||||
/// into the atlas texture.
|
||||
/// 用于动态图集将单个纹理复制到图集纹理中。
|
||||
///
|
||||
/// # Arguments | 参数
|
||||
/// * `id` - The texture ID to update | 要更新的纹理ID
|
||||
/// * `x` - X offset in the texture | 纹理中的X偏移
|
||||
/// * `y` - Y offset in the texture | 纹理中的Y偏移
|
||||
/// * `width` - Width of the region to update | 要更新的区域宽度
|
||||
/// * `height` - Height of the region to update | 要更新的区域高度
|
||||
/// * `pixels` - RGBA pixel data (4 bytes per pixel) | RGBA像素数据(每像素4字节)
|
||||
///
|
||||
/// # Returns | 返回
|
||||
/// Ok(()) on success, Err if texture not found or update failed
|
||||
/// 成功时返回 Ok(()),纹理未找到或更新失败时返回 Err
|
||||
pub fn update_texture_region(
|
||||
&self,
|
||||
id: u32,
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
pixels: &[u8],
|
||||
) -> Result<()> {
|
||||
let texture = self.textures.get(&id)
|
||||
.ok_or_else(|| EngineError::TextureLoadFailed(format!("Texture {} not found", id)))?;
|
||||
|
||||
// Validate pixel data size
|
||||
// 验证像素数据大小
|
||||
let expected_size = (width * height * 4) as usize;
|
||||
if pixels.len() != expected_size {
|
||||
return Err(EngineError::TextureLoadFailed(format!(
|
||||
"Pixel data size mismatch: expected {}, got {} | 像素数据大小不匹配:预期 {},实际 {}",
|
||||
expected_size, pixels.len(), expected_size, pixels.len()
|
||||
)));
|
||||
}
|
||||
|
||||
self.gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture.handle));
|
||||
|
||||
// Use texSubImage2D to update a region
|
||||
// 使用 texSubImage2D 更新区域
|
||||
self.gl.tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_u8_array(
|
||||
WebGl2RenderingContext::TEXTURE_2D,
|
||||
0,
|
||||
x as i32,
|
||||
y as i32,
|
||||
width as i32,
|
||||
height as i32,
|
||||
WebGl2RenderingContext::RGBA,
|
||||
WebGl2RenderingContext::UNSIGNED_BYTE,
|
||||
Some(pixels),
|
||||
).map_err(|e| EngineError::TextureLoadFailed(format!("texSubImage2D failed: {:?}", e)))?;
|
||||
|
||||
log::trace!("Updated texture {} region ({},{}) {}x{} | 更新纹理 {} 区域 ({},{}) {}x{}",
|
||||
id, x, y, width, height, id, x, y, width, height);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
259
packages/rust/engine/src/renderer/viewport.rs
Normal file
259
packages/rust/engine/src/renderer/viewport.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
//! Viewport and RenderTarget management for multi-view rendering.
|
||||
//! 多视图渲染的视口和渲染目标管理。
|
||||
|
||||
use std::collections::HashMap;
|
||||
use web_sys::{HtmlCanvasElement, WebGl2RenderingContext};
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
use super::camera::Camera2D;
|
||||
use crate::core::error::{EngineError, Result};
|
||||
|
||||
/// Viewport configuration and settings.
|
||||
/// 视口配置和设置。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ViewportConfig {
|
||||
/// Whether to show grid overlay.
|
||||
pub show_grid: bool,
|
||||
/// Whether to show gizmos.
|
||||
pub show_gizmos: bool,
|
||||
/// Clear color (RGBA).
|
||||
pub clear_color: [f32; 4],
|
||||
}
|
||||
|
||||
impl Default for ViewportConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
show_grid: true,
|
||||
show_gizmos: true,
|
||||
clear_color: [0.1, 0.1, 0.12, 1.0],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A render target representing a viewport.
|
||||
/// 表示视口的渲染目标。
|
||||
pub struct RenderTarget {
|
||||
/// Unique identifier for this viewport.
|
||||
pub id: String,
|
||||
/// The canvas element this viewport renders to.
|
||||
canvas: HtmlCanvasElement,
|
||||
/// WebGL context for this canvas.
|
||||
gl: WebGl2RenderingContext,
|
||||
/// Camera for this viewport.
|
||||
pub camera: Camera2D,
|
||||
/// Viewport configuration.
|
||||
pub config: ViewportConfig,
|
||||
/// Width in pixels.
|
||||
width: u32,
|
||||
/// Height in pixels.
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl RenderTarget {
|
||||
/// Create a new render target from a canvas ID.
|
||||
pub fn new(id: &str, canvas_id: &str) -> Result<Self> {
|
||||
let window = web_sys::window().expect("No window found");
|
||||
let document = window.document().expect("No document found");
|
||||
|
||||
let canvas = document
|
||||
.get_element_by_id(canvas_id)
|
||||
.ok_or_else(|| EngineError::CanvasNotFound(canvas_id.to_string()))?
|
||||
.dyn_into::<HtmlCanvasElement>()
|
||||
.map_err(|_| EngineError::CanvasNotFound(canvas_id.to_string()))?;
|
||||
|
||||
let gl = canvas
|
||||
.get_context("webgl2")
|
||||
.map_err(|_| EngineError::ContextCreationFailed)?
|
||||
.ok_or(EngineError::ContextCreationFailed)?
|
||||
.dyn_into::<WebGl2RenderingContext>()
|
||||
.map_err(|_| EngineError::ContextCreationFailed)?;
|
||||
|
||||
let width = canvas.width();
|
||||
let height = canvas.height();
|
||||
let camera = Camera2D::new(width as f32, height as f32);
|
||||
|
||||
log::info!(
|
||||
"RenderTarget created: {} ({}x{})",
|
||||
id, width, height
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
id: id.to_string(),
|
||||
canvas,
|
||||
gl,
|
||||
camera,
|
||||
config: ViewportConfig::default(),
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the WebGL context.
|
||||
#[inline]
|
||||
pub fn gl(&self) -> &WebGl2RenderingContext {
|
||||
&self.gl
|
||||
}
|
||||
|
||||
/// Get canvas reference.
|
||||
#[inline]
|
||||
pub fn canvas(&self) -> &HtmlCanvasElement {
|
||||
&self.canvas
|
||||
}
|
||||
|
||||
/// Get viewport dimensions.
|
||||
#[inline]
|
||||
pub fn dimensions(&self) -> (u32, u32) {
|
||||
(self.width, self.height)
|
||||
}
|
||||
|
||||
/// Resize the viewport.
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
self.canvas.set_width(width);
|
||||
self.canvas.set_height(height);
|
||||
self.gl.viewport(0, 0, width as i32, height as i32);
|
||||
self.camera.set_viewport(width as f32, height as f32);
|
||||
}
|
||||
|
||||
/// Clear the viewport with configured color.
|
||||
pub fn clear(&self) {
|
||||
let [r, g, b, a] = self.config.clear_color;
|
||||
self.gl.clear_color(r, g, b, a);
|
||||
self.gl.clear(
|
||||
WebGl2RenderingContext::COLOR_BUFFER_BIT | WebGl2RenderingContext::DEPTH_BUFFER_BIT,
|
||||
);
|
||||
}
|
||||
|
||||
/// Make this render target current (bind its context).
|
||||
pub fn bind(&self) {
|
||||
self.gl.viewport(0, 0, self.width as i32, self.height as i32);
|
||||
}
|
||||
|
||||
/// Set camera parameters.
|
||||
pub fn set_camera(&mut self, x: f32, y: f32, zoom: f32, rotation: f32) {
|
||||
self.camera.position.x = x;
|
||||
self.camera.position.y = y;
|
||||
self.camera.set_zoom(zoom);
|
||||
self.camera.rotation = rotation;
|
||||
}
|
||||
|
||||
/// Get camera parameters.
|
||||
pub fn get_camera(&self) -> (f32, f32, f32, f32) {
|
||||
(
|
||||
self.camera.position.x,
|
||||
self.camera.position.y,
|
||||
self.camera.zoom,
|
||||
self.camera.rotation,
|
||||
)
|
||||
}
|
||||
|
||||
/// Set clear color (RGBA, each component 0.0-1.0).
|
||||
pub fn set_clear_color(&mut self, r: f32, g: f32, b: f32, a: f32) {
|
||||
self.config.clear_color = [r, g, b, a];
|
||||
}
|
||||
|
||||
/// Get clear color.
|
||||
pub fn get_clear_color(&self) -> [f32; 4] {
|
||||
self.config.clear_color
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages multiple viewports for the engine.
|
||||
/// 管理引擎的多个视口。
|
||||
pub struct ViewportManager {
|
||||
/// All registered viewports.
|
||||
viewports: HashMap<String, RenderTarget>,
|
||||
/// Currently active viewport ID.
|
||||
active_viewport: Option<String>,
|
||||
}
|
||||
|
||||
impl ViewportManager {
|
||||
/// Create a new viewport manager.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
viewports: HashMap::new(),
|
||||
active_viewport: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a new viewport.
|
||||
pub fn register(&mut self, id: &str, canvas_id: &str) -> Result<()> {
|
||||
if self.viewports.contains_key(id) {
|
||||
log::warn!("Viewport already registered: {}", id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let target = RenderTarget::new(id, canvas_id)?;
|
||||
self.viewports.insert(id.to_string(), target);
|
||||
|
||||
// Set as active if it's the first viewport
|
||||
if self.active_viewport.is_none() {
|
||||
self.active_viewport = Some(id.to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unregister a viewport.
|
||||
pub fn unregister(&mut self, id: &str) {
|
||||
self.viewports.remove(id);
|
||||
if self.active_viewport.as_deref() == Some(id) {
|
||||
self.active_viewport = self.viewports.keys().next().cloned();
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the active viewport.
|
||||
pub fn set_active(&mut self, id: &str) -> bool {
|
||||
if self.viewports.contains_key(id) {
|
||||
self.active_viewport = Some(id.to_string());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the active viewport.
|
||||
pub fn active(&self) -> Option<&RenderTarget> {
|
||||
self.active_viewport
|
||||
.as_ref()
|
||||
.and_then(|id| self.viewports.get(id))
|
||||
}
|
||||
|
||||
/// Get mutable active viewport.
|
||||
pub fn active_mut(&mut self) -> Option<&mut RenderTarget> {
|
||||
let id = self.active_viewport.clone()?;
|
||||
self.viewports.get_mut(&id)
|
||||
}
|
||||
|
||||
/// Get a viewport by ID.
|
||||
pub fn get(&self, id: &str) -> Option<&RenderTarget> {
|
||||
self.viewports.get(id)
|
||||
}
|
||||
|
||||
/// Get mutable viewport by ID.
|
||||
pub fn get_mut(&mut self, id: &str) -> Option<&mut RenderTarget> {
|
||||
self.viewports.get_mut(id)
|
||||
}
|
||||
|
||||
/// Get all viewport IDs.
|
||||
pub fn viewport_ids(&self) -> Vec<&String> {
|
||||
self.viewports.keys().collect()
|
||||
}
|
||||
|
||||
/// Iterate over all viewports.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&String, &RenderTarget)> {
|
||||
self.viewports.iter()
|
||||
}
|
||||
|
||||
/// Iterate over all viewports mutably.
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&String, &mut RenderTarget)> {
|
||||
self.viewports.iter_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ViewportManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
55
packages/rust/engine/src/resource/handle.rs
Normal file
55
packages/rust/engine/src/resource/handle.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
//! Resource handle types.
|
||||
//! 资源句柄类型。
|
||||
|
||||
/// Type alias for resource handle IDs.
|
||||
/// 资源句柄ID的类型别名。
|
||||
pub type HandleId = u32;
|
||||
|
||||
/// Generic resource handle.
|
||||
/// 通用资源句柄。
|
||||
///
|
||||
/// A lightweight identifier for referencing loaded resources.
|
||||
/// 用于引用已加载资源的轻量级标识符。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Handle<T> {
|
||||
/// Unique identifier.
|
||||
/// 唯一标识符。
|
||||
id: HandleId,
|
||||
|
||||
/// Phantom data for type safety.
|
||||
/// 用于类型安全的幻象数据。
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> Handle<T> {
|
||||
/// Create a new handle with the given ID.
|
||||
/// 使用给定ID创建新句柄。
|
||||
#[inline]
|
||||
pub const fn new(id: HandleId) -> Self {
|
||||
Self {
|
||||
id,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the handle ID.
|
||||
/// 获取句柄ID。
|
||||
#[inline]
|
||||
pub const fn id(&self) -> HandleId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<HandleId> for Handle<T> {
|
||||
#[inline]
|
||||
fn from(id: HandleId) -> Self {
|
||||
Self::new(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Handle<T>> for HandleId {
|
||||
#[inline]
|
||||
fn from(handle: Handle<T>) -> Self {
|
||||
handle.id
|
||||
}
|
||||
}
|
||||
7
packages/rust/engine/src/resource/mod.rs
Normal file
7
packages/rust/engine/src/resource/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
//! Resource management system.
|
||||
//! 资源管理系统。
|
||||
|
||||
mod handle;
|
||||
|
||||
pub use handle::{Handle, HandleId};
|
||||
pub use crate::renderer::texture::{Texture, TextureManager};
|
||||
Reference in New Issue
Block a user