feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 (#228)

* feat: 集成Rust WASM渲染引擎与TypeScript ECS框架

* feat: 增强编辑器UI功能与跨平台支持

* fix: 修复CI测试和类型检查问题

* fix: 修复CI问题并提高测试覆盖率

* fix: 修复CI问题并提高测试覆盖率
This commit is contained in:
YHH
2025-11-21 10:03:18 +08:00
committed by GitHub
parent 8b9616837d
commit a768b890fd
107 changed files with 10221 additions and 477 deletions

View File

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

View File

@@ -0,0 +1,187 @@
//! Main engine implementation.
//! 主引擎实现。
use wasm_bindgen::prelude::*;
use super::context::WebGLContext;
use super::error::Result;
use crate::input::InputManager;
use crate::renderer::Renderer2D;
use crate::resource::TextureManager;
/// Engine configuration options.
/// 引擎配置选项。
#[derive(Debug, Clone)]
pub struct EngineConfig {
/// Maximum sprites per batch.
/// 每批次最大精灵数。
pub max_sprites: usize,
/// Enable debug mode.
/// 启用调试模式。
pub debug: bool,
}
impl Default for EngineConfig {
fn default() -> Self {
Self {
max_sprites: 10000,
debug: false,
}
}
}
/// Main game engine.
/// 主游戏引擎。
///
/// Coordinates all engine subsystems including rendering, input, and resources.
/// 协调所有引擎子系统,包括渲染、输入和资源。
pub struct Engine {
/// WebGL context.
/// WebGL上下文。
context: WebGLContext,
/// 2D renderer.
/// 2D渲染器。
renderer: Renderer2D,
/// Texture manager.
/// 纹理管理器。
texture_manager: TextureManager,
/// Input manager.
/// 输入管理器。
input_manager: InputManager,
/// Engine configuration.
/// 引擎配置。
#[allow(dead_code)]
config: EngineConfig,
}
impl Engine {
/// Create a new engine instance.
/// 创建新的引擎实例。
///
/// # Arguments | 参数
/// * `canvas_id` - The HTML canvas element ID | HTML canvas元素ID
/// * `config` - Engine configuration | 引擎配置
///
/// # Returns | 返回
/// A new Engine instance or an error | 新的Engine实例或错误
pub fn new(canvas_id: &str, config: EngineConfig) -> Result<Self> {
let context = WebGLContext::new(canvas_id)?;
// Initialize WebGL state | 初始化WebGL状态
context.set_viewport();
context.enable_blend();
// Create subsystems | 创建子系统
let renderer = Renderer2D::new(context.gl(), config.max_sprites)?;
let texture_manager = TextureManager::new(context.gl().clone());
let input_manager = InputManager::new();
log::info!("Engine created successfully | 引擎创建成功");
Ok(Self {
context,
renderer,
texture_manager,
input_manager,
config,
})
}
/// Create a new engine instance from external WebGL context.
/// 从外部 WebGL 上下文创建引擎实例。
///
/// This is designed for environments like WeChat MiniGame.
/// 适用于微信小游戏等环境。
pub fn from_external(
gl_context: JsValue,
width: u32,
height: u32,
config: EngineConfig,
) -> Result<Self> {
let context = WebGLContext::from_external(gl_context, width, height)?;
context.set_viewport();
context.enable_blend();
let renderer = Renderer2D::new(context.gl(), config.max_sprites)?;
let texture_manager = TextureManager::new(context.gl().clone());
let input_manager = InputManager::new();
log::info!("Engine created from external context | 从外部上下文创建引擎");
Ok(Self {
context,
renderer,
texture_manager,
input_manager,
config,
})
}
/// Clear the screen with specified color.
/// 使用指定颜色清除屏幕。
pub fn clear(&self, r: f32, g: f32, b: f32, a: f32) {
self.context.clear(r, g, b, a);
}
/// Get canvas width.
/// 获取画布宽度。
#[inline]
pub fn width(&self) -> u32 {
self.context.width()
}
/// Get canvas height.
/// 获取画布高度。
#[inline]
pub fn height(&self) -> u32 {
self.context.height()
}
/// Submit sprite batch data for rendering.
/// 提交精灵批次数据进行渲染。
pub fn submit_sprite_batch(
&mut self,
transforms: &[f32],
texture_ids: &[u32],
uvs: &[f32],
colors: &[u32],
) -> Result<()> {
self.renderer.submit_batch(
transforms,
texture_ids,
uvs,
colors,
&self.texture_manager,
)
}
/// Render the current frame.
/// 渲染当前帧。
pub fn render(&mut self) -> Result<()> {
self.renderer.render(self.context.gl())
}
/// Load a texture from URL.
/// 从URL加载纹理。
pub fn load_texture(&mut self, id: u32, url: &str) -> Result<()> {
self.texture_manager.load_texture(id, url)
}
/// Check if a key is currently pressed.
/// 检查某个键是否当前被按下。
pub fn is_key_down(&self, key_code: &str) -> bool {
self.input_manager.is_key_down(key_code)
}
/// Update input state.
/// 更新输入状态。
pub fn update_input(&mut self) {
self.input_manager.update();
}
}

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

195
packages/engine/src/lib.rs Normal file
View File

@@ -0,0 +1,195 @@
//! ES Engine - High-performance 2D game engine for web and mobile platforms.
//! ES引擎 - 高性能2D游戏引擎支持Web和移动平台。
//!
//! # Architecture | 架构
//!
//! The engine is designed with a modular architecture:
//! 引擎采用模块化架构设计:
//!
//! - `core` - Engine lifecycle and context management | 引擎生命周期和上下文管理
//! - `renderer` - 2D rendering with batch optimization | 2D渲染与批处理优化
//! - `math` - Mathematical primitives (vectors, matrices) | 数学基元(向量、矩阵)
//! - `resource` - Asset loading and management | 资源加载和管理
//! - `input` - Keyboard, mouse, and touch input | 键盘、鼠标和触摸输入
//! - `platform` - Platform abstraction layer | 平台抽象层
//!
//! # Example | 示例
//!
//! ```typescript
//! import { GameEngine } from 'es-engine';
//!
//! const engine = new GameEngine('canvas');
//! engine.loadTexture('player', 'assets/player.png');
//!
//! function gameLoop() {
//! engine.clear(0.0, 0.0, 0.0, 1.0);
//! engine.submitSpriteBatch(transforms, textureIds, uvs, colors);
//! engine.render();
//! requestAnimationFrame(gameLoop);
//! }
//! ```
#![warn(missing_docs)]
#![warn(rustdoc::missing_crate_level_docs)]
use wasm_bindgen::prelude::*;
// Module declarations | 模块声明
pub mod core;
pub mod math;
pub mod platform;
pub mod renderer;
pub mod resource;
pub mod input;
// Re-exports | 重新导出
pub use crate::core::{Engine, EngineConfig};
pub use crate::core::error::{EngineError, Result};
/// Initialize panic hook for better error messages in console.
/// 初始化panic hook以在控制台显示更好的错误信息。
#[wasm_bindgen(start)]
pub fn init() {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
// Initialize logger | 初始化日志
console_log::init_with_level(log::Level::Debug)
.expect("Failed to initialize logger | 日志初始化失败");
log::info!("ES Engine initialized | ES引擎初始化完成");
}
/// Game engine main interface exposed to JavaScript.
/// 暴露给JavaScript的游戏引擎主接口。
///
/// This is the primary entry point for the engine from TypeScript/JavaScript.
/// 这是从TypeScript/JavaScript访问引擎的主要入口点。
#[wasm_bindgen]
pub struct GameEngine {
engine: Engine,
}
#[wasm_bindgen]
impl GameEngine {
/// Create a new game engine instance.
/// 创建新的游戏引擎实例。
///
/// # Arguments | 参数
/// * `canvas_id` - The HTML canvas element ID | HTML canvas元素ID
///
/// # Returns | 返回
/// A new GameEngine instance or an error | 新的GameEngine实例或错误
#[wasm_bindgen(constructor)]
pub fn new(canvas_id: &str) -> std::result::Result<GameEngine, JsValue> {
let config = EngineConfig::default();
let engine = Engine::new(canvas_id, config)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(GameEngine { engine })
}
/// Create a new game engine from external WebGL context.
/// 从外部 WebGL 上下文创建引擎。
///
/// This is designed for WeChat MiniGame and similar environments.
/// 适用于微信小游戏等环境。
#[wasm_bindgen(js_name = fromExternal)]
pub fn from_external(
gl_context: JsValue,
width: u32,
height: u32,
) -> std::result::Result<GameEngine, JsValue> {
let config = EngineConfig::default();
let engine = Engine::from_external(gl_context, width, height, config)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(GameEngine { engine })
}
/// Clear the screen with specified color.
/// 使用指定颜色清除屏幕。
///
/// # Arguments | 参数
/// * `r` - Red component (0.0-1.0) | 红色分量
/// * `g` - Green component (0.0-1.0) | 绿色分量
/// * `b` - Blue component (0.0-1.0) | 蓝色分量
/// * `a` - Alpha component (0.0-1.0) | 透明度分量
pub fn clear(&self, r: f32, g: f32, b: f32, a: f32) {
self.engine.clear(r, g, b, a);
}
/// Get canvas width.
/// 获取画布宽度。
#[wasm_bindgen(getter)]
pub fn width(&self) -> u32 {
self.engine.width()
}
/// Get canvas height.
/// 获取画布高度。
#[wasm_bindgen(getter)]
pub fn height(&self) -> u32 {
self.engine.height()
}
/// Submit sprite batch data for rendering.
/// 提交精灵批次数据进行渲染。
///
/// # Arguments | 参数
/// * `transforms` - Float32Array [x, y, rotation, scaleX, scaleY, originX, originY] per sprite
/// 每个精灵的变换数据
/// * `texture_ids` - Uint32Array of texture IDs | 纹理ID数组
/// * `uvs` - Float32Array [u0, v0, u1, v1] per sprite | 每个精灵的UV坐标
/// * `colors` - Uint32Array of packed RGBA colors | 打包的RGBA颜色数组
#[wasm_bindgen(js_name = submitSpriteBatch)]
pub fn submit_sprite_batch(
&mut self,
transforms: &[f32],
texture_ids: &[u32],
uvs: &[f32],
colors: &[u32],
) -> std::result::Result<(), JsValue> {
self.engine
.submit_sprite_batch(transforms, texture_ids, uvs, colors)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
/// Render the current frame.
/// 渲染当前帧。
pub fn render(&mut self) -> std::result::Result<(), JsValue> {
self.engine
.render()
.map_err(|e| JsValue::from_str(&e.to_string()))
}
/// Load a texture from URL.
/// 从URL加载纹理。
///
/// # Arguments | 参数
/// * `id` - Unique texture identifier | 唯一纹理标识符
/// * `url` - Image URL to load | 要加载的图片URL
#[wasm_bindgen(js_name = loadTexture)]
pub fn load_texture(&mut self, id: u32, url: &str) -> std::result::Result<(), JsValue> {
self.engine
.load_texture(id, url)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
/// Check if a key is currently pressed.
/// 检查某个键是否当前被按下。
///
/// # Arguments | 参数
/// * `key_code` - The key code to check | 要检查的键码
#[wasm_bindgen(js_name = isKeyDown)]
pub fn is_key_down(&self, key_code: &str) -> bool {
self.engine.is_key_down(key_code)
}
/// Update input state. Should be called once per frame.
/// 更新输入状态。应该每帧调用一次。
#[wasm_bindgen(js_name = updateInput)]
pub fn update_input(&mut self) {
self.engine.update_input();
}
}

View File

@@ -0,0 +1,184 @@
//! 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
/// 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 (ABGR format for WebGL).
/// 转换为打包的u32WebGL的ABGR格式
#[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;
(a << 24) | (b << 16) | (g << 8) | r
}
/// Create from packed u32 (ABGR format).
/// 从打包的u32创建ABGR格式
#[inline]
pub fn from_packed(packed: u32) -> Self {
Self::from_rgba8(
(packed & 0xFF) as u8,
((packed >> 8) & 0xFF) as u8,
((packed >> 16) & 0xFF) as u8,
((packed >> 24) & 0xFF) as u8,
)
}
/// 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,148 @@
//! Rectangle implementation.
//! 矩形实现。
use super::Vec2;
/// Axis-aligned rectangle.
/// 轴对齐矩形。
///
/// # Examples | 示例
/// ```rust
/// 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,164 @@
//! 2D transform implementation.
//! 2D变换实现。
use super::Vec2;
use glam::Mat3;
/// 2D transformation combining position, rotation, and scale.
/// 组合位置、旋转和缩放的2D变换。
///
/// # Examples | 示例
/// ```rust
/// 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平移、旋转、缩放
pub fn to_matrix(&self) -> Mat3 {
let cos = self.rotation.cos();
let sin = self.rotation.sin();
// Construct TRS matrix directly for performance
// 直接构造TRS矩阵以提高性能
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 | 精灵高度
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
// 在旋转后应用原点偏移
let tx = self.position.x + ox * cos - oy * sin;
let ty = self.position.y + ox * sin + oy * cos;
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,214 @@
//! 2D vector implementation.
//! 2D向量实现。
use bytemuck::{Pod, Zeroable};
/// 2D vector for positions, velocities, and directions.
/// 用于位置、速度和方向的2D向量。
///
/// # Examples | 示例
/// ```rust
/// 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).
/// 按角度旋转向量(弧度)。
#[inline]
pub fn rotate(&self, angle: f32) -> Self {
let cos = angle.cos();
let sin = angle.sin();
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,8 @@
//! Sprite batch rendering system.
//! 精灵批处理渲染系统。
mod sprite_batch;
mod vertex;
pub use sprite_batch::SpriteBatch;
pub use vertex::{SpriteVertex, VERTEX_SIZE};

View File

@@ -0,0 +1,401 @@
//! Sprite batch renderer for efficient 2D rendering.
//! 用于高效2D渲染的精灵批处理渲染器。
use web_sys::{
WebGl2RenderingContext, WebGlBuffer, WebGlVertexArrayObject,
};
use crate::core::error::{EngineError, Result};
use crate::math::Color;
use crate::resource::TextureManager;
use super::vertex::FLOATS_PER_VERTEX;
/// Number of vertices per sprite (quad).
/// 每个精灵的顶点数(四边形)。
const VERTICES_PER_SPRITE: usize = 4;
/// Number of indices per sprite (2 triangles).
/// 每个精灵的索引数2个三角形
const INDICES_PER_SPRITE: usize = 6;
/// Transform data stride (x, y, rotation, scaleX, scaleY, originX, originY).
/// 变换数据步长。
const TRANSFORM_STRIDE: usize = 7;
/// UV data stride (u0, v0, u1, v1).
/// UV数据步长。
const UV_STRIDE: usize = 4;
/// Sprite batch renderer.
/// 精灵批处理渲染器。
///
/// Batches multiple sprites into a single draw call for optimal performance.
/// 将多个精灵合并为单次绘制调用以获得最佳性能。
///
/// # Performance | 性能
/// - Uses dynamic vertex buffer for efficient updates | 使用动态顶点缓冲区以高效更新
/// - Minimizes state changes and draw calls | 最小化状态更改和绘制调用
/// - Supports up to 10000+ sprites per batch | 每批次支持10000+精灵
pub struct SpriteBatch {
/// Vertex array object.
/// 顶点数组对象。
vao: WebGlVertexArrayObject,
/// Vertex buffer object.
/// 顶点缓冲区对象。
vbo: WebGlBuffer,
/// Index buffer object.
/// 索引缓冲区对象。
ibo: WebGlBuffer,
/// Maximum number of sprites.
/// 最大精灵数。
max_sprites: usize,
/// Vertex data buffer.
/// 顶点数据缓冲区。
vertices: Vec<f32>,
/// Current number of sprites in batch.
/// 当前批次中的精灵数。
sprite_count: usize,
/// Current texture ID being batched.
/// 当前正在批处理的纹理ID。
current_texture: Option<u32>,
}
impl SpriteBatch {
/// Create a new sprite batch.
/// 创建新的精灵批处理器。
///
/// # Arguments | 参数
/// * `gl` - WebGL2 context | WebGL2上下文
/// * `max_sprites` - Maximum sprites per batch | 每批次最大精灵数
pub fn new(gl: &WebGl2RenderingContext, max_sprites: usize) -> Result<Self> {
// Create VAO | 创建VAO
let vao = gl
.create_vertex_array()
.ok_or(EngineError::BufferCreationFailed)?;
gl.bind_vertex_array(Some(&vao));
// Create vertex buffer | 创建顶点缓冲区
let vbo = gl
.create_buffer()
.ok_or(EngineError::BufferCreationFailed)?;
gl.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&vbo));
// Allocate vertex buffer memory | 分配顶点缓冲区内存
let vertex_buffer_size = max_sprites * VERTICES_PER_SPRITE * FLOATS_PER_VERTEX * 4;
gl.buffer_data_with_i32(
WebGl2RenderingContext::ARRAY_BUFFER,
vertex_buffer_size as i32,
WebGl2RenderingContext::DYNAMIC_DRAW,
);
// Create and populate index buffer | 创建并填充索引缓冲区
let ibo = gl
.create_buffer()
.ok_or(EngineError::BufferCreationFailed)?;
gl.bind_buffer(WebGl2RenderingContext::ELEMENT_ARRAY_BUFFER, Some(&ibo));
let indices = Self::generate_indices(max_sprites);
unsafe {
let index_array = js_sys::Uint16Array::view(&indices);
gl.buffer_data_with_array_buffer_view(
WebGl2RenderingContext::ELEMENT_ARRAY_BUFFER,
&index_array,
WebGl2RenderingContext::STATIC_DRAW,
);
}
// Set up vertex attributes | 设置顶点属性
Self::setup_vertex_attributes(gl);
// Unbind VAO | 解绑VAO
gl.bind_vertex_array(None);
log::debug!(
"SpriteBatch created with capacity: {} sprites | SpriteBatch创建完成容量: {}个精灵",
max_sprites,
max_sprites
);
Ok(Self {
vao,
vbo,
ibo,
max_sprites,
vertices: Vec::with_capacity(max_sprites * VERTICES_PER_SPRITE * FLOATS_PER_VERTEX),
sprite_count: 0,
current_texture: None,
})
}
/// Generate index buffer data.
/// 生成索引缓冲区数据。
fn generate_indices(max_sprites: usize) -> Vec<u16> {
let mut indices = Vec::with_capacity(max_sprites * INDICES_PER_SPRITE);
for i in 0..max_sprites {
let base = (i * VERTICES_PER_SPRITE) as u16;
// Two triangles per sprite | 每个精灵两个三角形
// Triangle 1: 0, 1, 2 | 三角形1
// Triangle 2: 2, 3, 0 | 三角形2
indices.push(base);
indices.push(base + 1);
indices.push(base + 2);
indices.push(base + 2);
indices.push(base + 3);
indices.push(base);
}
indices
}
/// Set up vertex attribute pointers.
/// 设置顶点属性指针。
fn setup_vertex_attributes(gl: &WebGl2RenderingContext) {
let stride = (FLOATS_PER_VERTEX * 4) as i32;
// Position attribute (location = 0) | 位置属性
gl.enable_vertex_attrib_array(0);
gl.vertex_attrib_pointer_with_i32(
0,
2,
WebGl2RenderingContext::FLOAT,
false,
stride,
0,
);
// Texture coordinate attribute (location = 1) | 纹理坐标属性
gl.enable_vertex_attrib_array(1);
gl.vertex_attrib_pointer_with_i32(
1,
2,
WebGl2RenderingContext::FLOAT,
false,
stride,
8, // 2 floats * 4 bytes
);
// Color attribute (location = 2) | 颜色属性
gl.enable_vertex_attrib_array(2);
gl.vertex_attrib_pointer_with_i32(
2,
4,
WebGl2RenderingContext::FLOAT,
false,
stride,
16, // 4 floats * 4 bytes
);
}
/// Clear the batch for a new frame.
/// 为新帧清空批处理。
pub fn clear(&mut self) {
self.vertices.clear();
self.sprite_count = 0;
self.current_texture = None;
}
/// Add sprites from batch data.
/// 从批处理数据添加精灵。
///
/// # Arguments | 参数
/// * `transforms` - [x, y, rotation, scaleX, scaleY, originX, originY] per sprite
/// * `texture_ids` - Texture ID for each sprite | 每个精灵的纹理ID
/// * `uvs` - [u0, v0, u1, v1] per sprite | 每个精灵的UV坐标
/// * `colors` - Packed RGBA color per sprite | 每个精灵的打包RGBA颜色
/// * `texture_manager` - Texture manager for getting texture sizes | 纹理管理器
pub fn add_sprites(
&mut self,
transforms: &[f32],
texture_ids: &[u32],
uvs: &[f32],
colors: &[u32],
texture_manager: &TextureManager,
) -> Result<()> {
let sprite_count = texture_ids.len();
// Validate input data | 验证输入数据
if transforms.len() != sprite_count * TRANSFORM_STRIDE {
return Err(EngineError::InvalidBatchData(format!(
"Transform data length mismatch: expected {}, got {}",
sprite_count * TRANSFORM_STRIDE,
transforms.len()
)));
}
if uvs.len() != sprite_count * UV_STRIDE {
return Err(EngineError::InvalidBatchData(format!(
"UV data length mismatch: expected {}, got {}",
sprite_count * UV_STRIDE,
uvs.len()
)));
}
if colors.len() != sprite_count {
return Err(EngineError::InvalidBatchData(format!(
"Color data length mismatch: expected {}, got {}",
sprite_count,
colors.len()
)));
}
// Check capacity | 检查容量
if self.sprite_count + sprite_count > self.max_sprites {
return Err(EngineError::InvalidBatchData(format!(
"Batch capacity exceeded: {} + {} > {}",
self.sprite_count, sprite_count, self.max_sprites
)));
}
// Add each sprite | 添加每个精灵
for i in 0..sprite_count {
let t_offset = i * TRANSFORM_STRIDE;
let uv_offset = i * UV_STRIDE;
let x = transforms[t_offset];
let y = transforms[t_offset + 1];
let rotation = transforms[t_offset + 2];
let scale_x = transforms[t_offset + 3];
let scale_y = transforms[t_offset + 4];
let origin_x = transforms[t_offset + 5];
let origin_y = transforms[t_offset + 6];
let u0 = uvs[uv_offset];
let v0 = uvs[uv_offset + 1];
let u1 = uvs[uv_offset + 2];
let v1 = uvs[uv_offset + 3];
let color = Color::from_packed(colors[i]);
let color_arr = [color.r, color.g, color.b, color.a];
// Get texture size for this sprite | 获取此精灵的纹理尺寸
let (tex_width, tex_height) = texture_manager
.get_texture_size(texture_ids[i])
.unwrap_or((64.0, 64.0));
let width = tex_width * scale_x;
let height = tex_height * scale_y;
// Calculate transformed vertices | 计算变换后的顶点
self.add_sprite_vertices(
x, y, width, height, rotation, origin_x, origin_y,
u0, v0, u1, v1, color_arr,
);
}
self.sprite_count += sprite_count;
Ok(())
}
/// Add vertices for a single sprite.
/// 为单个精灵添加顶点。
#[inline]
fn add_sprite_vertices(
&mut self,
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],
) {
let cos = rotation.cos();
let sin = rotation.sin();
// Origin offset | 原点偏移
let ox = origin_x * width;
let oy = origin_y * height;
// Local corner positions (relative to origin) | 局部角点位置(相对于原点)
let corners = [
(-ox, -oy), // Top-left | 左上
(width - ox, -oy), // Top-right | 右上
(width - ox, height - oy), // Bottom-right | 右下
(-ox, height - oy), // Bottom-left | 左下
];
let tex_coords = [
[u0, v0], // Top-left
[u1, v0], // Top-right
[u1, v1], // Bottom-right
[u0, v1], // Bottom-left
];
// Transform and add each vertex | 变换并添加每个顶点
for i in 0..4 {
let (lx, ly) = corners[i];
// Apply rotation | 应用旋转
let rx = lx * cos - ly * sin;
let ry = lx * sin + ly * cos;
// Apply translation | 应用平移
let px = rx + x;
let py = ry + y;
// Position | 位置
self.vertices.push(px);
self.vertices.push(py);
// Texture coordinates | 纹理坐标
self.vertices.push(tex_coords[i][0]);
self.vertices.push(tex_coords[i][1]);
// Color | 颜色
self.vertices.extend_from_slice(&color);
}
}
/// Flush the batch to GPU and render.
/// 将批处理刷新到GPU并渲染。
pub fn flush(&mut self, gl: &WebGl2RenderingContext) {
if self.sprite_count == 0 {
return;
}
// Bind VAO | 绑定VAO
gl.bind_vertex_array(Some(&self.vao));
// Upload vertex data | 上传顶点数据
gl.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&self.vbo));
unsafe {
let vertex_array = js_sys::Float32Array::view(&self.vertices);
gl.buffer_sub_data_with_i32_and_array_buffer_view(
WebGl2RenderingContext::ARRAY_BUFFER,
0,
&vertex_array,
);
}
// Draw | 绘制
let index_count = (self.sprite_count * INDICES_PER_SPRITE) as i32;
gl.draw_elements_with_i32(
WebGl2RenderingContext::TRIANGLES,
index_count,
WebGl2RenderingContext::UNSIGNED_SHORT,
0,
);
// Unbind VAO | 解绑VAO
gl.bind_vertex_array(None);
}
/// Get current sprite count.
/// 获取当前精灵数量。
#[inline]
pub fn sprite_count(&self) -> usize {
self.sprite_count
}
}

