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:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View 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;

File diff suppressed because it is too large Load Diff

View 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,
);
}
}

File diff suppressed because it is too large Load Diff

View 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>;

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

View 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();
}
}

View 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();
}
}

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

View 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];
}
}

View 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();
}
}

File diff suppressed because it is too large Load Diff

View 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).
/// 转换为打包的 u320xRRGGBBAA 格式,行业标准)。
#[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]
}
}

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

View 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,
)
}
}

View 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);
}
}

View 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 }
}
}

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

View 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));
}
}

View 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);
}
}

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

View 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);
}
}

View 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);
}
}

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

View File

@@ -0,0 +1,147 @@
//! Vertex data structures for 3D rendering.
//! 用于3D渲染的顶点数据结构。
use bytemuck::{Pod, Zeroable};
/// Size of a single 3D vertex in bytes.
/// 单个3D顶点的字节大小。
pub const VERTEX3D_SIZE: usize = std::mem::size_of::<Vertex3D>();
/// Number of floats per 3D vertex.
/// 每个3D顶点的浮点数数量。
///
/// Layout: position(3) + tex_coord(2) + color(4) + normal(3) = 12
/// 布局: 位置(3) + 纹理坐标(2) + 颜色(4) + 法线(3) = 12
pub const FLOATS_PER_VERTEX_3D: usize = 12;
/// 3D vertex data.
/// 3D顶点数据。
///
/// Used for mesh rendering with optional lighting support.
/// 用于带可选光照支持的网格渲染。
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct Vertex3D {
/// Position (x, y, z) in world or local space.
/// 位置(世界或局部空间)。
pub position: [f32; 3],
/// Texture coordinates (u, v).
/// 纹理坐标。
pub tex_coord: [f32; 2],
/// Color (r, g, b, a).
/// 颜色。
pub color: [f32; 4],
/// Normal vector (nx, ny, nz) for lighting.
/// 用于光照的法线向量。
pub normal: [f32; 3],
}
impl Vertex3D {
/// Create a new 3D vertex.
/// 创建新的3D顶点。
#[inline]
pub const fn new(
position: [f32; 3],
tex_coord: [f32; 2],
color: [f32; 4],
normal: [f32; 3],
) -> Self {
Self {
position,
tex_coord,
color,
normal,
}
}
/// Create a simple vertex without normal (for unlit rendering).
/// 创建不带法线的简单顶点(用于无光照渲染)。
#[inline]
pub const fn simple(position: [f32; 3], tex_coord: [f32; 2], color: [f32; 4]) -> Self {
Self {
position,
tex_coord,
color,
normal: [0.0, 0.0, 1.0], // Default facing +Z
}
}
}
impl Default for Vertex3D {
fn default() -> Self {
Self {
position: [0.0, 0.0, 0.0],
tex_coord: [0.0, 0.0],
color: [1.0, 1.0, 1.0, 1.0],
normal: [0.0, 0.0, 1.0],
}
}
}
/// Simplified 3D vertex without normal (for unlit/billboard rendering).
/// 简化的3D顶点不带法线用于无光照/公告板渲染)。
///
/// Layout: position(3) + tex_coord(2) + color(4) = 9
/// 布局: 位置(3) + 纹理坐标(2) + 颜色(4) = 9
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct SimpleVertex3D {
/// Position (x, y, z).
/// 位置。
pub position: [f32; 3],
/// Texture coordinates (u, v).
/// 纹理坐标。
pub tex_coord: [f32; 2],
/// Color (r, g, b, a).
/// 颜色。
pub color: [f32; 4],
}
/// Size of a simple 3D vertex in bytes.
/// 简单3D顶点的字节大小。
pub const SIMPLE_VERTEX3D_SIZE: usize = std::mem::size_of::<SimpleVertex3D>();
/// Number of floats per simple 3D vertex.
/// 每个简单3D顶点的浮点数数量。
pub const FLOATS_PER_SIMPLE_VERTEX_3D: usize = 9;
impl SimpleVertex3D {
/// Create a new simple 3D vertex.
/// 创建新的简单3D顶点。
#[inline]
pub const fn new(position: [f32; 3], tex_coord: [f32; 2], color: [f32; 4]) -> Self {
Self {
position,
tex_coord,
color,
}
}
}
impl Default for SimpleVertex3D {
fn default() -> Self {
Self {
position: [0.0, 0.0, 0.0],
tex_coord: [0.0, 0.0],
color: [1.0, 1.0, 1.0, 1.0],
}
}
}
/// Convert SimpleVertex3D to Vertex3D with default normal.
/// 将SimpleVertex3D转换为带默认法线的Vertex3D。
impl From<SimpleVertex3D> for Vertex3D {
fn from(v: SimpleVertex3D) -> Self {
Self {
position: v.position,
tex_coord: v.tex_coord,
color: v.color,
normal: [0.0, 0.0, 1.0],
}
}
}

View File

@@ -0,0 +1,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] | NDCY向上原点在中心
/// - 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)
}
}

View File

