mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #11117 from Delete228/native-still-fluid-render-upstream
🤖 Agent: Native mods can draw a still-fluid volume in a block; the fluid shading follows Ponder's FluidRenderHelper (MIT, credited in THIRD_PARTY_NOTICES) and nothing changes without a mod calling it.
This commit is contained in:
13 files changed
+612
-30
No files matched your search
@@ -1,5 +1,15 @@
|
||||
# Third-party notices
|
||||
|
||||
## Catnip / Ponder
|
||||
|
||||
`crates/horizon-client-render/src/fluid_render.rs` translates the still-fluid face tiling and
|
||||
UV rules of Catnip's `FluidRenderHelper` from Ponder 1.0.82 for Minecraft 1.21.1
|
||||
(`net.createmod.ponder:ponder-neoforge:1.0.82+mc1.21.1`). Copyright (c) 2022 The Create Team,
|
||||
licensed under MIT (`LICENSE_Ponder` in that artifact); the full license heads the Rust file.
|
||||
The native renderer uses Horizon's own block-sprite arrays, pose and light packing. The
|
||||
development-only oracle calls the unchanged helper from the player's local artifact. No
|
||||
original classes, game shaders, textures or prepared resources are distributed here.
|
||||
|
||||
## wgpu-hal
|
||||
|
||||
`third_party/wgpu-hal` is [wgpu-hal](https://github.com/gfx-rs/wgpu) 30.0.1 by the gfx-rs
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
// Still-fluid tiling follows Catnip/Ponder 1.0.82 FluidRenderHelper.renderTiledFace.
|
||||
// Copyright (c) 2022 The Create Team
|
||||
//
|
||||
// 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.
|
||||
|
||||
use horizon_core::geometry::Direction;
|
||||
|
||||
/// Controls a still-fluid box. The tint is stack ARGB, independent of biome tint.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct NativeFluidRenderOptions {
|
||||
pub argb: Option<u32>,
|
||||
pub render_bottom: bool,
|
||||
/// TextureAtlasSprite.uvShrinkRatio: 4 / the largest stitched atlas dimension.
|
||||
/// Explicit because Horizon's block sprites occupy arrays rather than a stitched atlas.
|
||||
pub uv_shrink_ratio: f32,
|
||||
}
|
||||
|
||||
/// A corner before the caller's pose, with UVs within the still sprite, and its outward normal.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct FluidVertex {
|
||||
pub pos: [f32; 3],
|
||||
pub uv: [f32; 2],
|
||||
pub normal: [i8; 3],
|
||||
}
|
||||
|
||||
/// Preserve sky light and increase block light to the fluid's luminosity (native light packing).
|
||||
pub fn fluid_light(light: u32, luminosity: u8) -> u32 {
|
||||
(light & 0xf0) | (((light >> 8 & 0xf0).max(u32::from(luminosity.min(15)) * 16)) << 8)
|
||||
}
|
||||
|
||||
/// Emit whole tiled quads, or refuse invalid/unreasonably large geometry before emitting anything.
|
||||
/// Negative coordinates and exact integer boundaries retain the source's local UV fractions.
|
||||
pub fn fluid_box(
|
||||
bounds: [f32; 6],
|
||||
options: NativeFluidRenderOptions,
|
||||
mut emit: impl FnMut(FluidVertex),
|
||||
) -> bool {
|
||||
if !options.uv_shrink_ratio.is_finite()
|
||||
|| !(0.0..=1.0).contains(&options.uv_shrink_ratio)
|
||||
|| bounds
|
||||
.iter()
|
||||
.any(|x| !x.is_finite() || x.abs() > 1_048_576.0)
|
||||
|| (0..3).any(|i| bounds[i] > bounds[i + 3])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let tiles =
|
||||
std::array::from_fn::<_, 3, _>(|i| (bounds[i + 3].ceil() - bounds[i].floor()) as u64);
|
||||
let quads = tiles[0] * tiles[1] * 2
|
||||
+ tiles[1] * tiles[2] * 2
|
||||
+ tiles[0] * tiles[2] * if options.render_bottom { 2 } else { 1 };
|
||||
if quads > 65_536 {
|
||||
return false;
|
||||
}
|
||||
let [x0, y0, z0, x1, y1, z1] = bounds;
|
||||
for (side, face) in [
|
||||
(Direction::Down, [x0, z0, x1, z1, y0]),
|
||||
(Direction::Up, [x0, z0, x1, z1, y1]),
|
||||
(Direction::North, [x0, y0, x1, y1, z0]),
|
||||
(Direction::South, [x0, y0, x1, y1, z1]),
|
||||
(Direction::West, [z0, y0, z1, y1, x0]),
|
||||
(Direction::East, [z0, y0, z1, y1, x1]),
|
||||
] {
|
||||
if side != Direction::Down || options.render_bottom {
|
||||
tiled_face(&mut emit, side, face, options.uv_shrink_ratio);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn tiled_face(out: &mut impl FnMut(FluidVertex), side: Direction, face: [f32; 5], ratio: f32) {
|
||||
let [a0, b0, a1, b1, plane] = face;
|
||||
let positive = matches!(side, Direction::Up | Direction::South | Direction::East);
|
||||
let shrink = ratio * 0.25;
|
||||
let lerp = |v| v + shrink * (0.5 - v);
|
||||
let mut a = a0;
|
||||
while a < a1 {
|
||||
let end_a = (a.floor() + 1.0).min(a1);
|
||||
let (u0, u1) = if matches!(side, Direction::North | Direction::East) {
|
||||
(end_a.ceil() - end_a, end_a.ceil() - a)
|
||||
} else {
|
||||
(a - a.floor(), end_a - a.floor())
|
||||
};
|
||||
let (u0, u1) = (lerp(u0), lerp(u1));
|
||||
let mut b = b0;
|
||||
while b < b1 {
|
||||
let end_b = (b.floor() + 1.0).min(b1);
|
||||
let (v0, v1) = if side == Direction::Up {
|
||||
(b - b.floor(), end_b - b.floor())
|
||||
} else {
|
||||
(end_b.ceil() - end_b, end_b.ceil() - b)
|
||||
};
|
||||
let (v0, v1) = (lerp(v0), lerp(v1));
|
||||
let positions = match side {
|
||||
Direction::West | Direction::East => {
|
||||
let (near, far) = if positive { (end_a, a) } else { (a, end_a) };
|
||||
[
|
||||
[plane, end_b, near],
|
||||
[plane, b, near],
|
||||
[plane, b, far],
|
||||
[plane, end_b, far],
|
||||
]
|
||||
}
|
||||
Direction::North | Direction::South => {
|
||||
let (near, far) = if positive { (a, end_a) } else { (end_a, a) };
|
||||
[
|
||||
[near, end_b, plane],
|
||||
[near, b, plane],
|
||||
[far, b, plane],
|
||||
[far, end_b, plane],
|
||||
]
|
||||
}
|
||||
Direction::Down | Direction::Up => {
|
||||
let (near, far) = if positive { (b, end_b) } else { (end_b, b) };
|
||||
[
|
||||
[a, plane, near],
|
||||
[a, plane, far],
|
||||
[end_a, plane, far],
|
||||
[end_a, plane, near],
|
||||
]
|
||||
}
|
||||
};
|
||||
let normal = match side {
|
||||
Direction::Down => [0, -1, 0],
|
||||
Direction::Up => [0, 1, 0],
|
||||
Direction::North => [0, 0, -1],
|
||||
Direction::South => [0, 0, 1],
|
||||
Direction::West => [-1, 0, 0],
|
||||
Direction::East => [1, 0, 0],
|
||||
};
|
||||
for (pos, uv) in positions
|
||||
.into_iter()
|
||||
.zip([[u0, v0], [u0, v1], [u1, v1], [u1, v0]])
|
||||
{
|
||||
out(FluidVertex { pos, uv, normal });
|
||||
}
|
||||
b = end_b;
|
||||
}
|
||||
a = end_a;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn source_tiling_is_outward_and_does_not_inset_geometry() {
|
||||
let bounds = [-1.25, 0.125, -0.25, 1.5, 1.125, 0.75];
|
||||
let mut result = Vec::new();
|
||||
assert!(fluid_box(
|
||||
bounds,
|
||||
NativeFluidRenderOptions {
|
||||
argb: None,
|
||||
render_bottom: false,
|
||||
uv_shrink_ratio: 4.0 / 2048.0,
|
||||
},
|
||||
|v| result.push(v),
|
||||
));
|
||||
assert_eq!(result.len(), (4 * 2 * 2 + 2 * 2 * 2 + 4 * 2) * 4);
|
||||
for quad in result.as_chunks::<4>().0 {
|
||||
let [a, b, c, _] = quad.map(|v| glam::Vec3::from_array(v.pos));
|
||||
assert!(
|
||||
(b - a)
|
||||
.cross(c - a)
|
||||
.dot(glam::Vec3::from_array(quad[0].normal.map(f32::from)))
|
||||
> 0.0
|
||||
);
|
||||
assert!(
|
||||
quad.iter()
|
||||
.all(|v| (0..3).all(|i| v.pos[i] >= bounds[i] && v.pos[i] <= bounds[i + 3]))
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
result
|
||||
.iter()
|
||||
.any(|v| v.pos == [bounds[0], bounds[4], bounds[2]])
|
||||
);
|
||||
assert!(!fluid_box(
|
||||
[0.0, 0.0, 0.0, 1_000.0, 1_000.0, 1_000.0],
|
||||
NativeFluidRenderOptions {
|
||||
argb: None,
|
||||
render_bottom: true,
|
||||
uv_shrink_ratio: 0.0,
|
||||
},
|
||||
|_| panic!("invalid geometry must fail before emitting"),
|
||||
));
|
||||
}
|
||||
#[test]
|
||||
fn luminosity_changes_only_block_light() {
|
||||
assert_eq!(fluid_light(0x20a0, 15), 0xf0a0);
|
||||
assert_eq!(fluid_light(0x25a5, 15), 0xf0a0);
|
||||
assert_eq!(fluid_light(0xe030, 3), 0xe030);
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ fn static_entity_shader() -> String {
|
||||
mod crumbling_tests;
|
||||
mod entity_batch;
|
||||
mod entity_outline;
|
||||
mod fluid;
|
||||
#[cfg(test)]
|
||||
mod inter_stage_tests;
|
||||
#[cfg(test)]
|
||||
@@ -53,9 +54,9 @@ mod weather;
|
||||
mod world_text;
|
||||
use entity_batch::EntityBatch;
|
||||
pub use entity_batch::{
|
||||
DISSOLVE_OVERLAY, DRAGON_RAYS_OVERLAY, ENERGY_SWIRL_OVERLAY, OIT_ENTITY, SHADOW_OVERLAY,
|
||||
TEXT_POLYGON_OFFSET_OVERLAY, TRANSLUCENT_GLINT_ITEM_OVERLAY, TRANSLUCENT_ITEM_OVERLAY,
|
||||
TRANSLUCENT_PARTICLE_OVERLAY, WATER_MASK_OVERLAY, WORLD_TEXT_OVERLAY,
|
||||
DISSOLVE_OVERLAY, DRAGON_RAYS_OVERLAY, ENERGY_SWIRL_OVERLAY, FLUID_OVERLAY, OIT_ENTITY,
|
||||
SHADOW_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,
|
||||
};
|
||||
pub use view::{View, ViewSet};
|
||||
@@ -1205,6 +1206,7 @@ pub struct Renderer {
|
||||
world_primitives: world_primitives::WorldPrimitives,
|
||||
text_polygon_offset_pipeline: wgpu::RenderPipeline,
|
||||
world_text_pipeline: wgpu::RenderPipeline,
|
||||
fluid_pipeline: wgpu::RenderPipeline,
|
||||
celestial_pipeline: wgpu::RenderPipeline,
|
||||
celestial_buffer: wgpu::Buffer,
|
||||
end_sky: end_sky::EndSky,
|
||||
@@ -1240,6 +1242,8 @@ pub struct Renderer {
|
||||
/// Ordinary entity models posed on the GPU; `None` without compute shaders.
|
||||
entity_skin: Option<entity_skin::EntitySkin>,
|
||||
group: wgpu::BindGroup,
|
||||
fluid_group: wgpu::BindGroup,
|
||||
fluid_sampler: wgpu::Sampler,
|
||||
globals: wgpu::Buffer,
|
||||
/// Resource-pack normal/specular maps, split like the colour sprite arrays.
|
||||
pbr_maps: Option<Vec<[wgpu::Texture; 2]>>,
|
||||
@@ -2771,6 +2775,13 @@ impl Renderer {
|
||||
bind_group_layouts: &[Some(&bindings)],
|
||||
..Default::default()
|
||||
});
|
||||
let fluid_pipeline = fluid::pipeline(
|
||||
&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 {
|
||||
@@ -3329,6 +3340,18 @@ impl Renderer {
|
||||
&lightmap_sampler,
|
||||
&emission_stats.view,
|
||||
);
|
||||
let fluid_sampler = device.create_sampler(&fluid::SAMPLER);
|
||||
let fluid_group = terrain_group(
|
||||
&device,
|
||||
"Still fluid resources",
|
||||
&bindings,
|
||||
&globals,
|
||||
&sprite_views,
|
||||
&fluid_sampler,
|
||||
&lightmap_view,
|
||||
&lightmap_sampler,
|
||||
&emission_stats.view,
|
||||
);
|
||||
let crosshair_group = terrain_group(
|
||||
&device,
|
||||
"Debug crosshair resources",
|
||||
@@ -4718,6 +4741,7 @@ impl Renderer {
|
||||
world_primitives,
|
||||
text_polygon_offset_pipeline,
|
||||
world_text_pipeline,
|
||||
fluid_pipeline,
|
||||
celestial_pipeline,
|
||||
celestial_buffer,
|
||||
end_sky: end_sky::EndSky::default(),
|
||||
@@ -4750,6 +4774,8 @@ impl Renderer {
|
||||
entity_batch: EntityBatch::default(),
|
||||
entity_skin,
|
||||
group,
|
||||
fluid_group,
|
||||
fluid_sampler,
|
||||
globals,
|
||||
pbr_maps,
|
||||
lightmap,
|
||||
@@ -7051,6 +7077,17 @@ impl Renderer {
|
||||
&self.lightmap_sampler,
|
||||
&self.emission_stats.view,
|
||||
);
|
||||
self.fluid_group = terrain_group(
|
||||
&self.device,
|
||||
"Still fluid resources",
|
||||
&self.terrain_layout,
|
||||
&self.globals,
|
||||
&self.sprite_views,
|
||||
&self.fluid_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`).
|
||||
@@ -8692,6 +8729,7 @@ impl Renderer {
|
||||
translucent_particle_quads,
|
||||
translucent_item_quads,
|
||||
oit_entity_quads,
|
||||
fluid_quads,
|
||||
world_text_quads,
|
||||
world_text_offset_quads,
|
||||
] = self.entity_batch.update(
|
||||
@@ -8720,13 +8758,17 @@ impl Renderer {
|
||||
+ translucent_particle_quads
|
||||
+ translucent_item_quads
|
||||
+ oit_entity_quads
|
||||
+ fluid_quads
|
||||
+ world_text_quads
|
||||
+ world_text_offset_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.
|
||||
let base_quads =
|
||||
world_quads - oit_entity_quads - world_text_quads - world_text_offset_quads;
|
||||
let base_quads = world_quads
|
||||
- oit_entity_quads
|
||||
- fluid_quads
|
||||
- world_text_quads
|
||||
- world_text_offset_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) =
|
||||
@@ -10158,6 +10200,20 @@ impl Renderer {
|
||||
}
|
||||
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.
|
||||
if fluid_quads > 0 {
|
||||
pass.set_pipeline(&self.fluid_pipeline);
|
||||
pass.set_bind_group(0, &self.fluid_group, &[]);
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
pass.draw_indexed(
|
||||
0..fluid_quads * 6,
|
||||
((base_quads + oit_entity_quads) * 4) as i32,
|
||||
0..1,
|
||||
);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
}
|
||||
if crumbling_quads > 0 && !oit_on {
|
||||
pass.set_pipeline(&self.crumbling_pipeline);
|
||||
pass.set_bind_group(0, &self.entity_group, &[]);
|
||||
@@ -10544,7 +10600,7 @@ impl Renderer {
|
||||
pass.set_bind_group(0, &self.entity_group, &[]);
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
let first = base_quads + oit_entity_quads;
|
||||
let first = base_quads + oit_entity_quads + fluid_quads;
|
||||
for (pipeline, count, offset) in [
|
||||
(&self.world_text_pipeline, world_text_quads, first),
|
||||
(
|
||||
@@ -12604,7 +12660,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]);
|
||||
assert_eq!(counts, [4, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0]);
|
||||
assert_eq!(
|
||||
vertices
|
||||
.as_chunks::<4>()
|
||||
|
||||
@@ -14,8 +14,11 @@ use super::Vertex;
|
||||
/// material at the end: the render types vanilla gives an `OitPipelineSet`
|
||||
/// (translucent, emissive, eyes and breeze wind entity models). Off, the tag is
|
||||
/// ignored and they stay in the first one, in their order.
|
||||
/// Two native world-canvas text materials follow it, NORMAL then POLYGON_OFFSET.
|
||||
const MATERIALS: usize = 12;
|
||||
/// 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;
|
||||
/// 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
|
||||
/// (bits 16 to 23): the quad's render type has an `OitPipelineSet`
|
||||
/// (`RenderTypes.entityTranslucent`, `entityTranslucentEmissive`, `eyes`,
|
||||
@@ -60,12 +63,15 @@ 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 {
|
||||
if layer & 0xffff == FLUID_OVERLAY {
|
||||
return 10;
|
||||
}
|
||||
if oit && layer & OIT_ENTITY != 0 {
|
||||
return 9;
|
||||
}
|
||||
match layer & 0xffff {
|
||||
WORLD_TEXT_OVERLAY => 10,
|
||||
WORLD_TEXT_POLYGON_OFFSET_OVERLAY => 11,
|
||||
WORLD_TEXT_OVERLAY => 11,
|
||||
WORLD_TEXT_POLYGON_OFFSET_OVERLAY => 12,
|
||||
TEXT_POLYGON_OFFSET_OVERLAY => 1,
|
||||
11 => 2,
|
||||
12 => 3,
|
||||
@@ -148,9 +154,9 @@ impl EntityBatch {
|
||||
}
|
||||
}
|
||||
}
|
||||
// RenderType.text sorts alpha-blended quads from far to near. Keep
|
||||
// both world-canvas layers after existing materials (including eyes
|
||||
// moved out of additive); front text uses polygon offset.
|
||||
// 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..] {
|
||||
let start = self.combined.len();
|
||||
for &(first, count) in runs {
|
||||
@@ -189,7 +195,7 @@ mod tests {
|
||||
use super::*;
|
||||
use std::{hint::black_box, time::Instant};
|
||||
#[test]
|
||||
fn world_canvas_sorts_after_oit_eyes_before_frame_tails() {
|
||||
fn fluid_and_world_canvas_sort_after_oit_eyes_before_frame_tails() {
|
||||
let quad = |layer, x| {
|
||||
[Vertex {
|
||||
layer,
|
||||
@@ -204,6 +210,8 @@ mod tests {
|
||||
(WORLD_TEXT_OVERLAY, 4.0),
|
||||
(WORLD_TEXT_OVERLAY, 5.0),
|
||||
(WORLD_TEXT_POLYGON_OFFSET_OVERLAY, 6.0),
|
||||
(FLUID_OVERLAY, 11.0),
|
||||
(FLUID_OVERLAY | OIT_ENTITY, 12.0),
|
||||
]
|
||||
.into_iter()
|
||||
.flat_map(|(layer, x)| quad(layer, x))
|
||||
@@ -215,12 +223,14 @@ 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, 1]
|
||||
[1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 1]
|
||||
);
|
||||
let positions: Vec<_> = batch.combined.iter().step_by(4).map(|v| v.pos[0]).collect();
|
||||
assert_eq!(
|
||||
positions,
|
||||
[1.0, 2.0, 3.0, 7.0, 5.0, 4.0, 6.0, 8.0, 9.0, 10.0]
|
||||
[
|
||||
1.0, 2.0, 3.0, 7.0, 12.0, 11.0, 5.0, 4.0, 6.0, 8.0, 9.0, 10.0
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -320,12 +330,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]);
|
||||
assert_eq!(counts, [4, 0, 0, 0, 0, 0, 1, 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]);
|
||||
assert_eq!(counts, [2, 0, 0, 0, 0, 0, 1, 0, 0, 2, 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]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
//! Sorted still-fluid material: ordinary back culling, alpha blend and color/depth writes.
|
||||
pub(super) const SAMPLER: wgpu::SamplerDescriptor<'static> = wgpu::SamplerDescriptor {
|
||||
label: Some("Still fluid (nearest texels, linear mipmaps)"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Nearest,
|
||||
min_filter: wgpu::FilterMode::Nearest,
|
||||
mipmap_filter: wgpu::MipmapFilterMode::Linear,
|
||||
lod_min_clamp: 0.0,
|
||||
lod_max_clamp: 32.0,
|
||||
compare: None,
|
||||
anisotropy_clamp: 1,
|
||||
border_color: None,
|
||||
};
|
||||
|
||||
pub(super) fn pipeline(
|
||||
device: &wgpu::Device,
|
||||
layout: Option<&wgpu::PipelineLayout>,
|
||||
shader: &wgpu::ShaderModule,
|
||||
format: wgpu::TextureFormat,
|
||||
constants: &[(&str, f64)],
|
||||
) -> wgpu::RenderPipeline {
|
||||
let attributes = wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x2,2=>Uint32,3=>Unorm8x4,4=>Uint8x4,5=>Snorm8x4,6=>Float32x2];
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Still fluid"),
|
||||
layout,
|
||||
vertex: wgpu::VertexState {
|
||||
module: shader,
|
||||
entry_point: Some("vs_native_fluid"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
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_fluid"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants,
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
}
|
||||
@@ -454,9 +454,31 @@ impl PackEntities {
|
||||
let first = spans.first().map_or(entities.len(), |s| s.start as usize);
|
||||
extend_without_portals(rest, &entities[..first.min(entities.len())]);
|
||||
let (mut other, mut portal_faces, mut beam_faces) = (Vec::new(), Vec::new(), Vec::new());
|
||||
let mut without_fluids = Vec::new();
|
||||
for (span, range) in runs(entities.len(), spans) {
|
||||
let run_start = range.start as u32;
|
||||
let whole = &entities[range];
|
||||
// A native fluid carries sprite-array UVs, not shared-atlas UVs.
|
||||
// Keep it in its own material even when a shader pack draws the
|
||||
// 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)
|
||||
{
|
||||
without_fluids.clear();
|
||||
for quad in whole.chunks(4) {
|
||||
if quad[0].layer & 0xffff == super::FLUID_OVERLAY {
|
||||
if !span.hidden {
|
||||
rest.extend_from_slice(quad);
|
||||
}
|
||||
} else {
|
||||
without_fluids.extend_from_slice(quad);
|
||||
}
|
||||
}
|
||||
&without_fluids
|
||||
} else {
|
||||
whole
|
||||
};
|
||||
// A block entity's run can hold End portal faces and beams beside plain parts (an End
|
||||
// gateway: its cube and its beam): each goes to its own program.
|
||||
// Beacon beams come in a run of their own, at stage NONE (game.rs: no stage of 5.4
|
||||
@@ -1141,6 +1163,76 @@ fn skin_runs(skin: &super::entity_skin::SkinFrame) -> Vec<(EntitySpan, Range<u32
|
||||
mod gpu_span_tests {
|
||||
use super::*;
|
||||
use crate::gfx::entity_skin::{SkinFrame, SkinJob};
|
||||
#[test]
|
||||
#[ignore = "requires a Vulkan/Metal/DX12 adapter"]
|
||||
fn native_fluid_stays_native_while_model_uses_pack() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap();
|
||||
let (device, queue) =
|
||||
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"
|
||||
);
|
||||
} 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn posed_draws_preserve_subject_ids_overlays_and_hidden_shadow_casters() {
|
||||
let mut skin = SkinFrame {
|
||||
|
||||
@@ -43,6 +43,7 @@ pub mod effects;
|
||||
pub mod environment;
|
||||
pub mod extras;
|
||||
pub mod fast_hash;
|
||||
pub mod fluid_render;
|
||||
pub mod fog;
|
||||
pub mod font;
|
||||
pub mod fusion;
|
||||
|
||||
@@ -723,6 +723,33 @@ fn solid_finish_at(in: TerrainOut, texel: vec4<f32>, color: vec4<f32>, du: vec2<
|
||||
return apply_fog(color, in.spherical, in.cylindrical);
|
||||
}
|
||||
|
||||
// Native still-fluid boxes use block sprites and lightmap without terrain AO/emission
|
||||
// packing or entity-model diffuse shading. The absolute sprite layer occupies glint UV x.
|
||||
struct NativeFluidOut {
|
||||
@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,
|
||||
}
|
||||
@vertex fn vs_native_fluid(@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>,
|
||||
@location(3) color: vec4<f32>, @location(4) light: vec4<u32>,
|
||||
@location(6) sprite: vec2<f32>) -> NativeFluidOut {
|
||||
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 NativeFluidOut(globals.view_proj * vec4(position, 1.0), uv, lit, u32(sprite.x),
|
||||
length(position), max(length(position.xz), abs(position.y)));
|
||||
}
|
||||
@fragment fn fs_native_fluid(in: NativeFluidOut) -> @location(0) vec4<f32> {
|
||||
// The source translucent shader has no alpha discard; transparent texels write depth too.
|
||||
let color = sprite_grad(in.uv, in.sprite, dpdx(in.uv), dpdy(in.uv)) * in.color;
|
||||
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
|
||||
|
||||
@@ -13,17 +13,18 @@ pub use horizon_client_render::{
|
||||
block_entity_anim, block_entity_render, block_sprites, block_states, bluemap, bluemap_detect,
|
||||
border_render, breaking, brewing, camera, chat, chunk, chunk_map, cit, cit_armor, cit_assets,
|
||||
cit_glint, client_resources, clouds, collisions, command_block, containers, controller_ui,
|
||||
crash, custom_sky, datapack, dynamic_light, effects, environment, extras, fast_hash, fog, font,
|
||||
gfx, graphics, graphics_style, graphics_ui, hdr_calibration, hud, hud_layout, hud_overlays,
|
||||
inventory_effects, java_text, jigsaw_block, json_folder, level_loading, light, lod, logging,
|
||||
menu_blur, mesher, minimap, mipmap, mod_browser, mod_list, model_library, models,
|
||||
multiline_edit, native_emissive, options_file, options_fixes, options_ui, overlay,
|
||||
pack_browser, pack_screen, packs, pad_cursor, pad_glyph, par, particles, poi_sections,
|
||||
post_chain, potent_sulfur, preedit, providers, pvp, pvp_hud, screen_draw, 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,
|
||||
crash, custom_sky, datapack, dynamic_light, effects, environment, extras, fast_hash,
|
||||
fluid_render, fog, font, gfx, graphics, graphics_style, graphics_ui, hdr_calibration, hud,
|
||||
hud_layout, hud_overlays, inventory_effects, java_text, jigsaw_block, json_folder,
|
||||
level_loading, light, lod, logging, menu_blur, mesher, minimap, mipmap, mod_browser, mod_list,
|
||||
model_library, models, multiline_edit, native_emissive, options_file, options_fixes,
|
||||
options_ui, overlay, pack_browser, pack_screen, packs, pad_cursor, pad_glyph, par, particles,
|
||||
poi_sections, post_chain, potent_sulfur, preedit, providers, pvp, pvp_hud, screen_draw,
|
||||
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,
|
||||
};
|
||||
pub mod attack_strength;
|
||||
pub mod attribute_updates;
|
||||
|
||||
@@ -244,6 +244,7 @@ mod native_entity_motion;
|
||||
mod native_entity_refs;
|
||||
mod native_explosions;
|
||||
mod native_falling;
|
||||
mod native_fluid_render;
|
||||
mod native_hooks;
|
||||
mod native_host_bridge;
|
||||
mod native_inventories;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
//! Still-fluid geometry under the native block entity pose, with an ordinary translucent material.
|
||||
use super::{Game, native_block_entity_draw::BlockEntityDraw};
|
||||
use crate::{
|
||||
fluid_render::{fluid_box, fluid_light},
|
||||
gfx::FLUID_OVERLAY,
|
||||
mesher::Vertex,
|
||||
mod_api::{NativeFluidRenderOptions, NativeFluidStack},
|
||||
};
|
||||
use glam::Vec3;
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Game<'_> {
|
||||
/// Draw a still-fluid box under `draw`'s current pose. Supports water and lava with an empty
|
||||
/// component patch; unknown fluid/component payloads return false before emitting geometry.
|
||||
/// Empty accepted stacks return true without validating bounds. Nonempty stacks reject
|
||||
/// invalid/oversized bounds before emitting geometry. The tint is stack ARGB (plain water
|
||||
/// 0xff3f76e4, lava white), never biome water color; `options.argb` overrides it explicitly.
|
||||
/// The actual animated block sprite is sampled with mipmaps. Faces are sorted, alpha blended,
|
||||
/// back culled and write ordinary color/depth, with sky light retained and block luminosity
|
||||
/// raised to the fluid's (lava 15). No model diffuse shade, geometry inset or alpha cutoff.
|
||||
/// API @API:fluid_render@.
|
||||
pub(super) fn native_draw_fluid_box(
|
||||
&self,
|
||||
draw: &mut BlockEntityDraw,
|
||||
stack: &NativeFluidStack,
|
||||
bounds: [f32; 6],
|
||||
options: NativeFluidRenderOptions,
|
||||
) -> bool {
|
||||
let Some((sprite, tint, luminosity)) = fluid_visual(stack) else {
|
||||
return false;
|
||||
};
|
||||
if stack.amount <= 0 {
|
||||
return true;
|
||||
}
|
||||
let layers = &self.pack.render.sprite_layers;
|
||||
let Some(&layer) = layers
|
||||
.get(sprite)
|
||||
.or_else(|| layers.get(sprite.strip_prefix("minecraft:").unwrap_or(sprite)))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let pose = draw.pose();
|
||||
let light = fluid_light(draw.light, luminosity);
|
||||
let color = options.argb.unwrap_or(tint);
|
||||
let color = (color & 0xff00_ff00) | ((color >> 16) & 0xff) | ((color & 0xff) << 16);
|
||||
fluid_box(bounds, options, |v| {
|
||||
draw.vertices_mut().push(Vertex {
|
||||
pos: pose.transform_point3(Vec3::from_array(v.pos)).to_array(),
|
||||
uv: v.uv,
|
||||
layer: FLUID_OVERLAY,
|
||||
color,
|
||||
light,
|
||||
// The source TRANSLUCENT shader does not use normals for lighting.
|
||||
normal: [0; 4],
|
||||
glint_uv: [layer as f32, 0.0],
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn fluid_visual(stack: &NativeFluidStack) -> Option<(&'static str, u32, u8)> {
|
||||
if !stack.components.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match stack.fluid.as_str() {
|
||||
"minecraft:water" => Some(("minecraft:block/water_still", 0xff3f76e4, 0)),
|
||||
"minecraft:lava" => Some(("minecraft:block/lava_still", 0xffffffff, 15)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn component_payloads_and_unknown_fluids_are_not_guessed() {
|
||||
let mut stack = NativeFluidStack {
|
||||
fluid: "minecraft:water".into(),
|
||||
amount: 1000,
|
||||
components: Default::default(),
|
||||
};
|
||||
assert_eq!(
|
||||
fluid_visual(&stack),
|
||||
Some(("minecraft:block/water_still", 0xff3f76e4, 0))
|
||||
);
|
||||
stack.components.insert(
|
||||
"!minecraft:custom_data".into(),
|
||||
horizon_world::nbt::Tag::Compound(Default::default()),
|
||||
);
|
||||
assert!(fluid_visual(&stack).is_none());
|
||||
stack.components.clear();
|
||||
stack.fluid = "test:unknown".into();
|
||||
assert!(fluid_visual(&stack).is_none());
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ pub use crate::crafting::{CustomRecipe, Ingredient};
|
||||
pub use crate::extras::Corner;
|
||||
pub use crate::extras_hud::Corners;
|
||||
pub use crate::first_person::Hand;
|
||||
pub use crate::fluid_render::NativeFluidRenderOptions;
|
||||
pub use crate::fog::{FogData, FogType};
|
||||
pub use crate::game::NativeChunkSnapshot;
|
||||
pub use crate::game::NativeEffect;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"hooks": [],
|
||||
"helpers": [
|
||||
{"name":"draw_fluid_box", "target":"horizon_client::game::native_fluid_render::Game<'_>::native_draw_fluid_box", "mod_api":{}}
|
||||
],
|
||||
"rust_summary": "still-fluid boxes under a native draw pose, using sorted depth-writing translucency and the animated mipmapped block sprites (`native_draw_fluid_box`).",
|
||||
"doc_summary": "`native_draw_fluid_box(draw,stack,bounds,options)` draws a `NativeFluidStack` in `[xMin,yMin,zMin,xMax,yMax,zMax]` under a block entity/mob draw pose. Supports `minecraft:water` and `minecraft:lava` with an empty component patch; unsupported IDs/patches, nonfinite/reversed bounds and boxes exceeding 65,536 tiled quads return false without partial geometry. Empty accepted amounts return true without validating bounds and emit nothing. `NativeFluidRenderOptions` requires `argb:Option<u32>` (stack tint override, default plain water 0xff3f76e4/lava white), `render_bottom:bool`, and `uv_shrink_ratio:f32` (0..1, source TextureAtlasSprite ratio 4 / the largest stitched atlas dimension; explicit because Horizon uses sprite arrays). Faces tile at integer boundaries with local UV fractions, no geometry inset. The still sprite follows resource packs/animations/mipmap settings; nearest texels use linear mip selection. Luminosity raises block light (lava 15), preserving sky light; vertex colors have no model diffuse shading. A separate material sorts far to near, alpha blends, culls backs and writes ordinary color/depth, with no alpha discard. It draws with block entities before terrain transparency, separately from improved-transparency queues and the world canvas. Bundles need \"hooks\": @API:fluid_render@.",
|
||||
"comments": ["crates/horizon-client/src/game/native_fluid_render.rs"]
|
||||
}
|
||||
Reference in new issue
Block a user