View File

@@ -0,0 +1,60 @@
//! 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.
/// 每个顶点的浮点数数量。
pub const FLOATS_PER_VERTEX: usize = 8;
/// Sprite vertex data.
/// 精灵顶点数据。
///
/// Each sprite requires 4 vertices (quad), each with position, UV, and color.
/// 每个精灵需要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],
}
impl SpriteVertex {
/// Create a new sprite vertex.
/// 创建新的精灵顶点。
#[inline]
pub const fn new(
position: [f32; 2],
tex_coord: [f32; 2],
color: [f32; 4],
) -> Self {
Self {
position,
tex_coord,
color,
}
}
}
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],
}
}
}

View File

@@ -0,0 +1,144 @@
//! 2D camera implementation.
//! 2D相机实现。
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 screen coordinates
/// to normalized device coordinates.
/// 创建将屏幕坐标映射到标准化设备坐标的正交投影。
pub fn projection_matrix(&self) -> Mat3 {
// Orthographic projection | 正交投影
// Maps [0, width] x [0, height] to [-1, 1] x [-1, 1]
let sx = 2.0 / self.width * self.zoom;
let sy = -2.0 / self.height * self.zoom; // Flip Y axis | 翻转Y轴
let cos = self.rotation.cos();
let sin = self.rotation.sin();
// Apply zoom, rotation, and translation
// 应用缩放、旋转和平移
let tx = -self.position.x * sx * cos - self.position.y * sy * sin - 1.0;
let ty = -self.position.x * sx * sin + self.position.y * sy * cos + 1.0;
Mat3::from_cols(
glam::Vec3::new(sx * cos, sx * sin, 0.0),
glam::Vec3::new(sy * -sin, sy * cos, 0.0),
glam::Vec3::new(tx, ty, 1.0),
)
}
/// Convert screen coordinates to world coordinates.
/// 将屏幕坐标转换为世界坐标。
pub fn screen_to_world(&self, screen: Vec2) -> Vec2 {
let x = (screen.x / self.zoom) + self.position.x;
let y = (screen.y / self.zoom) + self.position.y;
if self.rotation != 0.0 {
let dx = x - self.position.x;
let dy = y - self.position.y;
let cos = (-self.rotation).cos();
let sin = (-self.rotation).sin();
Vec2::new(
dx * cos - dy * sin + self.position.x,
dx * sin + dy * cos + self.position.y,
)
} else {
Vec2::new(x, y)
}
}
/// Convert world coordinates to screen coordinates.
/// 将世界坐标转换为屏幕坐标。
pub fn world_to_screen(&self, world: Vec2) -> Vec2 {
let dx = world.x - self.position.x;
let dy = world.y - self.position.y;
if self.rotation != 0.0 {
let cos = self.rotation.cos();
let sin = self.rotation.sin();
let rx = dx * cos - dy * sin;
let ry = dx * sin + dy * cos;
Vec2::new(rx * self.zoom, ry * self.zoom)
} else {
Vec2::new(dx * self.zoom, dy * self.zoom)
}
}
/// 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.1, 10.0);
}
}
impl Default for Camera2D {
fn default() -> Self {
Self::new(800.0, 600.0)
}
}