@@ -0,0 +1,414 @@
//! 3D camera implementation.
//! 3D相机实现。
//!
//! Uses left-hand coordinate system convention (consistent with Camera2D):
//! 使用左手坐标系约定与Camera2D一致
//! - X axis: positive to the right / X 轴:正方向向右
//! - Y axis: positive upward / Y 轴:正方向向上
//! - Z axis: positive into the screen / Z 轴:正方向指向屏幕内
//!
//! Supports both perspective and orthographic projection.
//! 支持透视和正交两种投影模式。
use glam::{Mat4, Quat, Vec2, Vec3};
/// Projection type for 3D camera.
/// 3D相机的投影类型。
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ProjectionType {
/// Perspective projection with field of view.
/// 带视野角的透视投影。
Perspective,
/// Orthographic projection with fixed size.
/// 固定大小的正交投影。
Orthographic {
/// Half-height of the view in world units.
/// 视图半高度(世界单位)。
size: f32,
},
}
impl Default for ProjectionType {
fn default() -> Self {
ProjectionType::Perspective
}
}
/// 3D ray for raycasting.
/// 用于射线检测的3D射线。
#[derive(Debug, Clone, Copy)]
pub struct Ray3D {
/// Ray origin in world space.
/// 射线在世界空间中的起点。
pub origin: Vec3,
/// Ray direction (normalized).
/// 射线方向(已归一化)。
pub direction: Vec3,
}
impl Ray3D {
/// Create a new ray.
/// 创建新射线。
pub fn new(origin: Vec3, direction: Vec3) -> Self {
Self {
origin,
direction: direction.normalize(),
}
}
/// Get point along the ray at distance t.
/// 获取射线上距离为t的点。
#[inline]
pub fn point_at(&self, t: f32) -> Vec3 {
self.origin + self.direction * t
}
}
/// 3D camera supporting perspective and orthographic projection.
/// 支持透视和正交投影的3D相机。
///
/// Provides view, projection, and combined matrices for 3D rendering.
/// 提供用于3D渲染的视图、投影和组合矩阵。
#[derive(Debug, Clone)]
pub struct Camera3D {
/// Camera position in world space.
/// 相机在世界空间中的位置。
pub position: Vec3,
/// Camera rotation as quaternion.
/// 相机旋转(四元数)。
pub rotation: Quat,
/// Field of view in radians (for perspective projection).
/// 视野角(弧度,用于透视投影)。
pub fov: f32,
/// Near clipping plane distance.
/// 近裁剪面距离。
pub near: f32,
/// Far clipping plane distance.
/// 远裁剪面距离。
pub far: f32,
/// Aspect ratio (width / height).
/// 宽高比(宽度 / 高度)。
pub aspect: f32,
/// Projection type (perspective or orthographic).
/// 投影类型(透视或正交)。
pub projection_type: ProjectionType,
/// Viewport width in pixels.
/// 视口宽度(像素)。
viewport_width: f32,
/// Viewport height in pixels.
/// 视口高度(像素)。
viewport_height: f32,
}
impl Camera3D {
/// Create a new 3D perspective camera.
/// 创建新的3D透视相机。
///
/// # Arguments | 参数
/// * `width` - Viewport width | 视口宽度
/// * `height` - Viewport height | 视口高度
/// * `fov` - Field of view in radians | 视野角(弧度)
pub fn new(width: f32, height: f32, fov: f32) -> Self {
Self {
position: Vec3::new(0.0, 0.0, -10.0),
rotation: Quat::IDENTITY,
fov,
near: 0.1,
far: 1000.0,
aspect: width / height,
projection_type: ProjectionType::Perspective,
viewport_width: width,
viewport_height: height,
}
}
/// Create a new orthographic camera.
/// 创建新的正交相机。
///
/// # Arguments | 参数
/// * `width` - Viewport width | 视口宽度
/// * `height` - Viewport height | 视口高度
/// * `size` - Orthographic half-height | 正交视图半高度
pub fn new_orthographic(width: f32, height: f32, size: f32) -> Self {
Self {
position: Vec3::new(0.0, 0.0, -10.0),
rotation: Quat::IDENTITY,
fov: std::f32::consts::FRAC_PI_4,
near: 0.1,
far: 1000.0,
aspect: width / height,
projection_type: ProjectionType::Orthographic { size },
viewport_width: width,
viewport_height: height,
}
}
/// Update viewport size.
/// 更新视口大小。
pub fn set_viewport(&mut self, width: f32, height: f32) {
self.viewport_width = width;
self.viewport_height = height;
self.aspect = width / height;
}
/// Get the view matrix.
/// 获取视图矩阵。
///
/// Transforms world coordinates to camera/view space.
/// 将世界坐标转换为相机/视图空间。
pub fn view_matrix(&self) -> Mat4 {
// Camera forward is +Z in left-hand system
// 左手系中相机前方是 +Z
let forward = self.rotation * Vec3::Z;
let up = self.rotation * Vec3::Y;
let target = self.position + forward;
Mat4::look_at_lh(self.position, target, up)
}
/// Get the projection matrix.
/// 获取投影矩阵。
///
/// Transforms view space coordinates to clip space.
/// 将视图空间坐标转换为裁剪空间。
pub fn projection_matrix(&self) -> Mat4 {
match self.projection_type {
ProjectionType::Perspective => {
Mat4::perspective_lh(self.fov, self.aspect, self.near, self.far)
}
ProjectionType::Orthographic { size } => {
let half_width = size * self.aspect;
let half_height = size;
Mat4::orthographic_lh(
-half_width,
half_width,
-half_height,
half_height,
self.near,
self.far,
)
}
}
}
/// Get the combined view-projection matrix.
/// 获取组合的视图-投影矩阵。
///
/// Transforms world coordinates directly to clip space.
/// 将世界坐标直接转换为裁剪空间。
#[inline]
pub fn view_projection_matrix(&self) -> Mat4 {
self.projection_matrix() * self.view_matrix()
}
/// Convert screen coordinates to a world-space ray.
/// 将屏幕坐标转换为世界空间射线。
///
/// Screen: (0,0) at top-left, Y-down | 屏幕:(0,0)在左上角Y向下
/// Returns a ray from the camera through the screen point.
/// 返回从相机穿过屏幕点的射线。
pub fn screen_to_world_ray(&self, screen: Vec2) -> Ray3D {
// Convert screen to NDC [-1, 1]
// 将屏幕坐标转换为NDC [-1, 1]
let ndc_x = (2.0 * screen.x / self.viewport_width) - 1.0;
let ndc_y = 1.0 - (2.0 * screen.y / self.viewport_height); // Flip Y
// Get inverse matrices
// 获取逆矩阵
let inv_proj = self.projection_matrix().inverse();
let inv_view = self.view_matrix().inverse();
match self.projection_type {
ProjectionType::Perspective => {
// For perspective: ray from camera through near plane point
// 透视模式:从相机穿过近平面点的射线
let ray_clip = glam::Vec4::new(ndc_x, ndc_y, 0.0, 1.0);
let ray_eye = inv_proj * ray_clip;
let ray_eye = glam::Vec4::new(ray_eye.x, ray_eye.y, 1.0, 0.0); // Forward direction
let ray_world = inv_view * ray_eye;
let direction = Vec3::new(ray_world.x, ray_world.y, ray_world.z).normalize();
Ray3D::new(self.position, direction)
}
ProjectionType::Orthographic { size } => {
// For orthographic: parallel rays, origin varies
// 正交模式:平行射线,起点变化
let half_width = size * self.aspect;
let half_height = size;
let local_x = ndc_x * half_width;
let local_y = ndc_y * half_height;
// Ray origin in world space
// 世界空间中的射线起点
let right = self.rotation * Vec3::X;
let up = self.rotation * Vec3::Y;
let forward = self.rotation * Vec3::Z;
let origin = self.position + right * local_x + up * local_y;
Ray3D::new(origin, forward)
}
}
}
/// Convert world coordinates to screen coordinates.
/// 将世界坐标转换为屏幕坐标。
///
/// Returns None if the point is behind the camera.
/// 如果点在相机后面则返回None。
pub fn world_to_screen(&self, world: Vec3) -> Option<Vec2> {
let clip = self.view_projection_matrix() * world.extend(1.0);
// Check if behind camera (for perspective)
// 检查是否在相机后面(透视模式)
if clip.w <= 0.0 {
return None;
}
// Perspective divide
// 透视除法
let ndc = clip.truncate() / clip.w;
// Check if outside frustum
// 检查是否在视锥外
if ndc.x < -1.0 || ndc.x > 1.0 || ndc.y < -1.0 || ndc.y > 1.0 {
return None;
}
// Convert NDC to screen coordinates
// 将NDC转换为屏幕坐标
let screen_x = (ndc.x + 1.0) * 0.5 * self.viewport_width;
let screen_y = (1.0 - ndc.y) * 0.5 * self.viewport_height; // Flip Y
Some(Vec2::new(screen_x, screen_y))
}
/// Set position from Euler angles (in radians).
/// 从欧拉角设置旋转(弧度)。
///
/// Uses XYZ rotation order.
/// 使用XYZ旋转顺序。
pub fn set_rotation_euler(&mut self, pitch: f32, yaw: f32, roll: f32) {
self.rotation = Quat::from_euler(glam::EulerRot::XYZ, pitch, yaw, roll);
}
/// Get Euler angles from current rotation (in radians).
/// 从当前旋转获取欧拉角(弧度)。
///
/// Returns (pitch, yaw, roll) in XYZ order.
/// 返回 (pitch, yaw, roll) 以XYZ顺序。
pub fn get_rotation_euler(&self) -> (f32, f32, f32) {
self.rotation.to_euler(glam::EulerRot::XYZ)
}
/// Move camera by delta in local space.
/// 在局部空间中按增量移动相机。
pub fn translate_local(&mut self, delta: Vec3) {
let world_delta = self.rotation * delta;
self.position += world_delta;
}
/// Move camera by delta in world space.
/// 在世界空间中按增量移动相机。
#[inline]
pub fn translate(&mut self, delta: Vec3) {
self.position += delta;
}
/// Get the forward direction vector.
/// 获取前方方向向量。
#[inline]
pub fn forward(&self) -> Vec3 {
self.rotation * Vec3::Z
}
/// Get the right direction vector.
/// 获取右方方向向量。
#[inline]
pub fn right(&self) -> Vec3 {
self.rotation * Vec3::X
}
/// Get the up direction vector.
/// 获取上方方向向量。
#[inline]
pub fn up(&self) -> Vec3 {
self.rotation * Vec3::Y
}
/// Look at a target position.
/// 朝向目标位置。
pub fn look_at(&mut self, target: Vec3, up: Vec3) {
let forward = (target - self.position).normalize();
let right = up.cross(forward).normalize();
let actual_up = forward.cross(right);
// Build rotation matrix and convert to quaternion
// 构建旋转矩阵并转换为四元数
let rotation_matrix = Mat4::from_cols(
right.extend(0.0),
actual_up.extend(0.0),
forward.extend(0.0),
glam::Vec4::W,
);
self.rotation = Quat::from_mat4(&rotation_matrix);
}
#[inline]
pub fn viewport_width(&self) -> f32 {
self.viewport_width
}
#[inline]
pub fn viewport_height(&self) -> f32 {
self.viewport_height
}
}
impl Default for Camera3D {
fn default() -> Self {
Self::new(800.0, 600.0, std::f32::consts::FRAC_PI_4)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_camera_creation() {
let camera = Camera3D::new(800.0, 600.0, std::f32::consts::FRAC_PI_4);
assert_eq!(camera.position, Vec3::new(0.0, 0.0, -10.0));
assert!((camera.aspect - 800.0 / 600.0).abs() < 0.001);
}
#[test]
fn test_view_projection() {
let camera = Camera3D::new(800.0, 600.0, std::f32::consts::FRAC_PI_4);
let vp = camera.view_projection_matrix();
// Basic sanity check: matrix should not be identity
assert_ne!(vp, Mat4::IDENTITY);
}
#[test]
fn test_world_to_screen_center() {
let mut camera = Camera3D::new(800.0, 600.0, std::f32::consts::FRAC_PI_4);
camera.position = Vec3::new(0.0, 0.0, -10.0);
camera.rotation = Quat::IDENTITY;
// Point directly in front of camera should map to center
// 相机正前方的点应该映射到中心
let screen = camera.world_to_screen(Vec3::new(0.0, 0.0, 0.0));
if let Some(s) = screen {
assert!((s.x - 400.0).abs() < 1.0);
assert!((s.y - 300.0).abs() < 1.0);
}
}
}

View File

@@ -0,0 +1,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);
}
}

View File

