Use hand texture as render background

This commit is contained in:
lenn
2026-06-26 16:59:58 +08:00
parent 0b658d11c5
commit b3fa3c8341
13 changed files with 1962 additions and 1375180 deletions

View File

@@ -10,8 +10,8 @@ use crate::{
},
ui::{
ConfigPanelState, ConnectPanelState, FloatingPanelState, MatrixConfigState,
draw_config_panel, draw_connect_panel, draw_export_panel, draw_matrix_config_panel,
draw_stats_panel, panel_restore_item,
draw_config_panel, draw_export_panel, draw_matrix_config_panel, draw_stats_panel,
panel_restore_item,
},
};
use eframe::{egui, egui_wgpu};
@@ -257,14 +257,6 @@ impl EskinDesktopApp {
}
fn draw_floating_panels(&mut self, ctx: &egui::Context) {
draw_connect_panel(
ctx,
&mut self.connect_panel,
&mut self.connect_state,
&self.connection,
&self.recorder,
&mut self.export_path,
);
// draw_scene_panel(ctx, &mut self.scene_panel);
if let Some(next_mode) =
draw_config_panel(ctx, &mut self.config_panel, &mut self.config_state)
@@ -292,7 +284,6 @@ impl EskinDesktopApp {
ui.label("显示面板");
ui.separator();
panel_restore_item(ui, "连接", &mut self.connect_panel);
panel_restore_item(ui, "配置", &mut self.config_panel);
panel_restore_item(ui, "录制", &mut self.export_panel);
panel_restore_item(ui, "矩阵", &mut self.matrix_config_panel);
@@ -301,7 +292,6 @@ impl EskinDesktopApp {
ui.separator();
if ui.button("全部显示").clicked() {
self.connect_panel.visible = true;
self.config_panel.visible = true;
self.export_panel.visible = true;
self.matrix_config_panel.visible = true;

View File

@@ -2,6 +2,7 @@ use eframe::egui_wgpu::wgpu;
use std::ops::Range;
use crate::texture;
pub trait Vertex {
fn desc<'a>() -> wgpu::VertexBufferLayout<'a>;
}
@@ -12,6 +13,8 @@ pub struct ModelVertex {
pub position: [f32; 3],
pub tex_coords: [f32; 2],
pub normal: [f32; 3],
pub color: [f32; 4],
pub tangent: [f32; 4],
}
impl Vertex for ModelVertex {
@@ -36,6 +39,16 @@ impl Vertex for ModelVertex {
shader_location: 2,
format: wgpu::VertexFormat::Float32x3,
},
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress,
shader_location: 3,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress,
shader_location: 4,
format: wgpu::VertexFormat::Float32x4,
},
],
}
}
@@ -54,7 +67,8 @@ impl Instance {
pub fn to_raw(&self) -> InstanceRaw {
InstanceRaw {
model: (glam::Mat4::from_translation(self.position)
* glam::Mat4::from_quat(self.rotation))
* glam::Mat4::from_quat(self.rotation)
* glam::Mat4::from_scale(glam::Vec3::splat(128.0)))
.to_cols_array_2d(),
}
}
@@ -111,9 +125,43 @@ pub struct Model {
pub materials: Vec<Material>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum AlphaMode {
Opaque,
Mask,
Blend,
}
impl AlphaMode {
pub fn as_str(self) -> &'static str {
match self {
Self::Opaque => "OPAQUE",
Self::Mask => "MASK",
Self::Blend => "BLEND",
}
}
pub fn shader_value(self) -> f32 {
match self {
Self::Opaque => 0.0,
Self::Mask => 1.0,
Self::Blend => 2.0,
}
}
}
pub struct Material {
pub name: String,
pub diffuse_texture: texture::Texture,
pub alpha_mode: AlphaMode,
pub alpha_cutoff: f32,
pub base_color_factor: [f32; 4],
pub double_sided: bool,
pub base_color_texture: texture::Texture,
pub metallic_roughness_texture: texture::Texture,
pub normal_texture: texture::Texture,
pub occlusion_texture: texture::Texture,
pub emissive_texture: texture::Texture,
pub params_buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
}
@@ -123,6 +171,7 @@ pub struct Mesh {
pub index_buffer: wgpu::Buffer,
pub num_elements: u32,
pub material: usize,
pub center: [f32; 3],
}
pub trait DrawModel<'a> {

View File

@@ -1,7 +1,7 @@
use crate::{
matrix::{MatrixLayout, build_view_projection, glyph_world_position},
model::{Instance, InstanceRaw, Model, ModelVertex, Vertex},
resources,
model::{AlphaMode, InstanceRaw, ModelVertex, Vertex},
resources, texture,
};
use eframe::{
egui,
@@ -69,20 +69,86 @@ pub struct BackgroundRenderResources {
layout: MatrixLayout,
rows: u32,
cols: u32,
surface_is_srgb: bool,
uniform: MatrixUniform,
uniform_buffer: wgpu::Buffer,
uniform_bind_group: wgpu::BindGroup,
background_pipeline: wgpu::RenderPipeline,
hand_image_pipeline: wgpu::RenderPipeline,
glyph_pipeline: wgpu::RenderPipeline,
dot_pipeline: wgpu::RenderPipeline,
model_pipeline: wgpu::RenderPipeline,
hand_image_bind_group: wgpu::BindGroup,
hand_image_texture: texture::Texture,
model: Option<Model>,
model_instance_buffer: wgpu::Buffer,
glyph_vertex_buffer: wgpu::Buffer,
glyph_instance_buffer: wgpu::Buffer,
glyph_instances: Vec<GlyphInstance>,
render_options: RenderOptions,
}
struct ModelPipelines {
opaque: ModelCullPipelines,
mask: ModelCullPipelines,
blend: ModelCullPipelines,
}
struct ModelCullPipelines {
single_sided: wgpu::RenderPipeline,
double_sided: wgpu::RenderPipeline,
}
#[derive(Copy, Clone, Debug)]
struct RenderOptions {
debug_mode: u32,
ambient_enabled: bool,
tone_mapping_enabled: bool,
exposure: f32,
}
impl RenderOptions {
fn from_env() -> Self {
let debug_mode = std::env::var("ESKIN_MATERIAL_DEBUG")
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(0);
let ambient_enabled = env_bool("ESKIN_AMBIENT", true);
let tone_mapping_enabled = env_bool("ESKIN_TONEMAP", true);
let exposure = std::env::var("ESKIN_EXPOSURE")
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(1.0);
log::warn!(
"render options material_debug={debug_mode} ambient_enabled={ambient_enabled} tone_mapping_enabled={tone_mapping_enabled} exposure={exposure:.3}"
);
Self {
debug_mode,
ambient_enabled,
tone_mapping_enabled,
exposure,
}
}
fn as_uniform(self) -> [f32; 4] {
[
self.debug_mode as f32,
if self.ambient_enabled { 1.0 } else { 0.0 },
if self.tone_mapping_enabled { 1.0 } else { 0.0 },
self.exposure,
]
}
}
fn env_bool(name: &str, default: bool) -> bool {
std::env::var(name)
.ok()
.map(|value| {
let value = value.to_ascii_lowercase();
matches!(value.as_str(), "1" | "true" | "yes" | "on")
})
.unwrap_or(default)
}
impl BackgroundRenderResources {
@@ -94,7 +160,40 @@ impl BackgroundRenderResources {
cols: u32,
) -> Self {
let layout = MatrixLayout::new(rows, cols);
let uniform = MatrixUniform::new(1.0, 1.0, build_view_projection(1.0, &layout));
let surface_is_srgb = target_format.is_srgb();
let render_options = RenderOptions::from_env();
log::warn!("wgpu target surface format: {target_format:?}, srgb={surface_is_srgb}");
let hand_image_texture = match resources::load_texture("hand.png", device, _queue) {
Ok(texture) => texture,
Err(err) => {
log::warn!("failed to load hand.png background: {err:#}");
texture::Texture::from_rgba8(
device,
_queue,
&[0, 0, 0, 255],
1,
1,
"Fallback Hand Background Texture",
)
.expect("fallback hand texture should be valid")
}
};
log::warn!(
"using hand.png as render background: {}x{}",
hand_image_texture.width,
hand_image_texture.height
);
let uniform = MatrixUniform::new(
1.0,
1.0,
build_view_projection(1.0, &layout),
surface_is_srgb,
render_options,
[
hand_image_texture.width as f32,
hand_image_texture.height as f32,
],
);
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Pressure Matrix Uniform Buffer"),
contents: bytemuck::cast_slice(&[uniform]),
@@ -140,9 +239,9 @@ impl BackgroundRenderResources {
// source: wgpu::ShaderSource::Wgsl(include_str!("../static/wgsl/shader.wgsl").into()),
// });
let texture_bind_group_layout =
let hand_image_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("model_texture_bind_group_layout"),
label: Some("hand_image_bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
@@ -163,11 +262,132 @@ impl BackgroundRenderResources {
],
});
let hand_image_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("hand_image_bind_group"),
layout: &hand_image_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&hand_image_texture.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&hand_image_texture.sampler),
},
],
});
let texture_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("model_texture_bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 5,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 6,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 7,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 8,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 9,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 10,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Pressure Matrix Pipeline Layout"),
bind_group_layouts: &[Some(&uniform_bind_group_layout)],
immediate_size: 0,
});
let hand_image_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Hand Image Pipeline Layout"),
bind_group_layouts: &[
Some(&uniform_bind_group_layout),
Some(&hand_image_bind_group_layout),
],
immediate_size: 0,
});
let model_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Hand Model Pipeline Layout"),
@@ -180,32 +400,14 @@ impl BackgroundRenderResources {
let background_pipeline =
create_background_pipeline(device, target_format, &shader, &pipeline_layout);
let hand_image_pipeline =
create_hand_image_pipeline(device, target_format, &shader, &hand_image_pipeline_layout);
let glyph_pipeline =
create_glyph_pipeline(device, target_format, &shader, &pipeline_layout);
let dot_pipeline = create_dot_pipeline(device, target_format, &shader, &pipeline_layout);
let model_pipeline =
create_model_pipeline(device, target_format, &shader, &model_pipeline_layout);
let model =
match resources::load_model("hand.obj", device, _queue, &texture_bind_group_layout) {
Ok(model) => Some(model),
Err(err) => {
log::warn!("failed to load hand.obj: {err:#}");
None
}
};
let model_instance = Instance::new(
glam::Vec3::new(0.0, -2.0, 12.0),
glam::Quat::from_rotation_y(0.65) * glam::Quat::from_rotation_x(-0.35),
)
.to_raw();
let model_instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Hand Model Instance Buffer"),
contents: bytemuck::cast_slice(&[model_instance]),
usage: wgpu::BufferUsages::VERTEX,
});
let _model_pipelines =
create_model_pipelines(device, target_format, &shader, &model_pipeline_layout);
let glyph_vertices = [
GlyphVertex {
@@ -235,25 +437,36 @@ impl BackgroundRenderResources {
layout,
rows,
cols,
surface_is_srgb,
uniform,
uniform_buffer,
uniform_bind_group,
background_pipeline,
hand_image_pipeline,
glyph_pipeline,
dot_pipeline,
model_pipeline,
model,
model_instance_buffer,
hand_image_bind_group,
hand_image_texture,
glyph_vertex_buffer,
glyph_instance_buffer,
glyph_instances,
render_options,
}
}
fn prepare(&mut self, queue: &wgpu::Queue, width: f32, height: f32, pressure: &PressureFrame) {
let aspect = width / height.max(1.0);
self.uniform =
MatrixUniform::new(width, height, build_view_projection(aspect, &self.layout));
self.uniform = MatrixUniform::new(
width,
height,
build_view_projection(aspect, &self.layout),
self.surface_is_srgb,
self.render_options,
[
self.hand_image_texture.width as f32,
self.hand_image_texture.height as f32,
],
);
queue.write_buffer(
&self.uniform_buffer,
0,
@@ -280,6 +493,10 @@ impl BackgroundRenderResources {
render_pass.set_pipeline(&self.background_pipeline);
render_pass.draw(0..3, 0..1);
render_pass.set_pipeline(&self.hand_image_pipeline);
render_pass.set_bind_group(1, &self.hand_image_bind_group, &[]);
render_pass.draw(0..6, 0..1);
match active_mode {
ActiveMode::Finger(mode) => self.paint_finger(render_pass, mode),
ActiveMode::Hand(mode) => self.paint_hand(render_pass, mode),
@@ -303,20 +520,7 @@ impl BackgroundRenderResources {
fn paint_hand(&self, render_pass: &mut wgpu::RenderPass<'_>, mode: &HandGatewayMode) {
let _range = mode.range.clone();
if let Some(model) = &self.model {
render_pass.set_pipeline(&self.model_pipeline);
render_pass.set_vertex_buffer(1, self.model_instance_buffer.slice(..));
for mesh in &model.meshes {
if let Some(material) = model.materials.get(mesh.material) {
render_pass.set_bind_group(1, &material.bind_group, &[]);
}
render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
render_pass
.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_pass.draw_indexed(0..mesh.num_elements, 0, 0..1);
}
}
let _ = render_pass;
}
fn visible_instance_count(&self, rows: u32, cols: u32) -> u32 {
@@ -358,6 +562,39 @@ fn create_background_pipeline(
})
}
fn create_hand_image_pipeline(
device: &wgpu::Device,
target_format: &wgpu::TextureFormat,
shader: &wgpu::ShaderModule,
layout: &wgpu::PipelineLayout,
) -> wgpu::RenderPipeline {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Hand Image Background Pipeline"),
layout: Some(layout),
vertex: wgpu::VertexState {
module: shader,
entry_point: Some("vs_hand_image"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: shader,
entry_point: Some("fs_hand_image"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: *target_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
})
}
fn create_glyph_pipeline(
device: &wgpu::Device,
target_format: &wgpu::TextureFormat,
@@ -391,14 +628,88 @@ fn create_glyph_pipeline(
})
}
fn create_model_pipelines(
device: &wgpu::Device,
target_format: &wgpu::TextureFormat,
shader: &wgpu::ShaderModule,
layout: &wgpu::PipelineLayout,
) -> ModelPipelines {
ModelPipelines {
opaque: ModelCullPipelines {
single_sided: create_model_pipeline(
device,
target_format,
shader,
layout,
AlphaMode::Opaque,
false,
),
double_sided: create_model_pipeline(
device,
target_format,
shader,
layout,
AlphaMode::Opaque,
true,
),
},
mask: ModelCullPipelines {
single_sided: create_model_pipeline(
device,
target_format,
shader,
layout,
AlphaMode::Mask,
false,
),
double_sided: create_model_pipeline(
device,
target_format,
shader,
layout,
AlphaMode::Mask,
true,
),
},
blend: ModelCullPipelines {
single_sided: create_model_pipeline(
device,
target_format,
shader,
layout,
AlphaMode::Blend,
false,
),
double_sided: create_model_pipeline(
device,
target_format,
shader,
layout,
AlphaMode::Blend,
true,
),
},
}
}
fn create_model_pipeline(
device: &wgpu::Device,
target_format: &wgpu::TextureFormat,
shader: &wgpu::ShaderModule,
layout: &wgpu::PipelineLayout,
alpha_mode: AlphaMode,
double_sided: bool,
) -> wgpu::RenderPipeline {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Hand Model Pipeline"),
label: Some(&format!(
"Hand Model {} {} Pipeline",
alpha_mode.as_str(),
if double_sided {
"Double Sided"
} else {
"Single Sided"
}
)),
layout: Some(layout),
vertex: wgpu::VertexState {
module: shader,
@@ -412,10 +723,11 @@ fn create_model_pipeline(
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: *target_format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent::REPLACE,
alpha: wgpu::BlendComponent::REPLACE,
}),
blend: if alpha_mode == AlphaMode::Blend {
Some(wgpu::BlendState::ALPHA_BLENDING)
} else {
None
},
write_mask: wgpu::ColorWrites::ALL,
})],
}),
@@ -423,7 +735,11 @@ fn create_model_pipeline(
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
cull_mode: if double_sided {
None
} else {
Some(wgpu::Face::Back)
},
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
@@ -524,15 +840,26 @@ struct MatrixUniform {
viewport: [f32; 4],
glyph: [f32; 4],
color: [f32; 4],
render_options: [f32; 4],
image: [f32; 4],
}
impl MatrixUniform {
fn new(width: f32, height: f32, view_proj: [[f32; 4]; 4]) -> Self {
fn new(
width: f32,
height: f32,
view_proj: [[f32; 4]; 4],
surface_is_srgb: bool,
render_options: RenderOptions,
image_size: [f32; 2],
) -> Self {
Self {
view_proj,
viewport: [width.max(1.0), height.max(1.0), 0.0, 0.0],
glyph: [16.0, 0.0, 0.0, 0.0],
color: [0.05, 0.92, 0.32, 1.0],
color: [0.05, 0.92, 0.32, if surface_is_srgb { 1.0 } else { 0.0 }],
render_options: render_options.as_uniform(),
image: [image_size[0].max(1.0), image_size[1].max(1.0), 0.0, 0.0],
}
}
}

View File

@@ -1,8 +1,26 @@
use crate::{model, texture};
use crate::{
model::{self, AlphaMode},
texture::{self, TextureColorSpace, TextureSamplerDescriptor},
};
use anyhow::{Context, bail};
use eframe::wgpu;
use eframe::wgpu::util::DeviceExt;
use glam::{Mat3, Mat4, Vec2, Vec3};
use std::path::{Path, PathBuf};
const WHITE_TEXTURE: &[u8; 4] = &[255, 255, 255, 255];
const BLACK_TEXTURE: &[u8; 4] = &[0, 0, 0, 255];
const FLAT_NORMAL_TEXTURE: &[u8; 4] = &[128, 128, 255, 255];
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct MaterialParams {
base_color: [f32; 4],
metallic_roughness: [f32; 4],
emissive_alpha: [f32; 4],
flags: [f32; 4],
}
fn resource_path(file_name: &str) -> PathBuf {
Path::new(env!("RESOURCE_DIR")).join(file_name)
}
@@ -29,6 +47,26 @@ pub fn load_model(
device: &wgpu::Device,
queue: &wgpu::Queue,
texture_bind_group_layout: &wgpu::BindGroupLayout,
) -> anyhow::Result<model::Model> {
match Path::new(file_name)
.extension()
.and_then(|extension| extension.to_str())
.map(str::to_ascii_lowercase)
.as_deref()
{
Some("obj") => load_obj_model(file_name, device, queue, texture_bind_group_layout),
Some("gltf") | Some("glb") => {
load_gltf_model(file_name, device, queue, texture_bind_group_layout)
}
_ => bail!("unsupported model format: {file_name}"),
}
}
fn load_obj_model(
file_name: &str,
device: &wgpu::Device,
queue: &wgpu::Queue,
texture_bind_group_layout: &wgpu::BindGroupLayout,
) -> anyhow::Result<model::Model> {
let path = resource_path(file_name);
let (models, obj_materials) = tobj::load_obj(
@@ -42,44 +80,93 @@ pub fn load_model(
let mut materials = Vec::new();
for material in obj_materials? {
let diffuse_texture = match material.diffuse_texture.as_deref() {
let base_color = material
.diffuse
.map(|diffuse| [diffuse[0], diffuse[1], diffuse[2], 1.0])
.unwrap_or([1.0, 1.0, 1.0, 1.0]);
let base_color_texture = match material.diffuse_texture.as_deref() {
Some(texture_path) => load_texture(texture_path, device, queue)?,
None => texture::Texture::from_rgba8(
None => default_texture(
device,
queue,
&[255, 255, 255, 255],
1,
1,
&material.name,
TextureColorSpace::Srgb,
WHITE_TEXTURE,
)?,
};
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("{} Texture Bind Group", material.name)),
layout: texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&diffuse_texture.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&diffuse_texture.sampler),
},
],
});
let material_textures = MaterialTextures {
base_color_texture,
metallic_roughness_texture: default_texture(
device,
queue,
&format!("{} metallic-roughness", material.name),
TextureColorSpace::Linear,
WHITE_TEXTURE,
)?,
normal_texture: default_texture(
device,
queue,
&format!("{} normal", material.name),
TextureColorSpace::Linear,
FLAT_NORMAL_TEXTURE,
)?,
occlusion_texture: default_texture(
device,
queue,
&format!("{} occlusion", material.name),
TextureColorSpace::Linear,
WHITE_TEXTURE,
)?,
emissive_texture: default_texture(
device,
queue,
&format!("{} emissive", material.name),
TextureColorSpace::Srgb,
BLACK_TEXTURE,
)?,
};
let params = MaterialParams {
base_color,
metallic_roughness: [0.0, 0.5, 1.0, 1.0],
emissive_alpha: [0.0, 0.0, 0.0, 0.0],
flags: [0.5, 0.0, 0.0, 0.0],
};
log_material_debug(
materials.len(),
&material.name,
AlphaMode::Opaque,
0.5,
base_color[3],
false,
);
materials.push(create_material(
device,
texture_bind_group_layout,
material.name,
material_textures,
params,
AlphaMode::Opaque,
0.5,
base_color,
false,
));
}
materials.push(model::Material {
name: material.name,
diffuse_texture,
bind_group,
});
if materials.is_empty() {
materials.push(create_color_material(
"default",
[1.0, 1.0, 1.0, 1.0],
device,
queue,
texture_bind_group_layout,
)?);
}
let meshes = models
.into_iter()
.map(|m| {
let vertices = (0..m.mesh.positions.len() / 3)
let mut vertices = (0..m.mesh.positions.len() / 3)
.map(|i| model::ModelVertex {
position: [
m.mesh.positions[i * 3],
@@ -87,7 +174,7 @@ pub fn load_model(
m.mesh.positions[i * 3 + 2],
],
tex_coords: if m.mesh.texcoords.len() >= i * 2 + 2 {
[m.mesh.texcoords[i * 2], m.mesh.texcoords[i * 2 + 1]]
[m.mesh.texcoords[i * 2], 1.0 - m.mesh.texcoords[i * 2 + 1]]
} else {
[0.0, 0.0]
},
@@ -100,8 +187,11 @@ pub fn load_model(
} else {
[0.0, 1.0, 0.0]
},
color: [1.0, 1.0, 1.0, 1.0],
tangent: [1.0, 0.0, 0.0, 1.0],
})
.collect::<Vec<_>>();
generate_tangents(&mut vertices, &m.mesh.indices);
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("{} Vertex Buffer", m.name)),
@@ -120,9 +210,798 @@ pub fn load_model(
index_buffer,
num_elements: m.mesh.indices.len() as u32,
material: m.mesh.material_id.unwrap_or(0),
center: mesh_center(&vertices),
}
})
.collect::<Vec<_>>();
Ok(model::Model { meshes, materials })
}
fn load_gltf_model(
file_name: &str,
device: &wgpu::Device,
queue: &wgpu::Queue,
texture_bind_group_layout: &wgpu::BindGroupLayout,
) -> anyhow::Result<model::Model> {
let path = resource_path(file_name);
let (document, buffers, images) =
gltf::import(&path).with_context(|| format!("failed to import {file_name}"))?;
let mut materials = Vec::new();
for material in document.materials() {
materials.push(create_gltf_material(
&material,
&images,
device,
queue,
texture_bind_group_layout,
)?);
}
let default_material = materials.len();
materials.push(create_color_material(
"default",
[1.0, 1.0, 1.0, 1.0],
device,
queue,
texture_bind_group_layout,
)?);
let mut meshes = Vec::new();
let scene = document
.default_scene()
.or_else(|| document.scenes().next())
.context("glTF has no scene")?;
for node in scene.nodes() {
append_gltf_node_meshes(
&node,
Mat4::IDENTITY,
&buffers,
device,
&mut meshes,
default_material,
)?;
}
Ok(model::Model { meshes, materials })
}
struct MaterialTextures {
base_color_texture: texture::Texture,
metallic_roughness_texture: texture::Texture,
normal_texture: texture::Texture,
occlusion_texture: texture::Texture,
emissive_texture: texture::Texture,
}
fn create_gltf_material(
material: &gltf::Material<'_>,
images: &[gltf::image::Data],
device: &wgpu::Device,
queue: &wgpu::Queue,
texture_bind_group_layout: &wgpu::BindGroupLayout,
) -> anyhow::Result<model::Material> {
let pbr = material.pbr_metallic_roughness();
let name = material.name().unwrap_or("gltf_material");
let normal_info = material.normal_texture();
let occlusion_info = material.occlusion_texture();
let material_textures = MaterialTextures {
base_color_texture: texture_from_gltf_texture(
pbr.base_color_texture().map(|info| info.texture()),
images,
device,
queue,
&format!("{name} baseColor"),
TextureColorSpace::Srgb,
WHITE_TEXTURE,
)?,
metallic_roughness_texture: texture_from_gltf_texture(
pbr.metallic_roughness_texture().map(|info| info.texture()),
images,
device,
queue,
&format!("{name} metallicRoughness"),
TextureColorSpace::Linear,
WHITE_TEXTURE,
)?,
normal_texture: texture_from_gltf_texture(
normal_info.as_ref().map(|info| info.texture()),
images,
device,
queue,
&format!("{name} normal"),
TextureColorSpace::Linear,
FLAT_NORMAL_TEXTURE,
)?,
occlusion_texture: texture_from_gltf_texture(
occlusion_info.as_ref().map(|info| info.texture()),
images,
device,
queue,
&format!("{name} occlusion"),
TextureColorSpace::Linear,
WHITE_TEXTURE,
)?,
emissive_texture: texture_from_gltf_texture(
material.emissive_texture().map(|info| info.texture()),
images,
device,
queue,
&format!("{name} emissive"),
TextureColorSpace::Srgb,
BLACK_TEXTURE,
)?,
};
let alpha_mode = match material.alpha_mode() {
gltf::material::AlphaMode::Opaque => AlphaMode::Opaque,
gltf::material::AlphaMode::Mask => AlphaMode::Mask,
gltf::material::AlphaMode::Blend => AlphaMode::Blend,
};
let normal_scale = normal_info.as_ref().map(|info| info.scale()).unwrap_or(1.0);
let occlusion_strength = occlusion_info
.as_ref()
.map(|info| info.strength())
.unwrap_or(1.0);
let emissive_factor = material.emissive_factor();
let base_color_factor = pbr.base_color_factor();
let alpha_cutoff = material.alpha_cutoff().unwrap_or(0.5);
let double_sided = material.double_sided();
let params = MaterialParams {
base_color: base_color_factor,
metallic_roughness: [
pbr.metallic_factor(),
pbr.roughness_factor(),
normal_scale,
occlusion_strength,
],
emissive_alpha: [
emissive_factor[0],
emissive_factor[1],
emissive_factor[2],
alpha_mode.shader_value(),
],
flags: [
alpha_cutoff,
0.0,
if normal_info.is_some() { 1.0 } else { 0.0 },
if double_sided { 1.0 } else { 0.0 },
],
};
log_material_debug(
material.index().unwrap_or(usize::MAX),
name,
alpha_mode,
alpha_cutoff,
base_color_factor[3],
double_sided,
);
Ok(create_material(
device,
texture_bind_group_layout,
name.to_owned(),
material_textures,
params,
alpha_mode,
alpha_cutoff,
base_color_factor,
double_sided,
))
}
fn create_color_material(
name: &str,
color: [f32; 4],
device: &wgpu::Device,
queue: &wgpu::Queue,
texture_bind_group_layout: &wgpu::BindGroupLayout,
) -> anyhow::Result<model::Material> {
let material_textures = MaterialTextures {
base_color_texture: default_texture(
device,
queue,
name,
TextureColorSpace::Srgb,
WHITE_TEXTURE,
)?,
metallic_roughness_texture: default_texture(
device,
queue,
&format!("{name} metallic-roughness"),
TextureColorSpace::Linear,
WHITE_TEXTURE,
)?,
normal_texture: default_texture(
device,
queue,
&format!("{name} normal"),
TextureColorSpace::Linear,
FLAT_NORMAL_TEXTURE,
)?,
occlusion_texture: default_texture(
device,
queue,
&format!("{name} occlusion"),
TextureColorSpace::Linear,
WHITE_TEXTURE,
)?,
emissive_texture: default_texture(
device,
queue,
&format!("{name} emissive"),
TextureColorSpace::Srgb,
BLACK_TEXTURE,
)?,
};
let params = MaterialParams {
base_color: color,
metallic_roughness: [1.0, 1.0, 1.0, 1.0],
emissive_alpha: [0.0, 0.0, 0.0, 0.0],
flags: [0.5, 0.0, 0.0, 0.0],
};
log_material_debug(usize::MAX, name, AlphaMode::Opaque, 0.5, color[3], false);
Ok(create_material(
device,
texture_bind_group_layout,
name.to_owned(),
material_textures,
params,
AlphaMode::Opaque,
0.5,
color,
false,
))
}
fn create_material(
device: &wgpu::Device,
texture_bind_group_layout: &wgpu::BindGroupLayout,
name: String,
textures: MaterialTextures,
params: MaterialParams,
alpha_mode: AlphaMode,
alpha_cutoff: f32,
base_color_factor: [f32; 4],
double_sided: bool,
) -> model::Material {
let params_buffer = create_material_params_buffer(device, &name, params);
let bind_group = create_texture_bind_group(
device,
texture_bind_group_layout,
&name,
&textures,
&params_buffer,
);
model::Material {
name,
alpha_mode,
alpha_cutoff,
base_color_factor,
double_sided,
base_color_texture: textures.base_color_texture,
metallic_roughness_texture: textures.metallic_roughness_texture,
normal_texture: textures.normal_texture,
occlusion_texture: textures.occlusion_texture,
emissive_texture: textures.emissive_texture,
params_buffer,
bind_group,
}
}
fn default_texture(
device: &wgpu::Device,
queue: &wgpu::Queue,
label: &str,
color_space: TextureColorSpace,
rgba: &[u8; 4],
) -> anyhow::Result<texture::Texture> {
texture::Texture::from_rgba8_with_sampler(
device,
queue,
rgba,
1,
1,
label,
color_space,
TextureSamplerDescriptor::default(),
)
}
fn create_material_params_buffer(
device: &wgpu::Device,
name: &str,
params: MaterialParams,
) -> wgpu::Buffer {
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("{name} Material Params Buffer")),
contents: bytemuck::cast_slice(&[params]),
usage: wgpu::BufferUsages::UNIFORM,
})
}
fn create_texture_bind_group(
device: &wgpu::Device,
texture_bind_group_layout: &wgpu::BindGroupLayout,
name: &str,
textures: &MaterialTextures,
params_buffer: &wgpu::Buffer,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("{name} Texture Bind Group")),
layout: texture_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&textures.base_color_texture.view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&textures.base_color_texture.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: params_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(
&textures.metallic_roughness_texture.view,
),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::Sampler(
&textures.metallic_roughness_texture.sampler,
),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::TextureView(&textures.normal_texture.view),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::Sampler(&textures.normal_texture.sampler),
},
wgpu::BindGroupEntry {
binding: 7,
resource: wgpu::BindingResource::TextureView(&textures.occlusion_texture.view),
},
wgpu::BindGroupEntry {
binding: 8,
resource: wgpu::BindingResource::Sampler(&textures.occlusion_texture.sampler),
},
wgpu::BindGroupEntry {
binding: 9,
resource: wgpu::BindingResource::TextureView(&textures.emissive_texture.view),
},
wgpu::BindGroupEntry {
binding: 10,
resource: wgpu::BindingResource::Sampler(&textures.emissive_texture.sampler),
},
],
})
}
fn texture_from_gltf_texture(
gltf_texture: Option<gltf::Texture<'_>>,
images: &[gltf::image::Data],
device: &wgpu::Device,
queue: &wgpu::Queue,
label: &str,
color_space: TextureColorSpace,
fallback_rgba: &[u8; 4],
) -> anyhow::Result<texture::Texture> {
let Some(gltf_texture) = gltf_texture else {
return default_texture(device, queue, label, color_space, fallback_rgba);
};
let image_index = gltf_texture.source().index();
let image = images
.get(image_index)
.with_context(|| format!("glTF texture references missing image {image_index}"))?;
let rgba = gltf_image_to_rgba8(image)?;
let sampler = gltf_sampler_descriptor(&gltf_texture.sampler());
texture::Texture::from_rgba8_with_sampler(
device,
queue,
&rgba,
image.width,
image.height,
&format!("{label} image {image_index} {color_space:?}"),
color_space,
sampler,
)
}
fn gltf_sampler_descriptor(sampler: &gltf::texture::Sampler<'_>) -> TextureSamplerDescriptor {
use gltf::texture::{MagFilter, MinFilter};
let mag_filter = match sampler.mag_filter().unwrap_or(MagFilter::Linear) {
MagFilter::Nearest => wgpu::FilterMode::Nearest,
MagFilter::Linear => wgpu::FilterMode::Linear,
};
let (min_filter, mipmap_filter) = match sampler.min_filter().unwrap_or(MinFilter::Linear) {
MinFilter::Nearest | MinFilter::NearestMipmapNearest | MinFilter::NearestMipmapLinear => {
(wgpu::FilterMode::Nearest, wgpu::MipmapFilterMode::Nearest)
}
MinFilter::Linear | MinFilter::LinearMipmapNearest => {
(wgpu::FilterMode::Linear, wgpu::MipmapFilterMode::Nearest)
}
MinFilter::LinearMipmapLinear => (wgpu::FilterMode::Linear, wgpu::MipmapFilterMode::Linear),
};
TextureSamplerDescriptor {
address_mode_u: gltf_wrap_mode(sampler.wrap_s()),
address_mode_v: gltf_wrap_mode(sampler.wrap_t()),
mag_filter,
min_filter,
mipmap_filter,
}
}
fn gltf_wrap_mode(mode: gltf::texture::WrappingMode) -> wgpu::AddressMode {
use gltf::texture::WrappingMode;
match mode {
WrappingMode::ClampToEdge => wgpu::AddressMode::ClampToEdge,
WrappingMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat,
WrappingMode::Repeat => wgpu::AddressMode::Repeat,
}
}
fn append_gltf_node_meshes(
node: &gltf::Node<'_>,
parent_transform: Mat4,
buffers: &[gltf::buffer::Data],
device: &wgpu::Device,
meshes: &mut Vec<model::Mesh>,
default_material: usize,
) -> anyhow::Result<()> {
let local_transform = Mat4::from_cols_array_2d(&node.transform().matrix());
let world_transform = parent_transform * local_transform;
if let Some(gltf_mesh) = node.mesh() {
let node_name = node.name().unwrap_or("gltf_node");
let mesh_index = gltf_mesh.index();
let mesh_name = node
.name()
.or_else(|| gltf_mesh.name())
.unwrap_or("gltf_mesh");
for (primitive_index, primitive) in gltf_mesh.primitives().enumerate() {
append_gltf_primitive_mesh(
mesh_name,
primitive_index,
&primitive,
world_transform,
buffers,
device,
meshes,
default_material,
node_name,
mesh_index,
)?;
}
}
for child in node.children() {
append_gltf_node_meshes(
&child,
world_transform,
buffers,
device,
meshes,
default_material,
)?;
}
Ok(())
}
fn append_gltf_primitive_mesh(
mesh_name: &str,
primitive_index: usize,
primitive: &gltf::Primitive<'_>,
world_transform: Mat4,
buffers: &[gltf::buffer::Data],
device: &wgpu::Device,
meshes: &mut Vec<model::Mesh>,
default_material: usize,
node_name: &str,
mesh_index: usize,
) -> anyhow::Result<()> {
let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()]));
let positions = reader
.read_positions()
.with_context(|| format!("{mesh_name} primitive {primitive_index} has no positions"))?
.collect::<Vec<_>>();
let normals = reader
.read_normals()
.map(|iter| iter.collect::<Vec<_>>())
.unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; positions.len()]);
let tex_coords = reader
.read_tex_coords(0)
.map(|coords| coords.into_f32().collect::<Vec<_>>())
.unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
let colors = reader
.read_colors(0)
.map(|colors| colors.into_rgba_f32().collect::<Vec<_>>())
.unwrap_or_else(|| vec![[1.0, 1.0, 1.0, 1.0]; positions.len()]);
let tangents = reader.read_tangents().map(|iter| iter.collect::<Vec<_>>());
let has_tangents = tangents.is_some();
let tangents = tangents.unwrap_or_else(|| vec![[1.0, 0.0, 0.0, 1.0]; positions.len()]);
let normal_transform = Mat3::from_mat4(world_transform.inverse().transpose());
let mut vertices = positions
.iter()
.enumerate()
.map(|(index, position)| {
let world_position = world_transform
.transform_point3(Vec3::from_array(*position))
.to_array();
let world_normal = normal_transform
.mul_vec3(Vec3::from_array(
normals.get(index).copied().unwrap_or([0.0, 1.0, 0.0]),
))
.normalize_or_zero()
.to_array();
let tangent = tangents.get(index).copied().unwrap_or([1.0, 0.0, 0.0, 1.0]);
let world_tangent = normal_transform
.mul_vec3(Vec3::new(tangent[0], tangent[1], tangent[2]))
.normalize_or_zero();
model::ModelVertex {
position: world_position,
tex_coords: tex_coords.get(index).copied().unwrap_or([0.0, 0.0]),
normal: world_normal,
color: colors.get(index).copied().unwrap_or([1.0, 1.0, 1.0, 1.0]),
tangent: [
world_tangent.x,
world_tangent.y,
world_tangent.z,
tangent[3],
],
}
})
.collect::<Vec<_>>();
let indices = reader
.read_indices()
.map(|indices| indices.into_u32().collect::<Vec<_>>())
.unwrap_or_else(|| (0..vertices.len() as u32).collect());
let center = mesh_center(&vertices);
if !has_tangents {
generate_tangents(&mut vertices, &indices);
}
log_primitive_material_debug(
node_name,
mesh_index,
primitive_index,
&primitive.material(),
default_material,
);
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!(
"{mesh_name} Primitive {primitive_index} Vertex Buffer"
)),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!(
"{mesh_name} Primitive {primitive_index} Index Buffer"
)),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
meshes.push(model::Mesh {
name: format!("{mesh_name}_{primitive_index}"),
vertex_buffer,
index_buffer,
num_elements: indices.len() as u32,
material: primitive.material().index().unwrap_or(default_material),
center,
});
Ok(())
}
fn mesh_center(vertices: &[model::ModelVertex]) -> [f32; 3] {
if vertices.is_empty() {
return [0.0, 0.0, 0.0];
}
let sum = vertices.iter().fold(Vec3::ZERO, |sum, vertex| {
sum + Vec3::from_array(vertex.position)
});
(sum / vertices.len() as f32).to_array()
}
fn log_material_debug(
index: usize,
name: &str,
alpha_mode: AlphaMode,
alpha_cutoff: f32,
base_color_factor_alpha: f32,
double_sided: bool,
) {
log::warn!(
"material index={index} name=\"{name}\" alpha_mode={} alpha_cutoff={alpha_cutoff:.3} base_color_factor_alpha={base_color_factor_alpha:.3} double_sided={double_sided} selected_pipeline={}",
alpha_mode.as_str(),
selected_pipeline_label(alpha_mode, double_sided),
);
}
fn log_primitive_material_debug(
node_name: &str,
mesh_index: usize,
primitive_index: usize,
material: &gltf::Material<'_>,
default_material: usize,
) {
let pbr = material.pbr_metallic_roughness();
let material_index = material.index().unwrap_or(default_material);
let material_name = material.name().unwrap_or("default");
log::warn!(
"primitive node=\"{node_name}\" mesh_index={mesh_index} primitive_index={primitive_index} material_index={material_index} material_name=\"{material_name}\" baseColorFactor={:?} metallicFactor={:.3} roughnessFactor={:.3} alphaMode={:?}",
pbr.base_color_factor(),
pbr.metallic_factor(),
pbr.roughness_factor(),
material.alpha_mode(),
);
}
fn selected_pipeline_label(alpha_mode: AlphaMode, double_sided: bool) -> String {
format!(
"{}_{}",
alpha_mode.as_str(),
if double_sided {
"DOUBLE_SIDED"
} else {
"SINGLE_SIDED"
}
)
}
fn generate_tangents(vertices: &mut [model::ModelVertex], indices: &[u32]) {
let mut accumulated = vec![Vec3::ZERO; vertices.len()];
for triangle in indices.chunks_exact(3) {
let [i0, i1, i2] = [
triangle[0] as usize,
triangle[1] as usize,
triangle[2] as usize,
];
if i0 >= vertices.len() || i1 >= vertices.len() || i2 >= vertices.len() {
continue;
}
let p0 = Vec3::from_array(vertices[i0].position);
let p1 = Vec3::from_array(vertices[i1].position);
let p2 = Vec3::from_array(vertices[i2].position);
let uv0 = Vec2::from_array(vertices[i0].tex_coords);
let uv1 = Vec2::from_array(vertices[i1].tex_coords);
let uv2 = Vec2::from_array(vertices[i2].tex_coords);
let delta_pos1 = p1 - p0;
let delta_pos2 = p2 - p0;
let delta_uv1 = uv1 - uv0;
let delta_uv2 = uv2 - uv0;
let determinant = delta_uv1.x * delta_uv2.y - delta_uv1.y * delta_uv2.x;
if determinant.abs() < 1e-6 {
continue;
}
let tangent = (delta_pos1 * delta_uv2.y - delta_pos2 * delta_uv1.y) / determinant;
accumulated[i0] += tangent;
accumulated[i1] += tangent;
accumulated[i2] += tangent;
}
for (vertex, tangent) in vertices.iter_mut().zip(accumulated) {
let normal = Vec3::from_array(vertex.normal).normalize_or_zero();
let tangent = (tangent - normal * normal.dot(tangent)).normalize_or_zero();
let tangent = if tangent.length_squared() > 0.0 {
tangent
} else {
fallback_tangent(normal)
};
vertex.tangent = [tangent.x, tangent.y, tangent.z, 1.0];
}
}
fn fallback_tangent(normal: Vec3) -> Vec3 {
let axis = if normal.y.abs() < 0.9 {
Vec3::Y
} else {
Vec3::X
};
normal.cross(axis).normalize_or_zero()
}
fn gltf_image_to_rgba8(image: &gltf::image::Data) -> anyhow::Result<Vec<u8>> {
use gltf::image::Format;
match image.format {
Format::R8G8B8A8 => Ok(image.pixels.clone()),
Format::R8G8B8 => {
let mut rgba = Vec::with_capacity((image.width * image.height * 4) as usize);
for rgb in image.pixels.chunks_exact(3) {
rgba.extend_from_slice(&[rgb[0], rgb[1], rgb[2], 255]);
}
Ok(rgba)
}
Format::R8 => {
let mut rgba = Vec::with_capacity((image.width * image.height * 4) as usize);
for value in &image.pixels {
rgba.extend_from_slice(&[*value, *value, *value, 255]);
}
Ok(rgba)
}
Format::R8G8 => {
let mut rgba = Vec::with_capacity((image.width * image.height * 4) as usize);
for rg in image.pixels.chunks_exact(2) {
rgba.extend_from_slice(&[rg[0], rg[1], 0, 255]);
}
Ok(rgba)
}
Format::R16 => rgba_from_chunks(&image.pixels, 1, 2, read_u16_component),
Format::R16G16 => rgba_from_chunks(&image.pixels, 2, 2, read_u16_component),
Format::R16G16B16 => rgba_from_chunks(&image.pixels, 3, 2, read_u16_component),
Format::R16G16B16A16 => rgba_from_chunks(&image.pixels, 4, 2, read_u16_component),
Format::R32G32B32FLOAT => rgba_from_chunks(&image.pixels, 3, 4, read_f32_component),
Format::R32G32B32A32FLOAT => rgba_from_chunks(&image.pixels, 4, 4, read_f32_component),
}
}
fn rgba_from_chunks(
pixels: &[u8],
channels: usize,
component_width: usize,
read_component: fn(&[u8]) -> u8,
) -> anyhow::Result<Vec<u8>> {
let pixel_width = channels * component_width;
if pixel_width == 0 || pixels.len() % pixel_width != 0 {
bail!("invalid glTF image byte length for {channels} channels");
}
let mut rgba = Vec::with_capacity((pixels.len() / pixel_width) * 4);
for pixel in pixels.chunks_exact(pixel_width) {
let r = read_component(&pixel[0..component_width]);
let g = if channels > 1 {
read_component(&pixel[component_width..component_width * 2])
} else {
r
};
let b = if channels > 2 {
read_component(&pixel[component_width * 2..component_width * 3])
} else {
r
};
let a = if channels > 3 {
read_component(&pixel[component_width * 3..component_width * 4])
} else {
255
};
rgba.extend_from_slice(&[r, g, b, a]);
}
Ok(rgba)
}
fn read_u16_component(bytes: &[u8]) -> u8 {
let value = u16::from_ne_bytes([bytes[0], bytes[1]]);
(value / 257) as u8
}
fn read_f32_component(bytes: &[u8]) -> u8 {
let value = f32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
(value.clamp(0.0, 1.0) * 255.0).round() as u8
}