View File

@@ -0,0 +1,14 @@
//! 2D rendering system with batch optimization.
//! 带批处理优化的2D渲染系统。
pub mod batch;
pub mod shader;
pub mod texture;
mod renderer2d;
mod camera;
pub use renderer2d::Renderer2D;
pub use camera::Camera2D;
pub use batch::SpriteBatch;
pub use texture::{Texture, TextureManager};

View File

@@ -0,0 +1,134 @@
//! Main 2D renderer implementation.
//! 主2D渲染器实现。
use wasm_bindgen::JsCast;
use web_sys::WebGl2RenderingContext;
use crate::core::error::Result;
use crate::resource::TextureManager;
use super::batch::SpriteBatch;
use super::camera::Camera2D;
use super::shader::{ShaderProgram, SPRITE_VERTEX_SHADER, SPRITE_FRAGMENT_SHADER};
/// 2D renderer with batched sprite rendering.
/// 带批处理精灵渲染的2D渲染器。
///
/// Coordinates sprite batching, shader management, and camera transforms.
/// 协调精灵批处理、Shader管理和相机变换。
pub struct Renderer2D {
/// Sprite batch renderer.
/// 精灵批处理渲染器。
sprite_batch: SpriteBatch,
/// Sprite shader program.
/// 精灵Shader程序。
shader: ShaderProgram,
/// 2D camera.
/// 2D相机。
camera: Camera2D,
}
impl Renderer2D {
/// Create a new 2D renderer.
/// 创建新的2D渲染器。
///
/// # Arguments | 参数
/// * `gl` - WebGL2 context | WebGL2上下文
/// * `max_sprites` - Maximum sprites per batch | 每批次最大精灵数
pub fn new(gl: &WebGl2RenderingContext, max_sprites: usize) -> Result<Self> {
let sprite_batch = SpriteBatch::new(gl, max_sprites)?;
let shader = ShaderProgram::new(gl, SPRITE_VERTEX_SHADER, SPRITE_FRAGMENT_SHADER)?;
// Get canvas size for camera | 获取canvas尺寸用于相机
let canvas = gl.canvas()
.and_then(|c| c.dyn_into::<web_sys::HtmlCanvasElement>().ok())
.map(|c| (c.width() as f32, c.height() as f32))
.unwrap_or((800.0, 600.0));
let camera = Camera2D::new(canvas.0, canvas.1);
log::info!(
"Renderer2D initialized | Renderer2D初始化完成: {}x{}, max sprites: {}",
canvas.0, canvas.1, max_sprites
);
Ok(Self {
sprite_batch,
shader,
camera,
})
}
/// Submit sprite batch data for rendering.
/// 提交精灵批次数据进行渲染。
///
/// # Arguments | 参数
/// * `transforms` - Transform data for each sprite | 每个精灵的变换数据
/// * `texture_ids` - Texture ID for each sprite | 每个精灵的纹理ID
/// * `uvs` - UV coordinates for each sprite | 每个精灵的UV坐标
/// * `colors` - Packed color for each sprite | 每个精灵的打包颜色
/// * `texture_manager` - Texture manager | 纹理管理器
pub fn submit_batch(
&mut self,
transforms: &[f32],
texture_ids: &[u32],
uvs: &[f32],
colors: &[u32],
texture_manager: &TextureManager,
) -> Result<()> {
self.sprite_batch.add_sprites(
transforms,
texture_ids,
uvs,
colors,
texture_manager,
)
}
/// Render the current frame.
/// 渲染当前帧。
pub fn render(&mut self, gl: &WebGl2RenderingContext) -> Result<()> {
if self.sprite_batch.sprite_count() == 0 {
return Ok(());
}
// Bind shader | 绑定Shader
self.shader.bind(gl);
// Set projection matrix | 设置投影矩阵
let projection = self.camera.projection_matrix();
self.shader.set_uniform_mat3(gl, "u_projection", &projection.to_cols_array());
// Set texture sampler | 设置纹理采样器
self.shader.set_uniform_i32(gl, "u_texture", 0);
// Flush sprite batch | 刷新精灵批处理
self.sprite_batch.flush(gl);
// Clear batch for next frame | 清空批处理以供下一帧使用
self.sprite_batch.clear();
Ok(())
}
/// Get mutable reference to camera.
/// 获取相机的可变引用。
#[inline]
pub fn camera_mut(&mut self) -> &mut Camera2D {
&mut self.camera
}
/// Get reference to camera.
/// 获取相机的引用。
#[inline]
pub fn camera(&self) -> &Camera2D {
&self.camera
}
/// Update camera viewport size.
/// 更新相机视口大小。
pub fn resize(&mut self, width: f32, height: f32) {
self.camera.set_viewport(width, height);
}
}