@@ -0,0 +1,577 @@
//! 3D Gizmo renderer for editor overlays.
//! 编辑器 3D Gizmo 渲染器。
//!
//! Provides transform handles for 3D editing:
//! - Translation: XYZ axis arrows with plane handles
//! - Rotation: XYZ rotation circles
//! - Scale: XYZ axis with cube handles
//!
//! 提供 3D 编辑的变换手柄:
//! - 平移XYZ 轴箭头和平面手柄
//! - 旋转XYZ 旋转圆环
//! - 缩放XYZ 轴和立方体手柄
use es_engine_shared::{
traits::backend::{GraphicsBackend, BufferUsage},
types::{
handle::{ShaderHandle, BufferHandle, VertexArrayHandle},
vertex::{VertexLayout, VertexAttribute, VertexAttributeType},
blend::BlendMode,
},
Vec3, Mat4,
};
use super::camera3d::Camera3D;
use super::gizmo::TransformMode;
use std::f32::consts::PI;
const GIZMO3D_VERTEX_SHADER: &str = r#"#version 300 es
precision highp float;
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec4 a_color;
uniform mat4 u_viewProjection;
uniform mat4 u_model;
out vec4 v_color;
void main() {
gl_Position = u_viewProjection * u_model * vec4(a_position, 1.0);
v_color = a_color;
}
"#;
const GIZMO3D_FRAGMENT_SHADER: &str = r#"#version 300 es
precision highp float;
in vec4 v_color;
out vec4 fragColor;
void main() {
fragColor = v_color;
}
"#;
// Axis colors | 轴颜色
const X_AXIS_COLOR: [f32; 4] = [1.0, 0.3, 0.3, 1.0]; // Red
const Y_AXIS_COLOR: [f32; 4] = [0.3, 1.0, 0.3, 1.0]; // Green
const Z_AXIS_COLOR: [f32; 4] = [0.3, 0.3, 1.0, 1.0]; // Blue
const XY_PLANE_COLOR: [f32; 4] = [1.0, 1.0, 0.3, 0.5]; // Yellow (XY plane)
const XZ_PLANE_COLOR: [f32; 4] = [1.0, 0.3, 1.0, 0.5]; // Magenta (XZ plane)
const YZ_PLANE_COLOR: [f32; 4] = [0.3, 1.0, 1.0, 0.5]; // Cyan (YZ plane)
const CENTER_COLOR: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; // White center
/// 3D Gizmo renderer for transform handles.
/// 用于变换手柄的 3D Gizmo 渲染器。
pub struct Gizmo3DRenderer {
shader: ShaderHandle,
// Translation gizmo
translate_vbo: BufferHandle,
translate_vao: VertexArrayHandle,
translate_line_count: u32,
translate_arrow_vbo: BufferHandle,
translate_arrow_vao: VertexArrayHandle,
translate_arrow_count: u32,
// Rotation gizmo
rotate_vbo: BufferHandle,
rotate_vao: VertexArrayHandle,
rotate_vertex_count: u32,
// Scale gizmo
scale_vbo: BufferHandle,
scale_vao: VertexArrayHandle,
scale_line_count: u32,
scale_cube_vbo: BufferHandle,
scale_cube_vao: VertexArrayHandle,
scale_cube_count: u32,
// Center sphere
center_vbo: BufferHandle,
center_vao: VertexArrayHandle,
center_vertex_count: u32,
// Plane handles for translation
plane_vbo: BufferHandle,
plane_vao: VertexArrayHandle,
plane_vertex_count: u32,
/// Current transform mode
transform_mode: TransformMode,
/// Gizmo size (in world units at distance 1)
gizmo_size: f32,
}
impl Gizmo3DRenderer {
/// Create a new 3D gizmo renderer.
/// 创建新的 3D Gizmo 渲染器。
pub fn new(backend: &mut impl GraphicsBackend) -> Result<Self, String> {
let shader = backend.compile_shader(GIZMO3D_VERTEX_SHADER, GIZMO3D_FRAGMENT_SHADER)
.map_err(|e| format!("3D Gizmo shader: {:?}", e))?;
let layout = VertexLayout {
attributes: vec![
VertexAttribute {
name: "a_position".into(),
attr_type: VertexAttributeType::Float3,
offset: 0,
normalized: false,
},
VertexAttribute {
name: "a_color".into(),
attr_type: VertexAttributeType::Float4,
offset: 12,
normalized: false,
},
],
stride: 28,
};
let gizmo_size = 1.0;
// Translation axis lines
let translate_verts = Self::generate_axis_lines(gizmo_size);
let translate_line_count = (translate_verts.len() / 7) as u32;
let translate_vbo = backend.create_vertex_buffer(
bytemuck::cast_slice(&translate_verts),
BufferUsage::Static,
).map_err(|e| format!("Translate VBO: {:?}", e))?;
let translate_vao = backend.create_vertex_array(translate_vbo, None, &layout)
.map_err(|e| format!("Translate VAO: {:?}", e))?;
// Translation arrow cones
let arrow_verts = Self::generate_arrow_cones(gizmo_size, 16);
let translate_arrow_count = (arrow_verts.len() / 7) as u32;
let translate_arrow_vbo = backend.create_vertex_buffer(
bytemuck::cast_slice(&arrow_verts),
BufferUsage::Static,
).map_err(|e| format!("Arrow VBO: {:?}", e))?;
let translate_arrow_vao = backend.create_vertex_array(translate_arrow_vbo, None, &layout)
.map_err(|e| format!("Arrow VAO: {:?}", e))?;
// Rotation circles
let rotate_verts = Self::generate_rotation_circles(gizmo_size * 0.8, 48);
let rotate_vertex_count = (rotate_verts.len() / 7) as u32;
let rotate_vbo = backend.create_vertex_buffer(
bytemuck::cast_slice(&rotate_verts),
BufferUsage::Static,
).map_err(|e| format!("Rotate VBO: {:?}", e))?;
let rotate_vao = backend.create_vertex_array(rotate_vbo, None, &layout)
.map_err(|e| format!("Rotate VAO: {:?}", e))?;
// Scale axis lines (same as translate for now)
let scale_verts = Self::generate_axis_lines(gizmo_size);
let scale_line_count = (scale_verts.len() / 7) as u32;
let scale_vbo = backend.create_vertex_buffer(
bytemuck::cast_slice(&scale_verts),
BufferUsage::Static,
).map_err(|e| format!("Scale VBO: {:?}", e))?;
let scale_vao = backend.create_vertex_array(scale_vbo, None, &layout)
.map_err(|e| format!("Scale VAO: {:?}", e))?;
// Scale cubes at end of axes
let cube_verts = Self::generate_scale_cubes(gizmo_size, 0.08);
let scale_cube_count = (cube_verts.len() / 7) as u32;
let scale_cube_vbo = backend.create_vertex_buffer(
bytemuck::cast_slice(&cube_verts),
BufferUsage::Static,
).map_err(|e| format!("Scale cube VBO: {:?}", e))?;
let scale_cube_vao = backend.create_vertex_array(scale_cube_vbo, None, &layout)
.map_err(|e| format!("Scale cube VAO: {:?}", e))?;
// Center sphere
let center_verts = Self::generate_center_sphere(0.08, 12);
let center_vertex_count = (center_verts.len() / 7) as u32;
let center_vbo = backend.create_vertex_buffer(
bytemuck::cast_slice(&center_verts),
BufferUsage::Static,
).map_err(|e| format!("Center VBO: {:?}", e))?;
let center_vao = backend.create_vertex_array(center_vbo, None, &layout)
.map_err(|e| format!("Center VAO: {:?}", e))?;
// Plane handles
let plane_verts = Self::generate_plane_handles(gizmo_size * 0.3);
let plane_vertex_count = (plane_verts.len() / 7) as u32;
let plane_vbo = backend.create_vertex_buffer(
bytemuck::cast_slice(&plane_verts),
BufferUsage::Static,
).map_err(|e| format!("Plane VBO: {:?}", e))?;
let plane_vao = backend.create_vertex_array(plane_vbo, None, &layout)
.map_err(|e| format!("Plane VAO: {:?}", e))?;
Ok(Self {
shader,
translate_vbo,
translate_vao,
translate_line_count,
translate_arrow_vbo,
translate_arrow_vao,
translate_arrow_count,
rotate_vbo,
rotate_vao,
rotate_vertex_count,
scale_vbo,
scale_vao,
scale_line_count,
scale_cube_vbo,
scale_cube_vao,
scale_cube_count,
center_vbo,
center_vao,
center_vertex_count,
plane_vbo,
plane_vao,
plane_vertex_count,
transform_mode: TransformMode::Move,
gizmo_size,
})
}
/// Generate XYZ axis lines.
fn generate_axis_lines(length: f32) -> Vec<f32> {
let mut verts = Vec::new();
// X axis
verts.extend_from_slice(&[0.0, 0.0, 0.0]);
verts.extend_from_slice(&X_AXIS_COLOR);
verts.extend_from_slice(&[length, 0.0, 0.0]);
verts.extend_from_slice(&X_AXIS_COLOR);
// Y axis
verts.extend_from_slice(&[0.0, 0.0, 0.0]);
verts.extend_from_slice(&Y_AXIS_COLOR);
verts.extend_from_slice(&[0.0, length, 0.0]);
verts.extend_from_slice(&Y_AXIS_COLOR);
// Z axis
verts.extend_from_slice(&[0.0, 0.0, 0.0]);
verts.extend_from_slice(&Z_AXIS_COLOR);
verts.extend_from_slice(&[0.0, 0.0, length]);
verts.extend_from_slice(&Z_AXIS_COLOR);
verts
}
/// Generate arrow cones for translation gizmo.
fn generate_arrow_cones(axis_length: f32, segments: u32) -> Vec<f32> {
let mut verts = Vec::new();
let cone_length = 0.15;
let cone_radius = 0.05;
// Generate cone for each axis
for axis in 0..3 {
let color = match axis {
0 => X_AXIS_COLOR,
1 => Y_AXIS_COLOR,
_ => Z_AXIS_COLOR,
};
let tip = match axis {
0 => [axis_length + cone_length, 0.0, 0.0],
1 => [0.0, axis_length + cone_length, 0.0],
_ => [0.0, 0.0, axis_length + cone_length],
};
let base_center = match axis {
0 => [axis_length, 0.0, 0.0],
1 => [0.0, axis_length, 0.0],
_ => [0.0, 0.0, axis_length],
};
// Generate cone triangles (as lines for wireframe)
for i in 0..segments {
let angle1 = (i as f32) * 2.0 * PI / (segments as f32);
let angle2 = ((i + 1) as f32) * 2.0 * PI / (segments as f32);
let (p1, p2) = match axis {
0 => (
[base_center[0], cone_radius * angle1.cos(), cone_radius * angle1.sin()],
[base_center[0], cone_radius * angle2.cos(), cone_radius * angle2.sin()],
),
1 => (
[cone_radius * angle1.cos(), base_center[1], cone_radius * angle1.sin()],
[cone_radius * angle2.cos(), base_center[1], cone_radius * angle2.sin()],
),
_ => (
[cone_radius * angle1.cos(), cone_radius * angle1.sin(), base_center[2]],
[cone_radius * angle2.cos(), cone_radius * angle2.sin(), base_center[2]],
),
};
// Line from tip to base edge
verts.extend_from_slice(&tip);
verts.extend_from_slice(&color);
verts.extend_from_slice(&p1);
verts.extend_from_slice(&color);
// Base circle segment
verts.extend_from_slice(&p1);
verts.extend_from_slice(&color);
verts.extend_from_slice(&p2);
verts.extend_from_slice(&color);
}
}
verts
}
/// Generate rotation circles for each axis.
fn generate_rotation_circles(radius: f32, segments: u32) -> Vec<f32> {
let mut verts = Vec::new();
// X axis rotation (YZ plane) - Red
for i in 0..segments {
let angle1 = (i as f32) * 2.0 * PI / (segments as f32);
let angle2 = ((i + 1) as f32) * 2.0 * PI / (segments as f32);
verts.extend_from_slice(&[0.0, radius * angle1.cos(), radius * angle1.sin()]);
verts.extend_from_slice(&X_AXIS_COLOR);
verts.extend_from_slice(&[0.0, radius * angle2.cos(), radius * angle2.sin()]);
verts.extend_from_slice(&X_AXIS_COLOR);
}
// Y axis rotation (XZ plane) - Green
for i in 0..segments {
let angle1 = (i as f32) * 2.0 * PI / (segments as f32);
let angle2 = ((i + 1) as f32) * 2.0 * PI / (segments as f32);
verts.extend_from_slice(&[radius * angle1.cos(), 0.0, radius * angle1.sin()]);
verts.extend_from_slice(&Y_AXIS_COLOR);
verts.extend_from_slice(&[radius * angle2.cos(), 0.0, radius * angle2.sin()]);
verts.extend_from_slice(&Y_AXIS_COLOR);
}
// Z axis rotation (XY plane) - Blue
for i in 0..segments {
let angle1 = (i as f32) * 2.0 * PI / (segments as f32);
let angle2 = ((i + 1) as f32) * 2.0 * PI / (segments as f32);
verts.extend_from_slice(&[radius * angle1.cos(), radius * angle1.sin(), 0.0]);
verts.extend_from_slice(&Z_AXIS_COLOR);
verts.extend_from_slice(&[radius * angle2.cos(), radius * angle2.sin(), 0.0]);
verts.extend_from_slice(&Z_AXIS_COLOR);
}
verts
}
/// Generate scale cubes at end of axes.
fn generate_scale_cubes(axis_length: f32, cube_size: f32) -> Vec<f32> {
let mut verts = Vec::new();
let half = cube_size / 2.0;
// Generate cube wireframe for each axis
for axis in 0..3 {
let color = match axis {
0 => X_AXIS_COLOR,
1 => Y_AXIS_COLOR,
_ => Z_AXIS_COLOR,
};
let center = match axis {
0 => [axis_length, 0.0, 0.0],
1 => [0.0, axis_length, 0.0],
_ => [0.0, 0.0, axis_length],
};
// 12 edges of cube
let edges = [
// Bottom face
([-half, -half, -half], [half, -half, -half]),
([half, -half, -half], [half, -half, half]),
([half, -half, half], [-half, -half, half]),
([-half, -half, half], [-half, -half, -half]),
// Top face
([-half, half, -half], [half, half, -half]),
([half, half, -half], [half, half, half]),
([half, half, half], [-half, half, half]),
([-half, half, half], [-half, half, -half]),
// Vertical edges
([-half, -half, -half], [-half, half, -half]),
([half, -half, -half], [half, half, -half]),
([half, -half, half], [half, half, half]),
([-half, -half, half], [-half, half, half]),
];
for (p1, p2) in edges {
verts.extend_from_slice(&[center[0] + p1[0], center[1] + p1[1], center[2] + p1[2]]);
verts.extend_from_slice(&color);
verts.extend_from_slice(&[center[0] + p2[0], center[1] + p2[1], center[2] + p2[2]]);
verts.extend_from_slice(&color);
}
}
verts
}
/// Generate center sphere wireframe.
fn generate_center_sphere(radius: f32, segments: u32) -> Vec<f32> {
let mut verts = Vec::new();
// Three circles for sphere wireframe
// XY plane
for i in 0..segments {
let angle1 = (i as f32) * 2.0 * PI / (segments as f32);
let angle2 = ((i + 1) as f32) * 2.0 * PI / (segments as f32);
verts.extend_from_slice(&[radius * angle1.cos(), radius * angle1.sin(), 0.0]);
verts.extend_from_slice(&CENTER_COLOR);
verts.extend_from_slice(&[radius * angle2.cos(), radius * angle2.sin(), 0.0]);
verts.extend_from_slice(&CENTER_COLOR);
}
// XZ plane
for i in 0..segments {
let angle1 = (i as f32) * 2.0 * PI / (segments as f32);
let angle2 = ((i + 1) as f32) * 2.0 * PI / (segments as f32);
verts.extend_from_slice(&[radius * angle1.cos(), 0.0, radius * angle1.sin()]);
verts.extend_from_slice(&CENTER_COLOR);
verts.extend_from_slice(&[radius * angle2.cos(), 0.0, radius * angle2.sin()]);
verts.extend_from_slice(&CENTER_COLOR);
}
// YZ plane
for i in 0..segments {
let angle1 = (i as f32) * 2.0 * PI / (segments as f32);
let angle2 = ((i + 1) as f32) * 2.0 * PI / (segments as f32);
verts.extend_from_slice(&[0.0, radius * angle1.cos(), radius * angle1.sin()]);
verts.extend_from_slice(&CENTER_COLOR);
verts.extend_from_slice(&[0.0, radius * angle2.cos(), radius * angle2.sin()]);
verts.extend_from_slice(&CENTER_COLOR);
}
verts
}
/// Generate plane handles for translation.
fn generate_plane_handles(size: f32) -> Vec<f32> {
let mut verts = Vec::new();
let offset = size * 0.3;
// XY plane handle (Blue)
let xy = [
[offset, offset, 0.0],
[offset + size, offset, 0.0],
[offset + size, offset + size, 0.0],
[offset, offset + size, 0.0],
];
for i in 0..4 {
verts.extend_from_slice(&xy[i]);
verts.extend_from_slice(&XY_PLANE_COLOR);
verts.extend_from_slice(&xy[(i + 1) % 4]);
verts.extend_from_slice(&XY_PLANE_COLOR);
}
// XZ plane handle (Magenta)
let xz = [
[offset, 0.0, offset],
[offset + size, 0.0, offset],
[offset + size, 0.0, offset + size],
[offset, 0.0, offset + size],
];
for i in 0..4 {
verts.extend_from_slice(&xz[i]);
verts.extend_from_slice(&XZ_PLANE_COLOR);
verts.extend_from_slice(&xz[(i + 1) % 4]);
verts.extend_from_slice(&XZ_PLANE_COLOR);
}
// YZ plane handle (Cyan)
let yz = [
[0.0, offset, offset],
[0.0, offset + size, offset],
[0.0, offset + size, offset + size],
[0.0, offset, offset + size],
];
for i in 0..4 {
verts.extend_from_slice(&yz[i]);
verts.extend_from_slice(&YZ_PLANE_COLOR);
verts.extend_from_slice(&yz[(i + 1) % 4]);
verts.extend_from_slice(&YZ_PLANE_COLOR);
}
verts
}
/// Set the current transform mode.
pub fn set_transform_mode(&mut self, mode: TransformMode) {
self.transform_mode = mode;
}
/// Get the current transform mode.
pub fn get_transform_mode(&self) -> TransformMode {
self.transform_mode
}
/// Render the gizmo at a specific position.
/// 在指定位置渲染 Gizmo。
pub fn render(
&mut self,
backend: &mut impl GraphicsBackend,
camera: &Camera3D,
position: Vec3,
scale: f32,
) {
let vp = camera.view_projection_matrix();
// Calculate distance-based scale to keep gizmo constant screen size
let cam_pos = camera.position;
let distance = (position - cam_pos).length().max(0.1);
let screen_scale = distance * 0.15 * scale;
// Create model matrix (translation + uniform scale)
let model = Mat4::from_translation(position) * Mat4::from_scale(Vec3::splat(screen_scale));
backend.bind_shader(self.shader).ok();
backend.set_uniform_mat4("u_viewProjection", &vp).ok();
backend.set_uniform_mat4("u_model", &model).ok();
backend.set_blend_mode(BlendMode::Alpha);
// Render center
backend.draw_lines(self.center_vao, self.center_vertex_count, 0).ok();
match self.transform_mode {
TransformMode::Select => {
// Just render center
}
TransformMode::Move => {
// Render translation handles
backend.draw_lines(self.translate_vao, self.translate_line_count, 0).ok();
backend.draw_lines(self.translate_arrow_vao, self.translate_arrow_count, 0).ok();
backend.draw_lines(self.plane_vao, self.plane_vertex_count, 0).ok();
}
TransformMode::Rotate => {
// Render rotation handles
backend.draw_lines(self.rotate_vao, self.rotate_vertex_count, 0).ok();
}
TransformMode::Scale => {
// Render scale handles
backend.draw_lines(self.scale_vao, self.scale_line_count, 0).ok();
backend.draw_lines(self.scale_cube_vao, self.scale_cube_count, 0).ok();
}
}
}
/// Destroy renderer resources.
pub fn destroy(self, backend: &mut impl GraphicsBackend) {
backend.destroy_vertex_array(self.translate_vao);
backend.destroy_buffer(self.translate_vbo);
backend.destroy_vertex_array(self.translate_arrow_vao);
backend.destroy_buffer(self.translate_arrow_vbo);
backend.destroy_vertex_array(self.rotate_vao);
backend.destroy_buffer(self.rotate_vbo);
backend.destroy_vertex_array(self.scale_vao);
backend.destroy_buffer(self.scale_vbo);
backend.destroy_vertex_array(self.scale_cube_vao);
backend.destroy_buffer(self.scale_cube_vbo);
backend.destroy_vertex_array(self.center_vao);
backend.destroy_buffer(self.center_vbo);
backend.destroy_vertex_array(self.plane_vao);
backend.destroy_buffer(self.plane_vbo);
backend.destroy_shader(self.shader);
}
}

