mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #11118 from Delete228/native-model-material-stack-upstream
🤖 Agent: Native mods can draw terrain models with an explicit material (shade, UV shift); the shading follows Flywheel's (MIT, credited). Main merged in after #11117 landed; its test now builds faces with #11356's BakedFace::new (clippy -D warnings passes).
This commit is contained in:
17 files changed
+843
-72
No files matched your search
@@ -226,3 +226,31 @@ THE SOFTWARE.
|
||||
## Just Enough Items
|
||||
|
||||
The Recipes settings icon is from [Just Enough Items 31.17.0.108](https://github.com/mezz/JustEnoughItems) by mezz and contributors. It is distributed under the MIT license in `crates/horizon-client-render/src/jei_recipes/LICENSE.txt`.
|
||||
|
||||
## Flywheel native model directional shading
|
||||
|
||||
The two-light diffuse expression and transformed-normal policy in
|
||||
`terrain.wgsl`'s native model entries follow Flywheel 1.0.6
|
||||
(`internal/diffuse.glsl`, `instance/transformed.vert`, `internal/common.vert`).
|
||||
No Flywheel classes, compiled shaders or original model/texture assets are bundled.
|
||||
|
||||
Copyright (c) 2021-2024 Jozufozu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -34,6 +34,8 @@ pub struct ModelQuad {
|
||||
/// NeoForge's `render_type` names.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RenderType {
|
||||
/// A declared render type unknown to this baker; distinct from an absent default.
|
||||
Unsupported,
|
||||
Solid,
|
||||
Cutout,
|
||||
CutoutMipped,
|
||||
@@ -234,7 +236,10 @@ fn bake_into(
|
||||
skip_flat: bool,
|
||||
output: &mut Vec<ModelQuad>,
|
||||
) -> Result<()> {
|
||||
let render_type = model.render_type.as_deref().and_then(RenderType::by_name);
|
||||
let render_type = model
|
||||
.render_type
|
||||
.as_deref()
|
||||
.map(|name| RenderType::by_name(name).unwrap_or(RenderType::Unsupported));
|
||||
for element in model.elements() {
|
||||
let element = element
|
||||
.as_ref()
|
||||
@@ -292,7 +297,10 @@ fn bake_obj(
|
||||
transform: BlockModelTransform,
|
||||
output: &mut Vec<ModelQuad>,
|
||||
) -> Result<()> {
|
||||
let render_type = model.render_type.as_deref().and_then(RenderType::by_name);
|
||||
let render_type = model
|
||||
.render_type
|
||||
.as_deref()
|
||||
.map(|name| RenderType::by_name(name).unwrap_or(RenderType::Unsupported));
|
||||
for face in obj.faces.iter().filter(|f| !hidden.contains(&f.object)) {
|
||||
let slot = match obj.materials.get(&face.material) {
|
||||
Some(MtlTexture::Sprite(sprite)) => {
|
||||
|
||||
@@ -777,6 +777,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn an_unknown_declared_render_type_remains_distinct_from_absent() {
|
||||
let json = LIGHTS.replace("minecraft:cutout", "fixture:custom_material");
|
||||
let library = library(&[("block/custom", &json)]);
|
||||
let model = library.resolve("block/custom").unwrap();
|
||||
let quads = crate::bake::bake_block_model(
|
||||
&model,
|
||||
crate::bake::BlockModelTransform::new([0; 3], false),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!quads.is_empty());
|
||||
assert!(
|
||||
quads
|
||||
.iter()
|
||||
.all(|q| q.render_type == Some(crate::bake::RenderType::Unsupported))
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn render_type_comes_from_the_leaf_most_model_and_loses_its_prefix() {
|
||||
let library = library(&[
|
||||
("block/base", r#"{"render_type":"translucent"}"#),
|
||||
|
||||
@@ -48,6 +48,7 @@ mod entity_outline;
|
||||
mod fluid;
|
||||
#[cfg(test)]
|
||||
mod inter_stage_tests;
|
||||
mod terrain_model;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_gpu;
|
||||
mod weather;
|
||||
@@ -55,9 +56,10 @@ mod world_text;
|
||||
use entity_batch::EntityBatch;
|
||||
pub use entity_batch::{
|
||||
DISSOLVE_OVERLAY, DRAGON_RAYS_OVERLAY, ENERGY_SWIRL_OVERLAY, FLUID_OVERLAY, OIT_ENTITY,
|
||||
SHADOW_OVERLAY, TEXT_POLYGON_OFFSET_OVERLAY, TRANSLUCENT_GLINT_ITEM_OVERLAY,
|
||||
SHADOW_OVERLAY, TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY, TERRAIN_MODEL_CUTOUT_OVERLAY,
|
||||
TERRAIN_MODEL_SOLID_OVERLAY, TEXT_POLYGON_OFFSET_OVERLAY, TRANSLUCENT_GLINT_ITEM_OVERLAY,
|
||||
TRANSLUCENT_ITEM_OVERLAY, TRANSLUCENT_PARTICLE_OVERLAY, WATER_MASK_OVERLAY, WORLD_TEXT_OVERLAY,
|
||||
WORLD_TEXT_POLYGON_OFFSET_OVERLAY, mark_oit,
|
||||
WORLD_TEXT_POLYGON_OFFSET_OVERLAY, is_native_terrain, mark_oit,
|
||||
};
|
||||
pub use view::{View, ViewSet};
|
||||
|
||||
@@ -1207,6 +1209,7 @@ pub struct Renderer {
|
||||
text_polygon_offset_pipeline: wgpu::RenderPipeline,
|
||||
world_text_pipeline: wgpu::RenderPipeline,
|
||||
fluid_pipeline: wgpu::RenderPipeline,
|
||||
terrain_model_pipelines: [wgpu::RenderPipeline; 3],
|
||||
celestial_pipeline: wgpu::RenderPipeline,
|
||||
celestial_buffer: wgpu::Buffer,
|
||||
end_sky: end_sky::EndSky,
|
||||
@@ -1244,6 +1247,8 @@ pub struct Renderer {
|
||||
group: wgpu::BindGroup,
|
||||
fluid_group: wgpu::BindGroup,
|
||||
fluid_sampler: wgpu::Sampler,
|
||||
terrain_model_group: wgpu::BindGroup,
|
||||
terrain_model_sampler: wgpu::Sampler,
|
||||
globals: wgpu::Buffer,
|
||||
/// Resource-pack normal/specular maps, split like the colour sprite arrays.
|
||||
pbr_maps: Option<Vec<[wgpu::Texture; 2]>>,
|
||||
@@ -2784,6 +2789,13 @@ impl Renderer {
|
||||
config.format.remove_srgb_suffix(),
|
||||
&sprite_constants,
|
||||
);
|
||||
let terrain_model_pipelines = terrain_model::pipelines(
|
||||
&device,
|
||||
Some(&layout),
|
||||
&shader,
|
||||
config.format.remove_srgb_suffix(),
|
||||
&sprite_constants,
|
||||
);
|
||||
let instance_attributes = wgpu::vertex_attr_array![5=>Float32x4];
|
||||
let terrain_buffers = [
|
||||
Some(wgpu::VertexBufferLayout {
|
||||
@@ -3354,6 +3366,22 @@ impl Renderer {
|
||||
&lightmap_sampler,
|
||||
&emission_stats.view,
|
||||
);
|
||||
let terrain_model_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("Native cutout model (unmipped)"),
|
||||
lod_max_clamp: 0.0,
|
||||
..fluid::SAMPLER
|
||||
});
|
||||
let terrain_model_group = terrain_group(
|
||||
&device,
|
||||
"Native cutout model resources",
|
||||
&bindings,
|
||||
&globals,
|
||||
&sprite_views,
|
||||
&terrain_model_sampler,
|
||||
&lightmap_view,
|
||||
&lightmap_sampler,
|
||||
&emission_stats.view,
|
||||
);
|
||||
let crosshair_group = terrain_group(
|
||||
&device,
|
||||
"Debug crosshair resources",
|
||||
@@ -4744,6 +4772,7 @@ impl Renderer {
|
||||
text_polygon_offset_pipeline,
|
||||
world_text_pipeline,
|
||||
fluid_pipeline,
|
||||
terrain_model_pipelines,
|
||||
celestial_pipeline,
|
||||
celestial_buffer,
|
||||
end_sky: end_sky::EndSky::default(),
|
||||
@@ -4778,6 +4807,8 @@ impl Renderer {
|
||||
group,
|
||||
fluid_group,
|
||||
fluid_sampler,
|
||||
terrain_model_group,
|
||||
terrain_model_sampler,
|
||||
globals,
|
||||
pbr_maps,
|
||||
lightmap,
|
||||
@@ -7135,6 +7166,17 @@ impl Renderer {
|
||||
&self.lightmap_sampler,
|
||||
&self.emission_stats.view,
|
||||
);
|
||||
self.terrain_model_group = terrain_group(
|
||||
&self.device,
|
||||
"Native cutout model resources",
|
||||
&self.terrain_layout,
|
||||
&self.globals,
|
||||
&self.sprite_views,
|
||||
&self.terrain_model_sampler,
|
||||
&self.lightmap_view,
|
||||
&self.lightmap_sampler,
|
||||
&self.emission_stats.view,
|
||||
);
|
||||
}
|
||||
/// Block sprite layers per array on this device: a sprite's array is its layer over it, its
|
||||
/// layer in that array the rest (`block_sprites::Lookup`).
|
||||
@@ -8779,6 +8821,9 @@ impl Renderer {
|
||||
fluid_quads,
|
||||
world_text_quads,
|
||||
world_text_offset_quads,
|
||||
native_solid_quads,
|
||||
native_cutout_quads,
|
||||
native_cutout_mipped_quads,
|
||||
] = self.entity_batch.update(
|
||||
entities,
|
||||
frame.additive,
|
||||
@@ -8807,7 +8852,10 @@ impl Renderer {
|
||||
+ oit_entity_quads
|
||||
+ fluid_quads
|
||||
+ world_text_quads
|
||||
+ world_text_offset_quads;
|
||||
+ world_text_offset_quads
|
||||
+ native_solid_quads
|
||||
+ native_cutout_quads
|
||||
+ native_cutout_mipped_quads;
|
||||
laps.lap("r_draw_lists");
|
||||
// Where the tail of materials the classic and the improved passes
|
||||
// both draw ends: the shadows, particles and items count back from it.
|
||||
@@ -8815,7 +8863,10 @@ impl Renderer {
|
||||
- oit_entity_quads
|
||||
- fluid_quads
|
||||
- world_text_quads
|
||||
- world_text_offset_quads;
|
||||
- world_text_offset_quads
|
||||
- native_solid_quads
|
||||
- native_cutout_quads
|
||||
- native_cutout_mipped_quads;
|
||||
let native_clouds = self.native.as_ref().is_none_or(|n| n.vanilla_clouds);
|
||||
let cloud_quads = if let Some(input) = frame.clouds.as_ref().filter(|_| native_clouds) {
|
||||
let (rebuilt, uniform) =
|
||||
@@ -10247,6 +10298,36 @@ impl Renderer {
|
||||
}
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
}
|
||||
// Native terrain models use the block sprite arrays with their own
|
||||
// alpha/mipmap policies and ordinary opaque depth. Storage follows
|
||||
// the existing materials without moving their indices or frame tails.
|
||||
let mut first =
|
||||
world_quads - native_solid_quads - native_cutout_quads - native_cutout_mipped_quads;
|
||||
for (i, count) in [
|
||||
native_solid_quads,
|
||||
native_cutout_quads,
|
||||
native_cutout_mipped_quads,
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
if count > 0 {
|
||||
pass.set_pipeline(&self.terrain_model_pipelines[i]);
|
||||
pass.set_bind_group(
|
||||
0,
|
||||
if i == 1 {
|
||||
&self.terrain_model_group
|
||||
} else {
|
||||
&self.fluid_group
|
||||
},
|
||||
&[],
|
||||
);
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
pass.draw_indexed(0..count * 6, (first * 4) as i32, 0..1);
|
||||
}
|
||||
first += count;
|
||||
}
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
// Still-fluid block entities draw as their own ordinary sorted
|
||||
// translucent material, before terrain transparency. This material
|
||||
// always writes depth and never joins the improved-transparency queues.
|
||||
@@ -12714,7 +12795,7 @@ mod tests {
|
||||
let mut batch = super::EntityBatch::default();
|
||||
let counts = batch.update(&entities, &quad(3, 5), &quad(0, 6), &quad(0, 8), false);
|
||||
let vertices = &batch.combined;
|
||||
assert_eq!(counts, [4, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0]);
|
||||
assert_eq!(counts, [4, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
assert_eq!(
|
||||
vertices
|
||||
.as_chunks::<4>()
|
||||
|
||||
@@ -16,7 +16,20 @@ use super::Vertex;
|
||||
/// ignored and they stay in the first one, in their order.
|
||||
/// Sorted still-fluid geometry follows, then the two native world-canvas text materials,
|
||||
/// NORMAL and POLYGON_OFFSET. Material storage order is independent of their draw stages.
|
||||
const MATERIALS: usize = 13;
|
||||
const MATERIALS: usize = 16;
|
||||
pub const TERRAIN_MODEL_SOLID_OVERLAY: u32 = 42;
|
||||
pub const TERRAIN_MODEL_CUTOUT_OVERLAY: u32 = 43;
|
||||
pub const TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY: u32 = 44;
|
||||
/// Native terrain-array UV/payloads must never enter a shared-atlas shader-pack pass.
|
||||
pub fn is_native_terrain(layer: u32) -> bool {
|
||||
matches!(
|
||||
layer & 0xffff,
|
||||
FLUID_OVERLAY
|
||||
| TERRAIN_MODEL_SOLID_OVERLAY
|
||||
| TERRAIN_MODEL_CUTOUT_OVERLAY
|
||||
| TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY
|
||||
)
|
||||
}
|
||||
/// Sorted depth-writing still fluid, sampling terrain sprite arrays rather than the shared atlas.
|
||||
pub const FLUID_OVERLAY: u32 = 41;
|
||||
/// A bit of `Vertex::layer`, above the overlay (low 16 bits) and the HDR tier
|
||||
@@ -34,7 +47,9 @@ pub const DISSOLVE_OVERLAY: u32 = 38;
|
||||
/// (`OIT_ENTITY`): improved transparency draws them in its stages.
|
||||
pub fn mark_oit(quads: &mut [Vertex]) {
|
||||
for vertex in quads {
|
||||
vertex.layer |= OIT_ENTITY;
|
||||
if !is_native_terrain(vertex.layer) {
|
||||
vertex.layer |= OIT_ENTITY;
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The overlay of depth-only quads that hide water (`RenderTypes.waterMask`).
|
||||
@@ -63,6 +78,12 @@ pub const WORLD_TEXT_POLYGON_OFFSET_OVERLAY: u32 = 40;
|
||||
/// The material of a quad by its overlay (`MATERIALS` order): the overlay is the layer's low 16
|
||||
/// bits, as entity.wgsl reads it; HDR output's tier rides in the high ones.
|
||||
fn material(layer: u32, oit: bool) -> usize {
|
||||
match layer & 0xffff {
|
||||
TERRAIN_MODEL_SOLID_OVERLAY => return 13,
|
||||
TERRAIN_MODEL_CUTOUT_OVERLAY => return 14,
|
||||
TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY => return 15,
|
||||
_ => {}
|
||||
}
|
||||
if layer & 0xffff == FLUID_OVERLAY {
|
||||
return 10;
|
||||
}
|
||||
@@ -157,7 +178,7 @@ impl EntityBatch {
|
||||
// Fluid and RenderType.text sort their own alpha-blended quads far to
|
||||
// near. Fluid precedes both world-canvas layers, after existing materials
|
||||
// (including eyes moved out of additive).
|
||||
for runs in &self.runs[10..] {
|
||||
for runs in &self.runs[10..13] {
|
||||
let start = self.combined.len();
|
||||
for &(first, count) in runs {
|
||||
self.combined
|
||||
@@ -175,6 +196,13 @@ impl EntityBatch {
|
||||
distance(b).total_cmp(&distance(a))
|
||||
});
|
||||
}
|
||||
// Native solid/cutout models are depth writing and keep their submission order.
|
||||
for runs in &self.runs[13..] {
|
||||
for &(first, count) in runs {
|
||||
self.combined
|
||||
.extend_from_slice(&entities[first * 4..(first + count) * 4]);
|
||||
}
|
||||
}
|
||||
if self.moved_additive == 0 {
|
||||
self.combined.extend_from_slice(additive);
|
||||
} else {
|
||||
@@ -195,6 +223,56 @@ mod tests {
|
||||
use super::*;
|
||||
use std::{hint::black_box, time::Instant};
|
||||
#[test]
|
||||
fn native_models_keep_opaque_order_and_do_not_move_existing_materials_or_tails() {
|
||||
let quad = |layer, x| {
|
||||
[Vertex {
|
||||
layer,
|
||||
pos: [x, 0.0, 0.0],
|
||||
..geometry(1, false)[0]
|
||||
}; 4]
|
||||
};
|
||||
let entities: Vec<_> = [
|
||||
(FLUID_OVERLAY, 10.0),
|
||||
(WORLD_TEXT_OVERLAY, 11.0),
|
||||
(TERRAIN_MODEL_SOLID_OVERLAY | 279 << 16, 3.0),
|
||||
(TERRAIN_MODEL_SOLID_OVERLAY, 1.0),
|
||||
(TERRAIN_MODEL_CUTOUT_OVERLAY, 4.0),
|
||||
(TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY, 5.0),
|
||||
(OIT_ENTITY, 6.0),
|
||||
(0, 7.0),
|
||||
]
|
||||
.into_iter()
|
||||
.flat_map(|(layer, x)| quad(layer, x))
|
||||
.collect();
|
||||
let mut batch = EntityBatch::default();
|
||||
let counts = batch.update(
|
||||
&entities,
|
||||
&quad(0, 8.0),
|
||||
&quad(0, 9.0),
|
||||
&quad(0, 12.0),
|
||||
true,
|
||||
);
|
||||
assert_eq!(counts, [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 2, 1, 1]);
|
||||
let xs: Vec<_> = batch.combined.iter().step_by(4).map(|v| v.pos[0]).collect();
|
||||
assert_eq!(
|
||||
xs,
|
||||
[7.0, 6.0, 10.0, 11.0, 3.0, 1.0, 4.0, 5.0, 8.0, 9.0, 12.0]
|
||||
);
|
||||
assert_eq!(
|
||||
batch.combined[16].layer,
|
||||
TERRAIN_MODEL_SOLID_OVERLAY | 279 << 16
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn tagging_oit_does_not_corrupt_a_native_models_sprite_index() {
|
||||
let mut vertices = [geometry(1, false)[0]; 2];
|
||||
vertices[0].layer = TERRAIN_MODEL_CUTOUT_OVERLAY | 17 << 16;
|
||||
vertices[1].layer = 3;
|
||||
mark_oit(&mut vertices);
|
||||
assert_eq!(vertices[0].layer, TERRAIN_MODEL_CUTOUT_OVERLAY | 17 << 16);
|
||||
assert_eq!(vertices[1].layer, 3 | OIT_ENTITY);
|
||||
}
|
||||
#[test]
|
||||
fn fluid_and_world_canvas_sort_after_oit_eyes_before_frame_tails() {
|
||||
let quad = |layer, x| {
|
||||
[Vertex {
|
||||
@@ -223,7 +301,7 @@ mod tests {
|
||||
let mut batch = EntityBatch::default();
|
||||
assert_eq!(
|
||||
batch.update(&entities, &additive, &quad(0, 9.0), &quad(0, 10.0), true),
|
||||
[1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 1]
|
||||
[1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 1, 0, 0, 0]
|
||||
);
|
||||
let positions: Vec<_> = batch.combined.iter().step_by(4).map(|v| v.pos[0]).collect();
|
||||
assert_eq!(
|
||||
@@ -330,12 +408,12 @@ mod tests {
|
||||
let mut batch = EntityBatch::default();
|
||||
// Off: the tag changes nothing, the quads keep their place.
|
||||
let counts = batch.update(&entities, &[], &[], &[], false);
|
||||
assert_eq!(counts, [4, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]);
|
||||
assert_eq!(counts, [4, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
let xs: Vec<f32> = batch.combined.iter().step_by(4).map(|v| v.pos[0]).collect();
|
||||
assert_eq!(xs, [0.0, 1.0, 2.0, 4.0, 3.0]);
|
||||
// On: the tagged quads come last, in order.
|
||||
let counts = batch.update(&entities, &[], &[], &[], true);
|
||||
assert_eq!(counts, [2, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 0]);
|
||||
assert_eq!(counts, [2, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0]);
|
||||
let xs: Vec<f32> = batch.combined.iter().step_by(4).map(|v| v.pos[0]).collect();
|
||||
assert_eq!(xs, [0.0, 2.0, 3.0, 1.0, 4.0]);
|
||||
}
|
||||
|
||||
@@ -463,11 +463,11 @@ impl PackEntities {
|
||||
// model in the same block-entity run. Do not corrupt either UV space.
|
||||
let whole: &[Vertex] = if whole
|
||||
.chunks(4)
|
||||
.any(|q| q[0].layer & 0xffff == super::FLUID_OVERLAY)
|
||||
.any(|q| super::is_native_terrain(q[0].layer))
|
||||
{
|
||||
without_fluids.clear();
|
||||
for quad in whole.chunks(4) {
|
||||
if quad[0].layer & 0xffff == super::FLUID_OVERLAY {
|
||||
if super::is_native_terrain(quad[0].layer) {
|
||||
if !span.hidden {
|
||||
rest.extend_from_slice(quad);
|
||||
}
|
||||
@@ -1172,63 +1172,70 @@ mod gpu_span_tests {
|
||||
pollster::block_on(adapter.request_device(&Default::default())).unwrap();
|
||||
let layout = super::super::pack_draw::DrawUniforms::new(&device);
|
||||
let mut geometry = PackEntities::new(&device, &layout.layout);
|
||||
let mut mixed = vec![
|
||||
Vertex {
|
||||
layer: 0,
|
||||
..bytemuck::Zeroable::zeroed()
|
||||
};
|
||||
8
|
||||
];
|
||||
for v in &mut mixed[4..] {
|
||||
v.layer = super::super::FLUID_OVERLAY;
|
||||
v.uv = [0.25, 0.75];
|
||||
v.glint_uv = [17.0, 0.0];
|
||||
}
|
||||
let mut rest = Vec::new();
|
||||
for hidden in [false, true] {
|
||||
geometry.prepare(
|
||||
&device,
|
||||
&queue,
|
||||
&layout.layout,
|
||||
&mixed,
|
||||
&[EntitySpan {
|
||||
start: 0,
|
||||
id: 77,
|
||||
stage: render_stage("BLOCK_ENTITIES"),
|
||||
hidden,
|
||||
..Default::default()
|
||||
}],
|
||||
[true; PROGRAMS],
|
||||
([0.; 4], [1; 2]),
|
||||
Casters {
|
||||
blocks: true,
|
||||
..Default::default()
|
||||
},
|
||||
&mut rest,
|
||||
None,
|
||||
(&[], &[]),
|
||||
);
|
||||
assert_eq!(
|
||||
geometry.scratch.len(),
|
||||
4,
|
||||
"the model still uses the shader pack"
|
||||
);
|
||||
assert_eq!(geometry.draws.len(), 1);
|
||||
assert_eq!(
|
||||
(geometry.draws[0].pack, geometry.draws[0].shadow),
|
||||
(!hidden, true)
|
||||
);
|
||||
if hidden {
|
||||
assert!(
|
||||
rest.is_empty(),
|
||||
"hidden native fluid must not leak into the visible pass"
|
||||
for overlay in [
|
||||
super::super::FLUID_OVERLAY,
|
||||
super::super::TERRAIN_MODEL_SOLID_OVERLAY,
|
||||
super::super::TERRAIN_MODEL_CUTOUT_OVERLAY,
|
||||
super::super::TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY,
|
||||
] {
|
||||
let mut mixed = vec![
|
||||
Vertex {
|
||||
layer: 0,
|
||||
..bytemuck::Zeroable::zeroed()
|
||||
};
|
||||
8
|
||||
];
|
||||
for v in &mut mixed[4..] {
|
||||
v.layer = overlay | 279 << 16;
|
||||
v.uv = [0.25, 0.75];
|
||||
v.glint_uv = [17.0, 0.0];
|
||||
}
|
||||
let mut rest = Vec::new();
|
||||
for hidden in [false, true] {
|
||||
geometry.prepare(
|
||||
&device,
|
||||
&queue,
|
||||
&layout.layout,
|
||||
&mixed,
|
||||
&[EntitySpan {
|
||||
start: 0,
|
||||
id: 77,
|
||||
stage: render_stage("BLOCK_ENTITIES"),
|
||||
hidden,
|
||||
..Default::default()
|
||||
}],
|
||||
[true; PROGRAMS],
|
||||
([0.; 4], [1; 2]),
|
||||
Casters {
|
||||
blocks: true,
|
||||
..Default::default()
|
||||
},
|
||||
&mut rest,
|
||||
None,
|
||||
(&[], &[]),
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
bytemuck::cast_slice::<_, u8>(&rest),
|
||||
bytemuck::cast_slice::<_, u8>(&mixed[4..]),
|
||||
"fluid UV/layer payload cannot be sampled from the entity atlas"
|
||||
geometry.scratch.len(),
|
||||
4,
|
||||
"the model still uses the shader pack"
|
||||
);
|
||||
assert_eq!(geometry.draws.len(), 1);
|
||||
assert_eq!(
|
||||
(geometry.draws[0].pack, geometry.draws[0].shadow),
|
||||
(!hidden, true)
|
||||
);
|
||||
if hidden {
|
||||
assert!(
|
||||
rest.is_empty(),
|
||||
"hidden native fluid must not leak into the visible pass"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
bytemuck::cast_slice::<_, u8>(&rest),
|
||||
bytemuck::cast_slice::<_, u8>(&mixed[4..]),
|
||||
"fluid UV/layer payload cannot be sampled from the entity atlas"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Opaque/cutout terrain-sheet native models, with ordinary culling and depth writes.
|
||||
pub(super) fn pipelines(
|
||||
device: &wgpu::Device,
|
||||
layout: Option<&wgpu::PipelineLayout>,
|
||||
shader: &wgpu::ShaderModule,
|
||||
format: wgpu::TextureFormat,
|
||||
sprite_constants: &[(&str, f64)],
|
||||
) -> [wgpu::RenderPipeline; 3] {
|
||||
std::array::from_fn(|i| {
|
||||
let mut constants = sprite_constants.to_vec();
|
||||
constants.push(("NATIVE_MODEL_ALPHA_CUTOFF", [0.0, 0.1, 0.5][i]));
|
||||
// The ordinary Vertex ABI is unchanged. This material's private payload uses
|
||||
// normal's four bytes for a float z, and glint UV for float normal x/y.
|
||||
let attributes = wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Uint32,3=>Unorm8x4,4=>Uint8x4,5=>Uint32,6=>Float32x2];
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(
|
||||
[
|
||||
"Native model solid",
|
||||
"Native model cutout",
|
||||
"Native model cutout mipmapped",
|
||||
][i],
|
||||
),
|
||||
layout,
|
||||
vertex: wgpu::VertexState {
|
||||
module: shader,
|
||||
entry_point: Some("vs_native_model"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &constants,
|
||||
..Default::default()
|
||||
},
|
||||
buffers: &[Some(wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<super::Vertex>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &attributes,
|
||||
})],
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: Some(wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: Some(true),
|
||||
depth_compare: Some(wgpu::CompareFunction::GreaterEqual),
|
||||
stencil: Default::default(),
|
||||
bias: Default::default(),
|
||||
}),
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: shader,
|
||||
entry_point: Some("fs_native_model"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &constants,
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -58,6 +58,7 @@ pub mod hud_overlays;
|
||||
pub mod inventory_effects;
|
||||
pub mod java_text;
|
||||
pub mod jigsaw_block;
|
||||
pub mod terrain_model;
|
||||
pub use horizon_world::json_folder;
|
||||
pub mod level_loading;
|
||||
pub mod light;
|
||||
|
||||
@@ -184,7 +184,7 @@ fn bake_resolved(
|
||||
Some(RenderType::Translucent | RenderType::Tripwire) => (true, false),
|
||||
Some(RenderType::Solid) => (false, true),
|
||||
Some(RenderType::Cutout | RenderType::CutoutMipped) => (false, false),
|
||||
None => (
|
||||
None | Some(RenderType::Unsupported) => (
|
||||
quad.material.force_translucent || sprites.translucent[layer as usize],
|
||||
sprites.region_opaque(layer, &quad.uv),
|
||||
),
|
||||
|
||||
@@ -750,6 +750,45 @@ struct NativeFluidOut {
|
||||
return apply_fog(color, in.spherical, in.cylindrical);
|
||||
}
|
||||
|
||||
// Native terrain models retain floating-point transformed normals. Fullbright still receives
|
||||
// entity directional shading; the material's alpha test and mip policy are independent.
|
||||
// Diffuse/normal policy: Flywheel 1.0.6, copyright2021-2024 Jozufozu (MIT);
|
||||
// full permission/warranty notice in THIRD_PARTY_NOTICES.md.
|
||||
override NATIVE_MODEL_ALPHA_CUTOFF: f32 = 0.0;
|
||||
struct NativeModelOut {
|
||||
@builtin(position) clip: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
@location(2) @interpolate(flat) sprite: u32,
|
||||
@location(3) spherical: f32,
|
||||
@location(4) cylindrical: f32,
|
||||
@location(5) normal: vec3<f32>,
|
||||
}
|
||||
@vertex fn vs_native_model(@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>,
|
||||
@location(2) layer: u32, @location(3) color: vec4<f32>, @location(4) light: vec4<u32>,
|
||||
@location(5) normal_z: u32, @location(6) normal_xy: vec2<f32>) -> NativeModelOut {
|
||||
let block = max(f32(light.y), dynamic_block_light(position));
|
||||
var lit = color * sample_lightmap(f32(light.x) / 16.0, block / 16.0);
|
||||
if globals.colored_count.x > 0u {
|
||||
lit = vec4(min(lit.rgb + color.rgb * native_colored_light(position), vec3(1.0)), lit.a);
|
||||
}
|
||||
return NativeModelOut(globals.view_proj * vec4(position, 1.0), uv, lit, layer >> 16u,
|
||||
length(position), max(length(position.xz), abs(position.y)),
|
||||
vec3(normal_xy, bitcast<f32>(normal_z)));
|
||||
}
|
||||
@fragment fn fs_native_model(in: NativeModelOut) -> @location(0) vec4<f32> {
|
||||
var color = sprite_grad(in.uv, in.sprite, dpdx(in.uv), dpdy(in.uv)) * in.color;
|
||||
if NATIVE_MODEL_ALPHA_CUTOFF > 0.0 && color.a < NATIVE_MODEL_ALPHA_CUTOFF { discard; }
|
||||
if any(in.normal != vec3(0.0)) {
|
||||
let light0 = normalize(vec3(0.2, 1.0, -0.7));
|
||||
let light1 = normalize(vec3(-0.2, select(1.0, -1.0, globals.time.y > 0.5), 0.7));
|
||||
let shade = min(1.0, (max(0.0, dot(light0, in.normal))
|
||||
+ max(0.0, dot(light1, in.normal))) * 0.6 + 0.4);
|
||||
color = vec4(color.rgb * shade, color.a);
|
||||
}
|
||||
return apply_fog(color, in.spherical, in.cylindrical);
|
||||
}
|
||||
|
||||
// Sky (SkyRenderer): the 10-vertex sky disc fan at y = 16 (radius 512) in
|
||||
// the sky colour with sky fog (sky.fsh: fog 0..FogSkyEnd on the spherical
|
||||
// distance, a step at FogSkyEnd on the cylindrical one), evaluated per pixel
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
//! Native terrain-sheet models with explicit material and UV policy.
|
||||
use crate::{gfx, mesher::Vertex, models::catalog::BakedFace};
|
||||
use glam::{Mat3, Mat4, Vec3};
|
||||
use horizon_assets::bake::RenderType;
|
||||
fn same_sprite(left: &str, right: &str) -> bool {
|
||||
left.strip_prefix("minecraft:").unwrap_or(left)
|
||||
== right.strip_prefix("minecraft:").unwrap_or(right)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum NativeModelMaterial {
|
||||
/// Use each baked quad's material; unsupported translucent/tripwire quads fail the whole call.
|
||||
#[default]
|
||||
Model,
|
||||
Solid,
|
||||
Cutout,
|
||||
CutoutMipped,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum NativeModelShade {
|
||||
/// Respect the baked quad's directional-shading flag.
|
||||
#[default]
|
||||
Model,
|
||||
Off,
|
||||
/// The level's two entity light directions, even at full block/sky light.
|
||||
Entity,
|
||||
}
|
||||
|
||||
/// Transform UVs only on the explicitly named original sprite, in unit-sprite coordinates.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct NativeModelUvShift<'a> {
|
||||
pub source_sprite: &'a str,
|
||||
pub target_sprite: &'a str,
|
||||
pub scale: [f32; 2],
|
||||
pub offset: [f32; 2],
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct NativeModelRenderOptions<'a> {
|
||||
pub material: NativeModelMaterial,
|
||||
pub shade: NativeModelShade,
|
||||
pub uv_shift: Option<NativeModelUvShift<'a>>,
|
||||
/// Optional compatibility policy applied toward each face's UV bounds center before shifting.
|
||||
/// Zero retains the baked UVs. For an older JSON FaceBakery use its actual atlas shrink ratio;
|
||||
/// OBJ loaders that do not shrink UVs must keep zero.
|
||||
pub uv_shrink_ratio: f32,
|
||||
/// RGB multiplier on quads with a tint index; other quads keep their baked color.
|
||||
pub tint: u32,
|
||||
}
|
||||
impl Default for NativeModelRenderOptions<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
material: NativeModelMaterial::Model,
|
||||
shade: NativeModelShade::Model,
|
||||
uv_shift: None,
|
||||
uv_shrink_ratio: 0.0,
|
||||
tint: 0xff_ffff,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay(face: &BakedFace, policy: NativeModelMaterial) -> Option<u32> {
|
||||
use NativeModelMaterial as M;
|
||||
Some(match policy {
|
||||
M::Solid => gfx::TERRAIN_MODEL_SOLID_OVERLAY,
|
||||
M::Cutout => gfx::TERRAIN_MODEL_CUTOUT_OVERLAY,
|
||||
M::CutoutMipped => gfx::TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY,
|
||||
M::Model => match face.quad.render_type.unwrap_or(RenderType::Solid) {
|
||||
RenderType::Solid if !face.quad.material.force_translucent => {
|
||||
gfx::TERRAIN_MODEL_SOLID_OVERLAY
|
||||
}
|
||||
RenderType::Cutout => gfx::TERRAIN_MODEL_CUTOUT_OVERLAY,
|
||||
RenderType::CutoutMipped => gfx::TERRAIN_MODEL_CUTOUT_MIPPED_OVERLAY,
|
||||
_ => return None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// A whole draw validates before its first vertex. No temporary geometry allocation, resource
|
||||
/// scans, partial fallback or change to the ordinary native model helper.
|
||||
pub fn draw_model(
|
||||
faces: &[BakedFace],
|
||||
pose: Mat4,
|
||||
light: u32,
|
||||
options: NativeModelRenderOptions<'_>,
|
||||
sprite: impl Fn(&str) -> Option<u32>,
|
||||
mut emit: impl FnMut(Vertex),
|
||||
) -> bool {
|
||||
if !pose.is_finite()
|
||||
|| !options.uv_shrink_ratio.is_finite()
|
||||
|| !(0.0..=1.0).contains(&options.uv_shrink_ratio)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let normal_pose = Mat3::from_mat4(pose).inverse().transpose();
|
||||
if !normal_pose.is_finite() {
|
||||
return false;
|
||||
}
|
||||
let shift = match options.uv_shift {
|
||||
Some(shift) => {
|
||||
if shift
|
||||
.scale
|
||||
.iter()
|
||||
.chain(&shift.offset)
|
||||
.any(|v| !v.is_finite())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let (Some(source), Some(target)) =
|
||||
(sprite(shift.source_sprite), sprite(shift.target_sprite))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if source > u16::MAX.into() || target > u16::MAX.into() {
|
||||
return false;
|
||||
}
|
||||
if !faces
|
||||
.iter()
|
||||
.any(|face| same_sprite(&face.quad.material.sprite, shift.source_sprite))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Some((source, target, shift))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let vertices = |face: &BakedFace| -> Option<[Vertex; 4]> {
|
||||
let quad = &face.quad;
|
||||
let overlay = overlay(face, options.material)?;
|
||||
let mut layer = sprite(&quad.material.sprite)?;
|
||||
if layer > u16::MAX.into() || layer != face.layer {
|
||||
return None;
|
||||
}
|
||||
let mut uv = quad.uv;
|
||||
if uv.iter().flatten().any(|v| !v.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
if options.uv_shrink_ratio != 0.0 {
|
||||
let center = std::array::from_fn::<_, 2, _>(|i| {
|
||||
let min = uv.iter().map(|p| p[i]).fold(f32::INFINITY, f32::min);
|
||||
let max = uv.iter().map(|p| p[i]).fold(f32::NEG_INFINITY, f32::max);
|
||||
(min + min + max + max) / 4.0
|
||||
});
|
||||
for p in &mut uv {
|
||||
*p = std::array::from_fn(|i| p[i] + options.uv_shrink_ratio * (center[i] - p[i]));
|
||||
}
|
||||
}
|
||||
if let Some((_, target, shift)) = shift
|
||||
&& same_sprite(&quad.material.sprite, shift.source_sprite)
|
||||
{
|
||||
layer = target;
|
||||
for p in &mut uv {
|
||||
*p = std::array::from_fn(|i| p[i] * shift.scale[i] + shift.offset[i]);
|
||||
}
|
||||
}
|
||||
let shaded = match options.shade {
|
||||
NativeModelShade::Model => quad.shade,
|
||||
NativeModelShade::Off => false,
|
||||
NativeModelShade::Entity => true,
|
||||
};
|
||||
let normal = if shaded {
|
||||
let p = quad.positions.map(Vec3::from_array);
|
||||
let n = (p[1] - p[0]).cross(p[2] - p[0]).normalize_or_zero();
|
||||
// Baked model normals are signed bytes before the instance's normal matrix.
|
||||
// Keep the transformed normal as floats; quantizing it again changes rotated faces.
|
||||
let n = n.map(|v| ((v * 127.0) as i8) as f32 / 127.0);
|
||||
(normal_pose * n).normalize_or_zero()
|
||||
} else {
|
||||
Vec3::ZERO
|
||||
};
|
||||
let normal_bits = normal.z.to_ne_bytes().map(|b| b as i8);
|
||||
let argb = quad.color.map_or(u32::MAX, |v| v as u32);
|
||||
let mut color = (argb & 0xff00_ff00) | (argb >> 16 & 0xff) | (argb & 0xff) << 16;
|
||||
if quad.tint_index >= 0 {
|
||||
let tint = options.tint;
|
||||
let rgb = (tint >> 16 & 0xff) | (tint & 0xff00) | (tint & 0xff) << 16;
|
||||
color = (color & 0xff00_0000)
|
||||
| ((color & 0xff) * (rgb & 0xff) / 255)
|
||||
| (((color >> 8 & 0xff) * (rgb >> 8 & 0xff) / 255) << 8)
|
||||
| (((color >> 16 & 0xff) * (rgb >> 16 & 0xff) / 255) << 16);
|
||||
}
|
||||
let light = (light & 0xff).max(u32::from(quad.light[1]) * 16)
|
||||
| ((light >> 8 & 0xff).max(u32::from(quad.light[0].max(quad.light_emission)) * 16)
|
||||
<< 8);
|
||||
let positions = quad
|
||||
.positions
|
||||
.map(|p| pose.transform_point3(Vec3::from_array(p)));
|
||||
if positions.iter().any(|p| !p.is_finite()) || uv.iter().flatten().any(|v| !v.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
Some(std::array::from_fn(|i| Vertex {
|
||||
pos: positions[i].to_array(),
|
||||
uv: uv[i],
|
||||
layer: overlay | layer << 16,
|
||||
color,
|
||||
light,
|
||||
normal: normal_bits,
|
||||
glint_uv: [normal.x, normal.y],
|
||||
}))
|
||||
};
|
||||
if faces.iter().any(|face| vertices(face).is_none()) {
|
||||
return false;
|
||||
}
|
||||
for face in faces {
|
||||
for vertex in vertices(face).expect("validated immutable model") {
|
||||
emit(vertex);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "terrain_model_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,171 @@
|
||||
use super::*;
|
||||
use horizon_assets::{
|
||||
bake::bake_flat_face,
|
||||
model::{Direction, Material},
|
||||
};
|
||||
|
||||
fn face() -> BakedFace {
|
||||
BakedFace::new(
|
||||
bake_flat_face(
|
||||
[0.0; 3],
|
||||
[16.0; 3],
|
||||
[0.0, 0.0, 16.0, 16.0],
|
||||
Direction::South,
|
||||
Material {
|
||||
sprite: "test:source".into(),
|
||||
force_translucent: false,
|
||||
},
|
||||
-1,
|
||||
)
|
||||
.unwrap(),
|
||||
7,
|
||||
true,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
}
|
||||
fn sprite(name: &str) -> Option<u32> {
|
||||
match name {
|
||||
"test:source" => Some(7),
|
||||
"test:target" => Some(11),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn draw(
|
||||
faces: &[BakedFace],
|
||||
pose: Mat4,
|
||||
options: NativeModelRenderOptions<'_>,
|
||||
) -> (bool, Vec<Vertex>) {
|
||||
let mut output = Vec::new();
|
||||
let accepted = draw_model(faces, pose, 0xf0f0, options, sprite, |v| output.push(v));
|
||||
(accepted, output)
|
||||
}
|
||||
#[test]
|
||||
fn late_unknown_material_or_sprite_rejects_the_whole_model() {
|
||||
let first = face();
|
||||
let mut second = face();
|
||||
second.quad.render_type = Some(RenderType::Unsupported);
|
||||
let mut faces = [first, second];
|
||||
let (accepted, output) = draw(&faces, Mat4::IDENTITY, Default::default());
|
||||
assert!(!accepted);
|
||||
assert!(output.is_empty());
|
||||
let (accepted, output) = draw(
|
||||
&faces,
|
||||
Mat4::IDENTITY,
|
||||
NativeModelRenderOptions {
|
||||
material: NativeModelMaterial::Cutout,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(accepted);
|
||||
assert_eq!(output.len(), 8);
|
||||
faces[1].quad.material.sprite = "test:missing".into();
|
||||
let (accepted, output) = draw(&faces, Mat4::IDENTITY, Default::default());
|
||||
assert!(!accepted);
|
||||
assert!(output.is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn exact_sprite_shift_and_explicit_json_shrink_precede_array_mapping() {
|
||||
let options = NativeModelRenderOptions {
|
||||
uv_shrink_ratio: 4.0 / 2048.0,
|
||||
uv_shift: Some(NativeModelUvShift {
|
||||
source_sprite: "test:source",
|
||||
target_sprite: "test:target",
|
||||
scale: [0.5; 2],
|
||||
offset: [0.25, 0.125],
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let (accepted, output) = draw(&[face()], Mat4::IDENTITY, options);
|
||||
assert!(accepted);
|
||||
assert_eq!(output[0].uv, [0.25048828, 0.12548828]);
|
||||
assert_eq!(output[2].uv, [0.7495117, 0.6245117]);
|
||||
assert!(output.iter().all(|v| v.layer >> 16 == 11));
|
||||
let (accepted, output) = draw(
|
||||
&[face()],
|
||||
Mat4::IDENTITY,
|
||||
NativeModelRenderOptions {
|
||||
uv_shift: Some(NativeModelUvShift {
|
||||
source_sprite: "test:target",
|
||||
target_sprite: "test:source",
|
||||
..options.uv_shift.unwrap()
|
||||
}),
|
||||
..options
|
||||
},
|
||||
);
|
||||
assert!(!accepted);
|
||||
assert!(output.is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn transformed_slope_normal_stays_float_and_fullbright_keeps_shading() {
|
||||
let mut face = face();
|
||||
face.quad.positions = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 1.0],
|
||||
[1.0, 1.0, 1.0],
|
||||
[1.0, 0.0, 0.0],
|
||||
];
|
||||
let pose = Mat4::from_rotation_y(0.372) * Mat4::from_scale(Vec3::new(-2.0, 3.0, 1.0));
|
||||
let (accepted, output) = draw(&[face], pose, Default::default());
|
||||
assert!(accepted);
|
||||
let normal = Vec3::new(
|
||||
output[0].glint_uv[0],
|
||||
output[0].glint_uv[1],
|
||||
f32::from_ne_bytes(output[0].normal.map(|v| v as u8)),
|
||||
);
|
||||
assert!((normal.length() - 1.0).abs() < 1e-6);
|
||||
assert!((normal.y - 0.31622776).abs() < 1e-6);
|
||||
assert!((normal.x + 0.3448316).abs() < 1e-5, "{normal:?}");
|
||||
assert_eq!(output[0].light, 0xf0f0);
|
||||
assert_eq!(output[0].color, u32::MAX);
|
||||
// Float payload, not a second signed-byte normal quantization.
|
||||
assert_ne!(normal.y, (normal.y * 127.0) as i8 as f32 / 127.0);
|
||||
}
|
||||
#[test]
|
||||
fn invalid_shift_and_singular_pose_emit_nothing() {
|
||||
let options = NativeModelRenderOptions {
|
||||
uv_shrink_ratio: f32::NAN,
|
||||
..Default::default()
|
||||
};
|
||||
let (ok, output) = draw(&[face()], Mat4::IDENTITY, options);
|
||||
assert!(!ok);
|
||||
assert!(output.is_empty());
|
||||
let (ok, output) = draw(
|
||||
&[face()],
|
||||
Mat4::from_scale(Vec3::new(1.0, 0.0, 1.0)),
|
||||
Default::default(),
|
||||
);
|
||||
assert!(!ok);
|
||||
assert!(output.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_deduplicated_array_layer_is_not_the_same_named_source_sprite() {
|
||||
let mut alias = face();
|
||||
alias.quad.material.sprite = "test:alias".into();
|
||||
let mut output = Vec::new();
|
||||
assert!(draw_model(
|
||||
&[face(), alias],
|
||||
Mat4::IDENTITY,
|
||||
0xf0f0,
|
||||
NativeModelRenderOptions {
|
||||
uv_shift: Some(NativeModelUvShift {
|
||||
source_sprite: "test:source",
|
||||
target_sprite: "test:target",
|
||||
scale: [0.5; 2],
|
||||
offset: [0.25; 2]
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
|name| if name == "test:alias" {
|
||||
Some(7)
|
||||
} else {
|
||||
sprite(name)
|
||||
},
|
||||
|v| output.push(v)
|
||||
));
|
||||
assert_eq!(output[0].layer >> 16, 11);
|
||||
assert_eq!(output[4].layer >> 16, 7);
|
||||
assert_eq!(output[4].uv, [0.0, 0.0]);
|
||||
assert_eq!(output[6].uv, [1.0, 1.0]);
|
||||
}
|
||||
@@ -23,8 +23,8 @@ pub use horizon_client_render::{
|
||||
screen_effects, sculk, section_mask, server_terrain, settings, shader_options,
|
||||
shader_pack_list, signs, skins, sound_engine, sound_events, sound_preview, spans, split_layout,
|
||||
split_map, split_profiles, sprites, streaming, structure_block, tab_list, telemetry,
|
||||
test_block, test_instance_block, testgen, title_edition, trims, ui, vault, weather, widgets,
|
||||
world, worldborder, zip,
|
||||
terrain_model, test_block, test_instance_block, testgen, title_edition, trims, ui, vault,
|
||||
weather, widgets, world, worldborder, zip,
|
||||
};
|
||||
pub mod attack_strength;
|
||||
pub mod attribute_updates;
|
||||
|
||||
@@ -279,6 +279,7 @@ mod native_scene;
|
||||
mod native_server;
|
||||
mod native_shaders;
|
||||
mod native_structures;
|
||||
mod native_terrain_model;
|
||||
pub mod native_text_field;
|
||||
mod native_ticks;
|
||||
mod native_travel;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
//! Explicit terrain-sheet material/shading/UV policy for a native standalone model.
|
||||
use super::{Game, native_block_entity_draw::BlockEntityDraw};
|
||||
use crate::mod_api::NativeModelRenderOptions;
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Game<'_> {
|
||||
/// Draw a named standalone model using block sprite arrays and normal cull/depth writes.
|
||||
/// Model materials accept solid, cutout (.1 alpha cutoff, no mipmaps) and cutout-mipped
|
||||
/// (.5 cutoff, mipmaps); unknown materials/sprites or invalid UV/pose data fail before
|
||||
/// any geometry. An explicit material overrides the model's selection. Directional
|
||||
/// shading remains independent of light: fullbright shaded models remain shaded.
|
||||
/// UV shifts affect only the exact source sprite; a missing source match fails the
|
||||
/// whole call. Optional shrink is caller-specified, never inferred from an atlas guess.
|
||||
/// API @API:terrain_model@.
|
||||
pub(super) fn native_draw_terrain_model(
|
||||
&self,
|
||||
draw: &mut BlockEntityDraw,
|
||||
model: &str,
|
||||
options: NativeModelRenderOptions<'_>,
|
||||
) -> bool {
|
||||
let Some(model) = self.pack.render.model_catalog.standalone(model) else {
|
||||
return false;
|
||||
};
|
||||
let pose = draw.pose();
|
||||
let light = draw.light;
|
||||
let layers = &self.pack.render.sprite_layers;
|
||||
crate::terrain_model::draw_model(
|
||||
&model.faces,
|
||||
pose,
|
||||
light,
|
||||
options,
|
||||
|name| {
|
||||
layers
|
||||
.get(name)
|
||||
.or_else(|| layers.get(name.strip_prefix("minecraft:").unwrap_or(name)))
|
||||
.copied()
|
||||
},
|
||||
|vertex| draw.vertices_mut().push(vertex),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,9 @@ pub use crate::recipe_book::RecipeBookCategory;
|
||||
pub use crate::sound_engine::SoundPlayback;
|
||||
/// A named step of the server tick and its wall and CPU milliseconds (`native_server_tick_timed`).
|
||||
pub use crate::spans::Span as TickSpan;
|
||||
pub use crate::terrain_model::{
|
||||
NativeModelMaterial, NativeModelRenderOptions, NativeModelShade, NativeModelUvShift,
|
||||
};
|
||||
pub use crate::text::Text;
|
||||
pub use crate::world_primitives::{PrimitiveShape, WorldPrimitive, WorldPrimitives};
|
||||
pub use horizon_assets::item_model::{DisplayContext, StackView};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"hooks": [],
|
||||
"helpers": [
|
||||
{
|
||||
"name": "draw_terrain_model",
|
||||
"target": "horizon_client::game::native_terrain_model::Game<'_>::native_draw_terrain_model",
|
||||
"mod_api": {}
|
||||
}
|
||||
],
|
||||
"rust_summary": "explicit terrain-sheet native model materials, shading and UV shifts.",
|
||||
"doc_summary": "`native_draw_terrain_model(draw,model,NativeModelRenderOptions)` draws a standalone model with animated block sprite arrays. Material `Model` respects solid/cutout/cutout_mipped quads, or explicitly select `Solid`, `Cutout` or `CutoutMipped`. Cutout uses alpha .1 without mipmaps; mipmapped cutout uses .5. All cull backs and use ordinary opaque color/depth writes. `NativeModelShade::{Model,Off,Entity}` controls directional shading independently of light, including fullbright. `NativeModelUvShift` names original/target sprites and unit-sprite scale/offset; invalid/missing mappings fail before geometry. `uv_shrink_ratio` defaults zero and explicitly supports older FaceBakery UV policy without guessing atlas dimensions. `tint` defaults white and multiplies only tinted quads. Ordinary native model/fluid/Canvas/OIT behavior is unchanged. Bundles need \"hooks\": @API:terrain_model@.",
|
||||
"comments": [
|
||||
"crates/horizon-client/src/game/native_terrain_model.rs"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user