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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user