View File

@@ -0,0 +1,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);
}
}

View File

@@ -0,0 +1,450 @@
//! 3D Grid renderer for editor.
//! 编辑器 3D 网格渲染器。
//!
//! Features:
//! - Multi-level grid (major lines every 10 units, minor lines every 1 unit)
//! - Distance-based fade out for infinite grid effect
//! - RGB colored coordinate axes
//! - Origin marker
//!
//! 特性:
//! - 多层级网格主网格每10单位次网格每1单位
//! - 基于距离的淡出效果,实现无限网格效果
//! - RGB 彩色坐标轴
//! - 原点标记
use es_engine_shared::{
traits::backend::{GraphicsBackend, BufferUsage},
types::{
handle::{ShaderHandle, BufferHandle, VertexArrayHandle},
vertex::{VertexLayout, VertexAttribute, VertexAttributeType},
blend::BlendMode,
},
Vec3,
};
use super::camera3d::Camera3D;
use super::shader::{GRID3D_VERTEX_SHADER, GRID3D_FRAGMENT_SHADER};
/// Grid configuration.
/// 网格配置。
#[derive(Debug, Clone)]
pub struct GridConfig {
/// Size of the grid (extends from -size/2 to +size/2).
/// 网格大小(从 -size/2 延伸到 +size/2
pub size: f32,
/// Major grid spacing (typically 10 units).
/// 主网格间距通常为10单位
pub major_spacing: f32,
/// Minor grid spacing (typically 1 unit).
/// 次网格间距通常为1单位
pub minor_spacing: f32,
/// Major grid line alpha.
/// 主网格线透明度。
pub major_alpha: f32,
/// Minor grid line alpha.
/// 次网格线透明度。
pub minor_alpha: f32,
/// Distance at which fade starts.
/// 开始淡出的距离。
pub fade_start: f32,
/// Distance at which grid is fully transparent.
/// 完全透明的距离。
pub fade_end: f32,
/// Axis line length.
/// 坐标轴线长度。
pub axis_length: f32,
}
impl Default for GridConfig {
fn default() -> Self {
Self {
size: 100.0,
major_spacing: 10.0,
minor_spacing: 1.0,
major_alpha: 0.5,
minor_alpha: 0.15,
fade_start: 30.0,
fade_end: 50.0,
axis_length: 1000.0,
}
}
}
/// 3D grid renderer for displaying ground plane and axes.
/// 用于显示地面平面和坐标轴的 3D 网格渲染器。
pub struct Grid3DRenderer {
shader: ShaderHandle,
// Major grid (every 10 units)
major_grid_vbo: BufferHandle,
major_grid_vao: VertexArrayHandle,
major_grid_vertex_count: u32,
// Minor grid (every 1 unit)
minor_grid_vbo: BufferHandle,
minor_grid_vao: VertexArrayHandle,
minor_grid_vertex_count: u32,
// Coordinate axes
axis_vbo: BufferHandle,
axis_vao: VertexArrayHandle,
axis_vertex_count: u32,
// Origin marker
origin_vbo: BufferHandle,
origin_vao: VertexArrayHandle,
origin_vertex_count: u32,
config: GridConfig,
}
impl Grid3DRenderer {
/// Create a new 3D grid renderer with default configuration.
/// 使用默认配置创建新的 3D 网格渲染器。
pub fn new(backend: &mut impl GraphicsBackend) -> Result<Self, String> {
Self::with_config(backend, GridConfig::default())
}
/// Create a new 3D grid renderer with custom configuration.
/// 使用自定义配置创建新的 3D 网格渲染器。
pub fn with_config(backend: &mut impl GraphicsBackend, config: GridConfig) -> Result<Self, String> {
// Compile shader
let shader = backend.compile_shader(GRID3D_VERTEX_SHADER, GRID3D_FRAGMENT_SHADER)
.map_err(|e| format!("3D Grid shader: {:?}", e))?;
// Create vertex layout for 3D lines (position + color)
let layout = VertexLayout {
attributes: vec![
VertexAttribute {
name: "a_position".into(),
attr_type: VertexAttributeType::Float3,
offset: 0,
normalized: false,
},
VertexAttribute {
name: "a_color".into(),
attr_type: VertexAttributeType::Float4,
offset: 12,
normalized: false,
},
],
stride: 28, // 3 floats position + 4 floats color = 7 * 4 = 28 bytes
};
// Generate major grid vertices (every 10 units)
let major_vertices = Self::generate_grid_vertices(
config.size,
config.major_spacing,
[0.5, 0.5, 0.5, config.major_alpha],
true, // Skip center lines (will be axes)
);
let major_grid_vertex_count = (major_vertices.len() / 7) as u32;
let major_grid_vbo = backend.create_vertex_buffer(
bytemuck::cast_slice(&major_vertices),
BufferUsage::Static,
).map_err(|e| format!("3D Major Grid VBO: {:?}", e))?;
let major_grid_vao = backend.create_vertex_array(major_grid_vbo, None, &layout)
.map_err(|e| format!("3D Major Grid VAO: {:?}", e))?;
// Generate minor grid vertices (every 1 unit, skip major lines)
let minor_vertices = Self::generate_minor_grid_vertices(
config.size,
config.minor_spacing,
config.major_spacing,
[0.4, 0.4, 0.4, config.minor_alpha],
);
let minor_grid_vertex_count = (minor_vertices.len() / 7) as u32;
let minor_grid_vbo = backend.create_vertex_buffer(
bytemuck::cast_slice(&minor_vertices),
BufferUsage::Static,
).map_err(|e| format!("3D Minor Grid VBO: {:?}", e))?;
let minor_grid_vao = backend.create_vertex_array(minor_grid_vbo, None, &layout)
.map_err(|e| format!("3D Minor Grid VAO: {:?}", e))?;
// Generate axis vertices
let axis_vertices = Self::generate_axis_vertices(config.axis_length);
let axis_vertex_count = (axis_vertices.len() / 7) as u32;
let axis_vbo = backend.create_vertex_buffer(
bytemuck::cast_slice(&axis_vertices),
BufferUsage::Static,
).map_err(|e| format!("3D Axis VBO: {:?}", e))?;
let axis_vao = backend.create_vertex_array(axis_vbo, None, &layout)
.map_err(|e| format!("3D Axis VAO: {:?}", e))?;
// Generate origin marker
let origin_vertices = Self::generate_origin_marker(0.5);
let origin_vertex_count = (origin_vertices.len() / 7) as u32;
let origin_vbo = backend.create_vertex_buffer(
bytemuck::cast_slice(&origin_vertices),
BufferUsage::Static,
).map_err(|e| format!("3D Origin VBO: {:?}", e))?;
let origin_vao = backend.create_vertex_array(origin_vbo, None, &layout)
.map_err(|e| format!("3D Origin VAO: {:?}", e))?;
Ok(Self {
shader,
major_grid_vbo,
major_grid_vao,
major_grid_vertex_count,
minor_grid_vbo,
minor_grid_vao,
minor_grid_vertex_count,
axis_vbo,
axis_vao,
axis_vertex_count,
origin_vbo,
origin_vao,
origin_vertex_count,
config,
})
}
/// Generate grid vertices on XZ plane (Y = 0).
/// 在 XZ 平面上生成网格顶点Y = 0
fn generate_grid_vertices(size: f32, spacing: f32, color: [f32; 4], skip_center: bool) -> Vec<f32> {
let mut vertices = Vec::new();
let half_size = size / 2.0;
let line_count = (size / spacing) as i32;
// Generate lines along X axis (varying Z)
for i in -line_count/2..=line_count/2 {
let z = i as f32 * spacing;
// Skip center line if requested (will be drawn as axis)
if skip_center && i == 0 {
continue;
}
// Start point
vertices.extend_from_slice(&[-half_size, 0.0, z]);
vertices.extend_from_slice(&color);
// End point
vertices.extend_from_slice(&[half_size, 0.0, z]);
vertices.extend_from_slice(&color);
}
// Generate lines along Z axis (varying X)
for i in -line_count/2..=line_count/2 {
let x = i as f32 * spacing;
// Skip center line if requested (will be drawn as axis)
if skip_center && i == 0 {
continue;
}
// Start point
vertices.extend_from_slice(&[x, 0.0, -half_size]);
vertices.extend_from_slice(&color);
// End point
vertices.extend_from_slice(&[x, 0.0, half_size]);
vertices.extend_from_slice(&color);
}
vertices
}
/// Generate minor grid vertices, skipping major grid lines.
/// 生成次网格顶点,跳过主网格线。
fn generate_minor_grid_vertices(
size: f32,
minor_spacing: f32,
major_spacing: f32,
color: [f32; 4]
) -> Vec<f32> {
let mut vertices = Vec::new();
let half_size = size / 2.0;
let line_count = (size / minor_spacing) as i32;
let epsilon = minor_spacing * 0.01; // Small tolerance for float comparison
// Generate lines along X axis (varying Z)
for i in -line_count/2..=line_count/2 {
let z = i as f32 * minor_spacing;
// Skip if this is a major line or center line
let is_major = (z.abs() % major_spacing).abs() < epsilon
|| (z.abs() % major_spacing - major_spacing).abs() < epsilon;
if is_major || z.abs() < epsilon {
continue;
}
// Start point
vertices.extend_from_slice(&[-half_size, 0.0, z]);
vertices.extend_from_slice(&color);
// End point
vertices.extend_from_slice(&[half_size, 0.0, z]);
vertices.extend_from_slice(&color);
}
// Generate lines along Z axis (varying X)
for i in -line_count/2..=line_count/2 {
let x = i as f32 * minor_spacing;
// Skip if this is a major line or center line
let is_major = (x.abs() % major_spacing).abs() < epsilon
|| (x.abs() % major_spacing - major_spacing).abs() < epsilon;
if is_major || x.abs() < epsilon {
continue;
}
// Start point
vertices.extend_from_slice(&[x, 0.0, -half_size]);
vertices.extend_from_slice(&color);
// End point
vertices.extend_from_slice(&[x, 0.0, half_size]);
vertices.extend_from_slice(&color);
}
vertices
}
/// Generate axis vertices (X = red, Y = green, Z = blue).
/// 生成坐标轴顶点X = 红色Y = 绿色Z = 蓝色)。
fn generate_axis_vertices(length: f32) -> Vec<f32> {
let mut vertices = Vec::new();
// X axis (red) - extends in both directions
// X 轴(红色)- 双向延伸
vertices.extend_from_slice(&[-length, 0.0, 0.0]);
vertices.extend_from_slice(&[0.6, 0.2, 0.2, 0.6]); // Negative side dimmer
vertices.extend_from_slice(&[0.0, 0.0, 0.0]);
vertices.extend_from_slice(&[0.6, 0.2, 0.2, 0.6]);
vertices.extend_from_slice(&[0.0, 0.0, 0.0]);
vertices.extend_from_slice(&[1.0, 0.3, 0.3, 1.0]); // Positive side brighter
vertices.extend_from_slice(&[length, 0.0, 0.0]);
vertices.extend_from_slice(&[1.0, 0.3, 0.3, 1.0]);
// Y axis (green) - extends upward and downward
// Y 轴(绿色)- 向上和向下延伸
vertices.extend_from_slice(&[0.0, -length, 0.0]);
vertices.extend_from_slice(&[0.2, 0.6, 0.2, 0.6]); // Negative side dimmer
vertices.extend_from_slice(&[0.0, 0.0, 0.0]);
vertices.extend_from_slice(&[0.2, 0.6, 0.2, 0.6]);
vertices.extend_from_slice(&[0.0, 0.0, 0.0]);
vertices.extend_from_slice(&[0.3, 1.0, 0.3, 1.0]); // Positive side brighter
vertices.extend_from_slice(&[0.0, length, 0.0]);
vertices.extend_from_slice(&[0.3, 1.0, 0.3, 1.0]);
// Z axis (blue) - extends in both directions
// Z 轴(蓝色)- 双向延伸
vertices.extend_from_slice(&[0.0, 0.0, -length]);
vertices.extend_from_slice(&[0.2, 0.2, 0.6, 0.6]); // Negative side dimmer
vertices.extend_from_slice(&[0.0, 0.0, 0.0]);
vertices.extend_from_slice(&[0.2, 0.2, 0.6, 0.6]);
vertices.extend_from_slice(&[0.0, 0.0, 0.0]);
vertices.extend_from_slice(&[0.3, 0.3, 1.0, 1.0]); // Positive side brighter
vertices.extend_from_slice(&[0.0, 0.0, length]);
vertices.extend_from_slice(&[0.3, 0.3, 1.0, 1.0]);
vertices
}
/// Generate origin marker (small cross at origin).
/// 生成原点标记(原点处的小十字)。
fn generate_origin_marker(size: f32) -> Vec<f32> {
let mut vertices = Vec::new();
let color = [1.0, 1.0, 1.0, 0.8]; // White
// Small cross on XZ plane
vertices.extend_from_slice(&[-size, 0.0, 0.0]);
vertices.extend_from_slice(&color);
vertices.extend_from_slice(&[size, 0.0, 0.0]);
vertices.extend_from_slice(&color);
vertices.extend_from_slice(&[0.0, 0.0, -size]);
vertices.extend_from_slice(&color);
vertices.extend_from_slice(&[0.0, 0.0, size]);
vertices.extend_from_slice(&color);
// Vertical line
vertices.extend_from_slice(&[0.0, -size, 0.0]);
vertices.extend_from_slice(&color);
vertices.extend_from_slice(&[0.0, size, 0.0]);
vertices.extend_from_slice(&color);
vertices
}
/// Render the 3D grid (both major and minor lines).
/// 渲染 3D 网格(主网格线和次网格线)。
pub fn render(&mut self, backend: &mut impl GraphicsBackend, camera: &Camera3D) {
let vp = camera.view_projection_matrix();
let cam_pos = camera.position;
// Calculate fade distances based on camera height for dynamic density
// 根据相机高度计算淡出距离以实现动态密度
let cam_height = cam_pos.y.abs().max(1.0);
let minor_fade_start = (cam_height * 1.5).min(self.config.fade_start * 0.5);
let minor_fade_end = (cam_height * 3.0).min(self.config.fade_start);
// Bind shader and set common uniforms
backend.bind_shader(self.shader).ok();
backend.set_uniform_mat4("u_viewProjection", &vp).ok();
backend.set_uniform_vec3("u_cameraPos", Vec3::new(cam_pos.x, cam_pos.y, cam_pos.z)).ok();
backend.set_blend_mode(BlendMode::Alpha);
// Render minor grid (fades out faster when camera is close)
backend.set_uniform_f32("u_fadeStart", minor_fade_start).ok();
backend.set_uniform_f32("u_fadeEnd", minor_fade_end).ok();
backend.draw_lines(self.minor_grid_vao, self.minor_grid_vertex_count, 0).ok();
// Render major grid
backend.set_uniform_f32("u_fadeStart", self.config.fade_start).ok();
backend.set_uniform_f32("u_fadeEnd", self.config.fade_end).ok();
backend.draw_lines(self.major_grid_vao, self.major_grid_vertex_count, 0).ok();
// Render origin marker (always visible, no fade)
backend.set_uniform_f32("u_fadeStart", 1000.0).ok();
backend.set_uniform_f32("u_fadeEnd", 2000.0).ok();
backend.draw_lines(self.origin_vao, self.origin_vertex_count, 0).ok();
}
/// Render the coordinate axes.
/// 渲染坐标轴。
pub fn render_axes(&mut self, backend: &mut impl GraphicsBackend, camera: &Camera3D) {
let vp = camera.view_projection_matrix();
let cam_pos = camera.position;
backend.bind_shader(self.shader).ok();
backend.set_uniform_mat4("u_viewProjection", &vp).ok();
backend.set_uniform_vec3("u_cameraPos", Vec3::new(cam_pos.x, cam_pos.y, cam_pos.z)).ok();
// Axes fade slower than grid
backend.set_uniform_f32("u_fadeStart", self.config.fade_end * 2.0).ok();
backend.set_uniform_f32("u_fadeEnd", self.config.fade_end * 4.0).ok();
backend.set_blend_mode(BlendMode::Alpha);
backend.draw_lines(self.axis_vao, self.axis_vertex_count, 0).ok();
}
/// Destroy renderer resources.
/// 销毁渲染器资源。
pub fn destroy(self, backend: &mut impl GraphicsBackend) {
backend.destroy_vertex_array(self.major_grid_vao);
backend.destroy_buffer(self.major_grid_vbo);
backend.destroy_vertex_array(self.minor_grid_vao);
backend.destroy_buffer(self.minor_grid_vbo);
backend.destroy_vertex_array(self.axis_vao);
backend.destroy_buffer(self.axis_vbo);
backend.destroy_vertex_array(self.origin_vao);
backend.destroy_buffer(self.origin_vbo);
backend.destroy_shader(self.shader);
}
}

