aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-02-19 13:17:14 +0300
committerValentin Popov <valentin@popov.link>2026-02-19 13:17:14 +0300
commit7346e695c46bea705d7f9487fc6602b57866048a (patch)
tree565e0790bfa991503748746e5355e21a842861b1 /crates
parentbb827c3928ee6fc56c04e503be9f39ae70efee67 (diff)
downloadfparkan-7346e695c46bea705d7f9487fc6602b57866048a.tar.xz
fparkan-7346e695c46bea705d7f9487fc6602b57866048a.zip
feat(render-demo): обновить поддержку OpenGL с добавлением выбора между GLES2 и Core 3.3
Diffstat (limited to 'crates')
-rw-r--r--crates/render-demo/Cargo.toml7
-rw-r--r--crates/render-demo/README.md14
-rw-r--r--crates/render-demo/src/main.rs224
3 files changed, 190 insertions, 55 deletions
diff --git a/crates/render-demo/Cargo.toml b/crates/render-demo/Cargo.toml
index b7b62cd..d92f538 100644
--- a/crates/render-demo/Cargo.toml
+++ b/crates/render-demo/Cargo.toml
@@ -13,13 +13,18 @@ msh-core = { path = "../msh-core" }
nres = { path = "../nres" }
render-core = { path = "../render-core" }
texm = { path = "../texm" }
-sdl2 = { version = "0.37", optional = true, default-features = false, features = ["bundled", "static-link"] }
glow = { version = "0.16", optional = true }
image = { version = "0.25", optional = true, default-features = false, features = ["png"] }
[dev-dependencies]
common = { path = "../common" }
+[target.'cfg(target_os = "macos")'.dependencies]
+sdl2 = { version = "0.37", optional = true, default-features = false, features = ["use-pkgconfig"] }
+
+[target.'cfg(not(target_os = "macos"))'.dependencies]
+sdl2 = { version = "0.37", optional = true, default-features = false, features = ["bundled", "static-link"] }
+
[[bin]]
name = "parkan-render-demo"
path = "src/main.rs"
diff --git a/crates/render-demo/README.md b/crates/render-demo/README.md
index 32d85ef..fe7d199 100644
--- a/crates/render-demo/README.md
+++ b/crates/render-demo/README.md
@@ -1,6 +1,6 @@
# render-demo
-Тестовый рендерер Parkan-моделей на Rust (`SDL2 + OpenGL ES 2.0`).
+Тестовый рендерер Parkan-моделей на Rust (`SDL2 + OpenGL`: GLES2 с fallback на Core 3.3).
## Назначение
@@ -18,6 +18,16 @@ cargo run -p render-demo --features demo -- \
--group 0
```
+### macOS prerequisites
+
+Для macOS `render-demo` ожидает системный SDL2 через `pkg-config`:
+
+```bash
+brew install sdl2 pkg-config
+```
+
+После этого запускайте той же командой `cargo run ... --features demo`.
+
Параметры:
- `--archive` (обязательный): NRes-архив с `.msh` entry.
@@ -70,4 +80,4 @@ cargo run -p render-demo --features demo -- \
## Ограничения
- Используется только базовая texture-фаза (без полной material/fx анимации).
-- Вывод через `glDrawArrays(GL_TRIANGLES)` из расширенного triangle-list (позиции+UV).
+- Вывод через `glDrawElements(GL_TRIANGLES)` с index-buffer (позиции+UV).
diff --git a/crates/render-demo/src/main.rs b/crates/render-demo/src/main.rs
index 70f2209..f76e52c 100644
--- a/crates/render-demo/src/main.rs
+++ b/crates/render-demo/src/main.rs
@@ -26,6 +26,12 @@ struct GpuTexture {
handle: glow::NativeTexture,
}
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+enum GlBackend {
+ Gles2,
+ Core33,
+}
+
fn parse_args() -> Result<Args, String> {
let mut archive = None;
let mut model = None;
@@ -265,35 +271,7 @@ fn run(args: Args) -> Result<(), String> {
.video()
.map_err(|err| format!("failed to init SDL2 video: {err}"))?;
- {
- let gl_attr = video.gl_attr();
- gl_attr.set_context_profile(sdl2::video::GLProfile::GLES);
- gl_attr.set_context_version(2, 0);
- gl_attr.set_depth_size(24);
- gl_attr.set_double_buffer(true);
- }
-
- let mut window_builder = video.window(
- "Parkan Render Demo (SDL2 + OpenGL ES 2.0)",
- args.width,
- args.height,
- );
- window_builder.opengl();
- if args.capture.is_some() {
- window_builder.hidden();
- } else {
- window_builder.resizable();
- }
- let window = window_builder
- .build()
- .map_err(|err| format!("failed to create window: {err}"))?;
-
- let gl_ctx = window
- .gl_create_context()
- .map_err(|err| format!("failed to create OpenGL context: {err}"))?;
- window
- .gl_make_current(&gl_ctx)
- .map_err(|err| format!("failed to make GL context current: {err}"))?;
+ let (window, _gl_ctx, gl_backend) = create_window_and_context(&video, &args)?;
let _ = if args.capture.is_some() {
video.gl_set_swap_interval(0)
} else {
@@ -315,7 +293,7 @@ fn run(args: Args) -> Result<(), String> {
glow::Context::from_loader_function(|name| video.gl_get_proc_address(name) as *const _)
};
- let program = unsafe { create_program(&gl)? };
+ let program = unsafe { create_program(&gl, gl_backend)? };
let u_mvp = unsafe { gl.get_uniform_location(program, "u_mvp") };
let u_use_tex = unsafe { gl.get_uniform_location(program, "u_use_tex") };
let u_tex = unsafe { gl.get_uniform_location(program, "u_tex") };
@@ -334,6 +312,7 @@ fn run(args: Args) -> Result<(), String> {
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
gl.bind_buffer(glow::ARRAY_BUFFER, None);
}
+ let vao = unsafe { create_vertex_layout_if_needed(&gl, gl_backend, vbo, ebo, a_pos, a_uv)? };
let gpu_texture = if let Some(texture) = resolved_texture.as_ref() {
Some(unsafe { create_texture(&gl, texture)? })
@@ -352,6 +331,7 @@ fn run(args: Args) -> Result<(), String> {
a_uv,
vbo,
ebo,
+ vao,
gpu_texture.as_ref(),
mesh.indices.len(),
&args,
@@ -372,6 +352,7 @@ fn run(args: Args) -> Result<(), String> {
a_uv,
vbo,
ebo,
+ vao,
gpu_texture.as_ref(),
mesh.indices.len(),
&args,
@@ -384,6 +365,9 @@ fn run(args: Args) -> Result<(), String> {
if let Some(texture) = gpu_texture {
gl.delete_texture(texture.handle);
}
+ if let Some(vao) = vao {
+ gl.delete_vertex_array(vao);
+ }
gl.delete_buffer(ebo);
gl.delete_buffer(vbo);
gl.delete_program(program);
@@ -392,6 +376,97 @@ fn run(args: Args) -> Result<(), String> {
result
}
+fn create_window_and_context(
+ video: &sdl2::VideoSubsystem,
+ args: &Args,
+) -> Result<(sdl2::video::Window, sdl2::video::GLContext, GlBackend), String> {
+ let candidates = [
+ (GlBackend::Gles2, sdl2::video::GLProfile::GLES, 2, 0),
+ (GlBackend::Core33, sdl2::video::GLProfile::Core, 3, 3),
+ ];
+ let mut errors = Vec::new();
+
+ for (backend, profile, major, minor) in candidates {
+ {
+ let gl_attr = video.gl_attr();
+ gl_attr.set_context_profile(profile);
+ gl_attr.set_context_version(major, minor);
+ gl_attr.set_depth_size(24);
+ gl_attr.set_double_buffer(true);
+ }
+
+ let mut window_builder = video.window(
+ "Parkan Render Demo (SDL2 + OpenGL)",
+ args.width,
+ args.height,
+ );
+ window_builder.opengl();
+ if args.capture.is_some() {
+ window_builder.hidden();
+ } else {
+ window_builder.resizable();
+ }
+
+ let window = match window_builder.build() {
+ Ok(window) => window,
+ Err(err) => {
+ errors.push(format!(
+ "{profile:?} {major}.{minor}: window build failed ({err})"
+ ));
+ continue;
+ }
+ };
+
+ let gl_ctx = match window.gl_create_context() {
+ Ok(ctx) => ctx,
+ Err(err) => {
+ errors.push(format!(
+ "{profile:?} {major}.{minor}: context create failed ({err})"
+ ));
+ continue;
+ }
+ };
+
+ if let Err(err) = window.gl_make_current(&gl_ctx) {
+ errors.push(format!(
+ "{profile:?} {major}.{minor}: make current failed ({err})"
+ ));
+ continue;
+ }
+
+ return Ok((window, gl_ctx, backend));
+ }
+
+ Err(format!(
+ "failed to create OpenGL context. Attempts: {}",
+ errors.join(" | ")
+ ))
+}
+
+unsafe fn create_vertex_layout_if_needed(
+ gl: &glow::Context,
+ backend: GlBackend,
+ vbo: glow::NativeBuffer,
+ ebo: glow::NativeBuffer,
+ a_pos: u32,
+ a_uv: u32,
+) -> Result<Option<glow::NativeVertexArray>, String> {
+ if backend != GlBackend::Core33 {
+ return Ok(None);
+ }
+
+ let vao = gl.create_vertex_array().map_err(|e| e.to_string())?;
+ gl.bind_vertex_array(Some(vao));
+ gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
+ gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo));
+ gl.enable_vertex_attrib_array(a_pos);
+ gl.vertex_attrib_pointer_f32(a_pos, 3, glow::FLOAT, false, 20, 0);
+ gl.enable_vertex_attrib_array(a_uv);
+ gl.vertex_attrib_pointer_f32(a_uv, 2, glow::FLOAT, false, 20, 12);
+ gl.bind_vertex_array(None);
+ Ok(Some(vao))
+}
+
fn resolve_texture(args: &Args, model_name: &str) -> Result<Option<LoadedTexture>, String> {
if args.no_texture {
return Ok(None);
@@ -466,6 +541,7 @@ fn run_capture(
a_uv: u32,
vbo: glow::NativeBuffer,
ebo: glow::NativeBuffer,
+ vao: Option<glow::NativeVertexArray>,
texture: Option<&GpuTexture>,
index_count: usize,
args: &Args,
@@ -493,6 +569,7 @@ fn run_capture(
a_uv,
vbo,
ebo,
+ vao,
texture,
index_count,
args.width,
@@ -520,6 +597,7 @@ fn run_interactive(
a_uv: u32,
vbo: glow::NativeBuffer,
ebo: glow::NativeBuffer,
+ vao: Option<glow::NativeVertexArray>,
texture: Option<&GpuTexture>,
index_count: usize,
args: &Args,
@@ -560,6 +638,7 @@ fn run_interactive(
a_uv,
vbo,
ebo,
+ vao,
texture,
index_count,
w,
@@ -602,6 +681,7 @@ unsafe fn draw_frame(
a_uv: u32,
vbo: glow::NativeBuffer,
ebo: glow::NativeBuffer,
+ vao: Option<glow::NativeVertexArray>,
texture: Option<&GpuTexture>,
index_count: usize,
width: u32,
@@ -631,22 +711,33 @@ unsafe fn draw_frame(
gl.bind_texture(glow::TEXTURE_2D, None);
}
- gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
- gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo));
- gl.enable_vertex_attrib_array(a_pos);
- gl.vertex_attrib_pointer_f32(a_pos, 3, glow::FLOAT, false, 20, 0);
- gl.enable_vertex_attrib_array(a_uv);
- gl.vertex_attrib_pointer_f32(a_uv, 2, glow::FLOAT, false, 20, 12);
- gl.draw_elements(
- glow::TRIANGLES,
- index_count.min(i32::MAX as usize) as i32,
- glow::UNSIGNED_SHORT,
- 0,
- );
- gl.disable_vertex_attrib_array(a_uv);
- gl.disable_vertex_attrib_array(a_pos);
- gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
- gl.bind_buffer(glow::ARRAY_BUFFER, None);
+ if let Some(vao) = vao {
+ gl.bind_vertex_array(Some(vao));
+ gl.draw_elements(
+ glow::TRIANGLES,
+ index_count.min(i32::MAX as usize) as i32,
+ glow::UNSIGNED_SHORT,
+ 0,
+ );
+ gl.bind_vertex_array(None);
+ } else {
+ gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo));
+ gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(ebo));
+ gl.enable_vertex_attrib_array(a_pos);
+ gl.vertex_attrib_pointer_f32(a_pos, 3, glow::FLOAT, false, 20, 0);
+ gl.enable_vertex_attrib_array(a_uv);
+ gl.vertex_attrib_pointer_f32(a_uv, 2, glow::FLOAT, false, 20, 12);
+ gl.draw_elements(
+ glow::TRIANGLES,
+ index_count.min(i32::MAX as usize) as i32,
+ glow::UNSIGNED_SHORT,
+ 0,
+ );
+ gl.disable_vertex_attrib_array(a_uv);
+ gl.disable_vertex_attrib_array(a_pos);
+ gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, None);
+ gl.bind_buffer(glow::ARRAY_BUFFER, None);
+ }
gl.bind_texture(glow::TEXTURE_2D, None);
gl.use_program(None);
}
@@ -701,8 +792,13 @@ fn save_png(path: &Path, width: u32, height: u32, rgba: Vec<u8>) -> Result<(), S
.map_err(|err| format!("failed to save PNG {}: {err}", path.display()))
}
-unsafe fn create_program(gl: &glow::Context) -> Result<glow::NativeProgram, String> {
- let vs_src = r#"
+unsafe fn create_program(
+ gl: &glow::Context,
+ backend: GlBackend,
+) -> Result<glow::NativeProgram, String> {
+ let (vs_src, fs_src) = match backend {
+ GlBackend::Gles2 => (
+ r#"
attribute vec3 a_pos;
attribute vec2 a_uv;
uniform mat4 u_mvp;
@@ -711,9 +807,8 @@ void main() {
v_uv = a_uv;
gl_Position = u_mvp * vec4(a_pos, 1.0);
}
-"#;
-
- let fs_src = r#"
+"#,
+ r#"
precision mediump float;
uniform sampler2D u_tex;
uniform float u_use_tex;
@@ -723,7 +818,32 @@ void main() {
vec4 texColor = texture2D(u_tex, v_uv);
gl_FragColor = mix(base, texColor, u_use_tex);
}
-"#;
+"#,
+ ),
+ GlBackend::Core33 => (
+ r#"#version 330 core
+in vec3 a_pos;
+in vec2 a_uv;
+uniform mat4 u_mvp;
+out vec2 v_uv;
+void main() {
+ v_uv = a_uv;
+ gl_Position = u_mvp * vec4(a_pos, 1.0);
+}
+"#,
+ r#"#version 330 core
+uniform sampler2D u_tex;
+uniform float u_use_tex;
+in vec2 v_uv;
+out vec4 fragColor;
+void main() {
+ vec4 base = vec4(0.85, 0.90, 1.00, 1.0);
+ vec4 texColor = texture(u_tex, v_uv);
+ fragColor = mix(base, texColor, u_use_tex);
+}
+"#,
+ ),
+ };
let program = gl.create_program().map_err(|e| e.to_string())?;
let vs = gl