refactor(render): 抽象图形后端并迁移渲染器 (#313)
* refactor(render): 抽象图形后端并迁移渲染器 - 新增 engine-shared 包,定义 GraphicsBackend trait 抽象层 - 实现 WebGL2Backend 作为首个后端实现 - 迁移 Renderer2D、SpriteBatch、GridRenderer、GizmoRenderer 使用新抽象 - 修复 VAO 创建时索引缓冲区绑定状态泄漏问题 - 新增 create_vertex_buffer_sized 方法支持预分配缓冲区 * fix(serialization): 修复序列化循环引用导致栈溢出 - 在 serializeValue 添加 WeakSet 检测循环引用 - 跳过已访问对象避免无限递归 * refactor(serialization): 提取 ValueSerializer 统一序列化逻辑 - 新增 ValueSerializer 模块,函数式设计 - 支持可扩展类型处理器注册 - 移除 ComponentSerializer/SceneSerializer 重复代码 - 内置 Date/Map/Set 类型支持 * fix: CodeQL 类型检查警告
This commit is contained in:
10
packages/engine/src/backend/mod.rs
Normal file
10
packages/engine/src/backend/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! 图形后端实现
|
||||
//!
|
||||
//! Graphics backend implementations.
|
||||
//!
|
||||
//! 本模块提供 `GraphicsBackend` trait 的具体实现。
|
||||
//! This module provides concrete implementations of the `GraphicsBackend` trait.
|
||||
|
||||
mod webgl2;
|
||||
|
||||
pub use webgl2::WebGL2Backend;
|
||||
962
packages/engine/src/backend/webgl2.rs
Normal file
962
packages/engine/src/backend/webgl2.rs
Normal file
@@ -0,0 +1,962 @@
|
||||
//! WebGL2 后端实现
|
||||
//!
|
||||
//! WebGL2 backend implementation.
|
||||
|
||||
use es_engine_shared::{
|
||||
traits::backend::*,
|
||||
types::{
|
||||
handle::*,
|
||||
vertex::*,
|
||||
blend::*,
|
||||
texture::*,
|
||||
},
|
||||
Vec2, Vec3, Vec4, Mat3, Mat4,
|
||||
};
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{
|
||||
WebGl2RenderingContext as GL,
|
||||
WebGlProgram, WebGlShader, WebGlBuffer,
|
||||
WebGlTexture, WebGlVertexArrayObject,
|
||||
WebGlUniformLocation, HtmlCanvasElement,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// ==================== 内部数据结构 | Internal Data Structures ====================
|
||||
|
||||
/// 着色器数据
|
||||
///
|
||||
/// Shader data.
|
||||
struct ShaderData {
|
||||
/// WebGL 程序对象 | WebGL program object
|
||||
program: WebGlProgram,
|
||||
/// Uniform 位置缓存 | Uniform location cache
|
||||
uniform_locations: HashMap<String, Option<WebGlUniformLocation>>,
|
||||
}
|
||||
|
||||
/// 纹理数据
|
||||
///
|
||||
/// Texture data.
|
||||
struct TextureData {
|
||||
/// WebGL 纹理对象 | WebGL texture object
|
||||
handle: WebGlTexture,
|
||||
/// 纹理宽度 | Texture width
|
||||
width: u32,
|
||||
/// 纹理高度 | Texture height
|
||||
height: u32,
|
||||
}
|
||||
|
||||
/// VAO 数据
|
||||
///
|
||||
/// VAO data.
|
||||
struct VertexArrayData {
|
||||
/// WebGL VAO 对象 | WebGL VAO object
|
||||
vao: WebGlVertexArrayObject,
|
||||
/// 关联的索引缓冲区类型 | Associated index buffer type
|
||||
index_type: Option<IndexType>,
|
||||
}
|
||||
|
||||
/// 索引类型
|
||||
///
|
||||
/// Index type.
|
||||
#[derive(Clone, Copy)]
|
||||
enum IndexType {
|
||||
U16,
|
||||
U32,
|
||||
}
|
||||
|
||||
// ==================== WebGL2Backend ====================
|
||||
|
||||
/// WebGL2 图形后端
|
||||
///
|
||||
/// 实现 `GraphicsBackend` trait,提供 WebGL2 渲染能力。
|
||||
///
|
||||
/// WebGL2 graphics backend.
|
||||
/// Implements `GraphicsBackend` trait for WebGL2 rendering.
|
||||
pub struct WebGL2Backend {
|
||||
/// WebGL2 渲染上下文 | WebGL2 rendering context
|
||||
gl: GL,
|
||||
|
||||
/// 画布元素(可选,外部上下文时为 None)
|
||||
///
|
||||
/// Canvas element (None for external context).
|
||||
#[allow(dead_code)]
|
||||
canvas: Option<HtmlCanvasElement>,
|
||||
|
||||
/// 当前宽度 | Current width
|
||||
width: u32,
|
||||
|
||||
/// 当前高度 | Current height
|
||||
height: u32,
|
||||
|
||||
// ===== 资源管理 | Resource Management =====
|
||||
|
||||
/// 缓冲区映射 | Buffer map
|
||||
buffers: HandleMap<WebGlBuffer>,
|
||||
|
||||
/// 顶点数组对象映射 | VAO map
|
||||
vertex_arrays: HandleMap<VertexArrayData>,
|
||||
|
||||
/// 着色器映射 | Shader map
|
||||
shaders: HandleMap<ShaderData>,
|
||||
|
||||
/// 纹理映射 | Texture map
|
||||
textures: HandleMap<TextureData>,
|
||||
|
||||
// ===== 当前状态 | Current State =====
|
||||
|
||||
/// 当前绑定的着色器 | Currently bound shader
|
||||
current_shader: Option<ShaderHandle>,
|
||||
|
||||
/// 当前渲染状态 | Current render state
|
||||
current_render_state: RenderState,
|
||||
|
||||
/// 版本字符串 | Version string
|
||||
version: String,
|
||||
}
|
||||
|
||||
impl WebGL2Backend {
|
||||
/// 从 canvas ID 创建
|
||||
///
|
||||
/// Create from canvas ID.
|
||||
pub fn from_canvas(canvas_id: &str) -> GraphicsResult<Self> {
|
||||
let document = web_sys::window()
|
||||
.ok_or_else(|| GraphicsError::Backend("No window".into()))?
|
||||
.document()
|
||||
.ok_or_else(|| GraphicsError::Backend("No document".into()))?;
|
||||
|
||||
let canvas = document
|
||||
.get_element_by_id(canvas_id)
|
||||
.ok_or_else(|| GraphicsError::Backend(format!("Canvas '{}' not found", canvas_id)))?
|
||||
.dyn_into::<HtmlCanvasElement>()
|
||||
.map_err(|_| GraphicsError::Backend("Element is not a canvas".into()))?;
|
||||
|
||||
let gl = canvas
|
||||
.get_context("webgl2")
|
||||
.map_err(|e| GraphicsError::Backend(format!("Failed to get WebGL2 context: {:?}", e)))?
|
||||
.ok_or_else(|| GraphicsError::Backend("WebGL2 not supported".into()))?
|
||||
.dyn_into::<GL>()
|
||||
.map_err(|_| GraphicsError::Backend("Failed to cast to WebGL2".into()))?;
|
||||
|
||||
let width = canvas.width();
|
||||
let height = canvas.height();
|
||||
|
||||
let version = gl
|
||||
.get_parameter(GL::VERSION)
|
||||
.ok()
|
||||
.and_then(|v| v.as_string())
|
||||
.unwrap_or_else(|| "WebGL 2.0".to_string());
|
||||
|
||||
let mut backend = Self {
|
||||
gl,
|
||||
canvas: Some(canvas),
|
||||
width,
|
||||
height,
|
||||
buffers: HandleMap::new(),
|
||||
vertex_arrays: HandleMap::new(),
|
||||
shaders: HandleMap::new(),
|
||||
textures: HandleMap::new(),
|
||||
current_shader: None,
|
||||
current_render_state: RenderState::default(),
|
||||
version,
|
||||
};
|
||||
|
||||
backend.init_gl_state();
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
/// 从外部 GL 上下文创建(微信小游戏等)
|
||||
///
|
||||
/// Create from external GL context (WeChat Mini Game, etc.).
|
||||
pub fn from_external(gl_context: JsValue, width: u32, height: u32) -> GraphicsResult<Self> {
|
||||
let gl = gl_context
|
||||
.dyn_into::<GL>()
|
||||
.map_err(|_| GraphicsError::Backend("Failed to cast to WebGL2".into()))?;
|
||||
|
||||
let version = gl
|
||||
.get_parameter(GL::VERSION)
|
||||
.ok()
|
||||
.and_then(|v| v.as_string())
|
||||
.unwrap_or_else(|| "WebGL 2.0".to_string());
|
||||
|
||||
let mut backend = Self {
|
||||
gl,
|
||||
canvas: None,
|
||||
width,
|
||||
height,
|
||||
buffers: HandleMap::new(),
|
||||
vertex_arrays: HandleMap::new(),
|
||||
shaders: HandleMap::new(),
|
||||
textures: HandleMap::new(),
|
||||
current_shader: None,
|
||||
current_render_state: RenderState::default(),
|
||||
version,
|
||||
};
|
||||
|
||||
backend.init_gl_state();
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
/// 初始化 GL 状态
|
||||
///
|
||||
/// Initialize GL state.
|
||||
fn init_gl_state(&mut self) {
|
||||
self.gl.viewport(0, 0, self.width as i32, self.height as i32);
|
||||
self.gl.enable(GL::BLEND);
|
||||
self.gl.blend_func(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
|
||||
/// 获取 WebGL 上下文引用
|
||||
///
|
||||
/// Get WebGL context reference.
|
||||
pub fn gl(&self) -> &GL {
|
||||
&self.gl
|
||||
}
|
||||
|
||||
/// 获取或缓存 uniform 位置
|
||||
///
|
||||
/// Get or cache uniform location.
|
||||
fn get_uniform_location(&mut self, name: &str) -> Option<WebGlUniformLocation> {
|
||||
let shader_handle = self.current_shader?;
|
||||
let shader = self.shaders.get_mut(shader_handle)?;
|
||||
|
||||
if let Some(cached) = shader.uniform_locations.get(name) {
|
||||
return cached.clone();
|
||||
}
|
||||
|
||||
let location = self.gl.get_uniform_location(&shader.program, name);
|
||||
shader.uniform_locations.insert(name.to_string(), location.clone());
|
||||
location
|
||||
}
|
||||
|
||||
/// 编译单个着色器
|
||||
///
|
||||
/// Compile single shader.
|
||||
fn compile_shader_stage(&self, shader_type: u32, source: &str) -> GraphicsResult<WebGlShader> {
|
||||
let shader = self.gl.create_shader(shader_type)
|
||||
.ok_or_else(|| GraphicsError::ShaderCompilation("Failed to create shader".into()))?;
|
||||
|
||||
self.gl.shader_source(&shader, source);
|
||||
self.gl.compile_shader(&shader);
|
||||
|
||||
if !self.gl.get_shader_parameter(&shader, GL::COMPILE_STATUS)
|
||||
.as_bool()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let log = self.gl.get_shader_info_log(&shader).unwrap_or_default();
|
||||
self.gl.delete_shader(Some(&shader));
|
||||
let stage_name = if shader_type == GL::VERTEX_SHADER { "Vertex" } else { "Fragment" };
|
||||
return Err(GraphicsError::ShaderCompilation(format!("{} shader: {}", stage_name, log)));
|
||||
}
|
||||
|
||||
Ok(shader)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== GraphicsBackend 实现 | GraphicsBackend Implementation ====================
|
||||
|
||||
impl GraphicsBackend for WebGL2Backend {
|
||||
fn name(&self) -> &'static str {
|
||||
"WebGL2"
|
||||
}
|
||||
|
||||
fn version(&self) -> &str {
|
||||
&self.version
|
||||
}
|
||||
|
||||
fn resize(&mut self, width: u32, height: u32) {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
self.gl.viewport(0, 0, width as i32, height as i32);
|
||||
}
|
||||
|
||||
fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn begin_frame(&mut self) {
|
||||
// WebGL2 不需要显式 begin frame
|
||||
}
|
||||
|
||||
fn end_frame(&mut self) {
|
||||
// WebGL2 不需要显式 end frame(自动 swap)
|
||||
}
|
||||
|
||||
fn clear(&mut self, r: f32, g: f32, b: f32, a: f32) {
|
||||
self.gl.clear_color(r, g, b, a);
|
||||
self.gl.clear(GL::COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
fn set_viewport(&mut self, x: i32, y: i32, width: u32, height: u32) {
|
||||
self.gl.viewport(x, y, width as i32, height as i32);
|
||||
}
|
||||
|
||||
// ==================== 缓冲区操作 | Buffer Operations ====================
|
||||
|
||||
fn create_vertex_buffer(&mut self, data: &[u8], usage: BufferUsage) -> GraphicsResult<BufferHandle> {
|
||||
let buffer = self.gl.create_buffer()
|
||||
.ok_or_else(|| GraphicsError::BufferCreation("Failed to create vertex buffer".into()))?;
|
||||
|
||||
self.gl.bind_buffer(GL::ARRAY_BUFFER, Some(&buffer));
|
||||
|
||||
let gl_usage = match usage {
|
||||
BufferUsage::Static => GL::STATIC_DRAW,
|
||||
BufferUsage::Dynamic => GL::DYNAMIC_DRAW,
|
||||
BufferUsage::Stream => GL::STREAM_DRAW,
|
||||
};
|
||||
|
||||
if data.is_empty() {
|
||||
// Allocate empty buffer - should not happen in normal use
|
||||
self.gl.buffer_data_with_i32(GL::ARRAY_BUFFER, 0, gl_usage);
|
||||
} else {
|
||||
unsafe {
|
||||
let array = js_sys::Uint8Array::view(data);
|
||||
self.gl.buffer_data_with_array_buffer_view(GL::ARRAY_BUFFER, &array, gl_usage);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.buffers.insert(buffer))
|
||||
}
|
||||
|
||||
fn create_vertex_buffer_sized(&mut self, size: usize, usage: BufferUsage) -> GraphicsResult<BufferHandle> {
|
||||
let buffer = self.gl.create_buffer()
|
||||
.ok_or_else(|| GraphicsError::BufferCreation("Failed to create vertex buffer".into()))?;
|
||||
|
||||
self.gl.bind_buffer(GL::ARRAY_BUFFER, Some(&buffer));
|
||||
|
||||
let gl_usage = match usage {
|
||||
BufferUsage::Static => GL::STATIC_DRAW,
|
||||
BufferUsage::Dynamic => GL::DYNAMIC_DRAW,
|
||||
BufferUsage::Stream => GL::STREAM_DRAW,
|
||||
};
|
||||
|
||||
self.gl.buffer_data_with_i32(GL::ARRAY_BUFFER, size as i32, gl_usage);
|
||||
|
||||
Ok(self.buffers.insert(buffer))
|
||||
}
|
||||
|
||||
fn create_index_buffer(&mut self, data: &[u16], usage: BufferUsage) -> GraphicsResult<BufferHandle> {
|
||||
let buffer = self.gl.create_buffer()
|
||||
.ok_or_else(|| GraphicsError::BufferCreation("Failed to create index buffer".into()))?;
|
||||
|
||||
self.gl.bind_buffer(GL::ELEMENT_ARRAY_BUFFER, Some(&buffer));
|
||||
|
||||
let gl_usage = match usage {
|
||||
BufferUsage::Static => GL::STATIC_DRAW,
|
||||
BufferUsage::Dynamic => GL::DYNAMIC_DRAW,
|
||||
BufferUsage::Stream => GL::STREAM_DRAW,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let array = js_sys::Uint16Array::view(data);
|
||||
self.gl.buffer_data_with_array_buffer_view(GL::ELEMENT_ARRAY_BUFFER, &array, gl_usage);
|
||||
}
|
||||
|
||||
Ok(self.buffers.insert(buffer))
|
||||
}
|
||||
|
||||
fn create_index_buffer_u32(&mut self, data: &[u32], usage: BufferUsage) -> GraphicsResult<BufferHandle> {
|
||||
let buffer = self.gl.create_buffer()
|
||||
.ok_or_else(|| GraphicsError::BufferCreation("Failed to create index buffer".into()))?;
|
||||
|
||||
self.gl.bind_buffer(GL::ELEMENT_ARRAY_BUFFER, Some(&buffer));
|
||||
|
||||
let gl_usage = match usage {
|
||||
BufferUsage::Static => GL::STATIC_DRAW,
|
||||
BufferUsage::Dynamic => GL::DYNAMIC_DRAW,
|
||||
BufferUsage::Stream => GL::STREAM_DRAW,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let array = js_sys::Uint32Array::view(data);
|
||||
self.gl.buffer_data_with_array_buffer_view(GL::ELEMENT_ARRAY_BUFFER, &array, gl_usage);
|
||||
}
|
||||
|
||||
Ok(self.buffers.insert(buffer))
|
||||
}
|
||||
|
||||
fn update_buffer(&mut self, handle: BufferHandle, offset: usize, data: &[u8]) -> GraphicsResult<()> {
|
||||
let buffer = self.buffers.get(handle)
|
||||
.ok_or_else(|| GraphicsError::InvalidHandle("Buffer not found".into()))?;
|
||||
|
||||
self.gl.bind_buffer(GL::ARRAY_BUFFER, Some(buffer));
|
||||
|
||||
unsafe {
|
||||
let array = js_sys::Uint8Array::view(data);
|
||||
self.gl.buffer_sub_data_with_i32_and_array_buffer_view(
|
||||
GL::ARRAY_BUFFER,
|
||||
offset as i32,
|
||||
&array,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn destroy_buffer(&mut self, handle: BufferHandle) {
|
||||
if let Some(buffer) = self.buffers.remove(handle) {
|
||||
self.gl.delete_buffer(Some(&buffer));
|
||||
}
|
||||
}
|
||||
|
||||
fn create_vertex_array(
|
||||
&mut self,
|
||||
vertex_buffer: BufferHandle,
|
||||
index_buffer: Option<BufferHandle>,
|
||||
layout: &VertexLayout,
|
||||
) -> GraphicsResult<VertexArrayHandle> {
|
||||
let vao = self.gl.create_vertex_array()
|
||||
.ok_or_else(|| GraphicsError::BufferCreation("Failed to create VAO".into()))?;
|
||||
|
||||
self.gl.bind_vertex_array(Some(&vao));
|
||||
|
||||
// 绑定顶点缓冲区
|
||||
let vb = self.buffers.get(vertex_buffer)
|
||||
.ok_or_else(|| GraphicsError::InvalidHandle("Vertex buffer not found".into()))?;
|
||||
self.gl.bind_buffer(GL::ARRAY_BUFFER, Some(vb));
|
||||
|
||||
// 设置顶点属性
|
||||
for (idx, attr) in layout.attributes.iter().enumerate() {
|
||||
let (size, type_, normalized) = match attr.attr_type {
|
||||
VertexAttributeType::Float => (1, GL::FLOAT, false),
|
||||
VertexAttributeType::Float2 => (2, GL::FLOAT, false),
|
||||
VertexAttributeType::Float3 => (3, GL::FLOAT, false),
|
||||
VertexAttributeType::Float4 => (4, GL::FLOAT, false),
|
||||
VertexAttributeType::Int => (1, GL::INT, false),
|
||||
VertexAttributeType::Int2 => (2, GL::INT, false),
|
||||
VertexAttributeType::Int3 => (3, GL::INT, false),
|
||||
VertexAttributeType::Int4 => (4, GL::INT, false),
|
||||
VertexAttributeType::UInt => (1, GL::UNSIGNED_INT, false),
|
||||
VertexAttributeType::UInt2 => (2, GL::UNSIGNED_INT, false),
|
||||
VertexAttributeType::UInt3 => (3, GL::UNSIGNED_INT, false),
|
||||
VertexAttributeType::UInt4 => (4, GL::UNSIGNED_INT, false),
|
||||
VertexAttributeType::UByte4Norm => (4, GL::UNSIGNED_BYTE, true),
|
||||
};
|
||||
|
||||
self.gl.enable_vertex_attrib_array(idx as u32);
|
||||
|
||||
if attr.attr_type.is_integer() && !normalized {
|
||||
self.gl.vertex_attrib_i_pointer_with_i32(
|
||||
idx as u32,
|
||||
size,
|
||||
type_,
|
||||
layout.stride as i32,
|
||||
attr.offset as i32,
|
||||
);
|
||||
} else {
|
||||
self.gl.vertex_attrib_pointer_with_i32(
|
||||
idx as u32,
|
||||
size,
|
||||
type_,
|
||||
normalized || attr.normalized,
|
||||
layout.stride as i32,
|
||||
attr.offset as i32,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let index_type = if let Some(ib_handle) = index_buffer {
|
||||
if let Some(ib) = self.buffers.get(ib_handle) {
|
||||
self.gl.bind_buffer(GL::ELEMENT_ARRAY_BUFFER, Some(ib));
|
||||
Some(IndexType::U16)
|
||||
} else {
|
||||
self.gl.bind_buffer(GL::ELEMENT_ARRAY_BUFFER, None);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
self.gl.bind_buffer(GL::ELEMENT_ARRAY_BUFFER, None);
|
||||
None
|
||||
};
|
||||
|
||||
self.gl.bind_vertex_array(None);
|
||||
|
||||
let data = VertexArrayData { vao, index_type };
|
||||
Ok(self.vertex_arrays.insert(data))
|
||||
}
|
||||
|
||||
fn destroy_vertex_array(&mut self, handle: VertexArrayHandle) {
|
||||
if let Some(data) = self.vertex_arrays.remove(handle) {
|
||||
self.gl.delete_vertex_array(Some(&data.vao));
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 着色器操作 | Shader Operations ====================
|
||||
|
||||
fn compile_shader(&mut self, vertex_src: &str, fragment_src: &str) -> GraphicsResult<ShaderHandle> {
|
||||
// 编译顶点着色器
|
||||
let vert_shader = self.compile_shader_stage(GL::VERTEX_SHADER, vertex_src)?;
|
||||
|
||||
// 编译片段着色器
|
||||
let frag_shader = match self.compile_shader_stage(GL::FRAGMENT_SHADER, fragment_src) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
self.gl.delete_shader(Some(&vert_shader));
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
// 链接程序
|
||||
let program = self.gl.create_program()
|
||||
.ok_or_else(|| GraphicsError::ShaderLinking("Failed to create program".into()))?;
|
||||
|
||||
self.gl.attach_shader(&program, &vert_shader);
|
||||
self.gl.attach_shader(&program, &frag_shader);
|
||||
self.gl.link_program(&program);
|
||||
|
||||
// 删除着色器对象(已链接到程序)
|
||||
self.gl.delete_shader(Some(&vert_shader));
|
||||
self.gl.delete_shader(Some(&frag_shader));
|
||||
|
||||
if !self.gl.get_program_parameter(&program, GL::LINK_STATUS)
|
||||
.as_bool()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let log = self.gl.get_program_info_log(&program).unwrap_or_default();
|
||||
self.gl.delete_program(Some(&program));
|
||||
return Err(GraphicsError::ShaderLinking(log));
|
||||
}
|
||||
|
||||
let shader_data = ShaderData {
|
||||
program,
|
||||
uniform_locations: HashMap::new(),
|
||||
};
|
||||
|
||||
Ok(self.shaders.insert(shader_data))
|
||||
}
|
||||
|
||||
fn destroy_shader(&mut self, handle: ShaderHandle) {
|
||||
if let Some(data) = self.shaders.remove(handle) {
|
||||
self.gl.delete_program(Some(&data.program));
|
||||
}
|
||||
|
||||
// 如果销毁的是当前着色器,清除状态
|
||||
if self.current_shader == Some(handle) {
|
||||
self.current_shader = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_shader(&mut self, handle: ShaderHandle) -> GraphicsResult<()> {
|
||||
let data = self.shaders.get(handle)
|
||||
.ok_or_else(|| GraphicsError::InvalidHandle("Shader not found".into()))?;
|
||||
|
||||
self.gl.use_program(Some(&data.program));
|
||||
self.current_shader = Some(handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_uniform_f32(&mut self, name: &str, value: f32) -> GraphicsResult<()> {
|
||||
let location = self.get_uniform_location(name);
|
||||
self.gl.uniform1f(location.as_ref(), value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_uniform_vec2(&mut self, name: &str, value: Vec2) -> GraphicsResult<()> {
|
||||
let location = self.get_uniform_location(name);
|
||||
self.gl.uniform2f(location.as_ref(), value.x, value.y);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_uniform_vec3(&mut self, name: &str, value: Vec3) -> GraphicsResult<()> {
|
||||
let location = self.get_uniform_location(name);
|
||||
self.gl.uniform3f(location.as_ref(), value.x, value.y, value.z);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_uniform_vec4(&mut self, name: &str, value: Vec4) -> GraphicsResult<()> {
|
||||
let location = self.get_uniform_location(name);
|
||||
self.gl.uniform4f(location.as_ref(), value.x, value.y, value.z, value.w);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_uniform_mat3(&mut self, name: &str, value: &Mat3) -> GraphicsResult<()> {
|
||||
let location = self.get_uniform_location(name);
|
||||
self.gl.uniform_matrix3fv_with_f32_array(location.as_ref(), false, &value.to_cols_array());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_uniform_mat4(&mut self, name: &str, value: &Mat4) -> GraphicsResult<()> {
|
||||
let location = self.get_uniform_location(name);
|
||||
self.gl.uniform_matrix4fv_with_f32_array(location.as_ref(), false, &value.to_cols_array());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_uniform_i32(&mut self, name: &str, value: i32) -> GraphicsResult<()> {
|
||||
let location = self.get_uniform_location(name);
|
||||
self.gl.uniform1i(location.as_ref(), value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ==================== 纹理操作 | Texture Operations ====================
|
||||
|
||||
fn create_texture(&mut self, desc: &TextureDescriptor) -> GraphicsResult<TextureHandle> {
|
||||
let texture = self.gl.create_texture()
|
||||
.ok_or_else(|| GraphicsError::TextureCreation("Failed to create texture".into()))?;
|
||||
|
||||
self.gl.bind_texture(GL::TEXTURE_2D, Some(&texture));
|
||||
|
||||
// 设置过滤模式
|
||||
let min_filter = texture_filter_to_gl(desc.filter_min);
|
||||
let mag_filter = texture_filter_to_gl(desc.filter_mag);
|
||||
let wrap_s = texture_wrap_to_gl(desc.wrap_s);
|
||||
let wrap_t = texture_wrap_to_gl(desc.wrap_t);
|
||||
|
||||
self.gl.tex_parameteri(GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, min_filter as i32);
|
||||
self.gl.tex_parameteri(GL::TEXTURE_2D, GL::TEXTURE_MAG_FILTER, mag_filter as i32);
|
||||
self.gl.tex_parameteri(GL::TEXTURE_2D, GL::TEXTURE_WRAP_S, wrap_s as i32);
|
||||
self.gl.tex_parameteri(GL::TEXTURE_2D, GL::TEXTURE_WRAP_T, wrap_t as i32);
|
||||
|
||||
let data = TextureData {
|
||||
handle: texture,
|
||||
width: desc.width,
|
||||
height: desc.height,
|
||||
};
|
||||
|
||||
Ok(self.textures.insert(data))
|
||||
}
|
||||
|
||||
fn create_blank_texture(&mut self, width: u32, height: u32) -> GraphicsResult<TextureHandle> {
|
||||
let desc = TextureDescriptor::new(width, height);
|
||||
let handle = self.create_texture(&desc)?;
|
||||
|
||||
// 分配空白纹理内存
|
||||
if let Some(data) = self.textures.get(handle) {
|
||||
self.gl.bind_texture(GL::TEXTURE_2D, Some(&data.handle));
|
||||
self.gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
|
||||
GL::TEXTURE_2D,
|
||||
0,
|
||||
GL::RGBA as i32,
|
||||
width as i32,
|
||||
height as i32,
|
||||
0,
|
||||
GL::RGBA,
|
||||
GL::UNSIGNED_BYTE,
|
||||
None,
|
||||
).map_err(|e| GraphicsError::TextureCreation(format!("{:?}", e)))?;
|
||||
}
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
fn upload_texture_data(
|
||||
&mut self,
|
||||
handle: TextureHandle,
|
||||
data: &[u8],
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> GraphicsResult<()> {
|
||||
let tex_data = self.textures.get_mut(handle)
|
||||
.ok_or_else(|| GraphicsError::InvalidHandle("Texture not found".into()))?;
|
||||
|
||||
tex_data.width = width;
|
||||
tex_data.height = height;
|
||||
|
||||
self.gl.bind_texture(GL::TEXTURE_2D, Some(&tex_data.handle));
|
||||
|
||||
self.gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
|
||||
GL::TEXTURE_2D,
|
||||
0,
|
||||
GL::RGBA as i32,
|
||||
width as i32,
|
||||
height as i32,
|
||||
0,
|
||||
GL::RGBA,
|
||||
GL::UNSIGNED_BYTE,
|
||||
Some(data),
|
||||
).map_err(|e| GraphicsError::TextureCreation(format!("{:?}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_texture_region(
|
||||
&mut self,
|
||||
handle: TextureHandle,
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
data: &[u8],
|
||||
) -> GraphicsResult<()> {
|
||||
let tex_data = self.textures.get(handle)
|
||||
.ok_or_else(|| GraphicsError::InvalidHandle("Texture not found".into()))?;
|
||||
|
||||
self.gl.bind_texture(GL::TEXTURE_2D, Some(&tex_data.handle));
|
||||
|
||||
self.gl.tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_u8_array(
|
||||
GL::TEXTURE_2D,
|
||||
0,
|
||||
x as i32,
|
||||
y as i32,
|
||||
width as i32,
|
||||
height as i32,
|
||||
GL::RGBA,
|
||||
GL::UNSIGNED_BYTE,
|
||||
Some(data),
|
||||
).map_err(|e| GraphicsError::TextureCreation(format!("{:?}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn destroy_texture(&mut self, handle: TextureHandle) {
|
||||
if let Some(data) = self.textures.remove(handle) {
|
||||
self.gl.delete_texture(Some(&data.handle));
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_texture(&mut self, handle: TextureHandle, unit: u32) -> GraphicsResult<()> {
|
||||
let data = self.textures.get(handle)
|
||||
.ok_or_else(|| GraphicsError::InvalidHandle("Texture not found".into()))?;
|
||||
|
||||
self.gl.active_texture(GL::TEXTURE0 + unit);
|
||||
self.gl.bind_texture(GL::TEXTURE_2D, Some(&data.handle));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_texture_size(&self, handle: TextureHandle) -> Option<(u32, u32)> {
|
||||
self.textures.get(handle).map(|d| (d.width, d.height))
|
||||
}
|
||||
|
||||
// ==================== 渲染状态 | Render State ====================
|
||||
|
||||
fn apply_render_state(&mut self, state: &RenderState) {
|
||||
if self.current_render_state.blend_mode != state.blend_mode {
|
||||
self.set_blend_mode(state.blend_mode);
|
||||
}
|
||||
|
||||
if self.current_render_state.scissor != state.scissor {
|
||||
self.set_scissor(state.scissor);
|
||||
}
|
||||
|
||||
self.current_render_state = state.clone();
|
||||
}
|
||||
|
||||
fn set_blend_mode(&mut self, mode: BlendMode) {
|
||||
match mode {
|
||||
BlendMode::None => {
|
||||
self.gl.disable(GL::BLEND);
|
||||
}
|
||||
BlendMode::Alpha => {
|
||||
self.gl.enable(GL::BLEND);
|
||||
self.gl.blend_func(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
BlendMode::Additive => {
|
||||
self.gl.enable(GL::BLEND);
|
||||
self.gl.blend_func(GL::ONE, GL::ONE);
|
||||
}
|
||||
BlendMode::Multiply => {
|
||||
self.gl.enable(GL::BLEND);
|
||||
self.gl.blend_func(GL::DST_COLOR, GL::ZERO);
|
||||
}
|
||||
BlendMode::Screen => {
|
||||
self.gl.enable(GL::BLEND);
|
||||
self.gl.blend_func(GL::ONE, GL::ONE_MINUS_SRC_COLOR);
|
||||
}
|
||||
BlendMode::PremultipliedAlpha => {
|
||||
self.gl.enable(GL::BLEND);
|
||||
self.gl.blend_func(GL::ONE, GL::ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
}
|
||||
|
||||
self.current_render_state.blend_mode = mode;
|
||||
}
|
||||
|
||||
fn set_scissor(&mut self, rect: Option<ScissorRect>) {
|
||||
match rect {
|
||||
Some(r) => {
|
||||
self.gl.enable(GL::SCISSOR_TEST);
|
||||
// WebGL Y 轴翻转
|
||||
let gl_y = self.height as i32 - r.y - r.height as i32;
|
||||
self.gl.scissor(r.x, gl_y, r.width as i32, r.height as i32);
|
||||
}
|
||||
None => {
|
||||
self.gl.disable(GL::SCISSOR_TEST);
|
||||
}
|
||||
}
|
||||
|
||||
self.current_render_state.scissor = rect;
|
||||
}
|
||||
|
||||
// ==================== 绘制命令 | Draw Commands ====================
|
||||
|
||||
fn draw_indexed(
|
||||
&mut self,
|
||||
vao: VertexArrayHandle,
|
||||
index_count: u32,
|
||||
index_offset: u32,
|
||||
) -> GraphicsResult<()> {
|
||||
let vao_data = self.vertex_arrays.get(vao)
|
||||
.ok_or_else(|| GraphicsError::InvalidHandle("VAO not found".into()))?;
|
||||
|
||||
self.gl.bind_vertex_array(Some(&vao_data.vao));
|
||||
self.gl.draw_elements_with_i32(
|
||||
GL::TRIANGLES,
|
||||
index_count as i32,
|
||||
GL::UNSIGNED_SHORT,
|
||||
(index_offset * 2) as i32, // 2 bytes per u16
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_indexed_u32(
|
||||
&mut self,
|
||||
vao: VertexArrayHandle,
|
||||
index_count: u32,
|
||||
index_offset: u32,
|
||||
) -> GraphicsResult<()> {
|
||||
let vao_data = self.vertex_arrays.get(vao)
|
||||
.ok_or_else(|| GraphicsError::InvalidHandle("VAO not found".into()))?;
|
||||
|
||||
self.gl.bind_vertex_array(Some(&vao_data.vao));
|
||||
self.gl.draw_elements_with_i32(
|
||||
GL::TRIANGLES,
|
||||
index_count as i32,
|
||||
GL::UNSIGNED_INT,
|
||||
(index_offset * 4) as i32, // 4 bytes per u32
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&mut self,
|
||||
vao: VertexArrayHandle,
|
||||
vertex_count: u32,
|
||||
vertex_offset: u32,
|
||||
) -> GraphicsResult<()> {
|
||||
let vao_data = self.vertex_arrays.get(vao)
|
||||
.ok_or_else(|| GraphicsError::InvalidHandle("VAO not found".into()))?;
|
||||
|
||||
self.gl.bind_vertex_array(Some(&vao_data.vao));
|
||||
self.gl.draw_arrays(GL::TRIANGLES, vertex_offset as i32, vertex_count as i32);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_lines(
|
||||
&mut self,
|
||||
vao: VertexArrayHandle,
|
||||
vertex_count: u32,
|
||||
vertex_offset: u32,
|
||||
) -> GraphicsResult<()> {
|
||||
let vao_data = self.vertex_arrays.get(vao)
|
||||
.ok_or_else(|| GraphicsError::InvalidHandle("VAO not found".into()))?;
|
||||
|
||||
self.gl.bind_vertex_array(Some(&vao_data.vao));
|
||||
self.gl.draw_arrays(GL::LINES, vertex_offset as i32, vertex_count as i32);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_line_loop(
|
||||
&mut self,
|
||||
vao: VertexArrayHandle,
|
||||
vertex_count: u32,
|
||||
vertex_offset: u32,
|
||||
) -> GraphicsResult<()> {
|
||||
let vao_data = self.vertex_arrays.get(vao)
|
||||
.ok_or_else(|| GraphicsError::InvalidHandle("VAO not found".into()))?;
|
||||
|
||||
self.gl.bind_vertex_array(Some(&vao_data.vao));
|
||||
self.gl.draw_arrays(GL::LINE_LOOP, vertex_offset as i32, vertex_count as i32);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_line_strip(
|
||||
&mut self,
|
||||
vao: VertexArrayHandle,
|
||||
vertex_count: u32,
|
||||
vertex_offset: u32,
|
||||
) -> GraphicsResult<()> {
|
||||
let vao_data = self.vertex_arrays.get(vao)
|
||||
.ok_or_else(|| GraphicsError::InvalidHandle("VAO not found".into()))?;
|
||||
|
||||
self.gl.bind_vertex_array(Some(&vao_data.vao));
|
||||
self.gl.draw_arrays(GL::LINE_STRIP, vertex_offset as i32, vertex_count as i32);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ==================== 查询 | Queries ====================
|
||||
|
||||
fn max_texture_size(&self) -> u32 {
|
||||
self.gl.get_parameter(GL::MAX_TEXTURE_SIZE)
|
||||
.ok()
|
||||
.and_then(|v| v.as_f64())
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(4096)
|
||||
}
|
||||
|
||||
fn supports_feature(&self, feature: GraphicsFeature) -> bool {
|
||||
match feature {
|
||||
GraphicsFeature::AnisotropicFiltering => {
|
||||
self.gl.get_extension("EXT_texture_filter_anisotropic").is_ok()
|
||||
}
|
||||
GraphicsFeature::Instancing => true, // WebGL2 支持
|
||||
GraphicsFeature::ComputeShaders => false, // WebGL2 不支持
|
||||
GraphicsFeature::MultipleRenderTargets => true, // WebGL2 支持
|
||||
GraphicsFeature::FloatTextures => {
|
||||
self.gl.get_extension("EXT_color_buffer_float").is_ok()
|
||||
}
|
||||
GraphicsFeature::WebGPU => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn max_texture_units(&self) -> u32 {
|
||||
self.gl.get_parameter(GL::MAX_TEXTURE_IMAGE_UNITS)
|
||||
.ok()
|
||||
.and_then(|v| v.as_f64())
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(16)
|
||||
}
|
||||
|
||||
fn max_vertex_attributes(&self) -> u32 {
|
||||
self.gl.get_parameter(GL::MAX_VERTEX_ATTRIBS)
|
||||
.ok()
|
||||
.and_then(|v| v.as_f64())
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(16)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== WebGL2-Specific Extensions ====================
|
||||
|
||||
impl WebGL2Backend {
|
||||
/// Bind a raw WebGlTexture to a texture unit (for TextureManager compatibility).
|
||||
pub fn bind_texture_raw(&self, texture: Option<&WebGlTexture>, unit: u32) {
|
||||
self.gl.active_texture(GL::TEXTURE0 + unit);
|
||||
self.gl.bind_texture(GL::TEXTURE_2D, texture);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 | Helper Functions ====================
|
||||
|
||||
/// 纹理过滤转 GL 常量
|
||||
///
|
||||
/// Convert texture filter to GL constant.
|
||||
fn texture_filter_to_gl(filter: TextureFilter) -> u32 {
|
||||
match filter {
|
||||
TextureFilter::Nearest => GL::NEAREST,
|
||||
TextureFilter::Linear => GL::LINEAR,
|
||||
TextureFilter::NearestMipmapNearest => GL::NEAREST_MIPMAP_NEAREST,
|
||||
TextureFilter::LinearMipmapNearest => GL::LINEAR_MIPMAP_NEAREST,
|
||||
TextureFilter::NearestMipmapLinear => GL::NEAREST_MIPMAP_LINEAR,
|
||||
TextureFilter::LinearMipmapLinear => GL::LINEAR_MIPMAP_LINEAR,
|
||||
}
|
||||
}
|
||||
|
||||
/// 纹理环绕转 GL 常量
|
||||
///
|
||||
/// Convert texture wrap to GL constant.
|
||||
fn texture_wrap_to_gl(wrap: TextureWrap) -> u32 {
|
||||
match wrap {
|
||||
TextureWrap::ClampToEdge => GL::CLAMP_TO_EDGE,
|
||||
TextureWrap::Repeat => GL::REPEAT,
|
||||
TextureWrap::MirroredRepeat => GL::MIRRORED_REPEAT,
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,11 @@ use wasm_bindgen::prelude::*;
|
||||
|
||||
use super::context::WebGLContext;
|
||||
use super::error::Result;
|
||||
use crate::backend::WebGL2Backend;
|
||||
use crate::input::InputManager;
|
||||
use crate::renderer::{Renderer2D, GridRenderer, GizmoRenderer, TransformMode, ViewportManager};
|
||||
use crate::resource::TextureManager;
|
||||
use es_engine_shared::traits::backend::GraphicsBackend;
|
||||
|
||||
/// Engine configuration options.
|
||||
/// 引擎配置选项。
|
||||
@@ -41,6 +43,15 @@ pub struct Engine {
|
||||
/// WebGL上下文。
|
||||
context: WebGLContext,
|
||||
|
||||
/// Graphics backend abstraction layer.
|
||||
/// 图形后端抽象层。
|
||||
///
|
||||
/// Provides cross-platform graphics API abstraction.
|
||||
/// Currently WebGL2, future support for wgpu/native.
|
||||
/// 提供跨平台图形 API 抽象。
|
||||
/// 当前为 WebGL2,未来支持 wgpu/原生平台。
|
||||
backend: WebGL2Backend,
|
||||
|
||||
/// 2D renderer.
|
||||
/// 2D渲染器。
|
||||
renderer: Renderer2D,
|
||||
@@ -104,17 +115,34 @@ impl Engine {
|
||||
context.set_viewport();
|
||||
context.enable_blend();
|
||||
|
||||
// Create subsystems | 创建子系统
|
||||
let renderer = Renderer2D::new(context.gl(), config.max_sprites)?;
|
||||
let grid_renderer = GridRenderer::new(context.gl())?;
|
||||
let gizmo_renderer = GizmoRenderer::new(context.gl())?;
|
||||
// Create graphics backend abstraction | 创建图形后端抽象
|
||||
let backend = WebGL2Backend::from_canvas(canvas_id)
|
||||
.map_err(|e| crate::core::error::EngineError::WebGLError(
|
||||
format!("Failed to create graphics backend: {:?}", e)
|
||||
))?;
|
||||
|
||||
log::info!(
|
||||
"Graphics backend initialized: {} ({})",
|
||||
backend.name(),
|
||||
backend.version()
|
||||
);
|
||||
|
||||
let texture_manager = TextureManager::new(context.gl().clone());
|
||||
let input_manager = InputManager::new();
|
||||
|
||||
let mut backend = backend;
|
||||
let renderer = Renderer2D::new(&mut backend, config.max_sprites)
|
||||
.map_err(|e| crate::core::error::EngineError::WebGLError(e))?;
|
||||
let grid_renderer = GridRenderer::new(&mut backend)
|
||||
.map_err(|e| crate::core::error::EngineError::WebGLError(e))?;
|
||||
let gizmo_renderer = GizmoRenderer::new(&mut backend)
|
||||
.map_err(|e| crate::core::error::EngineError::WebGLError(e))?;
|
||||
|
||||
log::info!("Engine created successfully | 引擎创建成功");
|
||||
|
||||
Ok(Self {
|
||||
context,
|
||||
backend,
|
||||
renderer,
|
||||
grid_renderer,
|
||||
gizmo_renderer,
|
||||
@@ -124,7 +152,7 @@ impl Engine {
|
||||
show_grid: true,
|
||||
viewport_manager: ViewportManager::new(),
|
||||
show_gizmos: true,
|
||||
is_editor: true, // 默认为编辑器模式 | Default to editor mode
|
||||
is_editor: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -139,21 +167,39 @@ impl Engine {
|
||||
height: u32,
|
||||
config: EngineConfig,
|
||||
) -> Result<Self> {
|
||||
let context = WebGLContext::from_external(gl_context, width, height)?;
|
||||
let context = WebGLContext::from_external(gl_context.clone(), width, height)?;
|
||||
|
||||
context.set_viewport();
|
||||
context.enable_blend();
|
||||
|
||||
let renderer = Renderer2D::new(context.gl(), config.max_sprites)?;
|
||||
let grid_renderer = GridRenderer::new(context.gl())?;
|
||||
let gizmo_renderer = GizmoRenderer::new(context.gl())?;
|
||||
// Create graphics backend from external context | 从外部上下文创建图形后端
|
||||
let backend = WebGL2Backend::from_external(gl_context, width, height)
|
||||
.map_err(|e| crate::core::error::EngineError::WebGLError(
|
||||
format!("Failed to create graphics backend: {:?}", e)
|
||||
))?;
|
||||
|
||||
log::info!(
|
||||
"Graphics backend initialized: {} ({})",
|
||||
backend.name(),
|
||||
backend.version()
|
||||
);
|
||||
|
||||
let texture_manager = TextureManager::new(context.gl().clone());
|
||||
let input_manager = InputManager::new();
|
||||
|
||||
let mut backend = backend;
|
||||
let renderer = Renderer2D::new(&mut backend, config.max_sprites)
|
||||
.map_err(|e| crate::core::error::EngineError::WebGLError(e))?;
|
||||
let grid_renderer = GridRenderer::new(&mut backend)
|
||||
.map_err(|e| crate::core::error::EngineError::WebGLError(e))?;
|
||||
let gizmo_renderer = GizmoRenderer::new(&mut backend)
|
||||
.map_err(|e| crate::core::error::EngineError::WebGLError(e))?;
|
||||
|
||||
log::info!("Engine created from external context | 从外部上下文创建引擎");
|
||||
|
||||
Ok(Self {
|
||||
context,
|
||||
backend,
|
||||
renderer,
|
||||
grid_renderer,
|
||||
gizmo_renderer,
|
||||
@@ -163,7 +209,7 @@ impl Engine {
|
||||
show_grid: true,
|
||||
viewport_manager: ViewportManager::new(),
|
||||
show_gizmos: true,
|
||||
is_editor: true, // 默认为编辑器模式 | Default to editor mode
|
||||
is_editor: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -187,6 +233,50 @@ impl Engine {
|
||||
self.context.height()
|
||||
}
|
||||
|
||||
// ===== Graphics Backend API =====
|
||||
// ===== 图形后端 API =====
|
||||
|
||||
/// Get reference to the graphics backend.
|
||||
/// 获取图形后端的引用。
|
||||
///
|
||||
/// Use this for low-level graphics operations through the abstraction layer.
|
||||
/// 使用此方法通过抽象层进行低级图形操作。
|
||||
#[inline]
|
||||
pub fn backend(&self) -> &WebGL2Backend {
|
||||
&self.backend
|
||||
}
|
||||
|
||||
/// Get mutable reference to the graphics backend.
|
||||
/// 获取图形后端的可变引用。
|
||||
///
|
||||
/// Use this for low-level graphics operations through the abstraction layer.
|
||||
/// 使用此方法通过抽象层进行低级图形操作。
|
||||
#[inline]
|
||||
pub fn backend_mut(&mut self) -> &mut WebGL2Backend {
|
||||
&mut self.backend
|
||||
}
|
||||
|
||||
/// Get backend name (e.g., "WebGL2").
|
||||
/// 获取后端名称(如 "WebGL2")。
|
||||
#[inline]
|
||||
pub fn backend_name(&self) -> &'static str {
|
||||
self.backend.name()
|
||||
}
|
||||
|
||||
/// Get backend version string.
|
||||
/// 获取后端版本字符串。
|
||||
#[inline]
|
||||
pub fn backend_version(&self) -> &str {
|
||||
self.backend.version()
|
||||
}
|
||||
|
||||
/// Get maximum texture size supported by the backend.
|
||||
/// 获取后端支持的最大纹理尺寸。
|
||||
#[inline]
|
||||
pub fn max_texture_size(&self) -> u32 {
|
||||
self.backend.max_texture_size()
|
||||
}
|
||||
|
||||
/// Submit sprite batch data for rendering.
|
||||
/// 提交精灵批次数据进行渲染。
|
||||
pub fn submit_sprite_batch(
|
||||
@@ -197,41 +287,28 @@ impl Engine {
|
||||
colors: &[u32],
|
||||
material_ids: &[u32],
|
||||
) -> Result<()> {
|
||||
self.renderer.submit_batch(
|
||||
transforms,
|
||||
texture_ids,
|
||||
uvs,
|
||||
colors,
|
||||
material_ids,
|
||||
&self.texture_manager,
|
||||
)
|
||||
self.renderer.submit_batch(transforms, texture_ids, uvs, colors, material_ids)
|
||||
.map_err(|e| crate::core::error::EngineError::WebGLError(e))
|
||||
}
|
||||
|
||||
/// Render the current frame.
|
||||
/// 渲染当前帧。
|
||||
pub fn render(&mut self) -> Result<()> {
|
||||
// Clear background with clear color
|
||||
let [r, g, b, a] = self.renderer.get_clear_color();
|
||||
self.context.clear(r, g, b, a);
|
||||
|
||||
// Render grid first (background) - only in editor mode
|
||||
// 首先渲染网格(背景)- 仅在编辑器模式下
|
||||
let camera = self.renderer.camera().clone();
|
||||
|
||||
if self.is_editor && self.show_grid {
|
||||
self.grid_renderer.render(self.context.gl(), self.renderer.camera());
|
||||
self.grid_renderer.render_axes(self.context.gl(), self.renderer.camera());
|
||||
self.grid_renderer.render(&mut self.backend, &camera);
|
||||
self.grid_renderer.render_axes(&mut self.backend, &camera);
|
||||
}
|
||||
|
||||
// Render sprites
|
||||
self.renderer.render(self.context.gl(), &self.texture_manager)?;
|
||||
self.renderer.render(&mut self.backend, &self.texture_manager)
|
||||
.map_err(|e| crate::core::error::EngineError::WebGLError(e))?;
|
||||
|
||||
// Render gizmos on top - only in editor mode
|
||||
// 在顶部渲染 gizmos - 仅在编辑器模式下
|
||||
if self.is_editor && self.show_gizmos {
|
||||
self.gizmo_renderer.render(self.context.gl(), self.renderer.camera());
|
||||
// Render axis indicator in corner
|
||||
// 在角落渲染坐标轴指示器
|
||||
self.gizmo_renderer.render(&mut self.backend, &camera);
|
||||
self.gizmo_renderer.render_axis_indicator(
|
||||
self.context.gl(),
|
||||
&mut self.backend,
|
||||
self.context.width() as f32,
|
||||
self.context.height() as f32,
|
||||
);
|
||||
@@ -247,10 +324,8 @@ impl Engine {
|
||||
/// This is used for overlay rendering (e.g., UI layer on top of world).
|
||||
/// 用于叠加渲染(例如,UI 层叠加在世界上)。
|
||||
pub fn render_overlay(&mut self) -> Result<()> {
|
||||
// Render sprites without clearing
|
||||
// 渲染精灵但不清屏
|
||||
self.renderer.render(self.context.gl(), &self.texture_manager)?;
|
||||
Ok(())
|
||||
self.renderer.render(&mut self.backend, &self.texture_manager)
|
||||
.map_err(|e| crate::core::error::EngineError::WebGLError(e))
|
||||
}
|
||||
|
||||
/// Set scissor rect for clipping (screen coordinates, Y-down).
|
||||
@@ -616,52 +691,37 @@ impl Engine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Render to a specific viewport.
|
||||
/// 渲染到特定视口。
|
||||
pub fn render_to_viewport(&mut self, viewport_id: &str) -> Result<()> {
|
||||
let viewport = match self.viewport_manager.get(viewport_id) {
|
||||
Some(v) => v,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
// Get viewport settings
|
||||
let show_grid = viewport.config.show_grid;
|
||||
let show_gizmos = viewport.config.show_gizmos;
|
||||
let camera = viewport.camera.clone();
|
||||
let (vp_width, vp_height) = viewport.dimensions();
|
||||
|
||||
// Bind viewport and clear
|
||||
viewport.bind();
|
||||
viewport.clear();
|
||||
|
||||
// Update renderer camera to match viewport camera
|
||||
let renderer_camera = self.renderer.camera_mut();
|
||||
renderer_camera.position = camera.position;
|
||||
renderer_camera.set_zoom(camera.zoom);
|
||||
renderer_camera.rotation = camera.rotation;
|
||||
renderer_camera.set_viewport(camera.viewport_width(), camera.viewport_height());
|
||||
|
||||
// Render grid if enabled - only in editor mode
|
||||
// 渲染网格(如果启用)- 仅在编辑器模式下
|
||||
if self.is_editor && show_grid {
|
||||
self.grid_renderer.render(viewport.gl(), &camera);
|
||||
self.grid_renderer.render_axes(viewport.gl(), &camera);
|
||||
self.grid_renderer.render(&mut self.backend, &camera);
|
||||
self.grid_renderer.render_axes(&mut self.backend, &camera);
|
||||
}
|
||||
|
||||
// Render sprites
|
||||
self.renderer.render(viewport.gl(), &self.texture_manager)?;
|
||||
self.renderer.render(&mut self.backend, &self.texture_manager)
|
||||
.map_err(|e| crate::core::error::EngineError::WebGLError(e))?;
|
||||
|
||||
// Render gizmos if enabled - only in editor mode
|
||||
// 渲染 gizmos(如果启用)- 仅在编辑器模式下
|
||||
if self.is_editor && show_gizmos {
|
||||
self.gizmo_renderer.render(viewport.gl(), &camera);
|
||||
// Render axis indicator in corner
|
||||
// 在角落渲染坐标轴指示器
|
||||
let (vp_width, vp_height) = viewport.dimensions();
|
||||
self.gizmo_renderer.render_axis_indicator(
|
||||
viewport.gl(),
|
||||
vp_width as f32,
|
||||
vp_height as f32,
|
||||
);
|
||||
self.gizmo_renderer.render(&mut self.backend, &camera);
|
||||
self.gizmo_renderer.render_axis_indicator(&mut self.backend, vp_width as f32, vp_height as f32);
|
||||
}
|
||||
self.gizmo_renderer.clear();
|
||||
|
||||
@@ -684,7 +744,8 @@ impl Engine {
|
||||
vertex_source: &str,
|
||||
fragment_source: &str,
|
||||
) -> Result<u32> {
|
||||
self.renderer.compile_shader(self.context.gl(), vertex_source, fragment_source)
|
||||
self.renderer.compile_shader(&mut self.backend, vertex_source, fragment_source)
|
||||
.map_err(|e| crate::core::error::EngineError::WebGLError(e))
|
||||
}
|
||||
|
||||
/// Compile a shader with a specific ID.
|
||||
@@ -695,7 +756,8 @@ impl Engine {
|
||||
vertex_source: &str,
|
||||
fragment_source: &str,
|
||||
) -> Result<()> {
|
||||
self.renderer.compile_shader_with_id(self.context.gl(), shader_id, vertex_source, fragment_source)
|
||||
self.renderer.compile_shader_with_id(&mut self.backend, shader_id, vertex_source, fragment_source)
|
||||
.map_err(|e| crate::core::error::EngineError::WebGLError(e))
|
||||
}
|
||||
|
||||
/// Check if a shader exists.
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
// Module declarations | 模块声明
|
||||
pub mod backend;
|
||||
pub mod core;
|
||||
pub mod math;
|
||||
pub mod platform;
|
||||
@@ -45,6 +46,18 @@ pub mod input;
|
||||
// Re-exports | 重新导出
|
||||
pub use crate::core::{Engine, EngineConfig};
|
||||
pub use crate::core::error::{EngineError, Result};
|
||||
pub use crate::backend::WebGL2Backend;
|
||||
|
||||
// Re-export shared types for convenience | 重新导出共享类型以方便使用
|
||||
pub use es_engine_shared::{
|
||||
traits::backend::{GraphicsBackend, GraphicsError, GraphicsResult, GraphicsFeature, BufferUsage},
|
||||
types::{
|
||||
handle::{Handle, HandleMap, BufferHandle, TextureHandle, ShaderHandle, VertexArrayHandle},
|
||||
vertex::{VertexLayout, VertexAttribute, VertexAttributeType, SpriteVertex},
|
||||
blend::{BlendMode, RenderState, ScissorRect},
|
||||
texture::{TextureDescriptor, TextureFormat, TextureFilter, TextureWrap},
|
||||
},
|
||||
};
|
||||
|
||||
/// Initialize panic hook for better error messages in console.
|
||||
/// 初始化panic hook以在控制台显示更好的错误信息。
|
||||
@@ -819,4 +832,28 @@ impl GameEngine {
|
||||
.update_texture_region(id, x, y, width, height, pixels)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
// ===== Graphics Backend Info API =====
|
||||
// ===== 图形后端信息 API =====
|
||||
|
||||
/// Get the graphics backend name (e.g., "WebGL2").
|
||||
/// 获取图形后端名称(如 "WebGL2")。
|
||||
#[wasm_bindgen(js_name = getBackendName)]
|
||||
pub fn get_backend_name(&self) -> String {
|
||||
self.engine.backend_name().to_string()
|
||||
}
|
||||
|
||||
/// Get the graphics backend version string.
|
||||
/// 获取图形后端版本字符串。
|
||||
#[wasm_bindgen(js_name = getBackendVersion)]
|
||||
pub fn get_backend_version(&self) -> String {
|
||||
self.engine.backend_version().to_string()
|
||||
}
|
||||
|
||||
/// Get maximum texture size supported by the backend.
|
||||
/// 获取后端支持的最大纹理尺寸。
|
||||
#[wasm_bindgen(js_name = getMaxTextureSize)]
|
||||
pub fn get_max_texture_size(&self) -> u32 {
|
||||
self.engine.max_texture_size()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,149 +1,65 @@
|
||||
//! Sprite batch renderer for efficient 2D rendering.
|
||||
//! 用于高效2D渲染的精灵批处理渲染器。
|
||||
|
||||
use web_sys::{
|
||||
WebGl2RenderingContext, WebGlBuffer, WebGlVertexArrayObject,
|
||||
use es_engine_shared::{
|
||||
traits::backend::{GraphicsBackend, BufferUsage},
|
||||
types::{
|
||||
handle::{BufferHandle, VertexArrayHandle},
|
||||
vertex::{VertexLayout, VertexAttribute, VertexAttributeType},
|
||||
},
|
||||
};
|
||||
|
||||
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 FLOATS_PER_VERTEX: usize = 9;
|
||||
const TRANSFORM_STRIDE: usize = 7;
|
||||
|
||||
/// UV data stride (u0, v0, u1, v1).
|
||||
/// UV数据步长。
|
||||
const UV_STRIDE: usize = 4;
|
||||
|
||||
/// Batch key combining material and texture IDs.
|
||||
/// 组合材质ID和纹理ID的批次键。
|
||||
#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)]
|
||||
pub struct BatchKey {
|
||||
/// Material ID (0 = default material).
|
||||
/// 材质ID(0 = 默认材质)。
|
||||
pub material_id: u32,
|
||||
/// Texture ID.
|
||||
/// 纹理ID。
|
||||
pub texture_id: u32,
|
||||
}
|
||||
|
||||
/// Sprite batch renderer.
|
||||
/// 精灵批处理渲染器。
|
||||
///
|
||||
/// Batches multiple sprites into a single draw call for optimal performance.
|
||||
/// 将多个精灵合并为单次绘制调用以获得最佳性能。
|
||||
///
|
||||
/// # Performance | 性能
|
||||
/// - Uses dynamic vertex buffer for efficient updates | 使用动态顶点缓冲区以高效更新
|
||||
/// - Groups sprites by material and texture to minimize state changes | 按材质和纹理分组精灵以最小化状态更改
|
||||
/// - 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.
|
||||
/// 最大精灵数。
|
||||
vbo: BufferHandle,
|
||||
ibo: BufferHandle,
|
||||
vao: VertexArrayHandle,
|
||||
max_sprites: usize,
|
||||
|
||||
/// Batches stored as (key, vertices) pairs in submission order.
|
||||
/// 按提交顺序存储的批次(键,顶点)对。
|
||||
///
|
||||
/// Only consecutive sprites with the same BatchKey are batched together.
|
||||
/// Sprites with the same key but separated by different keys are kept in separate batches
|
||||
/// to preserve correct render order.
|
||||
/// 只有连续的相同 BatchKey 的 sprites 才会合批。
|
||||
/// 相同 key 但被其他 key 分隔的 sprites 保持在独立批次中以保证正确的渲染顺序。
|
||||
batches: Vec<(BatchKey, Vec<f32>)>,
|
||||
|
||||
/// Total sprite count across all batches.
|
||||
/// 所有批次的总精灵数。
|
||||
sprite_count: usize,
|
||||
|
||||
/// Last batch key used, for determining if we can merge into the last batch.
|
||||
/// 上一个使用的批次键,用于判断是否可以合并到最后一个批次。
|
||||
last_batch_key: Option<BatchKey>,
|
||||
}
|
||||
|
||||
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 | 分配顶点缓冲区内存
|
||||
pub fn new(backend: &mut impl GraphicsBackend, max_sprites: usize) -> Result<Self, String> {
|
||||
let vertex_buffer_size = max_sprites * VERTICES_PER_SPRITE * FLOATS_PER_VERTEX * 4;
|
||||
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 vbo = backend.create_vertex_buffer(
|
||||
&vec![0u8; vertex_buffer_size],
|
||||
BufferUsage::Dynamic,
|
||||
).map_err(|e| format!("VBO: {:?}", e))?;
|
||||
|
||||
let indices = Self::generate_indices(max_sprites);
|
||||
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,
|
||||
);
|
||||
}
|
||||
let ibo = backend.create_index_buffer(
|
||||
bytemuck::cast_slice(&indices),
|
||||
BufferUsage::Static,
|
||||
).map_err(|e| format!("IBO: {:?}", e))?;
|
||||
|
||||
// Set up vertex attributes | 设置顶点属性
|
||||
Self::setup_vertex_attributes(gl);
|
||||
let layout = VertexLayout {
|
||||
attributes: vec![
|
||||
VertexAttribute { name: "a_position".into(), attr_type: VertexAttributeType::Float2, offset: 0, normalized: false },
|
||||
VertexAttribute { name: "a_texcoord".into(), attr_type: VertexAttributeType::Float2, offset: 8, normalized: false },
|
||||
VertexAttribute { name: "a_color".into(), attr_type: VertexAttributeType::Float4, offset: 16, normalized: false },
|
||||
VertexAttribute { name: "a_aspect".into(), attr_type: VertexAttributeType::Float, offset: 32, normalized: false },
|
||||
],
|
||||
stride: FLOATS_PER_VERTEX * 4,
|
||||
};
|
||||
|
||||
// Unbind VAO | 解绑VAO
|
||||
gl.bind_vertex_array(None);
|
||||
|
||||
log::debug!(
|
||||
"SpriteBatch created with capacity: {} sprites | SpriteBatch创建完成,容量: {}个精灵",
|
||||
max_sprites,
|
||||
max_sprites
|
||||
);
|
||||
let vao = backend.create_vertex_array(vbo, Some(ibo), &layout)
|
||||
.map_err(|e| format!("VAO: {:?}", e))?;
|
||||
|
||||
Ok(Self {
|
||||
vao,
|
||||
vbo,
|
||||
ibo,
|
||||
vbo, ibo, vao,
|
||||
max_sprites,
|
||||
batches: Vec::new(),
|
||||
sprite_count: 0,
|
||||
@@ -151,104 +67,19 @@ impl SpriteBatch {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
(0..max_sprites).flat_map(|i| {
|
||||
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
|
||||
[base, base + 1, base + 2, base + 2, base + 3, base]
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// Set up vertex attribute pointers.
|
||||
/// 设置顶点属性指针。
|
||||
///
|
||||
/// Vertex layout (9 floats per vertex):
|
||||
/// 顶点布局(每顶点 9 个浮点数):
|
||||
/// - location 0: position (2 floats) - offset 0
|
||||
/// - location 1: tex_coord (2 floats) - offset 8
|
||||
/// - location 2: color (4 floats) - offset 16
|
||||
/// - location 3: aspect_ratio (1 float) - offset 32
|
||||
fn setup_vertex_attributes(gl: &WebGl2RenderingContext) {
|
||||
let stride = (FLOATS_PER_VERTEX * 4) as i32; // 9 * 4 = 36 bytes
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
// Aspect ratio attribute (location = 3) | 宽高比属性
|
||||
// Used by shaders for aspect-ratio-aware transformations
|
||||
// 用于着色器中的宽高比感知变换
|
||||
gl.enable_vertex_attrib_array(3);
|
||||
gl.vertex_attrib_pointer_with_i32(
|
||||
3,
|
||||
1,
|
||||
WebGl2RenderingContext::FLOAT,
|
||||
false,
|
||||
stride,
|
||||
32, // (2 + 2 + 4) floats * 4 bytes
|
||||
);
|
||||
}
|
||||
|
||||
/// Clear the batch for a new frame.
|
||||
/// 为新帧清空批处理。
|
||||
pub fn clear(&mut self) {
|
||||
self.batches.clear();
|
||||
self.sprite_count = 0;
|
||||
self.last_batch_key = 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颜色
|
||||
/// * `material_ids` - Material ID for each sprite (0 = default) | 每个精灵的材质ID(0 = 默认)
|
||||
/// * `_texture_manager` - Texture manager for getting texture sizes | 纹理管理器
|
||||
pub fn add_sprites(
|
||||
&mut self,
|
||||
transforms: &[f32],
|
||||
@@ -256,252 +87,106 @@ impl SpriteBatch {
|
||||
uvs: &[f32],
|
||||
colors: &[u32],
|
||||
material_ids: &[u32],
|
||||
_texture_manager: &TextureManager,
|
||||
) -> Result<()> {
|
||||
let sprite_count = texture_ids.len();
|
||||
) -> Result<(), String> {
|
||||
let 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 transforms.len() != count * TRANSFORM_STRIDE {
|
||||
return Err(format!("Transform mismatch: {} vs {}", transforms.len(), count * TRANSFORM_STRIDE));
|
||||
}
|
||||
if uvs.len() != count * UV_STRIDE {
|
||||
return Err(format!("UV mismatch: {} vs {}", uvs.len(), count * UV_STRIDE));
|
||||
}
|
||||
if colors.len() != count || material_ids.len() != count {
|
||||
return Err("Color/material count mismatch".into());
|
||||
}
|
||||
if self.sprite_count + count > self.max_sprites {
|
||||
return Err(format!("Batch overflow: {} + {} > {}", self.sprite_count, count, self.max_sprites));
|
||||
}
|
||||
|
||||
if uvs.len() != sprite_count * UV_STRIDE {
|
||||
return Err(EngineError::InvalidBatchData(format!(
|
||||
"UV data length mismatch: expected {}, got {}",
|
||||
sprite_count * UV_STRIDE,
|
||||
uvs.len()
|
||||
)));
|
||||
}
|
||||
for i in 0..count {
|
||||
let t = i * TRANSFORM_STRIDE;
|
||||
let uv = i * UV_STRIDE;
|
||||
|
||||
if colors.len() != sprite_count {
|
||||
return Err(EngineError::InvalidBatchData(format!(
|
||||
"Color data length mismatch: expected {}, got {}",
|
||||
sprite_count,
|
||||
colors.len()
|
||||
)));
|
||||
}
|
||||
|
||||
if material_ids.len() != sprite_count {
|
||||
return Err(EngineError::InvalidBatchData(format!(
|
||||
"Material ID data length mismatch: expected {}, got {}",
|
||||
sprite_count,
|
||||
material_ids.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 grouped by material and texture | 按材质和纹理分组添加每个精灵
|
||||
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 (x, y) = (transforms[t], transforms[t + 1]);
|
||||
let rotation = transforms[t + 2];
|
||||
let (width, height) = (transforms[t + 3], transforms[t + 4]);
|
||||
let (origin_x, origin_y) = (transforms[t + 5], transforms[t + 6]);
|
||||
|
||||
let (u0, v0, u1, v1) = (uvs[uv], uvs[uv + 1], uvs[uv + 2], uvs[uv + 3]);
|
||||
let color = Color::from_packed(colors[i]);
|
||||
let color_arr = [color.r, color.g, color.b, color.a];
|
||||
let aspect = if height.abs() > 0.001 { width / height } else { 1.0 };
|
||||
|
||||
// scale_x and scale_y are the actual display dimensions
|
||||
// scale_x 和 scale_y 是实际显示尺寸
|
||||
let width = scale_x;
|
||||
let height = scale_y;
|
||||
let key = BatchKey { material_id: material_ids[i], texture_id: texture_ids[i] };
|
||||
|
||||
// Calculate aspect ratio (width / height), default 1.0 for degenerate cases
|
||||
// 计算宽高比(宽度/高度),退化情况下默认为 1.0
|
||||
let aspect_ratio = if height.abs() > 0.001 {
|
||||
width / height
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let batch_key = BatchKey {
|
||||
material_id: material_ids[i],
|
||||
texture_id: texture_ids[i],
|
||||
};
|
||||
|
||||
// Only batch consecutive sprites with the same key to preserve render order
|
||||
// 只对连续相同 key 的 sprites 合批以保持渲染顺序
|
||||
let should_create_new_batch = match self.last_batch_key {
|
||||
Some(last_key) => batch_key != last_key,
|
||||
None => true,
|
||||
};
|
||||
|
||||
if should_create_new_batch {
|
||||
// Create a new batch | 创建新批次
|
||||
self.batches.push((batch_key, Vec::new()));
|
||||
self.last_batch_key = Some(batch_key);
|
||||
if self.last_batch_key != Some(key) {
|
||||
self.batches.push((key, Vec::new()));
|
||||
self.last_batch_key = Some(key);
|
||||
}
|
||||
|
||||
// Add to the last batch | 添加到最后一个批次
|
||||
let batch = &mut self.batches.last_mut().unwrap().1;
|
||||
|
||||
// Calculate transformed vertices and add to batch | 计算变换后的顶点并添加到批次
|
||||
Self::add_sprite_vertices_to_batch(
|
||||
batch,
|
||||
x, y, width, height, rotation, origin_x, origin_y,
|
||||
u0, v0, u1, v1, color_arr, aspect_ratio,
|
||||
);
|
||||
Self::add_sprite_vertices(batch, x, y, width, height, rotation, origin_x, origin_y,
|
||||
u0, v0, u1, v1, color_arr, aspect);
|
||||
}
|
||||
|
||||
self.sprite_count += sprite_count;
|
||||
self.sprite_count += count;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add vertices for a single sprite to a batch.
|
||||
/// 为单个精灵添加顶点到批次。
|
||||
///
|
||||
/// Each vertex contains: position(2) + tex_coord(2) + color(4) + aspect_ratio(1) = 9 floats
|
||||
/// 每个顶点包含: 位置(2) + 纹理坐标(2) + 颜色(4) + 宽高比(1) = 9 个浮点数
|
||||
#[inline]
|
||||
fn add_sprite_vertices_to_batch(
|
||||
fn add_sprite_vertices(
|
||||
batch: &mut Vec<f32>,
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
rotation: f32,
|
||||
origin_x: f32,
|
||||
origin_y: f32,
|
||||
u0: f32,
|
||||
v0: f32,
|
||||
u1: f32,
|
||||
v1: f32,
|
||||
color: [f32; 4],
|
||||
aspect_ratio: f32,
|
||||
x: f32, y: f32, width: f32, height: f32, rotation: f32,
|
||||
origin_x: f32, origin_y: f32,
|
||||
u0: f32, v0: f32, u1: f32, v1: f32,
|
||||
color: [f32; 4], aspect: f32,
|
||||
) {
|
||||
let cos = rotation.cos();
|
||||
let sin = rotation.sin();
|
||||
let (cos, sin) = (rotation.cos(), rotation.sin());
|
||||
let (ox, oy) = (origin_x * width, origin_y * height);
|
||||
|
||||
// Origin offset | 原点偏移
|
||||
// origin (0,0) = bottom-left, (1,1) = top-right
|
||||
// 原点 (0,0) = 左下角, (1,1) = 右上角
|
||||
let ox = origin_x * width;
|
||||
let oy = origin_y * height;
|
||||
let corners = [(-ox, height - oy), (width - ox, height - oy), (width - ox, -oy), (-ox, -oy)];
|
||||
let tex_coords = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
|
||||
|
||||
// Local corner positions (relative to origin) | 局部角点位置(相对于原点)
|
||||
// Y-up coordinate system | Y向上坐标系
|
||||
let corners = [
|
||||
(-ox, height - oy), // Top-left | 左上
|
||||
(width - ox, height - oy), // Top-right | 右上
|
||||
(width - ox, -oy), // Bottom-right | 右下
|
||||
(-ox, -oy), // Bottom-left | 左下
|
||||
];
|
||||
|
||||
// UV coordinates use image coordinate system (top-left origin, Y-down)
|
||||
// UV坐标使用图像坐标系(左上角为原点,Y轴向下)
|
||||
// Incoming UV: [u0, v0, u1, v1] where v0 < v1
|
||||
// 传入的 UV:[u0, v0, u1, v1] 其中 v0 < v1
|
||||
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 | 位置
|
||||
batch.push(px);
|
||||
batch.push(py);
|
||||
|
||||
// Texture coordinates | 纹理坐标
|
||||
batch.push(tex_coords[i][0]);
|
||||
batch.push(tex_coords[i][1]);
|
||||
|
||||
// Color | 颜色
|
||||
let (rx, ry) = (lx * cos - ly * sin, lx * sin + ly * cos);
|
||||
batch.extend_from_slice(&[rx + x, ry + y]);
|
||||
batch.extend_from_slice(&tex_coords[i]);
|
||||
batch.extend_from_slice(&color);
|
||||
|
||||
// Aspect ratio (same for all 4 vertices of a quad)
|
||||
// 宽高比(四边形的 4 个顶点相同)
|
||||
batch.push(aspect_ratio);
|
||||
batch.push(aspect);
|
||||
}
|
||||
}
|
||||
|
||||
/// Flush a batch to GPU and render.
|
||||
/// 将批次刷新到GPU并渲染。
|
||||
fn flush_batch(&self, gl: &WebGl2RenderingContext, vertices: &[f32]) {
|
||||
if vertices.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let sprite_count = vertices.len() / (VERTICES_PER_SPRITE * FLOATS_PER_VERTEX);
|
||||
|
||||
// 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(vertices);
|
||||
gl.buffer_sub_data_with_i32_and_array_buffer_view(
|
||||
WebGl2RenderingContext::ARRAY_BUFFER,
|
||||
0,
|
||||
&vertex_array,
|
||||
);
|
||||
}
|
||||
|
||||
// Draw | 绘制
|
||||
let index_count = (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 all batches for rendering (in submission order).
|
||||
/// 获取所有批次用于渲染(按提交顺序)。
|
||||
pub fn batches(&self) -> &[(BatchKey, Vec<f32>)] {
|
||||
&self.batches
|
||||
}
|
||||
|
||||
/// Flush a specific batch by index.
|
||||
/// 按索引刷新特定批次。
|
||||
pub fn flush_batch_at(&self, gl: &WebGl2RenderingContext, index: usize) {
|
||||
pub fn flush_batch(&self, backend: &mut impl GraphicsBackend, vertices: &[f32]) {
|
||||
if vertices.is_empty() { return; }
|
||||
|
||||
let sprite_count = vertices.len() / (VERTICES_PER_SPRITE * FLOATS_PER_VERTEX);
|
||||
backend.update_buffer(self.vbo, 0, bytemuck::cast_slice(vertices)).ok();
|
||||
backend.draw_indexed(self.vao, (sprite_count * INDICES_PER_SPRITE) as u32, 0).ok();
|
||||
}
|
||||
|
||||
pub fn flush_batch_at(&self, backend: &mut impl GraphicsBackend, index: usize) {
|
||||
if let Some((_, vertices)) = self.batches.get(index) {
|
||||
self.flush_batch(gl, vertices);
|
||||
self.flush_batch(backend, vertices);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current sprite count.
|
||||
/// 获取当前精灵数量。
|
||||
#[inline]
|
||||
pub fn sprite_count(&self) -> usize {
|
||||
self.sprite_count
|
||||
}
|
||||
|
||||
pub fn vao(&self) -> VertexArrayHandle {
|
||||
self.vao
|
||||
}
|
||||
|
||||
pub fn destroy(self, backend: &mut impl GraphicsBackend) {
|
||||
backend.destroy_vertex_array(self.vao);
|
||||
backend.destroy_buffer(self.vbo);
|
||||
backend.destroy_buffer(self.ibo);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,241 +1,210 @@
|
||||
//! Grid renderer for editor viewport.
|
||||
//! 编辑器视口的网格渲染器。
|
||||
|
||||
use web_sys::{WebGl2RenderingContext, WebGlBuffer, WebGlProgram};
|
||||
use crate::core::error::{Result, EngineError};
|
||||
use es_engine_shared::{
|
||||
traits::backend::{GraphicsBackend, BufferUsage},
|
||||
types::{
|
||||
handle::{ShaderHandle, BufferHandle, VertexArrayHandle},
|
||||
vertex::{VertexLayout, VertexAttribute, VertexAttributeType},
|
||||
blend::BlendMode,
|
||||
},
|
||||
Vec4,
|
||||
};
|
||||
use super::camera::Camera2D;
|
||||
|
||||
const GRID_VERTEX_SHADER: &str = r#"#version 300 es
|
||||
const VERTEX_SHADER: &str = r#"#version 300 es
|
||||
precision highp float;
|
||||
|
||||
layout(location = 0) in vec2 a_position;
|
||||
|
||||
uniform mat3 u_projection;
|
||||
|
||||
void main() {
|
||||
vec3 pos = u_projection * vec3(a_position, 1.0);
|
||||
gl_Position = vec4(pos.xy, 0.0, 1.0);
|
||||
}
|
||||
"#;
|
||||
|
||||
const GRID_FRAGMENT_SHADER: &str = r#"#version 300 es
|
||||
const FRAGMENT_SHADER: &str = r#"#version 300 es
|
||||
precision highp float;
|
||||
|
||||
uniform vec4 u_color;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
void main() {
|
||||
fragColor = u_color;
|
||||
}
|
||||
"#;
|
||||
|
||||
const GRID_COLOR: Vec4 = Vec4::new(0.3, 0.3, 0.35, 1.0);
|
||||
const X_AXIS_COLOR: Vec4 = Vec4::new(1.0, 0.3, 0.3, 1.0);
|
||||
const Y_AXIS_COLOR: Vec4 = Vec4::new(0.3, 1.0, 0.3, 1.0);
|
||||
|
||||
pub struct GridRenderer {
|
||||
program: WebGlProgram,
|
||||
vertex_buffer: WebGlBuffer,
|
||||
vertex_count: i32,
|
||||
last_zoom: f32,
|
||||
last_width: f32,
|
||||
last_height: f32,
|
||||
shader: ShaderHandle,
|
||||
grid_vbo: BufferHandle,
|
||||
grid_vao: VertexArrayHandle,
|
||||
axis_vbo: BufferHandle,
|
||||
axis_vao: VertexArrayHandle,
|
||||
grid_vertex_count: u32,
|
||||
cache: GridCache,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GridCache {
|
||||
zoom: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
}
|
||||
|
||||
impl GridCache {
|
||||
fn is_dirty(&self, camera: &Camera2D) -> bool {
|
||||
(camera.zoom - self.zoom).abs() > 0.001
|
||||
|| (camera.viewport_width() - self.width).abs() > 1.0
|
||||
|| (camera.viewport_height() - self.height).abs() > 1.0
|
||||
}
|
||||
|
||||
fn update(&mut self, camera: &Camera2D) {
|
||||
self.zoom = camera.zoom;
|
||||
self.width = camera.viewport_width();
|
||||
self.height = camera.viewport_height();
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_GRID_VERTICES: usize = 8000;
|
||||
|
||||
impl GridRenderer {
|
||||
pub fn new(gl: &WebGl2RenderingContext) -> Result<Self> {
|
||||
let program = Self::create_program(gl)?;
|
||||
let vertex_buffer = gl.create_buffer()
|
||||
.ok_or(EngineError::BufferCreationFailed)?;
|
||||
pub fn new(backend: &mut impl GraphicsBackend) -> Result<Self, String> {
|
||||
let shader = backend.compile_shader(VERTEX_SHADER, FRAGMENT_SHADER)
|
||||
.map_err(|e| format!("Grid shader: {:?}", e))?;
|
||||
|
||||
let layout = VertexLayout {
|
||||
attributes: vec![
|
||||
VertexAttribute {
|
||||
name: "a_position".into(),
|
||||
attr_type: VertexAttributeType::Float2,
|
||||
offset: 0,
|
||||
normalized: false,
|
||||
},
|
||||
],
|
||||
stride: 8,
|
||||
};
|
||||
|
||||
let grid_buffer_size = MAX_GRID_VERTICES * 2 * 4;
|
||||
let grid_vbo = backend.create_vertex_buffer_sized(grid_buffer_size, BufferUsage::Dynamic)
|
||||
.map_err(|e| format!("Grid VBO: {:?}", e))?;
|
||||
let grid_vao = backend.create_vertex_array(grid_vbo, None, &layout)
|
||||
.map_err(|e| format!("Grid VAO: {:?}", e))?;
|
||||
|
||||
let axis_data = Self::build_axis_vertices(1000.0);
|
||||
let axis_vbo = backend.create_vertex_buffer(
|
||||
bytemuck::cast_slice(&axis_data),
|
||||
BufferUsage::Dynamic,
|
||||
).map_err(|e| format!("Axis VBO: {:?}", e))?;
|
||||
let axis_vao = backend.create_vertex_array(axis_vbo, None, &layout)
|
||||
.map_err(|e| format!("Axis VAO: {:?}", e))?;
|
||||
|
||||
Ok(Self {
|
||||
program,
|
||||
vertex_buffer,
|
||||
vertex_count: 0,
|
||||
last_zoom: 0.0,
|
||||
last_width: 0.0,
|
||||
last_height: 0.0,
|
||||
shader,
|
||||
grid_vbo,
|
||||
grid_vao,
|
||||
axis_vbo,
|
||||
axis_vao,
|
||||
grid_vertex_count: 0,
|
||||
cache: GridCache::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn create_program(gl: &WebGl2RenderingContext) -> Result<WebGlProgram> {
|
||||
let vert_shader = gl.create_shader(WebGl2RenderingContext::VERTEX_SHADER)
|
||||
.ok_or_else(|| EngineError::ShaderCompileFailed("Failed to create vertex shader".into()))?;
|
||||
gl.shader_source(&vert_shader, GRID_VERTEX_SHADER);
|
||||
gl.compile_shader(&vert_shader);
|
||||
pub fn render(&mut self, backend: &mut impl GraphicsBackend, camera: &Camera2D) {
|
||||
self.update_grid_if_needed(backend, camera);
|
||||
|
||||
if !gl.get_shader_parameter(&vert_shader, WebGl2RenderingContext::COMPILE_STATUS)
|
||||
.as_bool()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let log = gl.get_shader_info_log(&vert_shader).unwrap_or_default();
|
||||
return Err(EngineError::ShaderCompileFailed(format!("Grid vertex shader: {}", log)));
|
||||
}
|
||||
|
||||
let frag_shader = gl.create_shader(WebGl2RenderingContext::FRAGMENT_SHADER)
|
||||
.ok_or_else(|| EngineError::ShaderCompileFailed("Failed to create fragment shader".into()))?;
|
||||
gl.shader_source(&frag_shader, GRID_FRAGMENT_SHADER);
|
||||
gl.compile_shader(&frag_shader);
|
||||
|
||||
if !gl.get_shader_parameter(&frag_shader, WebGl2RenderingContext::COMPILE_STATUS)
|
||||
.as_bool()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let log = gl.get_shader_info_log(&frag_shader).unwrap_or_default();
|
||||
return Err(EngineError::ShaderCompileFailed(format!("Grid fragment shader: {}", log)));
|
||||
}
|
||||
|
||||
let program = gl.create_program()
|
||||
.ok_or_else(|| EngineError::ProgramLinkFailed("Failed to create grid program".into()))?;
|
||||
gl.attach_shader(&program, &vert_shader);
|
||||
gl.attach_shader(&program, &frag_shader);
|
||||
gl.link_program(&program);
|
||||
|
||||
if !gl.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
|
||||
.as_bool()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let log = gl.get_program_info_log(&program).unwrap_or_default();
|
||||
return Err(EngineError::ProgramLinkFailed(format!("Grid program: {}", log)));
|
||||
}
|
||||
|
||||
gl.delete_shader(Some(&vert_shader));
|
||||
gl.delete_shader(Some(&frag_shader));
|
||||
|
||||
Ok(program)
|
||||
}
|
||||
|
||||
fn update_grid(&mut self, gl: &WebGl2RenderingContext, camera: &Camera2D) {
|
||||
let zoom = camera.zoom;
|
||||
let width = camera.viewport_width();
|
||||
let height = camera.viewport_height();
|
||||
|
||||
if (zoom - self.last_zoom).abs() < 0.001
|
||||
&& (width - self.last_width).abs() < 1.0
|
||||
&& (height - self.last_height).abs() < 1.0
|
||||
{
|
||||
if self.grid_vertex_count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.last_zoom = zoom;
|
||||
self.last_width = width;
|
||||
self.last_height = height;
|
||||
backend.bind_shader(self.shader).ok();
|
||||
backend.set_uniform_mat3("u_projection", &camera.projection_matrix()).ok();
|
||||
backend.set_uniform_vec4("u_color", GRID_COLOR).ok();
|
||||
backend.set_blend_mode(BlendMode::Alpha);
|
||||
backend.draw_lines(self.grid_vao, self.grid_vertex_count, 0).ok();
|
||||
}
|
||||
|
||||
let half_width = width / (2.0 * zoom);
|
||||
let half_height = height / (2.0 * zoom);
|
||||
let max_size = half_width.max(half_height) * 2.0;
|
||||
pub fn render_axes(&mut self, backend: &mut impl GraphicsBackend, camera: &Camera2D) {
|
||||
let axis_length = self.calculate_axis_length(camera);
|
||||
self.update_axis_buffer(backend, axis_length);
|
||||
|
||||
let base_step = if max_size > 10000.0 {
|
||||
1000.0
|
||||
} else if max_size > 1000.0 {
|
||||
100.0
|
||||
} else if max_size > 100.0 {
|
||||
10.0
|
||||
} else if max_size > 10.0 {
|
||||
1.0
|
||||
} else {
|
||||
0.1
|
||||
};
|
||||
backend.bind_shader(self.shader).ok();
|
||||
backend.set_uniform_mat3("u_projection", &camera.projection_matrix()).ok();
|
||||
backend.set_blend_mode(BlendMode::Alpha);
|
||||
|
||||
let fine_step = base_step;
|
||||
backend.set_uniform_vec4("u_color", X_AXIS_COLOR).ok();
|
||||
backend.draw_lines(self.axis_vao, 2, 0).ok();
|
||||
|
||||
backend.set_uniform_vec4("u_color", Y_AXIS_COLOR).ok();
|
||||
backend.draw_lines(self.axis_vao, 2, 2).ok();
|
||||
}
|
||||
|
||||
fn update_grid_if_needed(&mut self, backend: &mut impl GraphicsBackend, camera: &Camera2D) {
|
||||
if !self.cache.is_dirty(camera) {
|
||||
return;
|
||||
}
|
||||
self.cache.update(camera);
|
||||
|
||||
let vertices = self.build_grid_vertices(camera);
|
||||
self.grid_vertex_count = (vertices.len() / 2) as u32;
|
||||
|
||||
backend.update_buffer(self.grid_vbo, 0, bytemuck::cast_slice(&vertices)).ok();
|
||||
}
|
||||
|
||||
fn build_grid_vertices(&self, camera: &Camera2D) -> Vec<f32> {
|
||||
let half_w = camera.viewport_width() / (2.0 * camera.zoom);
|
||||
let half_h = camera.viewport_height() / (2.0 * camera.zoom);
|
||||
let max_size = half_w.max(half_h) * 2.0;
|
||||
|
||||
let step = Self::calculate_grid_step(max_size);
|
||||
let range = max_size * 1.5;
|
||||
let start = -range;
|
||||
let end = range;
|
||||
|
||||
let mut vertices = Vec::new();
|
||||
let start = (-range / step).floor() * step;
|
||||
let end = (range / step).ceil() * step;
|
||||
|
||||
let mut x = (start / fine_step).floor() * fine_step;
|
||||
while x <= end {
|
||||
vertices.extend_from_slice(&[x, start, x, end]);
|
||||
x += fine_step;
|
||||
let mut pos = start;
|
||||
while pos <= end {
|
||||
vertices.extend_from_slice(&[pos, -range, pos, range]);
|
||||
vertices.extend_from_slice(&[-range, pos, range, pos]);
|
||||
pos += step;
|
||||
}
|
||||
|
||||
let mut y = (start / fine_step).floor() * fine_step;
|
||||
while y <= end {
|
||||
vertices.extend_from_slice(&[start, y, end, y]);
|
||||
y += fine_step;
|
||||
}
|
||||
vertices
|
||||
}
|
||||
|
||||
self.vertex_count = (vertices.len() / 2) as i32;
|
||||
|
||||
gl.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&self.vertex_buffer));
|
||||
unsafe {
|
||||
let array = js_sys::Float32Array::view(&vertices);
|
||||
gl.buffer_data_with_array_buffer_view(
|
||||
WebGl2RenderingContext::ARRAY_BUFFER,
|
||||
&array,
|
||||
WebGl2RenderingContext::DYNAMIC_DRAW,
|
||||
);
|
||||
fn calculate_grid_step(max_size: f32) -> f32 {
|
||||
match max_size {
|
||||
s if s > 10000.0 => 1000.0,
|
||||
s if s > 1000.0 => 100.0,
|
||||
s if s > 100.0 => 10.0,
|
||||
s if s > 10.0 => 1.0,
|
||||
_ => 0.1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self, gl: &WebGl2RenderingContext, camera: &Camera2D) {
|
||||
self.update_grid(gl, camera);
|
||||
|
||||
if self.vertex_count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
gl.use_program(Some(&self.program));
|
||||
|
||||
let projection = camera.projection_matrix();
|
||||
let proj_loc = gl.get_uniform_location(&self.program, "u_projection");
|
||||
gl.uniform_matrix3fv_with_f32_array(proj_loc.as_ref(), false, &projection.to_cols_array());
|
||||
|
||||
let color_loc = gl.get_uniform_location(&self.program, "u_color");
|
||||
gl.uniform4f(color_loc.as_ref(), 0.3, 0.3, 0.35, 1.0);
|
||||
|
||||
gl.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&self.vertex_buffer));
|
||||
gl.enable_vertex_attrib_array(0);
|
||||
gl.vertex_attrib_pointer_with_i32(0, 2, WebGl2RenderingContext::FLOAT, false, 0, 0);
|
||||
|
||||
gl.draw_arrays(WebGl2RenderingContext::LINES, 0, self.vertex_count);
|
||||
|
||||
gl.disable_vertex_attrib_array(0);
|
||||
fn calculate_axis_length(&self, camera: &Camera2D) -> f32 {
|
||||
let half_w = camera.viewport_width() / (2.0 * camera.zoom);
|
||||
let half_h = camera.viewport_height() / (2.0 * camera.zoom);
|
||||
half_w.max(half_h) * 2.0
|
||||
}
|
||||
|
||||
pub fn render_axes(&self, gl: &WebGl2RenderingContext, camera: &Camera2D) {
|
||||
gl.use_program(Some(&self.program));
|
||||
fn build_axis_vertices(length: f32) -> Vec<f32> {
|
||||
vec![
|
||||
-length, 0.0, length, 0.0,
|
||||
0.0, -length, 0.0, length,
|
||||
]
|
||||
}
|
||||
|
||||
let projection = camera.projection_matrix();
|
||||
let proj_loc = gl.get_uniform_location(&self.program, "u_projection");
|
||||
gl.uniform_matrix3fv_with_f32_array(proj_loc.as_ref(), false, &projection.to_cols_array());
|
||||
fn update_axis_buffer(&mut self, backend: &mut impl GraphicsBackend, length: f32) {
|
||||
let data = Self::build_axis_vertices(length);
|
||||
backend.update_buffer(self.axis_vbo, 0, bytemuck::cast_slice(&data)).ok();
|
||||
}
|
||||
|
||||
let half_width = camera.viewport_width() / (2.0 * camera.zoom);
|
||||
let half_height = camera.viewport_height() / (2.0 * camera.zoom);
|
||||
let axis_length = half_width.max(half_height) * 2.0;
|
||||
|
||||
let color_loc = gl.get_uniform_location(&self.program, "u_color");
|
||||
|
||||
let axis_buffer = gl.create_buffer().unwrap();
|
||||
gl.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&axis_buffer));
|
||||
gl.enable_vertex_attrib_array(0);
|
||||
gl.vertex_attrib_pointer_with_i32(0, 2, WebGl2RenderingContext::FLOAT, false, 0, 0);
|
||||
|
||||
// X axis (red)
|
||||
let x_axis = [-axis_length, 0.0, axis_length, 0.0f32];
|
||||
unsafe {
|
||||
let array = js_sys::Float32Array::view(&x_axis);
|
||||
gl.buffer_data_with_array_buffer_view(
|
||||
WebGl2RenderingContext::ARRAY_BUFFER,
|
||||
&array,
|
||||
WebGl2RenderingContext::STATIC_DRAW,
|
||||
);
|
||||
}
|
||||
gl.uniform4f(color_loc.as_ref(), 1.0, 0.3, 0.3, 1.0);
|
||||
gl.draw_arrays(WebGl2RenderingContext::LINES, 0, 2);
|
||||
|
||||
// Y axis (green)
|
||||
let y_axis = [0.0, -axis_length, 0.0, axis_length];
|
||||
unsafe {
|
||||
let array = js_sys::Float32Array::view(&y_axis);
|
||||
gl.buffer_data_with_array_buffer_view(
|
||||
WebGl2RenderingContext::ARRAY_BUFFER,
|
||||
&array,
|
||||
WebGl2RenderingContext::STATIC_DRAW,
|
||||
);
|
||||
}
|
||||
gl.uniform4f(color_loc.as_ref(), 0.3, 1.0, 0.3, 1.0);
|
||||
gl.draw_arrays(WebGl2RenderingContext::LINES, 0, 2);
|
||||
|
||||
gl.disable_vertex_attrib_array(0);
|
||||
gl.delete_buffer(Some(&axis_buffer));
|
||||
pub fn destroy(self, backend: &mut impl GraphicsBackend) {
|
||||
backend.destroy_vertex_array(self.grid_vao);
|
||||
backend.destroy_vertex_array(self.axis_vao);
|
||||
backend.destroy_buffer(self.grid_vbo);
|
||||
backend.destroy_buffer(self.axis_vbo);
|
||||
backend.destroy_shader(self.shader);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,110 +1,96 @@
|
||||
//! Main 2D renderer implementation.
|
||||
//! 主2D渲染器实现。
|
||||
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::WebGl2RenderingContext;
|
||||
|
||||
use crate::core::error::Result;
|
||||
use crate::resource::TextureManager;
|
||||
use es_engine_shared::{
|
||||
traits::backend::GraphicsBackend,
|
||||
types::{
|
||||
handle::ShaderHandle,
|
||||
blend::ScissorRect,
|
||||
},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use crate::backend::WebGL2Backend;
|
||||
use super::batch::SpriteBatch;
|
||||
use super::camera::Camera2D;
|
||||
use super::shader::ShaderManager;
|
||||
use super::material::MaterialManager;
|
||||
use super::texture::TextureManager;
|
||||
use super::material::{Material, BlendMode, UniformValue};
|
||||
|
||||
fn to_shared_blend_mode(mode: BlendMode) -> es_engine_shared::types::blend::BlendMode {
|
||||
match mode {
|
||||
BlendMode::None => es_engine_shared::types::blend::BlendMode::None,
|
||||
BlendMode::Alpha => es_engine_shared::types::blend::BlendMode::Alpha,
|
||||
BlendMode::Additive => es_engine_shared::types::blend::BlendMode::Additive,
|
||||
BlendMode::Multiply => es_engine_shared::types::blend::BlendMode::Multiply,
|
||||
BlendMode::Screen => es_engine_shared::types::blend::BlendMode::Screen,
|
||||
BlendMode::PremultipliedAlpha => es_engine_shared::types::blend::BlendMode::PremultipliedAlpha,
|
||||
}
|
||||
}
|
||||
|
||||
const SPRITE_VERTEX_SHADER: &str = r#"#version 300 es
|
||||
precision highp float;
|
||||
layout(location = 0) in vec2 a_position;
|
||||
layout(location = 1) in vec2 a_texCoord;
|
||||
layout(location = 2) in vec4 a_color;
|
||||
uniform mat3 u_projection;
|
||||
out vec2 v_texCoord;
|
||||
out vec4 v_color;
|
||||
void main() {
|
||||
vec3 pos = u_projection * vec3(a_position, 1.0);
|
||||
gl_Position = vec4(pos.xy, 0.0, 1.0);
|
||||
v_texCoord = a_texCoord;
|
||||
v_color = a_color;
|
||||
}
|
||||
"#;
|
||||
|
||||
const SPRITE_FRAGMENT_SHADER: &str = r#"#version 300 es
|
||||
precision highp float;
|
||||
in vec2 v_texCoord;
|
||||
in vec4 v_color;
|
||||
uniform sampler2D u_texture;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 texColor = texture(u_texture, v_texCoord);
|
||||
fragColor = texColor * v_color;
|
||||
if (fragColor.a < 0.01) discard;
|
||||
}
|
||||
"#;
|
||||
|
||||
/// 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,
|
||||
|
||||
/// Shader manager.
|
||||
/// 着色器管理器。
|
||||
shader_manager: ShaderManager,
|
||||
|
||||
/// Material manager.
|
||||
/// 材质管理器。
|
||||
material_manager: MaterialManager,
|
||||
|
||||
/// 2D camera.
|
||||
/// 2D相机。
|
||||
default_shader: ShaderHandle,
|
||||
custom_shaders: HashMap<u32, ShaderHandle>,
|
||||
next_shader_id: u32,
|
||||
materials: HashMap<u32, Material>,
|
||||
camera: Camera2D,
|
||||
|
||||
/// Clear color (RGBA).
|
||||
/// 清除颜色 (RGBA)。
|
||||
clear_color: [f32; 4],
|
||||
|
||||
/// Current active shader ID.
|
||||
/// 当前激活的着色器ID。
|
||||
#[allow(dead_code)]
|
||||
current_shader_id: u32,
|
||||
|
||||
/// Current active material ID.
|
||||
/// 当前激活的材质ID。
|
||||
#[allow(dead_code)]
|
||||
current_material_id: u32,
|
||||
|
||||
/// Current scissor rect (x, y, width, height) in screen coordinates.
|
||||
/// None means scissor test is disabled.
|
||||
/// 当前裁剪矩形(屏幕坐标)。None 表示禁用裁剪测试。
|
||||
scissor_rect: Option<[f32; 4]>,
|
||||
|
||||
/// Viewport height for scissor coordinate conversion.
|
||||
/// 视口高度,用于裁剪坐标转换。
|
||||
scissor_rect: Option<ScissorRect>,
|
||||
viewport_height: f32,
|
||||
}
|
||||
|
||||
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_manager = ShaderManager::new(gl)?;
|
||||
let material_manager = MaterialManager::new();
|
||||
pub fn new(backend: &mut WebGL2Backend, max_sprites: usize) -> Result<Self, String> {
|
||||
let sprite_batch = SpriteBatch::new(backend, max_sprites)?;
|
||||
let default_shader = backend.compile_shader(SPRITE_VERTEX_SHADER, SPRITE_FRAGMENT_SHADER)
|
||||
.map_err(|e| format!("Default shader: {:?}", e))?;
|
||||
|
||||
// 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 (width, height) = (backend.width() as f32, backend.height() as f32);
|
||||
let camera = Camera2D::new(width, height);
|
||||
|
||||
let camera = Camera2D::new(canvas.0, canvas.1);
|
||||
|
||||
log::info!(
|
||||
"Renderer2D initialized | Renderer2D初始化完成: {}x{}, max sprites: {}",
|
||||
canvas.0, canvas.1, max_sprites
|
||||
);
|
||||
let mut materials = HashMap::new();
|
||||
materials.insert(0, Material::default());
|
||||
|
||||
Ok(Self {
|
||||
sprite_batch,
|
||||
shader_manager,
|
||||
material_manager,
|
||||
default_shader,
|
||||
custom_shaders: HashMap::new(),
|
||||
next_shader_id: 100,
|
||||
materials,
|
||||
camera,
|
||||
clear_color: [0.1, 0.1, 0.12, 1.0],
|
||||
current_shader_id: 0,
|
||||
current_material_id: 0,
|
||||
scissor_rect: None,
|
||||
viewport_height: canvas.1,
|
||||
viewport_height: height,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 | 每个精灵的打包颜色
|
||||
/// * `material_ids` - Material ID for each sprite (0 = default) | 每个精灵的材质ID(0 = 默认)
|
||||
/// * `texture_manager` - Texture manager | 纹理管理器
|
||||
pub fn submit_batch(
|
||||
&mut self,
|
||||
transforms: &[f32],
|
||||
@@ -112,277 +98,171 @@ impl Renderer2D {
|
||||
uvs: &[f32],
|
||||
colors: &[u32],
|
||||
material_ids: &[u32],
|
||||
texture_manager: &TextureManager,
|
||||
) -> Result<()> {
|
||||
self.sprite_batch.add_sprites(
|
||||
transforms,
|
||||
texture_ids,
|
||||
uvs,
|
||||
colors,
|
||||
material_ids,
|
||||
texture_manager,
|
||||
)
|
||||
) -> Result<(), String> {
|
||||
self.sprite_batch.add_sprites(transforms, texture_ids, uvs, colors, material_ids)
|
||||
}
|
||||
|
||||
/// Render the current frame.
|
||||
/// 渲染当前帧。
|
||||
pub fn render(&mut self, gl: &WebGl2RenderingContext, texture_manager: &TextureManager) -> Result<()> {
|
||||
pub fn render(&mut self, backend: &mut WebGL2Backend, texture_manager: &TextureManager) -> Result<(), String> {
|
||||
if self.sprite_batch.sprite_count() == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Apply scissor test if enabled
|
||||
// 如果启用,应用裁剪测试
|
||||
self.apply_scissor(gl);
|
||||
self.apply_scissor(backend);
|
||||
|
||||
// Track current state to minimize state changes | 跟踪当前状态以最小化状态切换
|
||||
let mut current_material_id: u32 = u32::MAX;
|
||||
let mut current_texture_id: u32 = u32::MAX;
|
||||
|
||||
// Get projection matrix once | 一次性获取投影矩阵
|
||||
let projection = self.camera.projection_matrix();
|
||||
let mut current_material_id = u32::MAX;
|
||||
let mut current_texture_id = u32::MAX;
|
||||
|
||||
// Iterate through batches in submission order (preserves render order)
|
||||
// 按提交顺序遍历批次(保持渲染顺序)
|
||||
for batch_idx in 0..self.sprite_batch.batches().len() {
|
||||
let (batch_key, vertices) = &self.sprite_batch.batches()[batch_idx];
|
||||
if vertices.is_empty() { continue; }
|
||||
|
||||
// Skip empty batches | 跳过空批次
|
||||
if vertices.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Switch material if needed | 如需切换材质
|
||||
if batch_key.material_id != current_material_id {
|
||||
current_material_id = batch_key.material_id;
|
||||
|
||||
// Get material (fallback to default if not found) | 获取材质(未找到则回退到默认)
|
||||
let material = self.material_manager.get_material(batch_key.material_id)
|
||||
.unwrap_or_else(|| self.material_manager.get_default_material());
|
||||
let material = self.materials.get(&batch_key.material_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
// Bind shader | 绑定Shader
|
||||
let shader = self.shader_manager.get_shader(material.shader_id)
|
||||
.unwrap_or_else(|| self.shader_manager.get_default_shader());
|
||||
shader.bind(gl);
|
||||
let shader = if material.shader_id == 0 {
|
||||
self.default_shader
|
||||
} else {
|
||||
self.custom_shaders.get(&material.shader_id)
|
||||
.copied()
|
||||
.unwrap_or(self.default_shader)
|
||||
};
|
||||
|
||||
// Apply blend mode | 应用混合模式
|
||||
MaterialManager::apply_blend_mode(gl, material.blend_mode);
|
||||
backend.bind_shader(shader).ok();
|
||||
backend.set_blend_mode(to_shared_blend_mode(material.blend_mode));
|
||||
backend.set_uniform_mat3("u_projection", &projection).ok();
|
||||
backend.set_uniform_i32("u_texture", 0).ok();
|
||||
|
||||
// Set projection matrix | 设置投影矩阵
|
||||
shader.set_uniform_mat3(gl, "u_projection", &projection.to_cols_array());
|
||||
|
||||
// Set texture sampler | 设置纹理采样器
|
||||
shader.set_uniform_i32(gl, "u_texture", 0);
|
||||
|
||||
// Apply material uniforms | 应用材质uniform
|
||||
material.uniforms.apply_to_shader(gl, shader);
|
||||
for name in material.uniforms.names() {
|
||||
if let Some(value) = material.uniforms.get(name) {
|
||||
match value {
|
||||
UniformValue::Float(v) => { backend.set_uniform_f32(name, *v).ok(); }
|
||||
UniformValue::Vec2(v) => { backend.set_uniform_vec2(name, es_engine_shared::Vec2::new(v[0], v[1])).ok(); }
|
||||
UniformValue::Vec3(v) => { backend.set_uniform_vec3(name, es_engine_shared::Vec3::new(v[0], v[1], v[2])).ok(); }
|
||||
UniformValue::Vec4(v) => { backend.set_uniform_vec4(name, es_engine_shared::Vec4::new(v[0], v[1], v[2], v[3])).ok(); }
|
||||
UniformValue::Int(v) => { backend.set_uniform_i32(name, *v).ok(); }
|
||||
UniformValue::Mat3(v) => { backend.set_uniform_mat3(name, &es_engine_shared::Mat3::from_cols_array(v)).ok(); }
|
||||
UniformValue::Mat4(v) => { backend.set_uniform_mat4(name, &es_engine_shared::Mat4::from_cols_array(v)).ok(); }
|
||||
UniformValue::Sampler(v) => { backend.set_uniform_i32(name, *v).ok(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Switch texture if needed | 如需切换纹理
|
||||
if batch_key.texture_id != current_texture_id {
|
||||
current_texture_id = batch_key.texture_id;
|
||||
texture_manager.bind_texture(batch_key.texture_id, 0);
|
||||
texture_manager.bind_texture_via_backend(backend, batch_key.texture_id, 0);
|
||||
}
|
||||
|
||||
// Flush this batch by index | 按索引刷新此批次
|
||||
self.sprite_batch.flush_batch_at(gl, batch_idx);
|
||||
self.sprite_batch.flush_batch_at(backend, batch_idx);
|
||||
}
|
||||
|
||||
// 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
|
||||
fn apply_scissor(&self, backend: &mut WebGL2Backend) {
|
||||
if let Some(rect) = &self.scissor_rect {
|
||||
backend.set_scissor(Some(ScissorRect {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
}));
|
||||
} else {
|
||||
backend.set_scissor(None);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get reference to camera.
|
||||
/// 获取相机的引用。
|
||||
#[inline]
|
||||
pub fn camera(&self) -> &Camera2D {
|
||||
&self.camera
|
||||
}
|
||||
pub fn camera_mut(&mut self) -> &mut Camera2D { &mut self.camera }
|
||||
|
||||
#[inline]
|
||||
pub fn camera(&self) -> &Camera2D { &self.camera }
|
||||
|
||||
/// Set clear color (RGBA, each component 0.0-1.0).
|
||||
/// 设置清除颜色。
|
||||
pub fn set_clear_color(&mut self, r: f32, g: f32, b: f32, a: f32) {
|
||||
self.clear_color = [r, g, b, a];
|
||||
}
|
||||
|
||||
/// Get clear color.
|
||||
/// 获取清除颜色。
|
||||
pub fn get_clear_color(&self) -> [f32; 4] {
|
||||
self.clear_color
|
||||
}
|
||||
pub fn get_clear_color(&self) -> [f32; 4] { self.clear_color }
|
||||
|
||||
/// Update camera viewport size.
|
||||
/// 更新相机视口大小。
|
||||
pub fn resize(&mut self, width: f32, height: f32) {
|
||||
self.camera.set_viewport(width, height);
|
||||
self.viewport_height = height;
|
||||
}
|
||||
|
||||
// ============= Scissor Test =============
|
||||
// ============= 裁剪测试 =============
|
||||
|
||||
/// Set scissor rect for clipping (screen coordinates, Y-down).
|
||||
/// 设置裁剪矩形(屏幕坐标,Y 轴向下)。
|
||||
///
|
||||
/// Content outside this rect will be clipped.
|
||||
/// 此矩形外的内容将被裁剪。
|
||||
///
|
||||
/// # Arguments | 参数
|
||||
/// * `x` - Left edge in screen coordinates | 屏幕坐标中的左边缘
|
||||
/// * `y` - Top edge in screen coordinates (Y-down) | 屏幕坐标中的上边缘(Y 向下)
|
||||
/// * `width` - Rect width | 矩形宽度
|
||||
/// * `height` - Rect height | 矩形高度
|
||||
pub fn set_scissor_rect(&mut self, x: f32, y: f32, width: f32, height: f32) {
|
||||
self.scissor_rect = Some([x, y, width, height]);
|
||||
self.scissor_rect = Some(ScissorRect {
|
||||
x: x as i32, y: y as i32,
|
||||
width: width as u32, height: height as u32,
|
||||
});
|
||||
}
|
||||
|
||||
/// Clear scissor rect (disable clipping).
|
||||
/// 清除裁剪矩形(禁用裁剪)。
|
||||
pub fn clear_scissor_rect(&mut self) {
|
||||
self.scissor_rect = None;
|
||||
pub fn clear_scissor_rect(&mut self) { self.scissor_rect = None; }
|
||||
|
||||
pub fn compile_shader(&mut self, backend: &mut WebGL2Backend, vertex: &str, fragment: &str) -> Result<u32, String> {
|
||||
let handle = backend.compile_shader(vertex, fragment)
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
let id = self.next_shader_id;
|
||||
self.next_shader_id += 1;
|
||||
self.custom_shaders.insert(id, handle);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Apply current scissor state to GL context.
|
||||
/// 应用当前裁剪状态到 GL 上下文。
|
||||
fn apply_scissor(&self, gl: &WebGl2RenderingContext) {
|
||||
if let Some([x, y, width, height]) = self.scissor_rect {
|
||||
gl.enable(WebGl2RenderingContext::SCISSOR_TEST);
|
||||
// WebGL scissor uses bottom-left origin with Y-up
|
||||
// Convert from screen coordinates (top-left origin, Y-down)
|
||||
// WebGL scissor 使用左下角原点,Y 轴向上
|
||||
// 从屏幕坐标转换(左上角原点,Y 轴向下)
|
||||
let gl_y = self.viewport_height - y - height;
|
||||
gl.scissor(x as i32, gl_y as i32, width as i32, height as i32);
|
||||
} else {
|
||||
gl.disable(WebGl2RenderingContext::SCISSOR_TEST);
|
||||
pub fn compile_shader_with_id(&mut self, backend: &mut WebGL2Backend, id: u32, vertex: &str, fragment: &str) -> Result<(), String> {
|
||||
let handle = backend.compile_shader(vertex, fragment)
|
||||
.map_err(|e| format!("{:?}", e))?;
|
||||
self.custom_shaders.insert(id, handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_shader(&self, id: u32) -> bool {
|
||||
id == 0 || self.custom_shaders.contains_key(&id)
|
||||
}
|
||||
|
||||
pub fn remove_shader(&mut self, id: u32) -> bool {
|
||||
if id < 100 { return false; }
|
||||
self.custom_shaders.remove(&id).is_some()
|
||||
}
|
||||
|
||||
pub fn register_material(&mut self, material: Material) -> u32 {
|
||||
let id = self.materials.keys().max().unwrap_or(&0) + 1;
|
||||
self.materials.insert(id, material);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn register_material_with_id(&mut self, id: u32, material: Material) {
|
||||
self.materials.insert(id, material);
|
||||
}
|
||||
|
||||
pub fn get_material(&self, id: u32) -> Option<&Material> { self.materials.get(&id) }
|
||||
pub fn get_material_mut(&mut self, id: u32) -> Option<&mut Material> { self.materials.get_mut(&id) }
|
||||
pub fn has_material(&self, id: u32) -> bool { self.materials.contains_key(&id) }
|
||||
pub fn remove_material(&mut self, id: u32) -> bool { self.materials.remove(&id).is_some() }
|
||||
|
||||
pub fn set_material_float(&mut self, id: u32, name: &str, value: f32) -> bool {
|
||||
if let Some(mat) = self.materials.get_mut(&id) {
|
||||
mat.uniforms.set_float(name, value);
|
||||
true
|
||||
} else { false }
|
||||
}
|
||||
|
||||
pub fn set_material_vec4(&mut self, id: u32, name: &str, x: f32, y: f32, z: f32, w: f32) -> bool {
|
||||
if let Some(mat) = self.materials.get_mut(&id) {
|
||||
mat.uniforms.set_vec4(name, x, y, z, w);
|
||||
true
|
||||
} else { false }
|
||||
}
|
||||
|
||||
pub fn destroy(self, backend: &mut WebGL2Backend) {
|
||||
self.sprite_batch.destroy(backend);
|
||||
backend.destroy_shader(self.default_shader);
|
||||
for (_, handle) in self.custom_shaders {
|
||||
backend.destroy_shader(handle);
|
||||
}
|
||||
}
|
||||
|
||||
// ============= Shader Management =============
|
||||
// ============= 着色器管理 =============
|
||||
|
||||
/// Compile and register a custom shader.
|
||||
/// 编译并注册自定义着色器。
|
||||
///
|
||||
/// # Returns | 返回
|
||||
/// The shader ID for referencing this shader | 用于引用此着色器的ID
|
||||
pub fn compile_shader(
|
||||
&mut self,
|
||||
gl: &WebGl2RenderingContext,
|
||||
vertex_source: &str,
|
||||
fragment_source: &str,
|
||||
) -> Result<u32> {
|
||||
self.shader_manager.compile_shader(gl, vertex_source, fragment_source)
|
||||
}
|
||||
|
||||
/// Compile a shader with a specific ID.
|
||||
/// 使用特定ID编译着色器。
|
||||
pub fn compile_shader_with_id(
|
||||
&mut self,
|
||||
gl: &WebGl2RenderingContext,
|
||||
shader_id: u32,
|
||||
vertex_source: &str,
|
||||
fragment_source: &str,
|
||||
) -> Result<()> {
|
||||
self.shader_manager.compile_shader_with_id(gl, shader_id, vertex_source, fragment_source)
|
||||
}
|
||||
|
||||
/// Check if a shader exists.
|
||||
/// 检查着色器是否存在。
|
||||
pub fn has_shader(&self, shader_id: u32) -> bool {
|
||||
self.shader_manager.has_shader(shader_id)
|
||||
}
|
||||
|
||||
/// Remove a shader.
|
||||
/// 移除着色器。
|
||||
pub fn remove_shader(&mut self, shader_id: u32) -> bool {
|
||||
self.shader_manager.remove_shader(shader_id)
|
||||
}
|
||||
|
||||
/// Get shader manager reference.
|
||||
/// 获取着色器管理器引用。
|
||||
pub fn shader_manager(&self) -> &ShaderManager {
|
||||
&self.shader_manager
|
||||
}
|
||||
|
||||
/// Get mutable shader manager reference.
|
||||
/// 获取可变着色器管理器引用。
|
||||
pub fn shader_manager_mut(&mut self) -> &mut ShaderManager {
|
||||
&mut self.shader_manager
|
||||
}
|
||||
|
||||
// ============= Material Management =============
|
||||
// ============= 材质管理 =============
|
||||
|
||||
/// Register a custom material.
|
||||
/// 注册自定义材质。
|
||||
///
|
||||
/// # Returns | 返回
|
||||
/// The material ID for referencing this material | 用于引用此材质的ID
|
||||
pub fn register_material(&mut self, material: super::material::Material) -> u32 {
|
||||
self.material_manager.register_material(material)
|
||||
}
|
||||
|
||||
/// Register a material with a specific ID.
|
||||
/// 使用特定ID注册材质。
|
||||
pub fn register_material_with_id(&mut self, material_id: u32, material: super::material::Material) {
|
||||
self.material_manager.register_material_with_id(material_id, material);
|
||||
}
|
||||
|
||||
/// Get a material by ID.
|
||||
/// 按ID获取材质。
|
||||
pub fn get_material(&self, material_id: u32) -> Option<&super::material::Material> {
|
||||
self.material_manager.get_material(material_id)
|
||||
}
|
||||
|
||||
/// Get a mutable material by ID.
|
||||
/// 按ID获取可变材质。
|
||||
pub fn get_material_mut(&mut self, material_id: u32) -> Option<&mut super::material::Material> {
|
||||
self.material_manager.get_material_mut(material_id)
|
||||
}
|
||||
|
||||
/// Check if a material exists.
|
||||
/// 检查材质是否存在。
|
||||
pub fn has_material(&self, material_id: u32) -> bool {
|
||||
self.material_manager.has_material(material_id)
|
||||
}
|
||||
|
||||
/// Remove a material.
|
||||
/// 移除材质。
|
||||
pub fn remove_material(&mut self, material_id: u32) -> bool {
|
||||
self.material_manager.remove_material(material_id)
|
||||
}
|
||||
|
||||
/// Set a material's float uniform.
|
||||
/// 设置材质的浮点uniform。
|
||||
pub fn set_material_float(&mut self, material_id: u32, name: &str, value: f32) -> bool {
|
||||
self.material_manager.set_material_float(material_id, name, value)
|
||||
}
|
||||
|
||||
/// Set a material's vec4 uniform.
|
||||
/// 设置材质的vec4 uniform。
|
||||
pub fn set_material_vec4(&mut self, material_id: u32, name: &str, x: f32, y: f32, z: f32, w: f32) -> bool {
|
||||
self.material_manager.set_material_vec4(material_id, name, x, y, z, w)
|
||||
}
|
||||
|
||||
/// Get material manager reference.
|
||||
/// 获取材质管理器引用。
|
||||
pub fn material_manager(&self) -> &MaterialManager {
|
||||
&self.material_manager
|
||||
}
|
||||
|
||||
/// Get mutable material manager reference.
|
||||
/// 获取可变材质管理器引用。
|
||||
pub fn material_manager_mut(&mut self) -> &mut MaterialManager {
|
||||
&mut self.material_manager
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use wasm_bindgen::JsCast;
|
||||
use web_sys::{HtmlImageElement, WebGl2RenderingContext, WebGlTexture};
|
||||
|
||||
use crate::core::error::{EngineError, Result};
|
||||
use crate::backend::WebGL2Backend;
|
||||
use super::Texture;
|
||||
|
||||
/// 纹理加载状态
|
||||
@@ -279,8 +280,6 @@ impl TextureManager {
|
||||
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 {
|
||||
// ID 0 is the default texture, no warning needed
|
||||
// ID 0 是默认纹理,不需要警告
|
||||
if id != 0 {
|
||||
log::warn!("Texture {} not found, using default | 未找到纹理 {},使用默认纹理", id, id);
|
||||
}
|
||||
@@ -290,6 +289,20 @@ impl TextureManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bind texture via backend.
|
||||
/// 通过后端绑定纹理。
|
||||
pub fn bind_texture_via_backend(&self, backend: &WebGL2Backend, id: u32, slot: u32) {
|
||||
let texture = if let Some(tex) = self.textures.get(&id) {
|
||||
Some(&tex.handle)
|
||||
} else {
|
||||
if id != 0 {
|
||||
log::warn!("Texture {} not found, using default | 未找到纹理 {},使用默认纹理", id, id);
|
||||
}
|
||||
self.default_texture.as_ref()
|
||||
};
|
||||
backend.bind_texture_raw(texture, slot);
|
||||
}
|
||||
|
||||
/// Check if texture is loaded.
|
||||
/// 检查纹理是否已加载。
|
||||
#[inline]
|
||||
|
||||
Reference in New Issue
Block a user