View File

@@ -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()
}
}

View 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).
/// 设置颜色uniformRGBA0.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
}
}

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

View 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).
/// 设置颜色uniformRGBA0.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));
}
}

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

View 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);
}
}
}

View File

@@ -0,0 +1,481 @@
//! Main 3D renderer implementation.
//! 主3D渲染器实现。
//!
//! Provides perspective and orthographic 3D rendering with depth testing.
//! 提供带深度测试的透视和正交3D渲染。
use es_engine_shared::{
traits::backend::GraphicsBackend,
types::{
handle::ShaderHandle,
blend::{RenderState, CompareFunc, CullMode, BlendMode as SharedBlendMode},
},
Mat4,
};
use std::collections::HashMap;
use crate::backend::WebGL2Backend;
use super::camera3d::Camera3D;
use super::batch::{SimpleVertex3D, FLOATS_PER_SIMPLE_VERTEX_3D};
use super::texture::TextureManager;
use super::material::{Material, BlendMode, UniformValue};
use super::shader::{SIMPLE3D_VERTEX_SHADER, SIMPLE3D_FRAGMENT_SHADER};
/// Convert local BlendMode to shared BlendMode.
/// 将本地 BlendMode 转换为共享 BlendMode。
fn to_shared_blend_mode(mode: BlendMode) -> SharedBlendMode {
match mode {
BlendMode::None => SharedBlendMode::None,
BlendMode::Alpha => SharedBlendMode::Alpha,
BlendMode::Additive => SharedBlendMode::Additive,
BlendMode::Multiply => SharedBlendMode::Multiply,
BlendMode::Screen => SharedBlendMode::Screen,
BlendMode::PremultipliedAlpha => SharedBlendMode::PremultipliedAlpha,
}
}
/// Mesh submission data for batched 3D rendering.
/// 用于批处理3D渲染的网格提交数据。
#[derive(Debug, Clone)]
pub struct MeshSubmission {
/// Vertex data (position, uv, color).
/// 顶点数据位置、UV、颜色
pub vertices: Vec<SimpleVertex3D>,
/// Index data.
/// 索引数据。
pub indices: Vec<u32>,
/// Model transformation matrix.
/// 模型变换矩阵。
pub transform: Mat4,
/// Material ID.
/// 材质 ID。
pub material_id: u32,
/// Texture ID.
/// 纹理 ID。
pub texture_id: u32,
}
/// 3D Renderer with perspective/orthographic camera support.
/// 支持透视/正交相机的3D渲染器。
pub struct Renderer3D {
/// 3D camera.
/// 3D相机。
camera: Camera3D,
/// Default 3D shader.
/// 默认3D着色器。
default_shader: ShaderHandle,
/// Custom shaders by ID.
/// 按ID存储的自定义着色器。
custom_shaders: HashMap<u32, ShaderHandle>,
/// Next shader ID for auto-assignment.
/// 自动分配的下一个着色器ID。
next_shader_id: u32,
/// Materials by ID.
/// 按ID存储的材质。
materials: HashMap<u32, Material>,
/// Pending mesh submissions for this frame.
/// 本帧待渲染的网格提交。
mesh_queue: Vec<MeshSubmission>,
/// Clear color.
/// 清除颜色。
clear_color: [f32; 4],
/// Whether depth test is enabled.
/// 是否启用深度测试。
depth_test_enabled: bool,
/// Whether depth write is enabled.
/// 是否启用深度写入。
depth_write_enabled: bool,
}
impl Renderer3D {
/// Create a new 3D renderer.
/// 创建新的3D渲染器。
pub fn new(backend: &mut WebGL2Backend) -> Result<Self, String> {
// Compile default 3D shader
// 编译默认3D着色器
let default_shader = backend
.compile_shader(SIMPLE3D_VERTEX_SHADER, SIMPLE3D_FRAGMENT_SHADER)
.map_err(|e| format!("Failed to compile 3D shader: {:?}", e))?;
let (width, height) = (backend.width() as f32, backend.height() as f32);
let camera = Camera3D::new(width, height, std::f32::consts::FRAC_PI_4);
let mut materials = HashMap::new();
materials.insert(0, Material::default());
Ok(Self {
camera,
default_shader,
custom_shaders: HashMap::new(),
next_shader_id: 100,
materials,
mesh_queue: Vec::new(),
clear_color: [0.1, 0.1, 0.12, 1.0],
depth_test_enabled: true,
depth_write_enabled: true,
})
}
/// Submit a mesh for rendering.
/// 提交网格进行渲染。
pub fn submit_mesh(&mut self, submission: MeshSubmission) {
self.mesh_queue.push(submission);
}
/// Submit a simple textured quad at position.
/// 在指定位置提交一个简单的纹理四边形。
pub fn submit_quad(
&mut self,
position: [f32; 3],
size: [f32; 2],
texture_id: u32,
color: [f32; 4],
material_id: u32,
) {
let half_w = size[0] / 2.0;
let half_h = size[1] / 2.0;
let vertices = vec![
SimpleVertex3D::new([-half_w, -half_h, 0.0], [0.0, 1.0], color),
SimpleVertex3D::new([half_w, -half_h, 0.0], [1.0, 1.0], color),
SimpleVertex3D::new([half_w, half_h, 0.0], [1.0, 0.0], color),
SimpleVertex3D::new([-half_w, half_h, 0.0], [0.0, 0.0], color),
];
let indices = vec![0, 1, 2, 2, 3, 0];
let transform = Mat4::from_translation(glam::Vec3::new(
position[0],
position[1],
position[2],
));
self.mesh_queue.push(MeshSubmission {
vertices,
indices,
transform,
material_id,
texture_id,
});
}
/// Render all submitted meshes.
/// 渲染所有已提交的网格。
pub fn render(
&mut self,
backend: &mut WebGL2Backend,
texture_manager: &TextureManager,
) -> Result<(), String> {
if self.mesh_queue.is_empty() {
return Ok(());
}
// Apply 3D render state (depth test enabled)
// 应用3D渲染状态启用深度测试
let render_state = RenderState {
blend_mode: SharedBlendMode::Alpha,
cull_mode: CullMode::Back,
depth_test: self.depth_test_enabled,
depth_write: self.depth_write_enabled,
depth_func: CompareFunc::LessEqual,
scissor: None,
};
backend.apply_render_state(&render_state);
// Get view-projection matrix
// 获取视图-投影矩阵
let view_projection = self.camera.view_projection_matrix();
let mut current_material_id = u32::MAX;
let mut current_texture_id = u32::MAX;
for submission in &self.mesh_queue {
// Bind material/shader if changed
// 如果材质/着色器变化则绑定
if submission.material_id != current_material_id {
current_material_id = submission.material_id;
let material = self
.materials
.get(&submission.material_id)
.cloned()
.unwrap_or_default();
let shader = if material.shader_id == 0 {
self.default_shader
} else {
self.custom_shaders
.get(&material.shader_id)
.copied()
.unwrap_or(self.default_shader)
};
backend.bind_shader(shader).ok();
backend.set_blend_mode(to_shared_blend_mode(material.blend_mode));
// Set view-projection matrix
// 设置视图-投影矩阵
backend
.set_uniform_mat4("u_viewProjection", &view_projection)
.ok();
backend.set_uniform_i32("u_texture", 0).ok();
// Apply custom uniforms
// 应用自定义 uniforms
for name in material.uniforms.names() {
if let Some(value) = material.uniforms.get(name) {
match value {
UniformValue::Float(v) => {
backend.set_uniform_f32(name, *v).ok();
}
UniformValue::Vec2(v) => {
backend
.set_uniform_vec2(
name,
es_engine_shared::Vec2::new(v[0], v[1]),
)
.ok();
}
UniformValue::Vec3(v) => {
backend
.set_uniform_vec3(
name,
es_engine_shared::Vec3::new(v[0], v[1], v[2]),
)
.ok();
}
UniformValue::Vec4(v) => {
backend
.set_uniform_vec4(
name,
es_engine_shared::Vec4::new(v[0], v[1], v[2], v[3]),
)
.ok();
}
UniformValue::Int(v) => {
backend.set_uniform_i32(name, *v).ok();
}
UniformValue::Mat3(v) => {
backend
.set_uniform_mat3(name, &es_engine_shared::Mat3::from_cols_array(v))
.ok();
}
UniformValue::Mat4(v) => {
backend
.set_uniform_mat4(name, &es_engine_shared::Mat4::from_cols_array(v))
.ok();
}
UniformValue::Sampler(v) => {
backend.set_uniform_i32(name, *v).ok();
}
}
}
}
}
// Bind texture if changed
// 如果纹理变化则绑定
if submission.texture_id != current_texture_id {
current_texture_id = submission.texture_id;
texture_manager.bind_texture_via_backend(backend, submission.texture_id, 0);
}
// Set model matrix for this mesh
// 设置此网格的模型矩阵
backend
.set_uniform_mat4("u_model", &submission.transform)
.ok();
// TODO: For now, we'll render each mesh individually
// In the future, implement proper mesh batching
// 目前我们逐个渲染网格,未来实现正确的网格批处理
// Create temporary vertex buffer and VAO for this mesh
// 为此网格创建临时顶点缓冲区和VAO
self.render_mesh_immediate(backend, &submission.vertices, &submission.indices)?;
}
// Reset to 2D render state
// 重置为2D渲染状态
let default_state = RenderState::default();
backend.apply_render_state(&default_state);
self.mesh_queue.clear();
Ok(())
}
/// Render a mesh immediately (no batching).
/// 立即渲染网格(无批处理)。
fn render_mesh_immediate(
&self,
backend: &mut WebGL2Backend,
vertices: &[SimpleVertex3D],
indices: &[u32],
) -> Result<(), String> {
use es_engine_shared::types::vertex::{VertexLayout, VertexAttribute, VertexAttributeType};
use es_engine_shared::BufferUsage;
// Create vertex layout for SimpleVertex3D
// 为SimpleVertex3D创建顶点布局
let layout = VertexLayout {
attributes: vec![
VertexAttribute {
name: "a_position",
attr_type: VertexAttributeType::Float3,
offset: 0,
normalized: false,
},
VertexAttribute {
name: "a_texCoord",
attr_type: VertexAttributeType::Float2,
offset: 12, // 3 * 4 bytes
normalized: false,
},
VertexAttribute {
name: "a_color",
attr_type: VertexAttributeType::Float4,
offset: 20, // 3 * 4 + 2 * 4 bytes
normalized: false,
},
],
stride: FLOATS_PER_SIMPLE_VERTEX_3D * 4, // 9 * 4 = 36 bytes
};
// Convert vertices to bytes
// 将顶点转换为字节
let vertex_data: &[u8] = bytemuck::cast_slice(vertices);
// Create buffers
// 创建缓冲区
let vertex_buffer = backend
.create_vertex_buffer(vertex_data, BufferUsage::Dynamic)
.map_err(|e| format!("Failed to create vertex buffer: {:?}", e))?;
let index_buffer = backend
.create_index_buffer_u32(indices, BufferUsage::Dynamic)
.map_err(|e| format!("Failed to create index buffer: {:?}", e))?;
// Create VAO
// 创建VAO
let vao = backend
.create_vertex_array(vertex_buffer, Some(index_buffer), &layout)
.map_err(|e| format!("Failed to create VAO: {:?}", e))?;
// Draw
// 绘制
backend
.draw_indexed_u32(vao, indices.len() as u32, 0)
.map_err(|e| format!("Failed to draw: {:?}", e))?;
// Cleanup
// 清理
backend.destroy_vertex_array(vao);
backend.destroy_buffer(vertex_buffer);
backend.destroy_buffer(index_buffer);
Ok(())
}
/// Get mutable reference to camera.
/// 获取相机的可变引用。
#[inline]
pub fn camera_mut(&mut self) -> &mut Camera3D {
&mut self.camera
}
/// Get reference to camera.
/// 获取相机的引用。
#[inline]
pub fn camera(&self) -> &Camera3D {
&self.camera
}
/// Set clear color.
/// 设置清除颜色。
pub fn set_clear_color(&mut self, r: f32, g: f32, b: f32, a: f32) {
self.clear_color = [r, g, b, a];
}
/// Get clear color.
/// 获取清除颜色。
pub fn get_clear_color(&self) -> [f32; 4] {
self.clear_color
}
/// Resize viewport.
/// 调整视口大小。
pub fn resize(&mut self, width: f32, height: f32) {
self.camera.set_viewport(width, height);
}
/// Enable or disable depth testing.
/// 启用或禁用深度测试。
pub fn set_depth_test(&mut self, enabled: bool) {
self.depth_test_enabled = enabled;
}
/// Enable or disable depth writing.
/// 启用或禁用深度写入。
pub fn set_depth_write(&mut self, enabled: bool) {
self.depth_write_enabled = enabled;
}
/// Compile a custom shader.
/// 编译自定义着色器。
pub fn compile_shader(
&mut self,
backend: &mut WebGL2Backend,
vertex: &str,
fragment: &str,
) -> Result<u32, String> {
let handle = backend
.compile_shader(vertex, fragment)
.map_err(|e| format!("{:?}", e))?;
let id = self.next_shader_id;
self.next_shader_id += 1;
self.custom_shaders.insert(id, handle);
Ok(id)
}
/// Register a material.
/// 注册材质。
pub fn register_material(&mut self, material: Material) -> u32 {
let id = self.materials.keys().max().unwrap_or(&0) + 1;
self.materials.insert(id, material);
id
}
/// Register material with specific ID.
/// 使用特定ID注册材质。
pub fn register_material_with_id(&mut self, id: u32, material: Material) {
self.materials.insert(id, material);
}
/// Get material by ID.
/// 按ID获取材质。
pub fn get_material(&self, id: u32) -> Option<&Material> {
self.materials.get(&id)
}
/// Get mutable material by ID.
/// 按ID获取可变材质。
pub fn get_material_mut(&mut self, id: u32) -> Option<&mut Material> {
self.materials.get_mut(&id)
}
/// Clean up resources.
/// 清理资源。
pub fn destroy(self, backend: &mut WebGL2Backend) {
backend.destroy_shader(self.default_shader);
for (_, handle) in self.custom_shaders {
backend.destroy_shader(handle);
}
}
}

View File

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

View 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()
}
}

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

View 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);
}
}
}

View File

@@ -0,0 +1,8 @@
//! Texture management system.
//! 纹理管理系统。
mod texture;
mod texture_manager;
pub use texture::Texture;
pub use texture_manager::{TextureManager, TextureState};

View 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
}
}

View 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(())
}
}

View 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()
}
}

View 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
}
}

View File

@@ -0,0 +1,7 @@
//! Resource management system.
//! 资源管理系统。
mod handle;
pub use handle::{Handle, HandleId};
pub use crate::renderer::texture::{Texture, TextureManager};