diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index c681db791..34024dc05 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -183,6 +183,7 @@ impl BlockCache { annotation_scale_handle: Option, all_visible: bool, bg_color: [f32; 4], + viewport: Option, // Scene draw-depth map ([depth, half] per handle) — source of each // block child's in-block rank, shared by cached wires and graph leaves. depth_map: &HashMap, @@ -220,6 +221,7 @@ impl BlockCache { annotation_scale_handle, all_visible, bg_color, + viewport, depth_map, )), ) @@ -238,6 +240,7 @@ impl BlockCache { annotation_scale_handle: Option, all_visible: bool, bg_color: [f32; 4], + viewport: Option, depth_map: &HashMap, ) -> Self { use crate::par::prelude::*; @@ -255,6 +258,7 @@ impl BlockCache { annotation_scale_handle, all_visible, bg_color, + viewport, depth_map, )), ) @@ -396,6 +400,7 @@ fn build_defn( annotation_scale_handle: Option, all_visible: bool, bg_color: [f32; 4], + viewport: Option, depth_map: &HashMap, ) -> BlockDefn { let br = match doc.block_records.get(block_name) { @@ -461,7 +466,7 @@ fn build_defn( } EntityType::Insert(nested_ins) => { subs.push(LocalSub::Nested(build_nested_ref( - nested_ins, doc, bg_color, depth_map, + nested_ins, doc, bg_color, viewport, depth_map, ))); } EntityType::Dimension(_) => { @@ -471,6 +476,7 @@ fn build_defn( anno_scale, annotation_scale_handle, bg_color, + viewport, depth_map, ) { subs.push(LocalSub::Wire(wire)); @@ -504,7 +510,8 @@ fn build_defn( annotation_scale_handle, all_visible, depth_map, - ); + ) + .with_viewport(viewport); graph.walk_insert( &insert, table.common.handle, @@ -518,6 +525,7 @@ fn build_defn( anno_scale, annotation_scale_handle, bg_color, + viewport, depth_map, ) { subs.push(LocalSub::Wire(wire)); @@ -531,6 +539,7 @@ fn build_defn( anno_scale, annotation_scale_handle, bg_color, + viewport, depth_map, ) { subs.push(LocalSub::Wire(wire)); @@ -544,6 +553,7 @@ fn build_defn( &insert, doc, bg_color, + viewport, depth_map, ))); } @@ -560,6 +570,7 @@ fn build_defn( anno_scale, annotation_scale_handle, bg_color, + viewport, depth_map, ) { subs.push(LocalSub::Wire(lw)); @@ -579,6 +590,7 @@ fn build_nested_ref( nested_ins: &acadrust::entities::Insert, doc: &CadDocument, bg_color: [f32; 4], + viewport: Option, depth_map: &HashMap, ) -> NestedRef { let _ = bg_color; @@ -595,7 +607,7 @@ fn build_nested_ref( NestedRef { block_name: nested_ins.block_name.clone(), xform, - style: crate::scene::render_graph::InsertStyleSpec::new(doc, nested_ins), + style: crate::scene::render_graph::InsertStyleSpec::new(doc, nested_ins, viewport), instance_offsets: crate::scene::render_graph::array_offsets(nested_ins), clip_poly, local_rank: depth_map @@ -610,6 +622,7 @@ fn tessellate_sub_local( anno_scale: f32, annotation_scale_handle: Option, bg_color: [f32; 4], + viewport: Option, depth_map: &HashMap, ) -> Vec { let h = sub.common().handle; @@ -626,7 +639,8 @@ fn tessellate_sub_local( // with the per-render bg, so the cache no longer has to rebuild on // BACKGROUND / layout-switch — the dynamic adaptation tracks the // live bg at render time. - let (sub_color, pat_len, pat, lw_px, aci) = crate::scene::view::render::render_style_for(doc, sub); + let (sub_color, pat_len, pat, lw_px, aci) = + crate::scene::view::render::render_style_for_viewport(doc, sub, viewport); let _ = bg_color; let has_book_color = diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index 1732d773b..50c7a344d 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -405,7 +405,7 @@ impl Scene { model_space: bool, ) -> bool { let _ = model_space; - let Some(cam) = self.camera_from_view( + let Some(cam) = self.camera_from_view_mode( view.direction, view.target, acadrust::types::Vector2 { @@ -414,6 +414,8 @@ impl Scene { }, view.height, view.twist_angle, + view.perspective, + view.lens_length, ) else { return false; }; @@ -463,16 +465,15 @@ impl Scene { /// their already-effective `view_target` / `view_center` / `view_height`. /// /// Returns `None` for a zero `view_height` (an uninitialised entry). - pub(super) fn camera_from_view( + pub(super) fn camera_from_view_mode( &self, view_direction: acadrust::types::Vector3, view_target: acadrust::types::Vector3, view_center: acadrust::types::Vector2, view_height: f64, twist: f64, - // Subtracted from `view_target` to reach wire-space. Model views pass - // `[0.0_f64; 3]`; paper-space views (whose entities carry no - // offset) pass `[0; 3]`. + perspective: bool, + lens_length: f64, ) -> Option { if view_height.abs() < 1e-9 { return None; @@ -516,14 +517,26 @@ impl Scene { let target = base + view_right.as_dvec3() * view_center.x + view_up.as_dvec3() * view_center.y; - let fov_y = 45.0_f32.to_radians(); + let lens_length = lens_length.max(1.0) as f32; + // The saved-view convention uses a 24 mm vertical aperture. This is + // the inverse of the projection path's documented + // `distance = view_height * lens / 24` relation. + let fov_y = if perspective { + 2.0 * (12.0 / lens_length).atan() + } else { + 45.0_f32.to_radians() + }; let distance = ((view_height as f32 / 2.0) / (fov_y * 0.5).tan()).max(0.001); Some(Camera { target, rotation, distance, fov_y, - projection: view::camera::Projection::Orthographic, + projection: if perspective { + view::camera::Projection::Perspective + } else { + view::camera::Projection::Orthographic + }, yaw, pitch, // Sized from the saved view height (not a zoom-scaled range) so a @@ -544,12 +557,14 @@ impl Scene { /// Decode a VPort table entry (model-space tiled view) into a `Camera`. fn camera_from_vport(&self, vp: &acadrust::tables::VPort) -> Option { - self.camera_from_view( + self.camera_from_view_mode( vp.view_direction, vp.view_target, vp.view_center, vp.view_height, vp.view_twist, + vp.perspective, + vp.lens_length, ) } @@ -580,6 +595,8 @@ impl Scene { z: view_dir.z as f64, }; entry.view_height = view_height as f64; + entry.perspective = cam.projection == view::camera::Projection::Perspective; + entry.lens_length = (12.0 / (cam.fov_y * 0.5).tan().max(1e-6)) as f64; entry.view_center = acadrust::types::Vector2::ZERO; // Stored twist = -roll, matching the decoder (roll = -twist). entry.view_twist = -cam.roll() as f64; @@ -796,7 +813,7 @@ impl Scene { // Paper-space entities carry no world_offset → decode with a zero // offset, through the same shared decoder (twist included). - let Some(cam) = self.camera_from_view( + let Some(cam) = self.camera_from_view_mode( vp.view_direction, vp.view_target, acadrust::types::Vector2 { @@ -805,6 +822,8 @@ impl Scene { }, vp.view_height, vp.twist_angle, + vp.status.perspective, + vp.lens_length, ) else { return false; }; @@ -847,6 +866,8 @@ impl Scene { vp.view_direction = vd3; vp.view_height = view_height as f64; vp.view_twist = twist; + vp.perspective = cam.projection == view::camera::Projection::Perspective; + vp.lens_length = (12.0 / (cam.fov_y * 0.5).tan().max(1e-6)) as f64; } // Persist the tiled layout as duplicate `*Active` VPort entries. @@ -878,6 +899,9 @@ impl Scene { vp.view_direction = vd3; vp.view_height = view_height as f64; vp.twist_angle = twist; + vp.status.perspective = + cam.projection == view::camera::Projection::Perspective; + vp.lens_length = (12.0 / (cam.fov_y * 0.5).tan().max(1e-6)) as f64; } true } @@ -985,6 +1009,7 @@ impl Scene { None, scale, self.annotation_all_visible(), + None, ) { let is_infinite = Self::handle_from_wire_name(&wire.name) .and_then(|handle| self.document.get_entity(handle)) @@ -1081,6 +1106,7 @@ impl Scene { None, scale, self.annotation_all_visible(), + None, ); // Ray / XLine tessellate as ±DISPLAY_EXTENT display segments // (entities/ray.rs) — their endpoints are rendering artifacts, not diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 8d104db0f..69b038bc8 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -442,7 +442,7 @@ pub(crate) fn tessellate_entity( // footprint doesn't drift when the camera // crosses the LOD threshold. See #19. let (entity_color, _, _, _, aci_idx) = - view::render::render_style_for(document, e); + view::render::render_style_for_viewport(document, e, active_viewport); let entity_color = view::render::adapt_to_bg(entity_color, bg_color); let entity_color = fade_if_locked(document, e, entity_color, bg_color); if is_3d_entity { @@ -529,11 +529,11 @@ pub(crate) fn tessellate_entity( } let (entity_color, pattern_length, pattern, line_weight_px, aci) = - view::render::render_style_for(document, e); + view::render::render_style_for_viewport(document, e, active_viewport); let entity_color = view::render::adapt_to_bg(entity_color, bg_color); let entity_color = fade_if_locked(document, e, entity_color, bg_color); let lt_scale = document.header.linetype_scale as f32 * e.common().linetype_scale as f32; - let lt_name = view::render::linetype_name_for(document, e); + let lt_name = view::render::linetype_name_for_viewport(document, e, active_viewport); // Paper-space linetype scaling belongs to the viewport uniform. Keeping // resident geometry at its model-space scale prevents every MSPACE wheel // tick from rebuilding and uploading the viewport's complete wire set. @@ -931,12 +931,16 @@ pub(crate) fn tessellate_entity( if let EntityType::Insert(ins) = e { // Resolve the INSERT's own style so ByBlock sub-entities can inherit it. let (ins_color, ins_pat_len, ins_pat, ins_lw_px, _) = - view::render::render_style_for(document, e); + view::render::render_style_for_viewport(document, e, active_viewport); let ins_color = view::render::adapt_to_bg(ins_color, bg_color); // Resolve the INSERT's *layer* style — the layer-0 inheritance target // for sub-entities on layer "0" with ByLayer properties (#221). let ins_layer = { - let mut s = view::render::layer_render_style(document, &ins.common.layer); + let mut s = view::render::layer_render_style_viewport( + document, + &ins.common.layer, + active_viewport, + ); s.color = view::render::adapt_to_bg(s.color, bg_color); s }; @@ -986,6 +990,7 @@ pub(crate) fn tessellate_entity( annotation_scale_handle, true, bg_color, + active_viewport, &fallback_depths, ); &fallback_cache diff --git a/src/scene/entity.rs b/src/scene/entity.rs index ddb837489..55b7fc0eb 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -799,6 +799,7 @@ impl Scene { frozen: Option<&rustc_hash::FxHashSet>, annotation_scale_handle: Option, all_visible: bool, + viewport: Option, ) -> Vec { let layer_hidden = |layer: &str| { self.document @@ -818,6 +819,11 @@ impl Scene { // Every content viewport supplies the block it renders. Do not depend // on camera/frustum culling to separate paper and model coordinates: // overlapping coordinates otherwise make foreign fills visible. + let hatch_bg = if self.current_layout != "Model" { + self.paper_bg_color + } else { + self.bg_color + }; let depth_map = self.draw_depth_map(); let mut models: Vec = self .hatches @@ -887,7 +893,12 @@ impl Scene { // emitted first so LessEqual layering keeps it underneath. let mut backdrop: Option = None; if let Some(e) = entity { - let style = self.render_style(e); + let mut style = crate::scene::view::render::render_style_for_viewport( + &self.document, + e, + viewport, + ); + style.0 = crate::scene::view::render::adapt_to_bg(style.0, hatch_bg); m.aci = style.4; m.line_weight_px = style.3; // A gradient's colour is its first stop (already baked into @@ -913,9 +924,10 @@ impl Scene { }) .unwrap_or(0); ( - crate::scene::view::render::layer_render_style( + crate::scene::view::render::layer_render_style_viewport( &self.document, &dxf.common.layer, + viewport, ) .color, aci, @@ -970,11 +982,6 @@ impl Scene { .collect(); // Background for adapting block-child hatch colours at the leaf (#221). - let hatch_bg: [f32; 4] = if self.current_layout != "Model" { - self.paper_bg_color - } else { - self.bg_color - }; // Instanced/owned hatch leaves are produced by the shared scene graph. models.extend(self.instanced_hatch_models( target_block, @@ -983,6 +990,7 @@ impl Scene { frozen, annotation_scale_handle, all_visible, + viewport, )); // Wide polyline bands remain on the wire path, including inside block @@ -1009,6 +1017,7 @@ impl Scene { frozen: Option<&rustc_hash::FxHashSet>, annotation_scale_handle: Option, all_visible: bool, + viewport: Option, ) -> Vec { self.instanced_hatch_models_filtered( layout_block, @@ -1017,6 +1026,7 @@ impl Scene { frozen, annotation_scale_handle, all_visible, + viewport, None, false, ) @@ -1055,6 +1065,7 @@ impl Scene { (!frozen.is_empty()).then_some(&frozen), self.displayed_annotation_scale_handle(), self.annotation_all_visible(), + self.active_viewport, Some(&targets), true, ) @@ -1068,6 +1079,7 @@ impl Scene { frozen: Option<&rustc_hash::FxHashSet>, annotation_scale_handle: Option, all_visible: bool, + viewport: Option, targets: Option<&rustc_hash::FxHashSet>, include_preview_hidden: bool, ) -> Vec { @@ -1078,7 +1090,8 @@ impl Scene { annotation_scale_handle, all_visible, depth_map.as_ref(), - ); + ) + .with_viewport(viewport); let mut hatch_block_memo = std::collections::HashMap::new(); let mut models = Vec::new(); graph.walk_root( diff --git a/src/scene/mod.rs b/src/scene/mod.rs index dfc5cbb67..7a63acb55 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1471,6 +1471,12 @@ struct SceneLight { attenuation_type: f32, attenuation_start: f32, attenuation_end: f32, + cast_shadows: bool, + shadow_softness: f32, + shadow_map_size: u32, + web_profile: [f32; 8], + web_rotation: [f32; 3], + web_enabled: bool, } pub struct Scene { @@ -2078,6 +2084,7 @@ impl Scene { &self, annotation_scale_handle: Option, all_visible: bool, + viewport: Option, ) -> Arc { let bg = if self.current_layout == "Model" { self.bg_color @@ -2089,6 +2096,7 @@ impl Scene { key = key.rotate_left(13) ^ component.to_bits() as u64; } key = key.rotate_left(13) ^ all_visible as u64; + key = key.rotate_left(13) ^ viewport.map(|handle| handle.value()).unwrap_or(0); { let cache = self.block_defn_cache.borrow(); if let Some((epoch, arc)) = cache.get(&key) { @@ -2107,6 +2115,7 @@ impl Scene { annotation_scale_handle, all_visible, bg, + viewport, &self.draw_depth_map(), ); let arc = Arc::new(built); @@ -2134,6 +2143,7 @@ impl Scene { scale, self.annotation_all_visible(), None, + None, ); let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed); self.last_model_wire_gen.set(gen); @@ -4634,7 +4644,7 @@ impl Scene { &self.document, &self.document.header.current_annotation_scale, ); - self.resident_wires_for(block, None, scale, None) + self.resident_wires_for(block, None, scale, None, None) } /// Unified static-hold wire builder — the ONE tessellation path every @@ -4682,6 +4692,7 @@ impl Scene { anno_scale_override: Option, annotation_scale_handle: Option, frozen_layers: Option<&HashSet>, + style_viewport: Option, ) -> Arc> { // Normalize an inert anno override away so distinct viewport scales // share one resident set when annotation can't change the wires. @@ -4705,6 +4716,7 @@ impl Scene { annotation_scale_handle, all_visible, frozen_layers, + style_viewport, ); { let sets = self.resident_wire_sets.borrow(); @@ -4728,6 +4740,7 @@ impl Scene { annotation_scale_handle, all_visible, frozen_layers, + style_viewport, ) { return arc; @@ -4744,6 +4757,7 @@ impl Scene { anno_scale_override, annotation_scale_handle, all_visible, + style_viewport, ); // Synthesized nonprint markers (geo-location daisy) live in model space // only and are derived from document objects, not entities — append them @@ -4791,6 +4805,7 @@ impl Scene { annotation_scale_handle: Option, all_visible: bool, frozen_layers: Option<&HashSet>, + style_viewport: Option, ) -> u64 { let mut key: u64 = 0xcbf2_9ce4_8422_2325; let mut mix = @@ -4804,6 +4819,7 @@ impl Scene { .unwrap_or(u64::MAX)); mix(annotation_scale_handle.map(|handle| handle.value()).unwrap_or(0)); mix(all_visible as u64); + mix(style_viewport.map(|handle| handle.value()).unwrap_or(0)); match frozen_layers { Some(frozen) => { let mut signature = 0u64; @@ -4903,7 +4919,11 @@ impl Scene { annotation_scale_handle: Option, all_visible: bool, frozen_layers: Option<&HashSet>, + style_viewport: Option, ) -> Option>> { + if style_viewport.is_some() { + return None; + } let perf = crate::perf::enabled(); let t_patch = iced::time::Instant::now(); // The entry must exist, be stale, and be uniquely held so we can move @@ -4951,7 +4971,7 @@ impl Scene { } else { 1.0 }; - let blk = self.block_cache_arc_for(annotation_scale_handle, all_visible); + let blk = self.block_cache_arc_for(annotation_scale_handle, all_visible, style_viewport); let empty_sel: HashSet = HashSet::default(); let mut new_runs: HashMap> = HashMap::default(); let mut memo_updates: Vec<(Handle, Arc>)> = Vec::new(); @@ -4976,7 +4996,7 @@ impl Scene { let raw = tessellate_entity( &self.document, &empty_sel, - self.active_viewport, + style_viewport.or(self.active_viewport), bg, anno, annotation_scale_handle, @@ -5265,7 +5285,7 @@ impl Scene { } let layout_block = self.current_layout_block_handle(); let scale = self.paper_annotation_scale_handle(); - let base = self.resident_wires_for(layout_block, None, scale, None); + let base = self.resident_wires_for(layout_block, None, scale, None, None); let mut wires = (*base).clone(); // The overall "sheet" viewport now IS the paper view itself, so its own // border rectangle must not be drawn as an entity on the sheet. @@ -5650,6 +5670,7 @@ impl Scene { None, scale, self.annotation_all_visible(), + None, )); self.hatch_cache.borrow_mut().insert( key, @@ -5796,7 +5817,8 @@ impl Scene { annotation_scale_handle, all_visible, depth_map.as_ref(), - ); + ) + .with_viewport(viewport); let mut models = Vec::new(); let root = if self.block_edit_block.is_none() { viewport.map(|viewport| render_graph::SceneRoot::Viewport { @@ -5923,6 +5945,7 @@ impl Scene { None, scale, self.annotation_all_visible(), + None, )); self.mesh_cache .borrow_mut() @@ -6034,6 +6057,7 @@ impl Scene { None, self.displayed_annotation_scale_handle(), self.annotation_all_visible(), + self.active_viewport, )); *self.interaction_mesh_cache.borrow_mut() = Some((self.geometry_epoch, key, Arc::clone(&meshes))); @@ -6083,6 +6107,7 @@ impl Scene { frozen: Option<&HashSet>, annotation_scale_handle: Option, all_visible: bool, + viewport: Option, ) -> Vec { // Top-level solids: drop those whose layer is off/frozen or that are // flagged invisible / isolated-hidden, mirroring the 2D wire path, plus @@ -6105,7 +6130,33 @@ impl Scene { }) .unwrap_or(false) }) - .map(|(_, set)| set.clone()) + .map(|(&handle, set)| { + let mut set = set.clone(); + if let Some(entity) = self.document.get_entity(handle) { + let style = crate::scene::view::render::render_style_for_viewport( + &self.document, + entity, + viewport, + ); + let attached_material = set + .material + .as_ref() + .is_some_and(|material| material.handle.is_some()); + for mesh in &mut set.lods { + if !attached_material { + mesh.color[0..3].copy_from_slice(&style.0[0..3]); + } + mesh.color[3] = style.0[3]; + } + if let Some(material) = set.material.as_mut() { + if !attached_material { + material.diffuse[0..3].copy_from_slice(&style.0[0..3]); + } + material.diffuse[3] = style.0[3]; + } + } + set + }) .collect(); // Block-definition solids are instanced per INSERT of the ACTIVE space's // block so a block placed at an INSERT scale renders at the right size @@ -6116,6 +6167,7 @@ impl Scene { frozen, annotation_scale_handle, all_visible, + viewport, )); all } @@ -6171,7 +6223,8 @@ impl Scene { let scale = self.viewport_scale_handle(viewport); let all_visible = self.annotation_all_visible(); let context_sig = scale.map_or(0, |handle| handle.value()) - ^ u64::from(all_visible).rotate_left(61); + ^ u64::from(all_visible).rotate_left(61) + ^ viewport.value().rotate_left(23); let sig = Self::frozen_layers_sig(frozen) ^ context_sig; let key = (target_block, self.current_layout.clone(), sig); let sel = self.selected_hatch_sig(); @@ -6185,6 +6238,7 @@ impl Scene { Some(frozen), scale, all_visible, + Some(viewport), )); self.frozen_hatch_cache .borrow_mut() @@ -6205,7 +6259,8 @@ impl Scene { let scale = self.viewport_scale_handle(viewport); let all_visible = self.annotation_all_visible(); let context_sig = scale.map_or(0, |handle| handle.value()) - ^ u64::from(all_visible).rotate_left(61); + ^ u64::from(all_visible).rotate_left(61) + ^ viewport.value().rotate_left(23); let sig = Self::frozen_layers_sig(frozen) ^ context_sig; let key = (target_block, self.current_layout.clone(), sig); if let Some((e, arc)) = self.frozen_wipeout_cache.borrow().get(&key) { @@ -6238,7 +6293,8 @@ impl Scene { let scale = self.viewport_scale_handle(viewport); let all_visible = self.annotation_all_visible(); let context_sig = scale.map_or(0, |handle| handle.value()) - ^ u64::from(all_visible).rotate_left(61); + ^ u64::from(all_visible).rotate_left(61) + ^ viewport.value().rotate_left(23); let sig = Self::frozen_layers_sig(frozen) ^ context_sig; let key = (target_block, sig); if let Some((e, arc)) = self.frozen_image_cache.borrow().get(&key) { @@ -6272,7 +6328,8 @@ impl Scene { let scale = self.viewport_scale_handle(viewport); let all_visible = self.annotation_all_visible(); let context_sig = scale.map_or(0, |handle| handle.value()) - ^ u64::from(all_visible).rotate_left(61); + ^ u64::from(all_visible).rotate_left(61) + ^ viewport.value().rotate_left(23); let sig = Self::frozen_layers_sig(frozen) ^ context_sig; let key = (target_block, self.current_layout.clone(), sig); if let Some((e, arc)) = self.frozen_mesh_cache.borrow().get(&key) { @@ -6285,6 +6342,7 @@ impl Scene { Some(frozen), scale, all_visible, + Some(viewport), )); self.frozen_mesh_cache .borrow_mut() @@ -6364,6 +6422,7 @@ impl Scene { frozen: Option<&HashSet>, annotation_scale_handle: Option, all_visible: bool, + viewport: Option, ) -> Vec { if self.block_meshes.is_empty() { return Vec::new(); @@ -6375,7 +6434,8 @@ impl Scene { annotation_scale_handle, all_visible, depth_map.as_ref(), - ); + ) + .with_viewport(viewport); let mut out = Vec::new(); graph.walk_root( self.render_scene_root(layout_block), @@ -7960,6 +8020,7 @@ impl Scene { anno_scale_override: Option, annotation_scale_handle: Option, all_visible: bool, + style_viewport: Option, ) -> Vec { use acadrust::objects::ObjectType; @@ -8065,7 +8126,7 @@ impl Scene { // `geometry_epoch` (a full model re-tessellation). let empty_sel: HashSet = HashSet::default(); let sel: &HashSet = &empty_sel; - let avp = self.active_viewport; + let avp = style_viewport.or(self.active_viewport); // A paper-space content viewport renders MODEL block entities while // the user is sitting in a paper layout — that path expects // `world_offset` subtraction even though `current_layout != "Model"`. @@ -8086,7 +8147,7 @@ impl Scene { // Content shown inside a paper-space viewport carries a scale override; // only there does PSLTSCALE resize linetypes by the viewport scale. let paper = anno_scale_override.is_some(); - let blk_cache = self.block_cache_arc_for(annotation_scale_handle, all_visible); + let blk_cache = self.block_cache_arc_for(annotation_scale_handle, all_visible, style_viewport); let blk_ref: &cache::block_cache::BlockCache = &blk_cache; // Zoom-adaptive curve sampling for top-level Edge tessellation. Target // ~0.5 px chord height — far-out arcs that used to emit hundreds of @@ -8863,6 +8924,7 @@ impl Scene { let blk_cache = self.block_cache_arc_for( annotation_scale_handle, self.annotation_all_visible(), + self.active_viewport, ); // tessellate_one is used for one-off lookups (hit test, properties). // Skip culling here so the caller always gets the full geometry. @@ -8945,6 +9007,7 @@ impl Scene { None, scale, self.annotation_all_visible(), + None, ); let mut min = glam::DVec2::splat(f64::INFINITY); let mut max = glam::DVec2::splat(f64::NEG_INFINITY); diff --git a/src/scene/model/image_model.rs b/src/scene/model/image_model.rs index 2c2c29f42..e34ed181a 100644 --- a/src/scene/model/image_model.rs +++ b/src/scene/model/image_model.rs @@ -349,7 +349,7 @@ impl ImageModel { /// Decoded RGBA image shared between the raster pipeline and the /// unresolved-reference probe. Cheap to clone — the pixels are `Arc`-shared. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct DecodedImage { pub pixels: Arc>, pub width: u32, diff --git a/src/scene/model/visual_style_model.rs b/src/scene/model/visual_style_model.rs index 60f52155b..6e58953f1 100644 --- a/src/scene/model/visual_style_model.rs +++ b/src/scene/model/visual_style_model.rs @@ -110,7 +110,7 @@ fn property_color(style: &VisualStyle, index: usize) -> Option<[f32; 4]> { } impl MeshVisualStyle { - fn from_dwg(handle: Handle, style: &VisualStyle) -> Self { + pub(crate) fn from_dwg(handle: Handle, style: &VisualStyle) -> Self { Self { full_handle: Some(handle), face_handle: None, @@ -232,6 +232,14 @@ impl MeshVisualStyle { } } +pub(crate) fn resolve_visual_style_handle( + document: &CadDocument, + handle: Handle, +) -> Option { + visual_style(document, Some(handle)) + .map(|(handle, style)| MeshVisualStyle::from_dwg(handle, style)) +} + fn visual_style( document: &CadDocument, handle: Option, diff --git a/src/scene/mspace.rs b/src/scene/mspace.rs index 31542838e..251b8af9e 100644 --- a/src/scene/mspace.rs +++ b/src/scene/mspace.rs @@ -360,6 +360,10 @@ impl Scene { vp.view_direction.y = dir.y as f64; vp.view_direction.z = dir.z as f64; vp.twist_angle = twist; + vp.status.perspective = + tmp.projection == view::camera::Projection::Perspective; + vp.lens_length = + (12.0 / (tmp.fov_y * 0.5).tan().max(1e-6)) as f64; return true; } false diff --git a/src/scene/paper.rs b/src/scene/paper.rs index e70d1ef8d..4f0578f31 100644 --- a/src/scene/paper.rs +++ b/src/scene/paper.rs @@ -477,6 +477,7 @@ impl Scene { None, annotation_scale_handle, all_visible, + None, ); models.extend(instanced); Arc::new(models) @@ -560,6 +561,7 @@ impl Scene { frozen, annotation_scale_handle, all_visible, + None, )); models } @@ -679,7 +681,7 @@ impl Scene { let center_set = vp.view_center.x.abs() > 1e-6 || vp.view_center.y.abs() > 1e-6; - if let Some(cam) = self.camera_from_view( + if let Some(cam) = self.camera_from_view_mode( vp.view_direction, vp.view_target, acadrust::types::Vector2 { @@ -688,6 +690,8 @@ impl Scene { }, saved_h, vp.twist_angle, + vp.status.perspective, + vp.lens_length, ) { if target_set || !center_set { return Some(cam); @@ -718,12 +722,14 @@ impl Scene { y: cy, z: vp.view_target.z, }; - self.camera_from_view( + self.camera_from_view_mode( vp.view_direction, tgt, acadrust::types::Vector2::ZERO, fit_h, vp.twist_angle, + vp.status.perspective, + vp.lens_length, ) } @@ -758,6 +764,7 @@ impl Scene { Some(self.viewport_annotation_multiplier(vp_handle)), scale_handle, Some(&frozen), + Some(vp_handle), ) } diff --git a/src/scene/pipeline/mesh_gpu.rs b/src/scene/pipeline/mesh_gpu.rs index b4d19032f..e09c69fef 100644 --- a/src/scene/pipeline/mesh_gpu.rs +++ b/src/scene/pipeline/mesh_gpu.rs @@ -612,7 +612,7 @@ pub fn create_material_bind_group( [ material.advanced_data_present as u32, material.is_anonymous as u32, - 0, + material.handle.is_some() as u32, 0, ] }), diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 403751fc0..ef2eb4260 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -32,6 +32,7 @@ use device_capabilities::DeviceCapabilities; /// MSAA sample count for the main drawing pipelines. const MSAA_SAMPLES: u32 = 4; +const SHADOW_MAP_SIZE: u32 = 2048; #[derive(Clone, Copy, PartialEq, Eq)] enum MeshHighlightKind { @@ -74,6 +75,8 @@ struct MeshCullUniform { } pub struct Pipeline { + background_pipeline: wgpu::RenderPipeline, + shadow_pipeline: wgpu::RenderPipeline, wire_pipeline: wgpu::RenderPipeline, /// Stamps clip-boundary polygons into the stencil buffer (viewports + XCLIP). clip_mask_pipeline: wgpu::RenderPipeline, @@ -130,7 +133,18 @@ pub struct Pipeline { /// writes depth). Paired with `mesh_depth_pipeline` for HiddenLine. face3d_depth_pipeline: wgpu::RenderPipeline, uniform_buffer: wgpu::Buffer, + frame_bgl: wgpu::BindGroupLayout, uniform_bind_group: wgpu::BindGroup, + shadow_uniform_bind_group: wgpu::BindGroup, + background_texture: wgpu::Texture, + environment_texture: wgpu::Texture, + background_sampler: wgpu::Sampler, + background_source_id: usize, + environment_source_id: usize, + _shadow_texture: wgpu::Texture, + shadow_view: wgpu::TextureView, + shadow_sampler: wgpu::Sampler, + shadow_enabled: bool, wipeout_bgl1: wgpu::BindGroupLayout, image_bgl1: wgpu::BindGroupLayout, /// Group-1 layout for the text pipeline (atlas texture + sampler). @@ -347,26 +361,198 @@ impl Pipeline { // Bind group layout 0 — shared by wire and hatch pipelines. let frame_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("viewer.frame_bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, }, - count: None, - }], + wgpu::BindGroupLayoutEntry { + binding: 1, + 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: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + 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::Depth, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison), + count: None, + }, + ], }); + let shadow_frame_bgl = device.create_bind_group_layout( + &wgpu::BindGroupLayoutDescriptor { + label: Some("shadow.frame_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }, + ); + let placeholder_texture = |label: &'static str, rgba: [u8; 4]| { + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some(label), + size: wgpu::Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + queue.write_texture( + texture.as_image_copy(), + &rgba, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(4), + rows_per_image: Some(1), + }, + wgpu::Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + ); + texture + }; + let background_texture = placeholder_texture("background.placeholder", [0, 0, 0, 255]); + let environment_texture = + placeholder_texture("environment.placeholder", [128, 128, 128, 255]); + let background_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("background.sampler"), + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::MipmapFilterMode::Linear, + ..Default::default() + }); + let shadow_texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("shadow.texture"), + size: wgpu::Extent3d { + width: SHADOW_MAP_SIZE, + height: SHADOW_MAP_SIZE, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Depth32Float, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + let shadow_view = shadow_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("shadow.sampler"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + compare: Some(wgpu::CompareFunction::LessEqual), + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + let background_view = background_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let environment_view = environment_texture.create_view(&wgpu::TextureViewDescriptor::default()); let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("viewer.bind_group"), layout: &frame_bgl, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: uniform_buffer.as_entire_binding(), - }], + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&background_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&background_sampler), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::TextureView(&environment_view), + }, + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::Sampler(&background_sampler), + }, + wgpu::BindGroupEntry { + binding: 5, + resource: wgpu::BindingResource::TextureView(&shadow_view), + }, + wgpu::BindGroupEntry { + binding: 6, + resource: wgpu::BindingResource::Sampler(&shadow_sampler), + }, + ], }); + let shadow_uniform_bind_group = device.create_bind_group( + &wgpu::BindGroupDescriptor { + label: Some("shadow.frame_bind_group"), + layout: &shadow_frame_bgl, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }], + }, + ); // ── Wire pipeline ────────────────────────────────────────────────── // Select once from actual device limits. Any device whose compositor @@ -424,6 +610,57 @@ impl Pipeline { write_mask: 0x00, }; + let background_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("background.shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!( + "../../shaders/background.wgsl" + ))), + }); + let background_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("background.pipeline_layout"), + bind_group_layouts: &[Some(&frame_bgl)], + immediate_size: 0, + }); + let background_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("background.pipeline"), + layout: Some(&background_layout), + vertex: wgpu::VertexState { + module: &background_shader, + entry_point: Some("vs_main"), + buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + cull_mode: None, + ..Default::default() + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth24PlusStencil8, + depth_write_enabled: Some(false), + depth_compare: Some(wgpu::CompareFunction::Always), + stencil: content_stencil.clone(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: MSAA_SAMPLES, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + module: &background_shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + multiview_mask: None, + cache: None, + }); + let wire_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("wire.pipeline"), layout: Some(&wire_layout), @@ -1033,6 +1270,55 @@ impl Pipeline { immediate_size: 0, }); + let shadow_source = include_str!("../../shaders/shadow.wgsl"); + let shadow_source = if mesh_storage_instancing { + std::borrow::Cow::Borrowed(shadow_source) + } else { + std::borrow::Cow::Owned(shadow_source.replace( + "var mesh_instances: array;", + "var mesh_instances: array;", + )) + }; + let shadow_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("shadow.shader"), + source: wgpu::ShaderSource::Wgsl(shadow_source), + }); + let shadow_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("shadow.pipeline_layout"), + bind_group_layouts: &[&shadow_frame_bgl, &mesh_material_bgl].map(Some), + immediate_size: 0, + }); + let shadow_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("shadow.pipeline"), + layout: Some(&shadow_layout), + vertex: wgpu::VertexState { + module: &shadow_shader, + entry_point: Some("vs_main"), + buffers: &[mesh_gpu::MeshVertex::layout()], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + cull_mode: None, + ..Default::default() + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: Some(true), + depth_compare: Some(wgpu::CompareFunction::LessEqual), + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState { + constant: 2, + slope_scale: 2.0, + clamp: 0.0, + }, + }), + multisample: wgpu::MultisampleState::default(), + fragment: None, + multiview_mask: None, + cache: None, + }); + let mesh_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("mesh.pipeline"), layout: Some(&mesh_layout), @@ -1612,6 +1898,8 @@ impl Pipeline { }); Self { + background_pipeline, + shadow_pipeline, wire_pipeline, clip_mask_pipeline, wire_black_pipeline, @@ -1654,7 +1942,18 @@ impl Pipeline { face3d_pipeline, face3d_depth_pipeline, uniform_buffer, + frame_bgl, uniform_bind_group, + shadow_uniform_bind_group, + background_texture, + environment_texture, + background_sampler, + background_source_id: 0, + environment_source_id: 0, + _shadow_texture: shadow_texture, + shadow_view, + shadow_sampler, + shadow_enabled: false, wipeout_bgl1, image_bgl1, depth_texture_size: Size::new(1, 1), @@ -2819,10 +3118,118 @@ impl Pipeline { } } - pub fn upload_uniforms(&self, queue: &wgpu::Queue, uniforms: &Uniforms) { + pub fn upload_uniforms(&mut self, queue: &wgpu::Queue, uniforms: &Uniforms) { + self.shadow_enabled = uniforms.shadow_params[0] > 0.5; queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(uniforms)); } + pub fn upload_background_images( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + background: Option<&crate::scene::model::image_model::DecodedImage>, + environment: Option<&crate::scene::model::image_model::DecodedImage>, + ) { + let source_id = |image: Option<&crate::scene::model::image_model::DecodedImage>| { + image + .map(|image| std::sync::Arc::as_ptr(&image.pixels) as usize) + .unwrap_or(0) + }; + let background_id = source_id(background); + let environment_id = source_id(environment); + if background_id == self.background_source_id + && environment_id == self.environment_source_id + { + return; + } + let upload = | + label: &'static str, + image: Option<&crate::scene::model::image_model::DecodedImage>, + fallback: [u8; 4], + | { + let (width, height, pixels) = image + .map(|image| (image.width, image.height, image.pixels.as_slice())) + .unwrap_or((1, 1, fallback.as_slice())); + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some(label), + size: wgpu::Extent3d { + width: width.max(1), + height: height.max(1), + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + queue.write_texture( + texture.as_image_copy(), + pixels, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(4 * width.max(1)), + rows_per_image: Some(height.max(1)), + }, + wgpu::Extent3d { + width: width.max(1), + height: height.max(1), + depth_or_array_layers: 1, + }, + ); + texture + }; + self.background_texture = upload("background.texture", background, [0, 0, 0, 255]); + self.environment_texture = upload( + "environment.texture", + environment, + [128, 128, 128, 255], + ); + let background_view = self + .background_texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let environment_view = self + .environment_texture + .create_view(&wgpu::TextureViewDescriptor::default()); + self.uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("viewer.bind_group"), + layout: &self.frame_bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: self.uniform_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&background_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&self.background_sampler), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::TextureView(&environment_view), + }, + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::Sampler(&self.background_sampler), + }, + wgpu::BindGroupEntry { + binding: 5, + resource: wgpu::BindingResource::TextureView(&self.shadow_view), + }, + wgpu::BindGroupEntry { + binding: 6, + resource: wgpu::BindingResource::Sampler(&self.shadow_sampler), + }, + ], + }); + self.background_source_id = background_id; + self.environment_source_id = environment_id; + } + /// Write the blit shader's UV crop uniform. Call in `prepare` (the only /// place with a `&Queue`) — `render` then just submits the draw call. pub fn upload_blit_uv(&self, queue: &wgpu::Queue, uv_offset: [f32; 2], uv_scale: [f32; 2]) { @@ -2869,11 +3276,15 @@ impl Pipeline { }; let msaa = &self.msaa_view; let [r, g, b, a] = bg_color; - let clear_color = wgpu::Color { - r: r as f64, - g: g as f64, - b: b as f64, - a: a as f64, + let clear_color = if self.clip_boundary.is_some() { + wgpu::Color::TRANSPARENT + } else { + wgpu::Color { + r: r as f64, + g: g as f64, + b: b as f64, + a: a as f64, + } }; // Non-rectangular viewport clip: the boundary is stamped into the // just-cleared (0x00) stencil with `Invert`, so an odd (interior) @@ -2883,6 +3294,51 @@ impl Pipeline { // render rectangle does the clipping). let stencil_ref: u32 = if self.clip_boundary.is_some() { 0xFF } else { 0 }; + if self.shadow_enabled && !self.skip_geometry && !mesh_wireframe { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("shadow.render_pass"), + color_attachments: &[], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &self.shadow_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + multiview_mask: None, + }); + pass.set_pipeline(&self.shadow_pipeline); + pass.set_bind_group(0, &self.shadow_uniform_bind_group, &[]); + for (_, chunk) in self.active_mesh_chunks_indexed() { + if !chunk.visible { + continue; + } + pass.set_bind_group( + 1, + chunk + .material_bind_group + .as_ref() + .unwrap_or(&self.mesh_default_material_bind_group), + &[], + ); + pass.set_vertex_buffer(0, chunk.vertex_buffer.slice(..)); + if chunk.index_count != 0 { + pass.set_index_buffer(chunk.index_buffer.slice(..), wgpu::IndexFormat::Uint32); + pass.draw_indexed(0..chunk.index_count, 0, 0..chunk.instance_count); + } + if chunk.transp_index_count != 0 { + pass.set_index_buffer( + chunk.transp_index_buffer.slice(..), + wgpu::IndexFormat::Uint32, + ); + pass.draw_indexed(0..chunk.transp_index_count, 0, 0..chunk.instance_count); + } + } + } + // Scene-render cache: when `prepare` found this frame pixel-identical // to the last (unchanged view / geometry / selection / preview), the // resolve texture already holds the image. Skip every geometry pass + @@ -2931,6 +3387,10 @@ impl Pipeline { pass.set_vertex_buffer(0, vbuf.slice(..)); pass.draw(0..*vcount, 0..1); } + pass.set_pipeline(&self.background_pipeline); + pass.set_bind_group(0, &self.uniform_bind_group, &[]); + pass.set_stencil_reference(stencil_ref); + pass.draw(0..3, 0..1); // The capability-selected façade dispatches storage or texture // draws before wires so outlines remain on top in either backend. // Skipped while navigating because per-pixel hatch work dominates diff --git a/src/scene/pipeline/uniforms.rs b/src/scene/pipeline/uniforms.rs index ac6084155..c493081bc 100644 --- a/src/scene/pipeline/uniforms.rs +++ b/src/scene/pipeline/uniforms.rs @@ -53,8 +53,51 @@ pub struct Uniforms { pub light_color_hotspot: [[f32; 4]; 4], /// x = attenuation mode, y/z = start/end limits, w = cosine of falloff. pub light_attenuation: [[f32; 4]; 4], - /// x = active light count, y = fallback ambient strength. + /// x = active light count, yzw = per-viewport ambient-light RGB. pub lighting: [f32; 4], + /// x = viewport face-colour mode, y = viewport face opacity, + /// z = highlight intensity for faces without an attached material, + /// w = visual-style brightness. + pub visual_style: [f32; 4], + /// Viewport visual-style mono/tint colour. + pub visual_style_color: [f32; 4], + /// RGB background selected by the viewport; alpha is one when a drawing + /// background object, rather than the application canvas, supplies it. + pub viewport_background: [f32; 4], + /// x/y = normalized linear brightness/contrast in the documented + /// -10..10 viewport range; z/w are reserved for exposure controls. + pub view_tone: [f32; 4], + pub background_top: [f32; 4], + pub background_middle: [f32; 4], + pub background_bottom: [f32; 4], + pub background_aux0: [f32; 4], + pub background_aux1: [f32; 4], + /// x = mode, y = horizon, z = height, w = rotation in radians. + pub background_params: [f32; 4], + /// x = fit, y = keep aspect, z = tile, w = decoded image aspect. + pub background_image_params: [f32; 4], + /// xy = image offset, zw = image scale. + pub background_image_transform: [f32; 4], + /// x = environment enabled, y = rotation, z = diffuse strength, + /// w = reflection strength. + pub environment_params: [f32; 4], + /// View-to-world rotation used to derive environment lookup directions. + pub environment_view: glam::Mat4, + /// Eye-relative light projection for the first shadow-casting source. + pub shadow_view_proj: glam::Mat4, + /// x = enabled, y = depth bias, z = softness, w = light index. + pub shadow_params: [f32; 4], + /// RGB = document render-environment fog colour. + pub fog_color: [f32; 4], + /// x = enabled, y = affect background, z/w = near/far density. + pub fog_params: [f32; 4], + /// x/y = near/far eye distance used to interpolate the density. + pub fog_distances: [f32; 4], + /// Eight normalized vertical-candela samples for each photometric light. + pub light_web_profile_a: [[f32; 4]; 4], + pub light_web_profile_b: [[f32; 4]; 4], + /// xyz = web rotation, w = profile enabled. + pub light_web_rotation: [[f32; 4]; 4], } impl Uniforms { @@ -83,7 +126,29 @@ impl Uniforms { light_direction_intensity: [[0.0; 4]; 4], light_color_hotspot: [[0.0; 4]; 4], light_attenuation: [[0.0; 4]; 4], - lighting: [0.0, 0.18, 0.0, 0.0], + lighting: [0.0, 0.18, 0.18, 0.18], + visual_style: [1.0, 1.0, -1.0, 0.0], + visual_style_color: [1.0, 1.0, 1.0, 1.0], + viewport_background: [0.0, 0.0, 0.0, 0.0], + view_tone: [0.0; 4], + background_top: [0.0; 4], + background_middle: [0.0; 4], + background_bottom: [0.0; 4], + background_aux0: [0.0; 4], + background_aux1: [0.0; 4], + background_params: [0.0; 4], + background_image_params: [0.0; 4], + background_image_transform: [0.0, 0.0, 1.0, 1.0], + environment_params: [0.0, 0.0, 0.25, 0.35], + environment_view: glam::Mat4::IDENTITY, + shadow_view_proj: glam::Mat4::IDENTITY, + shadow_params: [0.0, 0.0015, 1.0, 0.0], + fog_color: [0.0, 0.0, 0.0, 1.0], + fog_params: [0.0; 4], + fog_distances: [0.0, 1.0, 0.0, 0.0], + light_web_profile_a: [[1.0; 4]; 4], + light_web_profile_b: [[1.0; 4]; 4], + light_web_rotation: [[0.0; 4]; 4], } } } diff --git a/src/scene/render_graph.rs b/src/scene/render_graph.rs index 18eeb358a..235b37c80 100644 --- a/src/scene/render_graph.rs +++ b/src/scene/render_graph.rs @@ -12,8 +12,8 @@ use acadrust::{CadDocument, EntityType, Handle}; use rustc_hash::{FxHashMap, FxHashSet}; use crate::scene::view::render::{ - has_resolved_book_color, is_effective_layer_zero, layer_render_style, - render_style_for, render_style_for_block_sub, InheritStyle, + has_resolved_book_color, is_effective_layer_zero, layer_render_style_viewport, + render_style_for_block_sub_viewport, render_style_for_viewport, InheritStyle, }; pub type ResolvedStyle = ([f32; 4], f32, [f32; 8], f32, u8); @@ -75,12 +75,12 @@ pub struct InsertStyleSpec { } impl InsertStyleSpec { - pub fn new(document: &CadDocument, insert: &Insert) -> Self { + pub fn new(document: &CadDocument, insert: &Insert, viewport: Option) -> Self { let entity = EntityType::Insert(insert.clone()); let has_book_color = has_resolved_book_color(document, &entity); let linetype = &insert.common.linetype; Self { - own: BlockStyle::for_entity(document, &entity), + own: BlockStyle::for_entity(document, &entity, viewport), color_byblock: !has_book_color && insert.common.color == Color::ByBlock, color_bylayer: !has_book_color && insert.common.color == Color::ByLayer, linetype_byblock: linetype.eq_ignore_ascii_case("byblock"), @@ -136,10 +136,14 @@ impl InsertStyleSpec { } impl BlockStyle { - pub fn for_entity(document: &CadDocument, entity: &EntityType) -> Self { + pub fn for_entity( + document: &CadDocument, + entity: &EntityType, + viewport: Option, + ) -> Self { Self { - insert: render_style_for(document, entity), - layer0: layer_render_style(document, &entity.common().layer), + insert: render_style_for_viewport(document, entity, viewport), + layer0: layer_render_style_viewport(document, &entity.common().layer, viewport), layer0_aci: layer_aci(document, &entity.common().layer), } } @@ -148,27 +152,29 @@ impl BlockStyle { document: &CadDocument, insert: &Insert, parent: Self, + viewport: Option, ) -> Self { - InsertStyleSpec::new(document, insert).resolve(parent) + InsertStyleSpec::new(document, insert, viewport).resolve(parent) } pub fn for_owned( document: &CadDocument, entity: &EntityType, parent: Option, + viewport: Option, ) -> Self { let on_layer0 = is_effective_layer_zero(&entity.common().layer); let insert = parent - .map(|style| style.resolve(document, entity)) - .unwrap_or_else(|| render_style_for(document, entity)); + .map(|style| style.resolve(document, entity, viewport)) + .unwrap_or_else(|| render_style_for_viewport(document, entity, viewport)); Self { insert, layer0: if on_layer0 { parent .map(|style| style.layer0) - .unwrap_or_else(|| layer_render_style(document, &entity.common().layer)) + .unwrap_or_else(|| layer_render_style_viewport(document, &entity.common().layer, viewport)) } else { - layer_render_style(document, &entity.common().layer) + layer_render_style_viewport(document, &entity.common().layer, viewport) }, layer0_aci: if on_layer0 { parent @@ -180,8 +186,13 @@ impl BlockStyle { } } - pub fn resolve(self, document: &CadDocument, entity: &EntityType) -> ResolvedStyle { - let mut resolved = render_style_for_block_sub( + pub fn resolve( + self, + document: &CadDocument, + entity: &EntityType, + viewport: Option, + ) -> ResolvedStyle { + let mut resolved = render_style_for_block_sub_viewport( document, entity, self.insert.0, @@ -189,6 +200,7 @@ impl BlockStyle { self.insert.2, self.insert.3, self.layer0, + viewport, ); let common = entity.common(); let has_book_color = has_resolved_book_color(document, entity); @@ -228,10 +240,11 @@ pub struct RenderContext { pub depth_base: f32, pub depth_scale: f32, pub nesting_depth: usize, + pub viewport: Option, } impl RenderContext { - fn direct(depth_base: f32) -> Self { + fn direct(depth_base: f32, viewport: Option) -> Self { Self { transform: Transform::identity(), root_handle: Handle::NULL, @@ -242,6 +255,7 @@ impl RenderContext { depth_base, depth_scale: 1.0, nesting_depth: 0, + viewport, } } @@ -251,8 +265,8 @@ impl RenderContext { pub fn style_for(&self, document: &CadDocument, entity: &EntityType) -> ResolvedStyle { self.block_style - .map(|style| style.resolve(document, entity)) - .unwrap_or_else(|| render_style_for(document, entity)) + .map(|style| style.resolve(document, entity, self.viewport)) + .unwrap_or_else(|| render_style_for_viewport(document, entity, self.viewport)) } pub fn draw_depth(&self, handle: Handle, depths: &FxHashMap) -> f32 { @@ -276,6 +290,7 @@ pub struct RenderSceneGraph<'a> { annotation_scale_handle: Option, all_visible: bool, depths: &'a FxHashMap, + viewport: Option, } impl<'a> RenderSceneGraph<'a> { @@ -292,9 +307,15 @@ impl<'a> RenderSceneGraph<'a> { annotation_scale_handle, all_visible, depths, + viewport: None, } } + pub fn with_viewport(mut self, viewport: Option) -> Self { + self.viewport = viewport.filter(|handle| handle.is_valid()); + self + } + /// Walk direct root entities and every referenced block subtree. `visible` /// can add session-only rules such as isolate/preview hiding; returning /// false for an Insert removes its whole subtree. @@ -304,6 +325,10 @@ impl<'a> RenderSceneGraph<'a> { F: FnMut(&EntityType, &RenderContext), { let block = root.content_block(); + let viewport = match root { + SceneRoot::Viewport { viewport, .. } => Some(viewport), + SceneRoot::Block(_) => self.viewport, + }; let Some(record) = self .document .block_records @@ -326,7 +351,7 @@ impl<'a> RenderSceneGraph<'a> { .depths .get(&handle.value()) .map_or(0.0, |depth| depth[0]); - let context = RenderContext::direct(direct_depth); + let context = RenderContext::direct(direct_depth, viewport); if !self.document_visible(entity) || !visible(entity, &context) { continue; } @@ -358,7 +383,7 @@ impl<'a> RenderSceneGraph<'a> { .depths .get(&root_handle.value()) .map_or(0.0, |depth| depth[0]); - let context = RenderContext::direct(depth_base); + let context = RenderContext::direct(depth_base, self.viewport); if self.document_visible(&entity) && visible(&entity, &context) { self.walk_insert_instances(&root_insert, &context, &mut visible, &mut leaf); } @@ -378,8 +403,8 @@ impl<'a> RenderSceneGraph<'a> { let insert_entity = EntityType::Insert(insert.clone()); let block_style = parent .block_style - .map(|style| BlockStyle::for_nested(self.document, insert, style)) - .unwrap_or_else(|| BlockStyle::for_entity(self.document, &insert_entity)); + .map(|style| BlockStyle::for_nested(self.document, insert, style, self.viewport)) + .unwrap_or_else(|| BlockStyle::for_entity(self.document, &insert_entity, self.viewport)); let root_handle = if parent.root_handle.is_null() { insert.common.handle } else { @@ -525,6 +550,7 @@ impl<'a> RenderSceneGraph<'a> { self.document, entity, context.block_style, + self.viewport, )); owned.nesting_depth += 1; stack.push(block_name.to_string()); diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 7a53003d4..96cb7819c 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -14,9 +14,12 @@ use std::sync::Arc; use crate::scene::pipeline::viewcube::{hover_id, VIEWCUBE_PX}; use crate::scene::pipeline::MultiPipeline; use crate::scene::convert::tess_util; +use crate::scene::model::visual_style_model::{ + resolve_visual_style_handle, MeshVisualStyle, +}; use crate::scene::{ - vp_effective_scale, HatchModel, ImageModel, MeshLodSet, NavPerfSample, Scene, SceneLight, - Uniforms, ViewportInstance, WireModel, + vp_effective_scale, Camera, HatchModel, ImageModel, MeshLodSet, NavPerfSample, Scene, + SceneLight, Uniforms, ViewportInstance, WireModel, }; // ── Camera hover state (shader::Program::State) ─────────────────────────── @@ -26,6 +29,75 @@ pub struct CameraState { pub hover_region: Option, } +#[derive(Clone, Copy)] +struct ViewportLightingSettings { + force_default: bool, + default_type: i16, + ambient: [f32; 3], +} + +#[derive(Clone)] +struct ViewportDisplaySettings { + visual_style: Option, + brightness: f32, + contrast: f32, + background: ViewportBackgroundSettings, +} + +#[derive(Clone, Debug)] +struct ViewportBackgroundSettings { + base: [f32; 4], + colors: [[f32; 4]; 5], + params: [f32; 4], + image_params: [f32; 4], + image_transform: [f32; 4], + environment_params: [f32; 4], + fog_color: [f32; 4], + fog_params: [f32; 4], + fog_distances: [f32; 4], + image: Option, + environment: Option, +} + +impl ViewportBackgroundSettings { + fn canvas(color: [f32; 4]) -> Self { + Self { + base: color, + colors: [[0.0; 4]; 5], + params: [0.0; 4], + image_params: [0.0; 4], + image_transform: [0.0, 0.0, 1.0, 1.0], + environment_params: [0.0, 0.0, 0.25, 0.35], + fog_color: [0.0, 0.0, 0.0, 1.0], + fog_params: [0.0; 4], + fog_distances: [0.0, 1.0, 0.0, 0.0], + image: None, + environment: None, + } + } +} + +impl Default for ViewportDisplaySettings { + fn default() -> Self { + Self { + visual_style: None, + brightness: 0.0, + contrast: 0.0, + background: ViewportBackgroundSettings::canvas([0.0; 4]), + } + } +} + +impl Default for ViewportLightingSettings { + fn default() -> Self { + Self { + force_default: true, + default_type: 1, + ambient: [0.18; 3], + } + } +} + // ── GPU primitive ───────────────────────────────────────────────────────── /// Everything needed to render one viewport: its geometry, camera, render @@ -79,6 +151,10 @@ pub struct ViewportData { pub(in crate::scene) wipeout_hatches: Arc>, pub(in crate::scene) images: Arc>, pub(in crate::scene) meshes: Arc>, + pub(in crate::scene) background_image: + Option, + pub(in crate::scene) environment_image: + Option, pub(in crate::scene) uniforms: Uniforms, /// World-space camera forward — the parallel view direction. DISPSILH /// silhouettes use this alone (not the eye position), so the outline follows @@ -333,6 +409,12 @@ impl shader::Primitive for Primitive { let (uo_x, us_x) = uv_crop_axis(sr.x, sr.width); let (uo_y, us_y) = uv_crop_axis(sr.y, sr.height); inner.upload_blit_uv(queue, [uo_x, uo_y], [us_x, us_y]); + inner.upload_background_images( + device, + queue, + vp.background_image.as_ref(), + vp.environment_image.as_ref(), + ); inner.upload_uniforms(queue, &vp.uniforms); // ── Scene-render cache ──────────────────────────────────────── @@ -1140,6 +1222,16 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 { // one shot. Identical camera state recomputes to identical bits, so a still // view never spuriously misses the cache. bytemuck::bytes_of(&vp.uniforms).hash(&mut h); + vp.background_image + .as_ref() + .map(|image| std::sync::Arc::as_ptr(&image.pixels) as usize) + .unwrap_or(0) + .hash(&mut h); + vp.environment_image + .as_ref() + .map(|image| std::sync::Arc::as_ptr(&image.pixels) as usize) + .unwrap_or(0) + .hash(&mut h); vp.geometry_epoch.hash(&mut h); vp.selection_generation.hash(&mut h); vp.selected_sig.hash(&mut h); @@ -1373,7 +1465,7 @@ impl Scene { if !light.status { return None; } - let direction = normalized( + let mut direction = normalized( [ light.target.x - light.position.x, light.target.y - light.position.y, @@ -1391,14 +1483,110 @@ impl Scene { } else { scene.layer_color(&light.common.layer) }; + let mut color = [rgba[0], rgba[1], rgba[2]]; + let mut intensity = light.intensity.max(0.0) as f32; + let mut attenuation_type = light.attenuation_type as f32; + let mut attenuation_start = if light.use_attenuation_limits { + light.attenuation_start_limit as f32 + } else { + 0.0 + }; + let mut attenuation_end = if light.use_attenuation_limits { + light.attenuation_end_limit as f32 + } else { + 0.0 + }; + let mut area_softness = 0.0_f32; + let mut web_profile = [1.0_f32; 8]; + let mut web_rotation = [0.0_f32; 3]; + let mut web_enabled = false; + if light.photometric_mode { + if let Some(photo) = light.photometric_data.as_ref() { + let solid_angle = if light.is_spot() && light.falloff_angle > 0.0 { + 2.0 * std::f64::consts::PI + * (1.0 - (light.falloff_angle * 0.5).cos()) + } else { + 4.0 * std::f64::consts::PI + }; + let physical = if photo.physical_intensity > 0.0 { + photo.physical_intensity + } else { + photo.web_flux.max(0.0) + }; + let candela = match photo.physical_intensity_method { + 1 => physical / solid_angle.max(1e-6), + 2 => physical * photo.illuminance_distance.max(1e-6).powi(2), + _ => physical, + }; + if candela > 0.0 && candela.is_finite() { + intensity *= (candela / 1500.0) as f32; + } + if photo.lamp_color_temperature >= 1000.0 { + let kelvin = photo.lamp_color_temperature.clamp(1000.0, 40000.0) / 100.0; + let red = if kelvin <= 66.0 { + 255.0 + } else { + 329.698_727_446 * (kelvin - 60.0).powf(-0.133_204_759_2) + }; + let green = if kelvin <= 66.0 { + 99.470_802_586_1 * kelvin.ln() - 161.119_568_166_1 + } else { + 288.122_169_528_3 * (kelvin - 60.0).powf(-0.075_514_849_2) + }; + let blue = if kelvin >= 66.0 { + 255.0 + } else if kelvin <= 19.0 { + 0.0 + } else { + 138.517_731_223_1 * (kelvin - 10.0).ln() - 305.044_792_730_7 + }; + let lamp = [red, green, blue].map(|channel| { + (channel.clamp(0.0, 255.0) / 255.0) as f32 + }); + for index in 0..3 { + color[index] *= lamp[index]; + } + } + attenuation_type = 2.0; + attenuation_start = 0.0; + attenuation_end = 0.0; + area_softness = match photo.extended_light_shape { + 1 => photo.extended_light_length, + 2 => photo.extended_light_length.max(photo.extended_light_width), + 3 => photo.extended_light_radius * 2.0, + _ => 0.0, + } + .max(0.0) as f32; + if photo.has_web_file { + if let Some(profile) = scene.photometric_web_profile(&photo.web_file) { + web_profile = profile; + web_rotation = [ + photo.web_rotation.x as f32, + photo.web_rotation.y as f32, + photo.web_rotation.z as f32, + ]; + let rotation = glam::Quat::from_euler( + glam::EulerRot::XYZ, + web_rotation[0], + web_rotation[1], + web_rotation[2], + ); + direction = (rotation * glam::Vec3::from_array(direction)) + .normalize_or(glam::Vec3::NEG_Z) + .to_array(); + web_enabled = true; + } + } + } + } Some(SceneLight { handle: light.common.handle, color_layer, light_type: light.light_type as f32, position: [light.position.x, light.position.y, light.position.z], direction, - color: [rgba[0], rgba[1], rgba[2]], - intensity: light.intensity.max(0.0) as f32, + color, + intensity, hotspot_cos: if light.hotspot_angle > 0.0 { (light.hotspot_angle * 0.5).cos() as f32 } else { @@ -1409,17 +1597,16 @@ impl Scene { } else { -1.0 }, - attenuation_type: light.attenuation_type as f32, - attenuation_start: if light.use_attenuation_limits { - light.attenuation_start_limit as f32 - } else { - 0.0 - }, - attenuation_end: if light.use_attenuation_limits { - light.attenuation_end_limit as f32 - } else { - 0.0 - }, + attenuation_type, + attenuation_start, + attenuation_end, + cast_shadows: light.cast_shadows, + shadow_softness: light.shadow_map_softness as f32 / 255.0 + area_softness, + shadow_map_size: u32::try_from(light.shadow_map_size.max(0)) + .unwrap_or(0), + web_profile, + web_rotation, + web_enabled, }) } @@ -1487,6 +1674,12 @@ impl Scene { attenuation_type: 0.0, attenuation_start: 0.0, attenuation_end: 0.0, + cast_shadows: sun.has_shadow, + shadow_softness: sun.shadow_softness as f32 / 255.0, + shadow_map_size: u32::try_from(sun.shadow_map_size.max(0)).unwrap_or(0), + web_profile: [1.0; 8], + web_rotation: [0.0; 3], + web_enabled: false, }); break; } @@ -1498,6 +1691,7 @@ impl Scene { uniforms: &mut Uniforms, target_block: Handle, frozen: &rustc_hash::FxHashSet, + viewport: &ViewportInstance, ) { let key = (target_block, Self::frozen_layers_sig(frozen)); if !self.lighting_cache.borrow().contains_key(&key) { @@ -1532,13 +1726,19 @@ impl Scene { }) .take(4) .collect(); + let settings = self.viewport_lighting_settings(viewport); + uniforms.lighting[1..4].copy_from_slice(&settings.ambient); + if settings.force_default || visible_lights.is_empty() { + Self::apply_default_lighting(uniforms, &viewport.camera, settings.default_type); + return; + } let eye = [ uniforms.eye_high[0] as f64 + uniforms.eye_low[0] as f64, uniforms.eye_high[1] as f64 + uniforms.eye_low[1] as f64, uniforms.eye_high[2] as f64 + uniforms.eye_low[2] as f64, ]; uniforms.lighting[0] = visible_lights.len() as f32; - for (index, light) in visible_lights.into_iter().enumerate() { + for (index, light) in visible_lights.iter().copied().enumerate() { let color = light .color_layer .as_deref() @@ -1569,6 +1769,575 @@ impl Scene { light.attenuation_end, light.falloff_cos, ]; + uniforms.light_web_profile_a[index] + .copy_from_slice(&light.web_profile[..4]); + uniforms.light_web_profile_b[index] + .copy_from_slice(&light.web_profile[4..]); + uniforms.light_web_rotation[index] = [ + light.web_rotation[0], + light.web_rotation[1], + light.web_rotation[2], + light.web_enabled as u8 as f32, + ]; + } + if let Some((index, light)) = visible_lights + .iter() + .enumerate() + .find(|(_, light)| light.cast_shadows) + { + let target = (viewport.camera.target - viewport.camera.eye()).as_vec3(); + let up_for = |direction: glam::Vec3| { + if direction.z.abs() > 0.95 { + glam::Vec3::Y + } else { + glam::Vec3::Z + } + }; + let radius = viewport + .camera + .ortho_size() + .max(viewport.camera.distance * 0.25) + .max(1.0); + let shadow_view_proj = if light.light_type < 1.5 { + let direction = glam::Vec3::from_array(light.direction) + .normalize_or(glam::Vec3::NEG_Z); + let light_eye = target - direction * radius * 2.0; + let view = glam::camera::rh::view::look_at_mat4( + light_eye, + target, + up_for(direction), + ); + let aspect = (uniforms.viewport_size[0] / uniforms.viewport_size[1].max(1.0)) + .max(1.0); + let projection = glam::camera::rh::proj::directx::orthographic( + -radius * aspect, + radius * aspect, + -radius, + radius, + 0.01, + radius * 5.0, + ); + projection * view + } else { + let position = glam::Vec3::new( + uniforms.light_position_type[index][0], + uniforms.light_position_type[index][1], + uniforms.light_position_type[index][2], + ); + let direction = if light.light_type < 2.5 { + (target - position).normalize_or(glam::Vec3::NEG_Z) + } else { + glam::Vec3::from_array(light.direction) + .normalize_or(glam::Vec3::NEG_Z) + }; + let view = glam::camera::rh::view::look_at_mat4( + position, + position + direction, + up_for(direction), + ); + let fov = if light.light_type < 2.5 { + 170.0_f32.to_radians() + } else { + (2.0 * light.falloff_cos.clamp(-0.99, 0.99).acos()) + .clamp(5.0_f32.to_radians(), 170.0_f32.to_radians()) + }; + let far = if light.attenuation_end > 0.01 { + light.attenuation_end + } else { + radius * 5.0 + }; + glam::camera::rh::proj::directx::perspective( + fov, + 1.0, + 0.01, + far.max(0.02), + ) * view + }; + uniforms.shadow_view_proj = shadow_view_proj; + let requested_size = light.shadow_map_size.max(256) as f32; + uniforms.shadow_params = [ + 1.0, + (2.0 / requested_size).clamp(0.0002, 0.004), + light.shadow_softness.clamp(0.0, 2.0), + index as f32, + ]; + } + } + + fn viewport_lighting_settings( + &self, + viewport: &ViewportInstance, + ) -> ViewportLightingSettings { + let ambient = |color: &AcadColor| { + color.rgb().map_or([0.18; 3], |(r, g, b)| { + [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0] + }) + }; + let from_entity = |value: &acadrust::entities::Viewport| ViewportLightingSettings { + force_default: value.default_lighting, + default_type: value.default_lighting_type, + ambient: ambient(&value.ambient_color), + }; + let from_table = |value: &acadrust::tables::VPort| ViewportLightingSettings { + force_default: value.use_default_lights, + default_type: value.default_lighting_type, + ambient: ambient(&value.ambient_color), + }; + + if viewport.paper_sheet { + let handle = self.current_layout_sheet_viewport_handle(); + if let Some(EntityType::Viewport(value)) = self.document.get_entity(handle) { + return from_entity(value); + } + } else if viewport.handle.is_valid() { + if let Some(EntityType::Viewport(value)) = self.document.get_entity(viewport.handle) { + return from_entity(value); + } + } else if self.current_layout == "Model" { + let index = viewport.tile_idx.unwrap_or(0); + if let Some(value) = self + .document + .vports + .iter() + .filter(|value| value.name.eq_ignore_ascii_case("*Active")) + .nth(index) + { + return from_table(value); + } + } + ViewportLightingSettings::default() + } + + fn viewport_display_settings( + &self, + viewport: &ViewportInstance, + ) -> ViewportDisplaySettings { + let canvas_background = if viewport.paper_sheet { + self.paper_bg_color + } else { + self.bg_color + }; + let build = |visual_style_handle: Handle, + brightness: f64, + contrast: f64, + background_handle: Handle| { + let visual_style = resolve_visual_style_handle( + &self.document, + visual_style_handle, + ); + let mut background = self + .viewport_background(background_handle, canvas_background, 0); + self.apply_document_render_environment(&mut background); + ViewportDisplaySettings { + visual_style, + brightness: (brightness as f32 / 10.0).clamp(-1.0, 1.0), + contrast: (contrast as f32 / 10.0).clamp(-1.0, 1.0), + background, + } + }; + + if viewport.paper_sheet { + let handle = self.current_layout_sheet_viewport_handle(); + if let Some(EntityType::Viewport(value)) = self.document.get_entity(handle) { + return build( + value.visual_style_handle, + value.brightness, + value.contrast, + value.background_handle, + ); + } + } else if viewport.handle.is_valid() { + if let Some(EntityType::Viewport(value)) = self.document.get_entity(viewport.handle) { + return build( + value.visual_style_handle, + value.brightness, + value.contrast, + value.background_handle, + ); + } + } else if self.current_layout == "Model" { + let index = viewport.tile_idx.unwrap_or(0); + if let Some(value) = self + .document + .vports + .iter() + .filter(|value| value.name.eq_ignore_ascii_case("*Active")) + .nth(index) + { + return build( + value.visual_style_handle, + value.brightness, + value.contrast, + value.background_handle, + ); + } + } + + let mut background = ViewportBackgroundSettings::canvas(canvas_background); + self.apply_document_render_environment(&mut background); + ViewportDisplaySettings { + background, + ..ViewportDisplaySettings::default() + } + } + + fn packed_background_color(color: u32) -> [f32; 4] { + [ + ((color >> 16) & 0xff) as f32 / 255.0, + ((color >> 8) & 0xff) as f32 / 255.0, + (color & 0xff) as f32 / 255.0, + 1.0, + ] + } + + fn background_image( + &self, + reference: &str, + ) -> Option { + let reference = reference.trim(); + if reference.is_empty() { + return None; + } + if reference.starts_with("http://") || reference.starts_with("https://") { + return crate::scene::model::image_model::resolve_image(reference); + } + let normalized = reference.replace('\\', "/"); + let source = std::path::PathBuf::from(&normalized); + let mut candidates = Vec::new(); + if source.is_absolute() { + candidates.push(source.clone()); + } + if let Some(base) = self.material_base_dir.as_deref() { + candidates.push(base.join(&source)); + if let Some(name) = source.file_name() { + candidates.push(base.join(name)); + for folder in ["Textures", "textures", "Materials", "materials"] { + candidates.push(base.join(folder).join(name)); + } + } + } + candidates + .into_iter() + .find(|path| path.is_file()) + .and_then(|path| { + crate::scene::model::image_model::resolve_image(&path.to_string_lossy()) + }) + } + + fn photometric_web_profile(&self, reference: &str) -> Option<[f32; 8]> { + #[cfg(target_arch = "wasm32")] + { + let _ = (self, reference); + None + } + #[cfg(not(target_arch = "wasm32"))] + { + let reference = reference.trim(); + if reference.is_empty() { + return None; + } + let source = std::path::PathBuf::from(reference.replace('\\', "/")); + let mut candidates = Vec::new(); + if source.is_absolute() { + candidates.push(source.clone()); + } + if let Some(base) = self.material_base_dir.as_deref() { + candidates.push(base.join(&source)); + if let Some(name) = source.file_name() { + candidates.push(base.join(name)); + for folder in ["Photometric", "photometric", "Web", "web", "Lights", "lights"] { + candidates.push(base.join(folder).join(name)); + } + } + } + let path = candidates.into_iter().find(|path| path.is_file())?; + let contents = std::fs::read_to_string(path).ok()?; + let tilt_start = contents.lines().position(|line| { + line.trim_start().to_ascii_uppercase().starts_with("TILT=") + })?; + let lines: Vec<&str> = contents.lines().collect(); + let tilt = lines[tilt_start] + .trim() + .split_once('=') + .map(|(_, value)| value.trim().to_ascii_uppercase())?; + if tilt != "NONE" && tilt != "INCLUDE" { + return None; + } + let values: Vec = lines[tilt_start + 1..] + .iter() + .flat_map(|line| line.split(|character: char| character.is_whitespace() || character == ',')) + .filter(|token| !token.is_empty()) + .filter_map(|token| token.parse::().ok()) + .collect(); + let mut cursor = 0usize; + if tilt == "INCLUDE" { + let angle_count = values.get(cursor + 1).copied()?.round() as usize; + cursor = cursor.checked_add(2 + angle_count.checked_mul(2)?)?; + } + let header = values.get(cursor..cursor + 13)?; + let candela_multiplier = header[2].max(0.0); + let vertical_count = header[3].round() as usize; + let horizontal_count = header[4].round() as usize; + if vertical_count < 2 + || horizontal_count == 0 + || vertical_count > 4096 + || horizontal_count > 4096 + { + return None; + } + cursor += 13; + let vertical = values.get(cursor..cursor + vertical_count)?; + cursor += vertical_count; + cursor += horizontal_count; + let candela_count = vertical_count.checked_mul(horizontal_count)?; + let candela = values.get(cursor..cursor + candela_count)?; + let sample_row = |row: &[f64], angle: f64| { + if angle <= vertical[0] { + return row[0]; + } + if angle >= vertical[vertical_count - 1] { + return row[vertical_count - 1]; + } + let upper = vertical.partition_point(|candidate| *candidate < angle); + let lower = upper.saturating_sub(1); + let span = (vertical[upper] - vertical[lower]).max(1e-9); + let amount = (angle - vertical[lower]) / span; + row[lower] + (row[upper] - row[lower]) * amount + }; + let mut profile = [0.0_f32; 8]; + for (sample, value) in profile.iter_mut().enumerate() { + let angle = sample as f64 * 180.0 / 7.0; + let sum = candela + .chunks_exact(vertical_count) + .map(|row| sample_row(row, angle)) + .sum::(); + *value = (sum * candela_multiplier / horizontal_count as f64).max(0.0) as f32; + } + let peak = profile.iter().copied().fold(0.0_f32, f32::max); + if !peak.is_finite() || peak <= 1e-9 { + return None; + } + for value in &mut profile { + *value = (*value / peak).clamp(0.0, 1.0); + } + Some(profile) + } + } + + fn apply_document_render_environment( + &self, + background: &mut ViewportBackgroundSettings, + ) { + use acadrust::objects::{ClassObjectData, ObjectType}; + + let environment = self + .document + .objects + .iter() + .filter_map(|(handle, object)| match object { + ObjectType::ClassObject(value) => match &value.data { + ClassObjectData::RenderEnvironment(environment) => { + Some((*handle, environment)) + } + _ => None, + }, + _ => None, + }) + .min_by_key(|(handle, _)| handle.value()) + .map(|(_, environment)| environment); + + if let Some(environment) = environment { + if environment.fog_enabled { + background.fog_color = [ + environment.fog_color[0] as f32 / 255.0, + environment.fog_color[1] as f32 / 255.0, + environment.fog_color[2] as f32 / 255.0, + 1.0, + ]; + let normalized_density = |density: f64| { + let density = density.max(0.0) as f32; + if density > 1.0 { + (density / 100.0).clamp(0.0, 1.0) + } else { + density.clamp(0.0, 1.0) + } + }; + background.fog_params = [ + 1.0, + environment.fog_background_enabled as u8 as f32, + normalized_density(environment.fog_density_near), + normalized_density(environment.fog_density_far), + ]; + let near = environment.fog_distance_near.max(0.0) as f32; + let far = environment.fog_distance_far.max(near as f64 + 1e-6) as f32; + background.fog_distances = [near, far, 0.0, 0.0]; + } + if background.environment.is_none() && environment.environment_image_enabled { + if let Some(image) = self.background_image(&environment.environment_image_filename) { + background.environment_params = [1.0, 0.0, 0.25, 0.35]; + background.environment = Some(image); + } + } + } + + if background.environment.is_some() { + return; + } + let preset = self + .document + .objects + .iter() + .filter_map(|(handle, object)| match object { + ObjectType::ClassObject(value) => { + let settings = match &value.data { + ClassObjectData::RenderSettings(settings) => settings, + ClassObjectData::MentalRayRenderSettings(settings) => &settings.base, + ClassObjectData::RapidRtRenderSettings(settings) => &settings.base, + _ => return None, + }; + (settings.environment_image_enabled + && !settings.environment_image_filename.is_empty()) + .then_some((*handle, settings)) + } + _ => None, + }) + .min_by_key(|(handle, settings)| (!settings.has_predefined, handle.value())) + .map(|(_, settings)| settings); + if let Some(settings) = preset { + if let Some(image) = self.background_image(&settings.environment_image_filename) { + background.environment_params = [1.0, 0.0, 0.25, 0.35]; + background.environment = Some(image); + } + } + } + + fn viewport_background( + &self, + handle: Handle, + canvas: [f32; 4], + depth: usize, + ) -> ViewportBackgroundSettings { + use acadrust::objects::{ClassObjectData, ObjectType}; + + if depth > 4 || !handle.is_valid() { + return ViewportBackgroundSettings::canvas(canvas); + } + let Some(ObjectType::ClassObject(value)) = self.document.objects.get(&handle) else { + return ViewportBackgroundSettings::canvas(canvas); + }; + match &value.data { + ClassObjectData::SolidBackground(background) => { + let mut result = ViewportBackgroundSettings::canvas( + Self::packed_background_color(background.color), + ); + result.params[0] = 1.0; + result + } + ClassObjectData::GradientBackground(background) => { + let mut result = ViewportBackgroundSettings::canvas(canvas); + result.colors[0] = Self::packed_background_color(background.color_top); + result.colors[1] = Self::packed_background_color(background.color_middle); + result.colors[2] = Self::packed_background_color(background.color_bottom); + result.base = result.colors[1]; + result.params = [ + 2.0, + background.horizon as f32, + background.height as f32, + background.rotation as f32, + ]; + result + } + ClassObjectData::GroundPlaneBackground(background) => { + let mut result = ViewportBackgroundSettings::canvas(canvas); + result.colors[0] = Self::packed_background_color(background.color_sky_zenith); + result.colors[1] = Self::packed_background_color(background.color_sky_horizon); + result.colors[2] = + Self::packed_background_color(background.color_underground_horizon); + result.colors[3] = + Self::packed_background_color(background.color_underground_azimuth); + result.colors[4] = Self::packed_background_color(background.color_near); + result.base = Self::packed_background_color(background.color_far); + result.params[0] = 3.0; + result + } + ClassObjectData::ImageBackground(background) => { + let mut result = ViewportBackgroundSettings::canvas(canvas); + if let Some(image) = self.background_image(&background.filename) { + result.image_params = [ + background.fit_to_screen as u8 as f32, + background.maintain_aspect_ratio as u8 as f32, + background.use_tiling as u8 as f32, + image.width as f32 / image.height.max(1) as f32, + ]; + result.image_transform = [ + background.offset.x as f32, + background.offset.y as f32, + background.scale.x as f32, + background.scale.y as f32, + ]; + result.params[0] = 4.0; + result.image = Some(image); + } + result + } + ClassObjectData::IblBackground(background) => { + let mut result = self.viewport_background( + background.secondary_background, + canvas, + depth + 1, + ); + if background.enabled { + if let Some(environment) = self.background_image(&background.name) { + result.environment_params = [ + 1.0, + background.rotation as f32, + 0.25, + 0.35, + ]; + if background.display_image { + result.params = [5.0, 0.0, 0.0, background.rotation as f32]; + result.image_params[3] = + environment.width as f32 / environment.height.max(1) as f32; + result.image = Some(environment.clone()); + } + result.environment = Some(environment); + } + } + result + } + ClassObjectData::SkyLightBackground(_) => { + let mut result = ViewportBackgroundSettings::canvas(canvas); + result.colors[0] = [0.18, 0.42, 0.78, 1.0]; + result.colors[2] = [0.78, 0.86, 0.95, 1.0]; + result.base = result.colors[2]; + result.params[0] = 6.0; + result + } + _ => ViewportBackgroundSettings::canvas(canvas), + } + } + + fn apply_default_lighting(uniforms: &mut Uniforms, camera: &Camera, default_type: i16) { + let sources = [ + (glam::Vec3::new(0.5, 0.8, 0.6), 0.6_f32), + (glam::Vec3::new(-0.7, 0.3, 0.4), 0.4_f32), + ]; + let count = if default_type == 0 { 1 } else { 2 }; + uniforms.lighting[0] = count as f32; + for (index, (view_direction, two_light_intensity)) in + sources.into_iter().take(count).enumerate() + { + let toward_source = (camera.rotation * view_direction).normalize_or_zero(); + let intensity = if count == 1 { 1.0 } else { two_light_intensity }; + uniforms.light_position_type[index][3] = 1.0; + uniforms.light_direction_intensity[index] = [ + -toward_source.x, + -toward_source.y, + -toward_source.z, + intensity, + ]; + uniforms.light_color_hotspot[index] = [1.0, 1.0, 1.0, 1.0]; + uniforms.light_attenuation[index][3] = -1.0; } } @@ -1607,8 +2376,28 @@ pub(in crate::scene) fn layer_locked(document: &CadDocument, e: &EntityType) -> /// Resolves the effective linetype name for an entity, falling back to the /// layer's linetype when the entity's own linetype is "ByLayer". pub(in crate::scene) fn linetype_name_for<'a>(document: &'a CadDocument, e: &'a EntityType) -> &'a str { + linetype_name_for_viewport(document, e, None) +} + +pub(in crate::scene) fn linetype_name_for_viewport<'a>( + document: &'a CadDocument, + e: &'a EntityType, + viewport: Option, +) -> &'a str { let elt = &e.common().linetype; if elt.is_empty() || elt.eq_ignore_ascii_case("bylayer") { + if let Some(handle) = viewport_override( + document, + &e.common().layer, + viewport, + acadrust::objects::KnownXRecordKind::LayerViewportLinetypeOverride, + ) + .and_then(|value| value.as_handle()) + { + if let Some(line_type) = document.line_types.iter().find(|line_type| line_type.handle == handle) { + return line_type.name.as_str(); + } + } document .layers .get(&e.common().layer) @@ -1624,6 +2413,28 @@ pub(in crate::scene) fn linetype_name_for<'a>(document: &'a CadDocument, e: &'a pub(in crate::scene) fn render_style_for( document: &CadDocument, e: &EntityType, +) -> ([f32; 4], f32, [f32; 8], f32, u8) { + render_style_for_viewport(document, e, None) +} + +fn viewport_override( + document: &CadDocument, + layer_name: &str, + viewport: Option, + kind: acadrust::objects::KnownXRecordKind, +) -> Option { + let viewport = viewport.filter(|handle| handle.is_valid())?; + let layer = document.layers.get(layer_name)?; + document + .layer_viewport_overrides(layer.handle, kind) + .into_iter() + .find_map(|(handle, value)| (handle == viewport).then_some(value)) +} + +pub(in crate::scene) fn render_style_for_viewport( + document: &CadDocument, + e: &EntityType, + viewport: Option, ) -> ([f32; 4], f32, [f32; 8], f32, u8) { let layer_name = &e.common().layer; let (entity_color, aci) = { @@ -1637,12 +2448,26 @@ pub(in crate::scene) fn render_style_for( _ => None, }); let ec = book_color.unwrap_or(&common.color); + let viewport_color = (!book_color.is_some() && *ec == AcadColor::ByLayer) + .then(|| { + viewport_override( + document, + layer_name, + viewport, + acadrust::objects::KnownXRecordKind::LayerViewportColorOverride, + ) + .and_then(|value| value.as_i32()) + .map(AcadColor::from_true_color_value) + }) + .flatten(); let resolved = if *ec == AcadColor::ByLayer { - document - .layers - .get(layer_name) - .map(|l| &l.color) - .unwrap_or(&AcadColor::WHITE) + viewport_color.as_ref().unwrap_or_else(|| { + document + .layers + .get(layer_name) + .map(|l| &l.color) + .unwrap_or(&AcadColor::WHITE) + }) } else { ec }; @@ -1652,11 +2477,16 @@ pub(in crate::scene) fn render_style_for( }; let [r, g, b, _] = tess_util::aci_to_rgba(resolved); let transparency = if common.transparency.alpha() == 0 { - document - .layers - .get(layer_name) - .map(|layer| layer.transparency) - .unwrap_or(common.transparency) + viewport_override( + document, + layer_name, + viewport, + acadrust::objects::KnownXRecordKind::LayerViewportAlphaOverride, + ) + .and_then(|value| value.as_i32()) + .map(|value| acadrust::types::Transparency::from_alpha_value(value as u32)) + .or_else(|| document.layers.get(layer_name).map(|layer| layer.transparency)) + .unwrap_or(common.transparency) } else { common.transparency }; @@ -1664,7 +2494,7 @@ pub(in crate::scene) fn render_style_for( ([r, g, b, alpha], aci) }; - let lt_name = linetype_name_for(document, e); + let lt_name = linetype_name_for_viewport(document, e, viewport); // Effective scale = global LTSCALE × per-entity scale (both default to 1.0). let lt_scale = document.header.linetype_scale as f32 * e.common().linetype_scale as f32; let (pattern_length, pattern) = resolve_pattern(&document.line_types, lt_name, lt_scale); @@ -1675,11 +2505,22 @@ pub(in crate::scene) fn render_style_for( // entity's resolved (layer-inherited) weight. Toggling lineweight // visibility costs only a uniform write, not a retessellate. let ew = &e.common().line_weight; + let viewport_lineweight = matches!(ew, LineWeight::ByLayer | LineWeight::Default) + .then(|| { + viewport_override( + document, + layer_name, + viewport, + acadrust::objects::KnownXRecordKind::LayerViewportLineweightOverride, + ) + .and_then(|value| value.as_i32()) + .map(|value| LineWeight::from_value(value as i16)) + }) + .flatten(); let resolved = match ew { - LineWeight::ByLayer | LineWeight::ByBlock | LineWeight::Default => document - .layers - .get(layer_name) - .map(|l| &l.line_weight) + LineWeight::ByLayer | LineWeight::ByBlock | LineWeight::Default => viewport_lineweight + .as_ref() + .or_else(|| document.layers.get(layer_name).map(|l| &l.line_weight)) .unwrap_or(&LineWeight::Default), _ => ew, }; @@ -1730,16 +2571,64 @@ pub(crate) fn lineweight_to_px(lw: &LineWeight) -> f32 { /// happens at emit time). Falls back to white / Continuous / 1 px when the /// layer is missing. pub(crate) fn layer_render_style(document: &CadDocument, layer_name: &str) -> InheritStyle { + layer_render_style_viewport(document, layer_name, None) +} + +pub(crate) fn layer_render_style_viewport( + document: &CadDocument, + layer_name: &str, + viewport: Option, +) -> InheritStyle { let layer = document.layers.get(layer_name); - let color = layer.map(|l| &l.color).unwrap_or(&AcadColor::WHITE); + let viewport_color = viewport_override( + document, + layer_name, + viewport, + acadrust::objects::KnownXRecordKind::LayerViewportColorOverride, + ) + .and_then(|value| value.as_i32()) + .map(AcadColor::from_true_color_value); + let color = viewport_color + .as_ref() + .or_else(|| layer.map(|layer| &layer.color)) + .unwrap_or(&AcadColor::WHITE); let [r, g, b, _] = tess_util::aci_to_rgba(color); - let alpha = layer - .map(|layer| 1.0 - layer.transparency.as_percent() as f32) + let alpha = viewport_override( + document, + layer_name, + viewport, + acadrust::objects::KnownXRecordKind::LayerViewportAlphaOverride, + ) + .and_then(|value| value.as_i32()) + .map(|value| acadrust::types::Transparency::from_alpha_value(value as u32)) + .or_else(|| layer.map(|layer| layer.transparency)) + .map(|transparency| 1.0 - transparency.as_percent() as f32) .unwrap_or(1.0); - let lt_name = layer.map(|l| l.line_type.as_str()).unwrap_or("Continuous"); + let lt_name = viewport_override( + document, + layer_name, + viewport, + acadrust::objects::KnownXRecordKind::LayerViewportLinetypeOverride, + ) + .and_then(|value| value.as_handle()) + .and_then(|handle| document.line_types.iter().find(|line_type| line_type.handle == handle)) + .map(|line_type| line_type.name.as_str()) + .or_else(|| layer.map(|layer| layer.line_type.as_str())) + .unwrap_or("Continuous"); let lt_scale = document.header.linetype_scale as f32; let (pat_len, pat) = resolve_pattern(&document.line_types, lt_name, lt_scale); - let lw = layer.map(|l| &l.line_weight).unwrap_or(&LineWeight::Default); + let viewport_lineweight = viewport_override( + document, + layer_name, + viewport, + acadrust::objects::KnownXRecordKind::LayerViewportLineweightOverride, + ) + .and_then(|value| value.as_i32()) + .map(|value| LineWeight::from_value(value as i16)); + let lw = viewport_lineweight + .as_ref() + .or_else(|| layer.map(|layer| &layer.line_weight)) + .unwrap_or(&LineWeight::Default); InheritStyle { color: [r, g, b, alpha], pat_len, @@ -1774,7 +2663,29 @@ pub(crate) fn render_style_for_block_sub( insert_lw_px: f32, l0: InheritStyle, ) -> ([f32; 4], f32, [f32; 8], f32, u8) { - let (color, pat_len, pat, lw_px, aci) = render_style_for(document, e); + render_style_for_block_sub_viewport( + document, + e, + insert_color, + insert_pat_len, + insert_pat, + insert_lw_px, + l0, + None, + ) +} + +pub(crate) fn render_style_for_block_sub_viewport( + document: &CadDocument, + e: &EntityType, + insert_color: [f32; 4], + insert_pat_len: f32, + insert_pat: [f32; 8], + insert_lw_px: f32, + l0: InheritStyle, + viewport: Option, +) -> ([f32; 4], f32, [f32; 8], f32, u8) { + let (color, pat_len, pat, lw_px, aci) = render_style_for_viewport(document, e, viewport); let common = e.common(); let on_l0 = is_effective_layer_zero(&common.layer); @@ -2076,7 +2987,7 @@ impl Scene { } _ => annotation_scale, }; - let block_cache = self.block_cache_arc_for(Some(scale), true); + let block_cache = self.block_cache_arc_for(Some(scale), true, self.active_viewport); let mut context_wires = crate::scene::tessellate_entity( &self.document, &empty_selection, @@ -2247,7 +3158,18 @@ impl Scene { hover_region: Option, show_viewcube: bool, ) -> Option { - let flags = render_mode_flags(inst.render_mode); + let display = self.viewport_display_settings(inst); + let mut flags = render_mode_flags(inst.render_mode); + if let Some(style) = display.visual_style.as_ref() { + if flags.mesh_fill { + flags.face3d_fill &= style.face_visible(); + flags.mesh_fill &= style.face_visible(); + flags.show_3d_edges = style.edges_visible(); + if style.face_lighting_quality == 1 { + flags.flat_shade = true; + } + } + } let view_wireframe = !flags.face3d_fill; // Clip the viewport rect to the canvas; size the per-viewport MSAA @@ -2463,7 +3385,42 @@ impl Scene { uniforms.viewport_size = [visible_w, visible_h]; uniforms.flat_shade = if flags.flat_shade { 1.0 } else { 0.0 }; uniforms.transparency_enable = if self.transparency_display { 1.0 } else { 0.0 }; - self.apply_document_lighting(&mut uniforms, lighting_block, &vp_frozen); + uniforms.viewport_background = display.background.base; + uniforms.view_tone = [display.brightness, display.contrast, 0.0, 0.0]; + uniforms.background_top = display.background.colors[0]; + uniforms.background_middle = display.background.colors[1]; + uniforms.background_bottom = display.background.colors[2]; + uniforms.background_aux0 = display.background.colors[3]; + uniforms.background_aux1 = display.background.colors[4]; + uniforms.background_params = display.background.params; + uniforms.background_image_params = display.background.image_params; + uniforms.background_image_transform = display.background.image_transform; + uniforms.environment_params = display.background.environment_params; + uniforms.environment_view = glam::Mat4::from_quat(inst.camera.rotation); + uniforms.fog_color = display.background.fog_color; + uniforms.fog_params = display.background.fog_params; + uniforms.fog_distances = display.background.fog_distances; + if let Some(style) = display.visual_style.as_ref() { + let opacity = if style.face_modifier & 1 != 0 { + style.face_opacity + } else { + 1.0 + }; + let highlight = if style.face_modifier & 2 != 0 { + (style.face_specular / 100.0).clamp(0.0, 1.0) + } else { + -1.0 + }; + uniforms.visual_style = [ + style.face_color_mode as f32, + opacity, + highlight, + (style.brightness / 10.0).clamp(-1.0, 1.0), + ]; + uniforms.visual_style_color = + style.mono_color.unwrap_or([1.0, 1.0, 1.0, 1.0]); + } + self.apply_document_lighting(&mut uniforms, lighting_block, &vp_frozen, inst); // `screen_rect` carries the *visible* sub-rectangle in normalized // canvas coords — that's what `Pipeline::prepare` uses to size @@ -2609,6 +3566,8 @@ impl Scene { wipeout_hatches, images, meshes, + background_image: display.background.image.clone(), + environment_image: display.background.environment.clone(), uniforms, view_dir: (inst.camera.rotation * glam::Vec3::NEG_Z).normalize_or(glam::Vec3::NEG_Z), cam_rotation: inst.camera.view_rotation_mat() * self.viewcube_ucs_mat(), @@ -2624,7 +3583,11 @@ impl Scene { show_2d_solid_fills: flags.show_2d_solid_fills, mesh_fill: flags.mesh_fill, show_3d_edges: flags.show_3d_edges, - display_silhouette: flags.hidden_line || self.document.header.display_silhouette, + display_silhouette: flags.hidden_line + || display.visual_style.as_ref().is_some_and(|style| { + style.silhouette_width > 0 && style.edges_visible() + }) + || self.document.header.display_silhouette, hidden_line: flags.hidden_line, // Interaction LOD: suppress the costly hatch pass while the view is // actively moving; the scene-render cache holds the full-quality diff --git a/src/shaders/background.wgsl b/src/shaders/background.wgsl new file mode 100644 index 000000000..12b33205d --- /dev/null +++ b/src/shaders/background.wgsl @@ -0,0 +1,139 @@ +struct Uniforms { + viewport_size: vec2, + world_per_pixel: f32, + lwdisplay_enable: f32, + flat_shade: f32, + transparency_enable: f32, + linetype_scale: f32, + _pad: f32, + view_rot: mat4x4, + eye_high: vec3, + _pad_eh: f32, + eye_low: vec3, + _pad_el: f32, + light_position_type: array, 4>, + light_direction_intensity: array, 4>, + light_color_hotspot: array, 4>, + light_attenuation: array, 4>, + lighting: vec4, + visual_style: vec4, + visual_style_color: vec4, + viewport_background: vec4, + view_tone: vec4, + background_top: vec4, + background_middle: vec4, + background_bottom: vec4, + background_aux0: vec4, + background_aux1: vec4, + background_params: vec4, + background_image_params: vec4, + background_image_transform: vec4, + environment_params: vec4, + environment_view: mat4x4, + shadow_view_proj: mat4x4, + shadow_params: vec4, + fog_color: vec4, + fog_params: vec4, + fog_distances: vec4, + light_web_profile_a: array, 4>, + light_web_profile_b: array, 4>, + light_web_rotation: array, 4>, +}; + +@group(0) @binding(0) var u: Uniforms; +@group(0) @binding(1) var background_texture: texture_2d; +@group(0) @binding(2) var background_sampler: sampler; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, + @location(1) ndc: vec2, +}; + +var POSITIONS: array, 3> = array, 3>( + vec2(-1.0, -1.0), + vec2( 3.0, -1.0), + vec2(-1.0, 3.0), +); + +@vertex +fn vs_main(@builtin(vertex_index) index: u32) -> VertexOutput { + let position = POSITIONS[index]; + var out: VertexOutput; + out.position = vec4(position, 0.999, 1.0); + out.uv = vec2(position.x * 0.5 + 0.5, 0.5 - position.y * 0.5); + out.ndc = position; + return out; +} + +fn equirect_uv(ndc: vec2, rotation: f32) -> vec2 { + let direction = normalize((u.environment_view * vec4(ndc, -1.0, 0.0)).xyz); + let longitude = atan2(direction.x, -direction.z) + rotation; + let latitude = asin(clamp(direction.y, -1.0, 1.0)); + return vec2(fract(longitude / (2.0 * 3.14159265359) + 0.5), + 0.5 - latitude / 3.14159265359); +} + +fn image_uv(source_uv: vec2) -> vec2 { + var uv = (source_uv - u.background_image_transform.xy) + / max(abs(u.background_image_transform.zw), vec2(1e-6)); + if u.background_image_params.y > 0.5 { + let image_aspect = max(u.background_image_params.w, 1e-6); + let viewport_aspect = max(u.viewport_size.x / max(u.viewport_size.y, 1.0), 1e-6); + if image_aspect > viewport_aspect { + let scale = viewport_aspect / image_aspect; + uv.y = (uv.y - 0.5) / scale + 0.5; + } else { + let scale = image_aspect / viewport_aspect; + uv.x = (uv.x - 0.5) / scale + 0.5; + } + } + if u.background_image_params.z > 0.5 { + return fract(uv); + } + return clamp(uv, vec2(0.0), vec2(1.0)); +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let mode = u32(max(u.background_params.x, 0.0) + 0.5); + var color: vec3; + if mode == 1u { + color = u.viewport_background.rgb; + } else if mode == 2u { + let angle = u.background_params.w; + let axis = vec2(-sin(angle), cos(angle)); + let coordinate = dot(in.uv - vec2(0.5), axis) + 0.5; + let horizon = clamp(u.background_params.y, 0.0, 1.0); + let half_height = max(abs(u.background_params.z) * 0.5, 1e-3); + let lower = smoothstep(horizon - half_height, horizon, coordinate); + let upper = smoothstep(horizon, horizon + half_height, coordinate); + let low_color = mix(u.background_bottom.rgb, u.background_middle.rgb, lower); + color = mix(low_color, u.background_top.rgb, upper); + } else if mode == 3u { + let horizon = 0.5; + if in.uv.y <= horizon { + let sky = smoothstep(0.0, horizon, in.uv.y); + color = mix(u.background_top.rgb, u.background_middle.rgb, sky); + } else { + let ground = smoothstep(horizon, 1.0, in.uv.y); + let underground = mix(u.background_bottom.rgb, u.background_aux0.rgb, ground); + let distance_tint = mix(u.background_aux1.rgb, u.viewport_background.rgb, ground); + color = mix(underground, distance_tint, 0.35); + } + } else if mode == 4u { + color = textureSample(background_texture, background_sampler, image_uv(in.uv)).rgb; + } else if mode == 5u { + color = textureSample(background_texture, background_sampler, + equirect_uv(in.ndc, u.background_params.w)).rgb; + } else if mode == 6u { + let t = smoothstep(0.0, 1.0, in.uv.y); + color = mix(u.background_bottom.rgb, u.background_top.rgb, t); + } else { + color = u.viewport_background.rgb; + } + if u.fog_params.x > 0.5 && u.fog_params.y > 0.5 { + color = mix(color, u.fog_color.rgb, clamp(u.fog_params.w, 0.0, 1.0)); + } + return vec4(color, 1.0); +} diff --git a/src/shaders/mesh.wgsl b/src/shaders/mesh.wgsl index 35fd01cd5..219c5e351 100644 --- a/src/shaders/mesh.wgsl +++ b/src/shaders/mesh.wgsl @@ -2,8 +2,9 @@ // // Vertex layout: position [f32;3], normal [f32;3], color [f32;4] (40 bytes) // -// Lighting: simple half-Lambert with a fixed directional light. Two -// shading paths share this shader, picked per-frame via `u.flat_shade`: +// Lighting: viewport default lights or drawing light entities supplied by the +// frame uniform. Two shading paths share this shader, picked per-frame via +// `u.flat_shade`: // - 0.0 → per-vertex normals interpolated to the fragment (Gouraud). // - 1.0 → per-triangle face normal from screen-space derivatives // `cross(dpdx(pos), dpdy(pos))`, so each triangle reads as a single @@ -27,10 +28,36 @@ struct Uniforms { light_color_hotspot: array, 4>, light_attenuation: array, 4>, lighting: vec4, + visual_style: vec4, + visual_style_color: vec4, + viewport_background: vec4, + view_tone: vec4, + background_top: vec4, + background_middle: vec4, + background_bottom: vec4, + background_aux0: vec4, + background_aux1: vec4, + background_params: vec4, + background_image_params: vec4, + background_image_transform: vec4, + environment_params: vec4, + environment_view: mat4x4, + shadow_view_proj: mat4x4, + shadow_params: vec4, + fog_color: vec4, + fog_params: vec4, + fog_distances: vec4, + light_web_profile_a: array, 4>, + light_web_profile_b: array, 4>, + light_web_rotation: array, 4>, }; @group(0) @binding(0) var u: Uniforms; +@group(0) @binding(3) var environment_texture: texture_2d; +@group(0) @binding(4) var environment_sampler: sampler; +@group(0) @binding(5) var shadow_texture: texture_depth_2d; +@group(0) @binding(6) var shadow_sampler: sampler_comparison; struct MaterialMaps { blends0: vec4, @@ -202,6 +229,63 @@ fn map_weight(uv: vec2, tiling: u32) -> f32 { return 1.0; } +fn environment_uv(direction: vec3) -> vec2 { + let d = normalize(direction); + let longitude = atan2(d.x, -d.z) + u.environment_params.y; + let latitude = asin(clamp(d.y, -1.0, 1.0)); + return vec2( + fract(longitude / (2.0 * 3.14159265359) + 0.5), + 0.5 - latitude / 3.14159265359, + ); +} + +fn shadow_visibility(position: vec3) -> f32 { + if u.shadow_params.x < 0.5 { + return 1.0; + } + let clip = u.shadow_view_proj * vec4(position, 1.0); + let ndc = clip.xyz / max(abs(clip.w), 1e-6); + if ndc.z <= 0.0 || ndc.z >= 1.0 || any(abs(ndc.xy) > vec2(1.0)) { + return 1.0; + } + let uv = vec2(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5); + let dimensions = vec2(textureDimensions(shadow_texture)); + let texel = 1.0 / max(dimensions, vec2(1.0)); + let radius = i32(clamp(ceil(u.shadow_params.z * 2.0), 0.0, 2.0)); + var visibility = 0.0; + var samples = 0.0; + for (var y = -2; y <= 2; y = y + 1) { + for (var x = -2; x <= 2; x = x + 1) { + if abs(x) <= radius && abs(y) <= radius { + visibility += textureSampleCompare( + shadow_texture, + shadow_sampler, + uv + vec2(f32(x), f32(y)) * texel, + ndc.z - u.shadow_params.y, + ); + samples += 1.0; + } + } + } + return visibility / max(samples, 1.0); +} + +fn photometric_web(index: u32, cosine: f32) -> f32 { + if u.light_web_rotation[index].w < 0.5 { + return 1.0; + } + let profile_a = u.light_web_profile_a[index]; + let profile_b = u.light_web_profile_b[index]; + let angle_index = clamp(acos(clamp(cosine, -1.0, 1.0)) / 3.14159265359 * 7.0, 0.0, 7.0); + let lower = u32(floor(angle_index)); + let upper = min(lower + 1u, 7u); + let value_at = array( + profile_a.x, profile_a.y, profile_a.z, profile_a.w, + profile_b.x, profile_b.y, profile_b.z, profile_b.w, + ); + return mix(value_at[lower], value_at[upper], fract(angle_index)); +} + @fragment fn fs_main(in: VertexOut) -> @location(0) vec4 { let two_sided = maps.render_modes.x != 0u; @@ -252,79 +336,6 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { ); } - // Native AcDbLight / Sun lighting. When a drawing has no active lights, - // retain the neutral three-point editor rig so ordinary models remain - // readable. - let l0 = normalize(vec3( 0.5, 0.8, 0.6)); // key (upper front) - let l1 = normalize(vec3(-0.7, 0.3, 0.4)); // fill (left) - let l2 = normalize(vec3( 0.2, -0.6, -0.8)); // back/under - var direct_light = vec3( - 0.45 * select(max(dot(n, l0), 0.0), abs(dot(n, l0)), two_sided) - + 0.30 * select(max(dot(n, l1), 0.0), abs(dot(n, l1)), two_sided) - + 0.25 * select(max(dot(n, l2), 0.0), abs(dot(n, l2)), two_sided) - ); - var key_light = l0; - if (u.lighting.x > 0.5) { - direct_light = vec3(0.0); - let count = min(u32(u.lighting.x), 4u); - for (var index = 0u; index < count; index = index + 1u) { - let position_type = u.light_position_type[index]; - let direction_intensity = u.light_direction_intensity[index]; - let color_hotspot = u.light_color_hotspot[index]; - let attenuation_data = u.light_attenuation[index]; - var light_vector = -normalize(direction_intensity.xyz); - var attenuation = 1.0; - if (position_type.w > 1.5) { - let delta = position_type.xyz - in.world_pos; - let distance = max(length(delta), 1e-5); - light_vector = delta / distance; - if (attenuation_data.x > 1.5) { - attenuation /= max(distance * distance, 1.0); - } else if (attenuation_data.x > 0.5) { - attenuation /= max(distance, 1.0); - } - if (attenuation_data.z > attenuation_data.y) { - attenuation *= 1.0 - smoothstep( - attenuation_data.y, - attenuation_data.z, - distance, - ); - } - if (position_type.w > 2.5) { - let source_to_fragment = -light_vector; - let cone = dot( - source_to_fragment, - normalize(direction_intensity.xyz), - ); - attenuation *= smoothstep( - attenuation_data.w, - color_hotspot.w, - cone, - ); - } - } - let strength = max(direction_intensity.w, 0.0) * attenuation; - let response = select( - max(dot(n, light_vector), 0.0), - abs(dot(n, light_vector)), - two_sided, - ); - direct_light += color_hotspot.rgb * response * strength; - if (index == 0u) { - key_light = light_vector; - } - } - } - let view = normalize(in.eye_vec); - let half_vec = normalize(key_light + view); - let gloss_exp = mix(2.0, 128.0, clamp(in.material.x, 0.0, 1.0)); - let fresnel0 = pow( - (max(in.specular.w, 1.0) - 1.0) / (max(in.specular.w, 1.0) + 1.0), - 2.0, - ); - let fresnel = fresnel0 - + (1.0 - fresnel0) * pow(1.0 - abs(dot(n, view)), 5.0); - let specular_strength = clamp(1.0 + in.material.y + fresnel, 0.0, 1.5); var albedo = in.color.rgb; if (maps.present0.x != 0u) { let texel = textureSample(diffuse_map, diffuse_sampler, in.uv_diffuse).rgb; @@ -335,6 +346,23 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { ); albedo = mix(albedo, texel, blend); } + + // Viewport face-colour modes apply to unmaterialed faces. An attached + // material remains authoritative, including its own diffuse texture. + if (maps.source_state.z == 0u) { + let mode = u32(max(u.visual_style.x, 0.0) + 0.5); + if (mode == 2u) { + albedo = u.viewport_background.rgb; + } else if (mode == 3u || mode == 4u) { + albedo = u.visual_style_color.rgb; + } else if (mode == 5u) { + albedo = mix(albedo, u.visual_style_color.rgb, 0.5); + } else if (mode == 6u) { + let luminance = dot(albedo, vec3(0.2126, 0.7152, 0.0722)); + albedo = mix(albedo, vec3(luminance), 0.3); + } + } + var specular_color = select(in.specular.rgb, albedo, in.flags.x == 1u); if (maps.present0.y != 0u) { let texel = textureSample(specular_map, specular_sampler, in.uv_specular).rgb; @@ -345,17 +373,124 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { ); specular_color = mix(specular_color, texel, blend); } - let half_response = select( - max(dot(n, half_vec), 0.0), - abs(dot(n, half_vec)), - two_sided, + let view = normalize(in.eye_vec); + let gloss_exp = mix(2.0, 128.0, clamp(in.material.x, 0.0, 1.0)); + let fresnel0 = pow( + (max(in.specular.w, 1.0) - 1.0) / (max(in.specular.w, 1.0) + 1.0), + 2.0, ); - let specular = specular_color - * pow(half_response, gloss_exp) - * specular_strength - * max(in.advanced.z, 0.0); - let ambient_light = albedo * clamp(in.ambient.rgb, vec3(0.0), vec3(1.0)); - var lit = ambient_light + albedo * clamp(direct_light, vec3(0.0), vec3(2.0)) + specular; + let fresnel = fresnel0 + + (1.0 - fresnel0) * pow(1.0 - abs(dot(n, view)), 5.0); + let specular_strength = clamp(1.0 + in.material.y + fresnel, 0.0, 1.5); + let viewport_highlight = select( + 1.0, + max(u.visual_style.z, 0.0), + maps.source_state.z == 0u && u.visual_style.z >= 0.0, + ); + + // The CPU resolves the viewport's default-light policy before upload, so + // this loop handles both generated distant lights and drawing light data. + // Diffuse and specular are accumulated from every visible light using the + // same colour, intensity, cone and attenuation terms. + var direct_light = vec3(0.0); + var direct_specular = vec3(0.0); + let shadow = shadow_visibility(in.world_pos); + let shadow_light_index = u32(max(u.shadow_params.w, 0.0) + 0.5); + let count = min(u32(u.lighting.x), 4u); + for (var index = 0u; index < count; index = index + 1u) { + let position_type = u.light_position_type[index]; + let direction_intensity = u.light_direction_intensity[index]; + let color_hotspot = u.light_color_hotspot[index]; + let attenuation_data = u.light_attenuation[index]; + var light_vector = -normalize(direction_intensity.xyz); + var attenuation = 1.0; + if (position_type.w > 1.5) { + let delta = position_type.xyz - in.world_pos; + let distance = max(length(delta), 1e-5); + light_vector = delta / distance; + if (attenuation_data.x > 1.5) { + attenuation /= max(distance * distance, 1.0); + } else if (attenuation_data.x > 0.5) { + attenuation /= max(distance, 1.0); + } + if (attenuation_data.z > attenuation_data.y) { + attenuation *= 1.0 - smoothstep( + attenuation_data.y, + attenuation_data.z, + distance, + ); + } + if (position_type.w > 2.5) { + let source_to_fragment = -light_vector; + let cone = dot( + source_to_fragment, + normalize(direction_intensity.xyz), + ); + attenuation *= smoothstep( + attenuation_data.w, + color_hotspot.w, + cone, + ); + } + attenuation *= photometric_web( + index, + dot(-light_vector, normalize(direction_intensity.xyz)), + ); + } + let visibility = select(1.0, shadow, index == shadow_light_index); + let strength = max(direction_intensity.w, 0.0) * attenuation * visibility; + let response = select( + max(dot(n, light_vector), 0.0), + abs(dot(n, light_vector)), + two_sided, + ); + direct_light += color_hotspot.rgb * response * strength; + let half_vec = normalize(light_vector + view); + let half_response = select( + max(dot(n, half_vec), 0.0), + abs(dot(n, half_vec)), + two_sided, + ); + direct_specular += color_hotspot.rgb + * specular_color + * pow(half_response, gloss_exp) + * specular_strength + * viewport_highlight + * max(in.advanced.z, 0.0) + * strength; + } + let ambient_light = clamp(in.ambient.rgb, vec3(0.0), vec3(1.0)) + * clamp(u.lighting.yzw, vec3(0.0), vec3(1.0)); + var lit = ambient_light + + albedo * clamp(direct_light, vec3(0.0), vec3(2.0)) + + direct_specular; + if (u.environment_params.x > 0.5) { + let diffuse_environment = textureSample( + environment_texture, + environment_sampler, + environment_uv(n), + ).rgb; + let reflection_direction = reflect(-view, n); + let reflected_environment = textureSample( + environment_texture, + environment_sampler, + environment_uv(reflection_direction), + ).rgb; + let indirect_enabled = maps.source_state.z == 0u + || maps.render_modes.z >= 2u + || maps.render_modes.w != 0u; + let color_bleed = select(0.0, max(maps.indirect.x, 0.0), indirect_enabled); + lit += albedo + * diffuse_environment + * max(u.environment_params.z, 0.0) + * color_bleed; + lit += reflected_environment + * specular_color + * specular_strength + * viewport_highlight + * max(in.advanced.z, 0.0) + * max(u.environment_params.w, 0.0); + } if (maps.present0.z != 0u) { let reflected = textureSample( reflection_map, @@ -398,6 +533,21 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { if (in.flags.w == 1u) { rgb = lit + albedo * max(in.material.w, 0.0); } + let contrast = 1.0 + clamp(u.view_tone.y, -1.0, 1.0); + rgb = (rgb - vec3(0.5)) * contrast + vec3(0.5); + rgb += vec3(clamp(u.view_tone.x, -1.0, 1.0) * 0.5); + rgb *= max(1.0 + u.visual_style.w, 0.0); + if (u.fog_params.x > 0.5) { + let fog_range = max(u.fog_distances.y - u.fog_distances.x, 1e-6); + let fog_distance = length(in.world_pos); + let fog_position = clamp( + (fog_distance - u.fog_distances.x) / fog_range, + 0.0, + 1.0, + ); + let fog_density = mix(u.fog_params.z, u.fog_params.w, fog_position); + rgb = mix(rgb, u.fog_color.rgb, clamp(fog_density, 0.0, 1.0)); + } var material_alpha = in.color.a; if (maps.present0.w != 0u) { let opacity = textureSample(opacity_map, opacity_sampler, in.uv_opacity).r; @@ -409,6 +559,7 @@ fn fs_main(in: VertexOut) -> @location(0) vec4 { material_alpha *= mix(1.0, opacity, blend); } material_alpha *= 1.0 - clamp(in.ambient.a * max(in.advanced.w, 0.0), 0.0, 0.95); + material_alpha *= clamp(u.visual_style.y, 0.0, 1.0); let alpha = select(1.0, material_alpha, u.transparency_enable > 0.5); return vec4(rgb, alpha); } diff --git a/src/shaders/shadow.wgsl b/src/shaders/shadow.wgsl new file mode 100644 index 000000000..957e4ffab --- /dev/null +++ b/src/shaders/shadow.wgsl @@ -0,0 +1,79 @@ +struct Uniforms { + viewport_size: vec2, + world_per_pixel: f32, + lwdisplay_enable: f32, + flat_shade: f32, + transparency_enable: f32, + linetype_scale: f32, + _pad: f32, + view_rot: mat4x4, + eye_high: vec3, + _pad_eh: f32, + eye_low: vec3, + _pad_el: f32, + light_position_type: array, 4>, + light_direction_intensity: array, 4>, + light_color_hotspot: array, 4>, + light_attenuation: array, 4>, + lighting: vec4, + visual_style: vec4, + visual_style_color: vec4, + viewport_background: vec4, + view_tone: vec4, + background_top: vec4, + background_middle: vec4, + background_bottom: vec4, + background_aux0: vec4, + background_aux1: vec4, + background_params: vec4, + background_image_params: vec4, + background_image_transform: vec4, + environment_params: vec4, + environment_view: mat4x4, + shadow_view_proj: mat4x4, + shadow_params: vec4, + fog_color: vec4, + fog_params: vec4, + fog_distances: vec4, + light_web_profile_a: array, 4>, + light_web_profile_b: array, 4>, + light_web_rotation: array, 4>, +}; + +struct MeshInstance { + model_row_0: vec4, + model_row_1: vec4, + model_row_2: vec4, + translation_low: vec4, + normal_row_0: vec4, + normal_row_1: vec4, + normal_row_2: vec4, +}; + +@group(0) @binding(0) var u: Uniforms; +@group(1) @binding(15) var mesh_instances: array; + +struct VertexIn { + @location(0) position: vec3, + @location(3) position_low: vec3, +}; + +@vertex +fn vs_main( + vertex: VertexIn, + @builtin(instance_index) instance_index: u32, +) -> @builtin(position) vec4 { + let instance = mesh_instances[instance_index]; + let world_high = vec3( + dot(instance.model_row_0.xyz, vertex.position) + instance.model_row_0.w, + dot(instance.model_row_1.xyz, vertex.position) + instance.model_row_1.w, + dot(instance.model_row_2.xyz, vertex.position) + instance.model_row_2.w, + ); + let world_low = vec3( + dot(instance.model_row_0.xyz, vertex.position_low), + dot(instance.model_row_1.xyz, vertex.position_low), + dot(instance.model_row_2.xyz, vertex.position_low), + ) + instance.translation_low.xyz; + let relative = (world_high - u.eye_high) + (world_low - u.eye_low); + return u.shadow_view_proj * vec4(relative, 1.0); +}