The breeze's wind and the wind charge scroll their textures

Vanilla draws the breeze's wind layer and the wind charge with
`RenderTypes.breezeWind`, whose texture scrolls with the entity's age: by
0.02 of the texture per tick for the breeze's wind layer and 0.03 for the
wind charge. Horizon drew both with a still texture, so the breeze's gust
and the wind charge did not move.

The draw now takes a scroll offset and wraps the vertex's U inside the
texture, as the texture matrix of the vanilla's render type does.

Fixes #2760

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
This commit is contained in:
levzzzandminimax-m3.1-flash committed 2026-10-27 19:30:29 +05:00
1 parent 6f4b1bf4c2
commit f507a067cd
2 files changed
+89 -13

No files matched your search

+5 -1
View File
@@ -570,11 +570,15 @@ impl Game<'_> {
poses.yrot("wind", age * 16.0_f32.to_radians());
let base = glam::Mat4::from_translation(at)
* glam::Mat4::from_scale(glam::Vec3::new(-1.0, -1.0, 1.0));
horizon_client::mobs::render::draw(
horizon_client::mobs::render::draw_scrolled(
model,
base,
&poses,
[r.u0, r.v0, r.u1, r.v1],
horizon_client::mobs::render::scroll_offset(
age,
horizon_client::mobs::render::WIND_CHARGE_SCROLL,
),
0xFFFF_FFFF,
light,
0,
+84 -12
View File
@@ -20,12 +20,32 @@ pub type Atlas<'a> = &'a dyn Fn(&str) -> Option<[f32; 4]>;
/// the model UV and reads `enchanted_glint_armor`.
pub const ARMOR_GLINT: u32 = 13;
/// `RenderTypes.breezeWind`: the texture of the breeze's wind layer scrolls
/// by 0.02 of the texture per tick.
pub const BREEZE_WIND_SCROLL: f32 = 0.02;
/// `WindChargeRenderer.xOffset`: the wind charge's texture scrolls by 0.03 of
/// the texture per tick.
pub const WIND_CHARGE_SCROLL: f32 = 0.03;
/// The offset of a scrolling texture: `RenderTypes.breezeWind` takes the
/// texture's age times the speed, modulo one.
pub fn scroll_offset(age: f32, speed: f32) -> f32 {
(age * speed) % 1.0
}
/// The vertex's U with the scroll: the texture matrix of
/// `RenderTypes.breezeWind` moves the texture under the model, so the U
/// moves by the offset and wraps around the texture.
fn scroll_u(u: f32, scroll: f32) -> f32 {
if scroll == 0.0 { u } else { (u + scroll) % 1.0 }
}
/// One draw of a model with a texture.
pub struct Layer {
pub model: String,
pub texture: String,
pub color: u32,
pub full_bright: bool,
/// `RenderTypes.breezeWind`: the texture scrolls with the entity's age.
pub scroll: f32,
}
/// DyeColor.getTextureDiffuseColor (RGB).
pub const DYE: [u32; 16] = [
@@ -436,6 +456,7 @@ pub fn layers(m: &Mob) -> Vec<Layer> {
texture: texture.into(),
color,
full_bright,
scroll: 0.0,
})
};
match m.species() {
@@ -465,16 +486,17 @@ pub fn layers(m: &Mob) -> Vec<Layer> {
push(&mut v, "happy_ghast_harness#main", &texture, white, false);
}
}
// BreezeWindLayer (the scroll is not reproduced: atlas sprites do
// not wrap) and BreezeEyesLayer.
// BreezeWindLayer and BreezeEyesLayer. The wind layer's texture
// scrolls (`RenderTypes.breezeWind`): the offset is taken inside the
// sprite, so the atlas does not have to wrap.
S::Breeze => {
push(
&mut v,
"breeze#wind",
"entity/breeze/breeze_wind",
white,
false,
);
v.push(Layer {
model: "breeze#wind".into(),
texture: "entity/breeze/breeze_wind".into(),
color: white,
full_bright: false,
scroll: scroll_offset(m.age as f32, BREEZE_WIND_SCROLL),
});
push(
&mut v,
"breeze#eyes",
@@ -801,6 +823,7 @@ fn humanoid_armor(m: &Mob, v: &mut Vec<Layer>) {
texture: format!("entity/equipment/{folder}/{asset}"),
color: tint,
full_bright: false,
scroll: 0.0,
});
if asset == "leather" {
v.push(Layer {
@@ -808,6 +831,7 @@ fn humanoid_armor(m: &Mob, v: &mut Vec<Layer>) {
texture: format!("entity/equipment/{folder}/leather_overlay"),
color: 0xFFFF_FFFF,
full_bright: false,
scroll: 0.0,
});
}
}
@@ -1009,6 +1033,23 @@ pub fn draw(
light: u32,
overlay: u32,
out: &mut Vec<Vertex>,
) {
draw_scrolled(part, base, poses, uv, 0.0, color, light, overlay, out);
}
/// `draw` for a texture that scrolls: the vertex's U is moved by the offset
/// and wraps inside the texture, as the texture matrix of
/// `RenderTypes.breezeWind` does.
#[allow(clippy::too_many_arguments)]
pub fn draw_scrolled(
part: &Part,
base: glam::Mat4,
poses: &Poses,
uv: [f32; 4],
scroll: f32,
color: u32,
light: u32,
overlay: u32,
out: &mut Vec<Vertex>,
) {
draw_normals(
part,
@@ -1016,6 +1057,7 @@ pub fn draw(
ModelNormals::new(base),
poses,
uv,
scroll,
color,
light,
overlay,
@@ -1029,6 +1071,7 @@ fn draw_normals(
normals: ModelNormals,
poses: &Poses,
uv: [f32; 4],
scroll: f32,
color: u32,
light: u32,
overlay: u32,
@@ -1066,7 +1109,7 @@ fn draw_normals(
normal,
pos: pos.to_array(),
uv: [
uv[0] + (uv[2] - uv[0]) * v[3],
uv[0] + (uv[2] - uv[0]) * scroll_u(v[3], scroll),
uv[1] + (uv[3] - uv[1]) * v[4],
],
layer: overlay,
@@ -1078,7 +1121,7 @@ fn draw_normals(
}
}
for c in &part.children {
draw_normals(c, m, normals, poses, uv, color, light, overlay, out);
draw_normals(c, m, normals, poses, uv, scroll, color, light, overlay, out);
}
}
/// Render a mob with its variant model, texture and layers.
@@ -1199,6 +1242,7 @@ fn render_visible(
texture: texture.into(),
color: ((a.clamp(0.0, 1.0) * 255.0) as u32) << 24 | 0x00FF_FFFF,
full_bright: true,
scroll: 0.0,
});
}
}
@@ -1240,11 +1284,12 @@ fn render_visible(
None => overlay,
};
let start = out.len();
draw(
draw_scrolled(
&part,
base,
&layer_poses,
uv,
layer.scroll,
layer.color,
light,
overlay,
@@ -1618,3 +1663,30 @@ mod tests {
}
}
}
#[cfg(test)]
mod scroll_tests {
use super::*;
#[test]
fn the_scrolling_texture_wraps_around() {
// RenderTypes.breezeWind: the breeze's wind layer's texture scrolls
// by 0.02 of the texture per tick, the wind charge's by 0.03.
assert_eq!(scroll_offset(0.0, BREEZE_WIND_SCROLL), 0.0);
assert!((scroll_offset(25.0, BREEZE_WIND_SCROLL) - 0.5).abs() < 1e-6);
assert!((scroll_offset(50.0, BREEZE_WIND_SCROLL) - 0.0).abs() < 1e-6);
assert!((scroll_offset(50.0, WIND_CHARGE_SCROLL) - 0.5).abs() < 1e-6);
assert!((scroll_offset(100.0, WIND_CHARGE_SCROLL) - 0.0).abs() < 1e-6);
}
#[test]
fn the_scrolled_u_wraps_inside_the_texture() {
// Without a scroll the U is the vertex's own.
assert_eq!(scroll_u(0.25, 0.0), 0.25);
assert_eq!(scroll_u(1.0, 0.0), 1.0);
// With one, it moves and wraps around the texture.
assert!((scroll_u(0.25, 0.5) - 0.75).abs() < 1e-6);
assert!((scroll_u(0.75, 0.5) - 0.25).abs() < 1e-6);
assert!((scroll_u(1.0, 0.5) - 0.5).abs() < 1e-6);
}
}