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