View File

@@ -0,0 +1,63 @@
//! Built-in shader source code.
//! 内置Shader源代码。
/// 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;
}
}
"#;

View File

@@ -0,0 +1,8 @@
//! Shader management system.
//! Shader管理系统。
mod program;
mod builtin;
pub use program::ShaderProgram;
pub use builtin::{SPRITE_VERTEX_SHADER, SPRITE_FRAGMENT_SHADER};

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;

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,217 @@
//! Texture loading and management.
//! 纹理加载和管理。
use std::collections::HashMap;
use std::cell::RefCell;
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 super::Texture;
/// Texture manager for loading and caching textures.
/// 用于加载和缓存纹理的纹理管理器。
pub struct TextureManager {
/// WebGL context.
/// WebGL上下文。
gl: WebGl2RenderingContext,
/// Loaded textures.
/// 已加载的纹理。
textures: HashMap<u32, Texture>,
/// Default white texture for untextured rendering.
/// 用于无纹理渲染的默认白色纹理。
default_texture: Option<WebGlTexture>,
}
impl TextureManager {
/// Create a new texture manager.
/// 创建新的纹理管理器。
pub fn new(gl: WebGl2RenderingContext) -> Self {
let mut manager = Self {
gl,
textures: HashMap::new(),
default_texture: None,
};
// 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.
/// 注意:这是一个异步操作。纹理在图片加载后可用。
pub fn load_texture(&mut self, id: u32, url: &str) -> Result<()> {
// Create placeholder texture | 创建占位纹理
let texture = self.gl
.create_texture()
.ok_or_else(|| EngineError::TextureLoadFailed("Failed to create texture".into()))?;
// Set up temporary 1x1 texture | 设置临时1x1纹理
self.gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture));
let placeholder: [u8; 4] = [128, 128, 128, 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(&placeholder),
);
// Store texture with placeholder size | 存储带占位符尺寸的纹理
self.textures.insert(id, Texture::new(texture.clone(), 1, 1));
// Load actual image asynchronously | 异步加载实际图片
let gl = self.gl.clone();
let texture_rc = Rc::new(RefCell::new(texture));
let texture_clone = Rc::clone(&texture_rc);
// We need to update the stored texture size after loading
// For MVP, we'll handle this through a callback mechanism
// 加载后需要更新存储的纹理尺寸
// 对于MVP我们通过回调机制处理
let image = HtmlImageElement::new()
.map_err(|_| EngineError::TextureLoadFailed("Failed to create image element".into()))?;
// Clone image for use in closure | 克隆图片用于闭包
let image_clone = image.clone();
// Set up load callback | 设置加载回调
let onload = Closure::wrap(Box::new(move || {
let tex = texture_clone.borrow();
gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&tex));
// Use the captured image element | 使用捕获的图片元素
let _ = 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,
);
// 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,
);
log::debug!("Texture loaded | 纹理加载完成");
}) as Box<dyn Fn()>);
image.set_onload(Some(onload.as_ref().unchecked_ref()));
onload.forget(); // Prevent closure from being dropped | 防止闭包被销毁
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获取纹理尺寸。
#[inline]
pub fn get_texture_size(&self, id: u32) -> Option<(f32, f32)> {
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 {
self.gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(default));
}
}
/// Check if texture is loaded.
/// 检查纹理是否已加载。
#[inline]
pub fn has_texture(&self, id: u32) -> bool {
self.textures.contains_key(&id)
}
/// 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));
}
}
}

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