View File

@@ -1,16 +1,51 @@
#![allow(dead_code)]
use anyhow::*;
use eframe::{
egui,
egui_wgpu::{self, wgpu},
wgpu::util::DeviceExt,
};
use eframe::wgpu;
use image::GenericImageView;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum TextureColorSpace {
Srgb,
Linear,
}
impl TextureColorSpace {
pub fn format(self) -> wgpu::TextureFormat {
match self {
Self::Srgb => wgpu::TextureFormat::Rgba8UnormSrgb,
Self::Linear => wgpu::TextureFormat::Rgba8Unorm,
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct TextureSamplerDescriptor {
pub address_mode_u: wgpu::AddressMode,
pub address_mode_v: wgpu::AddressMode,
pub mag_filter: wgpu::FilterMode,
pub min_filter: wgpu::FilterMode,
pub mipmap_filter: wgpu::MipmapFilterMode,
}
impl Default for TextureSamplerDescriptor {
fn default() -> Self {
Self {
address_mode_u: wgpu::AddressMode::Repeat,
address_mode_v: wgpu::AddressMode::Repeat,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
}
}
}
pub struct Texture {
pub texture: wgpu::Texture,
pub view: wgpu::TextureView,
pub sampler: wgpu::Sampler,
pub width: u32,
pub height: u32,
}
impl Texture {
@@ -55,6 +90,8 @@ impl Texture {
texture,
view,
sampler,
width: config.width,
height: config.height,
}
}
@@ -89,7 +126,7 @@ impl Texture {
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
format: TextureColorSpace::Srgb.format(),
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
@@ -125,6 +162,8 @@ impl Texture {
texture,
view,
sampler,
width: dimensions.0,
height: dimensions.1,
})
}
@@ -135,6 +174,28 @@ impl Texture {
width: u32,
height: u32,
label: &str,
) -> Result<Self> {
Self::from_rgba8_with_sampler(
device,
queue,
rgba,
width,
height,
label,
TextureColorSpace::Srgb,
TextureSamplerDescriptor::default(),
)
}
pub fn from_rgba8_with_sampler(
device: &wgpu::Device,
queue: &wgpu::Queue,
rgba: &[u8],
width: u32,
height: u32,
label: &str,
color_space: TextureColorSpace,
sampler_desc: TextureSamplerDescriptor,
) -> Result<Self> {
let size = wgpu::Extent3d {
width,
@@ -147,7 +208,7 @@ impl Texture {
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
format: color_space.format(),
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
@@ -170,12 +231,12 @@ impl Texture {
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_u: sampler_desc.address_mode_u,
address_mode_v: sampler_desc.address_mode_v,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
mag_filter: sampler_desc.mag_filter,
min_filter: sampler_desc.min_filter,
mipmap_filter: sampler_desc.mipmap_filter,
..Default::default()
});
@@ -183,6 +244,8 @@ impl Texture {
texture,
view,
sampler,
width,
height,
})
}
}