mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Render enderman carried block states with vanilla tints
Use the baked block-state model instead of the block item's model so flowers, fungi and roots keep their cross geometry and state properties survive. Remove the item-centering compensation while retaining the vanilla carry transform. Apply BlockTintSource.color(state) to tinted block-model faces using the active grass colormap and vanilla default foliage colours. Keep in-world sugar cane and lily pad colours unchanged. Use BlockModelResolver's fixed model seed 42. Add a differential adapter checking 6,616 state/tint-index combinations against Minecraft 26.3 and a scratch-world offscreen screenshot driver. The existing captured-vertex transform regression now checks block coordinates directly. Implemented by OpenAI Codex on behalf of NetuosoDEV. NetuosoDEV manually tested the fix in game and confirmed it works. Co-Authored-By: OpenAI Codex <[email protected]>
This commit is contained in:
1 parent
818a2ec52a
commit
d3e2fe8bf3
8 files changed
+271
-31
No files matched your search
@@ -0,0 +1,37 @@
|
||||
//! Block-model tints without a level, including blank and invalid tint indices.
|
||||
use horizon_client::{
|
||||
assets::Assets, blocks::BlockFacts, item_render::ItemRenderer, mesher::MeshTables, models,
|
||||
};
|
||||
use horizon_world::registry::BlockStateId;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
fs,
|
||||
io::{BufWriter, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = crate::args().collect();
|
||||
let input: Value = serde_json::from_slice(&fs::read(&args[1])?)?;
|
||||
let root = Path::new(input["resources"].as_str().unwrap());
|
||||
let assets = Assets::load(root)?;
|
||||
let render = assets.reload(root)?;
|
||||
let facts = BlockFacts::load(root, assets.registry)?;
|
||||
let tables = MeshTables::new(
|
||||
&assets,
|
||||
&render,
|
||||
&facts,
|
||||
models::load_visibility(root, assets.registry)?,
|
||||
)?;
|
||||
let grass = ItemRenderer::load(root)?.grass_color(0.5, 1.0) as u32;
|
||||
let mut output = BufWriter::new(fs::File::create(&args[2])?);
|
||||
for row in input["cases"].as_array().unwrap() {
|
||||
let state = row[0].as_u64().unwrap() as u32;
|
||||
let index = row[1].as_i64().unwrap() as i32;
|
||||
let color = tables
|
||||
.default_tint(BlockStateId(state), index, grass)
|
||||
.map_or(-1, |rgb| (0xFF00_0000 | rgb) as i32);
|
||||
writeln!(output, "{state} {index} {color}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -19,6 +19,7 @@ mod block_entity_anim_fixture;
|
||||
mod block_entity_render_fixture;
|
||||
mod block_entity_tickers_fixture;
|
||||
mod block_particles_fixture;
|
||||
mod block_tint_probe;
|
||||
mod block_transform_fixture;
|
||||
mod breaking_fixture;
|
||||
mod camera_probe;
|
||||
@@ -109,6 +110,7 @@ probes! {
|
||||
block_particles_fixture,
|
||||
block_transform_fixture,
|
||||
breaking_fixture,
|
||||
block_tint_probe,
|
||||
camera_probe,
|
||||
squid_client_probe,
|
||||
creative_mining_probe,
|
||||
|
||||
@@ -1215,7 +1215,7 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Baked block faces with terrain shade or block-item diffuse lighting.
|
||||
/// Baked block-state faces with default block tints and terrain shade or diffuse lighting.
|
||||
pub(super) fn block_model_vertices(
|
||||
&self,
|
||||
state: BlockStateId,
|
||||
@@ -1250,6 +1250,20 @@ impl Game<'_> {
|
||||
} else {
|
||||
0xFF00_0000 | shade << 16 | shade << 8 | shade
|
||||
};
|
||||
let tint = self.pack.context.tables.default_tint(
|
||||
state,
|
||||
face.quad.tint_index,
|
||||
self.pack.item_renderer.grass_color(0.5, 1.0) as u32,
|
||||
);
|
||||
let color = if let Some(tint) = tint {
|
||||
let tint = horizon_client::mobs::render::abgr(tint);
|
||||
let channel = |shift: u32| {
|
||||
((color >> shift & 255) * (tint >> shift & 255) / 255) << shift
|
||||
};
|
||||
0xFF00_0000 | channel(0) | channel(8) | channel(16)
|
||||
} else {
|
||||
color
|
||||
};
|
||||
let normal = if diffuse {
|
||||
use horizon_client::item_render::{direction_vector, transform_normal};
|
||||
let n = transform_normal(m, direction_vector(face.quad.facing));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Blocks mobs carry: block items in a hand (`ItemInHandLayer`) and the
|
||||
//! block an enderman holds (`CarriedBlockLayer`), drawn through the same
|
||||
//! item model pipeline as inventory and dropped block items. Also the block
|
||||
//! block state an enderman holds (`CarriedBlockLayer`). Also the block
|
||||
//! models mobs wear (`SnowGolemHeadLayer`, `MushroomCowMushroomLayer`).
|
||||
use super::Game;
|
||||
use horizon_client::{
|
||||
@@ -9,8 +8,7 @@ use horizon_client::{
|
||||
mobs::{Mob, Species, render},
|
||||
};
|
||||
|
||||
/// `CarriedBlockLayer.submit`, adapted from unit-cube block coordinates to
|
||||
/// the item pipeline, which centers the model by translating (-0.5, -0.5, -0.5).
|
||||
/// `CarriedBlockLayer.submit`: block models use coordinates in [0, 1].
|
||||
fn carried_block_matrix(base: glam::Mat4) -> glam::Mat4 {
|
||||
base * glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.6875, -0.75))
|
||||
* glam::Mat4::from_rotation_x(20.0f32.to_radians())
|
||||
@@ -18,8 +16,6 @@ fn carried_block_matrix(base: glam::Mat4) -> glam::Mat4 {
|
||||
* glam::Mat4::from_translation(glam::Vec3::new(0.25, 0.1875, 0.25))
|
||||
* glam::Mat4::from_scale(glam::Vec3::new(-0.5, -0.5, 0.5))
|
||||
* glam::Mat4::from_rotation_y(90.0f32.to_radians())
|
||||
// Cancel item centering: vanilla submits the block in [0, 1], not [-0.5, 0.5].
|
||||
* glam::Mat4::from_translation(glam::Vec3::splat(0.5))
|
||||
}
|
||||
|
||||
/// `CustomHeadLayer.translateToHead` and the banner's `head` display.
|
||||
@@ -169,15 +165,8 @@ impl Game<'_> {
|
||||
// CarriedBlockLayer: OverlayTexture.NO_OVERLAY, never the hurt tint.
|
||||
let base = render::base_transform(mob, alpha as f32, camera);
|
||||
let t = carried_block_matrix(base);
|
||||
// Resolve the inventory model's textures and per-quad tints.
|
||||
let stack = ItemStack::new(&self.res.facts().get(state).name, 1);
|
||||
let item = self.resolve_item(
|
||||
&stack,
|
||||
horizon_assets::item_model::DisplayContext::None,
|
||||
None,
|
||||
0,
|
||||
);
|
||||
self.submit_item_world(&item, t, light, out);
|
||||
// Preserve cross models (flowers, fungi) and state properties (facing).
|
||||
self.block_model_vertices(state, 42, t, light, true, out);
|
||||
}
|
||||
}
|
||||
/// `SnowGolemHeadLayer` and `MushroomCowMushroomLayer`: the carved pumpkin
|
||||
@@ -343,7 +332,7 @@ mod tests {
|
||||
use glam::{Mat4, Vec3};
|
||||
|
||||
#[test]
|
||||
fn carried_item_vertices_match_vanilla_block_coordinates() {
|
||||
fn carried_block_vertices_match_vanilla_block_coordinates() {
|
||||
// Captured from 26.3 CarriedBlockLayer.submit with an identity PoseStack:
|
||||
// origin, three unit axes, opposite corner, and the block's center.
|
||||
let samples = [
|
||||
@@ -354,18 +343,15 @@ mod tests {
|
||||
(Vec3::ONE, Vec3::new(-0.3535534, 0.3938461, -0.85688126)),
|
||||
(Vec3::splat(0.5), Vec3::new(0.0, 0.6287693, -0.77137625)),
|
||||
];
|
||||
let item_center = horizon_client::item_render::glam_matrix(
|
||||
&horizon_assets::item_bake::item_transform_matrix(None, false),
|
||||
);
|
||||
for base in [
|
||||
Mat4::IDENTITY,
|
||||
Mat4::from_translation(Vec3::new(3.0, 2.0, -7.0))
|
||||
* Mat4::from_rotation_y(1.2)
|
||||
* Mat4::from_scale(Vec3::new(-1.0, -1.0, 1.0)),
|
||||
] {
|
||||
let item = carried_block_matrix(base) * item_center;
|
||||
let block = carried_block_matrix(base);
|
||||
for (point, expected) in samples {
|
||||
let actual = item.transform_point3(point);
|
||||
let actual = block.transform_point3(point);
|
||||
let expected = base.transform_point3(expected);
|
||||
assert!(
|
||||
actual.abs_diff_eq(expected, 1e-6),
|
||||
|
||||
@@ -263,6 +263,8 @@ enum Tint {
|
||||
/// `BlockTintSources.grassBlock`: grass in the world, none on its particles.
|
||||
GrassBlock,
|
||||
TallGrassUpper,
|
||||
SugarCane,
|
||||
LilyPad,
|
||||
Foliage,
|
||||
DryFoliage,
|
||||
Water,
|
||||
@@ -329,6 +331,10 @@ pub struct MeshTables {
|
||||
lava_still: u16,
|
||||
lava_flow: u16,
|
||||
}
|
||||
fn stem_color(age: u8) -> u32 {
|
||||
let a = u32::from(age);
|
||||
(a * 32) << 16 | (255 - a * 8) << 8 | (a * 4)
|
||||
}
|
||||
fn redstone_color(power: u8) -> u32 {
|
||||
// RedstoneWireBlock.COLORS
|
||||
let f = f32::from(power) / 15.0;
|
||||
@@ -377,10 +383,9 @@ impl MeshTables {
|
||||
([Tint::Grass; 2], 1)
|
||||
}
|
||||
}
|
||||
"fern" | "short_grass" | "potted_fern" | "bush" | "sugar_cane" => {
|
||||
([Tint::Grass; 2], 1)
|
||||
}
|
||||
"fern" | "short_grass" | "potted_fern" | "bush" => ([Tint::Grass; 2], 1),
|
||||
"grass_block" => ([Tint::GrassBlock; 2], 1),
|
||||
"sugar_cane" => ([Tint::SugarCane; 2], 1),
|
||||
"pink_petals" | "wildflowers" => ([Tint::None, Tint::Grass], 2),
|
||||
"spruce_leaves" => ([Tint::Constant(0x61_9961); 2], 1),
|
||||
"birch_leaves" => ([Tint::Constant(0x80_A755); 2], 1),
|
||||
@@ -395,7 +400,7 @@ impl MeshTables {
|
||||
([Tint::Constant(0xE0_C71C); 2], 1)
|
||||
}
|
||||
"melon_stem" | "pumpkin_stem" => ([Tint::Stem(prop("age")); 2], 1),
|
||||
"lily_pad" => ([Tint::Constant(0x20_8030); 2], 1),
|
||||
"lily_pad" => ([Tint::LilyPad; 2], 1),
|
||||
_ => ([Tint::None; 2], 0),
|
||||
};
|
||||
// getShadeBrightness overrides; the default is 0.2 for full collision shapes.
|
||||
@@ -491,6 +496,25 @@ impl MeshTables {
|
||||
tint_sources: i.tint_count,
|
||||
})
|
||||
}
|
||||
/// `BlockTintSource.color(state)`: block-model colour without a level.
|
||||
/// `grass` is `GrassColor.getDefaultColor()` from the active pack's colormap.
|
||||
/// Untinted faces, blank sources and invalid tint indices return `None`.
|
||||
pub fn default_tint(&self, state: BlockStateId, index: i32, grass: u32) -> Option<u32> {
|
||||
let info = self.info.get(state.0 as usize)?;
|
||||
if index < 0 || index >= i32::from(info.tint_count) {
|
||||
return None;
|
||||
}
|
||||
Some(match info.tints[index as usize] {
|
||||
Tint::None | Tint::SugarCane | Tint::Water => return None,
|
||||
Tint::Grass | Tint::GrassBlock | Tint::TallGrassUpper => grass & 0xFF_FFFF,
|
||||
Tint::Foliage => 0x48_B518,
|
||||
Tint::DryFoliage => 0x5C_3C32,
|
||||
Tint::LilyPad => 0x71_C35C,
|
||||
Tint::Constant(c) => c,
|
||||
Tint::Redstone(p) => redstone_color(p),
|
||||
Tint::Stem(age) => stem_color(age),
|
||||
})
|
||||
}
|
||||
/// `TerrainParticle`'s colour: tint source 0's `colorAsTerrainParticle`,
|
||||
/// which is the in-world (biome-blended) colour except on grass blocks,
|
||||
/// whose dirt crumbs stay untinted. `None` when the state has no source.
|
||||
@@ -838,17 +862,15 @@ impl Mesher<'_> {
|
||||
}
|
||||
Some(match state.tints[(index as usize).min(1)] {
|
||||
Tint::None => return None,
|
||||
Tint::Grass | Tint::GrassBlock => tints.get(Resolver::Grass, x, y, z),
|
||||
Tint::Grass | Tint::GrassBlock | Tint::SugarCane => tints.get(Resolver::Grass, x, y, z),
|
||||
Tint::TallGrassUpper => tints.get(Resolver::Grass, x, y, z),
|
||||
Tint::Foliage => tints.get(Resolver::Foliage, x, y, z),
|
||||
Tint::DryFoliage => tints.get(Resolver::Dry, x, y, z),
|
||||
Tint::Water => tints.get(Resolver::Water, x, y, z),
|
||||
Tint::Constant(c) => c,
|
||||
Tint::LilyPad => 0x20_8030,
|
||||
Tint::Redstone(p) => redstone_color(p),
|
||||
Tint::Stem(age) => {
|
||||
let a = u32::from(age);
|
||||
(a * 32) << 16 | (255 - a * 8) << 8 | (a * 4)
|
||||
}
|
||||
Tint::Stem(age) => stem_color(age),
|
||||
})
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package dev.horizonlab.parity;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import javax.imageio.ImageIO;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.client.color.block.BlockColors;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.GrassColor;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
|
||||
/** Original adapter calling the pinned client's BlockTintSource.color(state). */
|
||||
public final class VanillaBlockTintOracle {
|
||||
public static void main(String[] args) throws Exception {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
var input = JsonParser.parseString(Files.readString(Path.of(args[0]))).getAsJsonObject();
|
||||
var root = Path.of(input.get("resources").getAsString());
|
||||
var image = ImageIO.read(root.resolve("assets/minecraft/textures/colormap/grass.png").toFile());
|
||||
GrassColor.init(image.getRGB(0, 0, 256, 256, null, 0, 256));
|
||||
var colors = BlockColors.createDefault();
|
||||
try (var output = Files.newBufferedWriter(Path.of(args[1]))) {
|
||||
for (var value : input.getAsJsonArray("cases")) {
|
||||
var row = value.getAsJsonArray();
|
||||
int stateId = row.get(0).getAsInt(), index = row.get(1).getAsInt();
|
||||
var state = Block.stateById(stateId);
|
||||
var source = index < 0 ? null : colors.getTintSource(state, index);
|
||||
int color = source == null ? -1 : source.color(state);
|
||||
output.write(stateId + " " + index + " " + color + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture real carried grass/cross models from a vanilla 26.3 server.
|
||||
|
||||
Uses fresh scratch homes and a scratch flat world. The vanilla server supplies
|
||||
carriedBlockState metadata (including properties); Horizon renders the frames.
|
||||
Screenshots are evidence for visual review, not an automatic framebuffer diff.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import lab
|
||||
from integration_env import game_environment
|
||||
from test_dedicated_server_parity import Server
|
||||
from test_rust_parity import verify_artifacts
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--bin-dir', type=Path, default=lab.ROOT / 'target/fast')
|
||||
parser.add_argument('--out', type=Path, default=lab.LOCAL / 'enderman-render-e2e')
|
||||
args = parser.parse_args()
|
||||
out = args.out.resolve()
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
_, jars, libraries = verify_artifacts()
|
||||
with tempfile.TemporaryDirectory(prefix='hz-enderman-') as temporary:
|
||||
work = Path(temporary)
|
||||
directory = work / 'server'
|
||||
directory.mkdir()
|
||||
port = lab.free_port()
|
||||
props = {'server-port': str(port), 'server-ip': '127.0.0.1', 'online-mode': 'false', 'white-list': 'false',
|
||||
'level-type': 'minecraft\\:flat', 'generate-structures': 'false', 'gamemode': 'creative',
|
||||
'view-distance': '3', 'simulation-distance': '3', 'spawn-protection': '0',
|
||||
'pause-when-empty-seconds': '-1'}
|
||||
(directory / 'server.properties').write_text(''.join(f'{k}={v}\n' for k, v in props.items()))
|
||||
(directory / 'eula.txt').write_text('eula=true\n')
|
||||
server = Server(['java', '-Xmx2g', '-cp', os.pathsep.join(map(str, [jars['server'], *libraries])),
|
||||
'net.minecraft.server.Main', 'nogui'], directory, 'vanilla')
|
||||
socket = work / 'ctl.sock'
|
||||
env = game_environment(os.environ, work / 'client')
|
||||
game = None
|
||||
log = (out / 'client.log').open('w')
|
||||
|
||||
def ctl(*words):
|
||||
reply = subprocess.run([str(args.bin_dir / 'horizon-ctl'), '--socket', str(socket),
|
||||
'--timeout', '30', *map(str, words)], env=env, cwd=work,
|
||||
check=True, capture_output=True, text=True, timeout=40)
|
||||
value = json.loads(reply.stdout)
|
||||
if 'error' in value:
|
||||
raise RuntimeError(value)
|
||||
return value
|
||||
|
||||
try:
|
||||
server.wait_for(r'Done \(', timeout=60)
|
||||
game = subprocess.Popen([str(args.bin_dir / 'horizon-client'), '--resources', str(lab.RESOURCES),
|
||||
'--server', f'127.0.0.1:{port}', '--name', 'RenderTest', '--offscreen',
|
||||
'--width', '1280', '--height', '720', '--render-distance', '4',
|
||||
'--control-socket', str(socket)], cwd=work, env=env, stdout=log, stderr=log)
|
||||
for _ in range(150):
|
||||
if game.poll() is not None:
|
||||
raise RuntimeError((out / 'client.log').read_text())
|
||||
if socket.exists():
|
||||
break
|
||||
time.sleep(0.1)
|
||||
ctl('wait-for', 'loaded', '--timeout', '60')
|
||||
for command in ['gamerule spawn_mobs false', 'gamerule advance_time false', 'time set 6000',
|
||||
'weather clear', 'fill -9 100 -9 9 100 9 smooth_quartz',
|
||||
'fill -9 101 -9 9 108 9 air', 'tp RenderTest 0 101 -3 0 10']:
|
||||
server.console(command)
|
||||
ctl('wait', 20)
|
||||
ctl('options-set', 'chatVisibility', '2')
|
||||
ctl('key', 'f1', 'tap')
|
||||
ctl('options-set', 'maxFps', '60')
|
||||
for label, blocks in [('grass-poppy', ['grass_block', 'poppy']),
|
||||
('leaves-pumpkin', ['spruce_leaves', 'carved_pumpkin'])]:
|
||||
server.console('kill @e[type=minecraft:enderman]')
|
||||
ctl('wait', 25)
|
||||
for x, block in zip([-1.2, 1.2], blocks):
|
||||
props = ',properties:{facing:"east"}' if block == 'carved_pumpkin' else ''
|
||||
server.console(f'summon enderman {x} 101 2 {{NoAI:1b,PersistenceRequired:1b,Rotation:[180f,0f],'
|
||||
f'carriedBlockState:{{id:"minecraft:{block}"{props}}}}}')
|
||||
ctl('wait', 20)
|
||||
entities = ctl('entities')
|
||||
assert len([e for e in entities['entities'] if e['type'] == 'minecraft:enderman']) == 2, entities
|
||||
assert not any('Failed to decode' in line for line in server.lines), '\n'.join(server.lines[-20:])
|
||||
(out / f'{label}.json').write_text(json.dumps(entities, indent=2))
|
||||
ctl('screenshot', out / f'{label}.png')
|
||||
print(f'Carried-block screenshots: {out}', flush=True)
|
||||
finally:
|
||||
if game is not None:
|
||||
try:
|
||||
ctl('quit')
|
||||
game.wait(timeout=15)
|
||||
except (subprocess.SubprocessError, RuntimeError):
|
||||
game.terminate()
|
||||
game.wait(timeout=10)
|
||||
log.close()
|
||||
server.stop()
|
||||
(out / 'server.log').write_text(server.log_path.read_text())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare level-free block-model tints with actual 26.3 BlockTintSources.
|
||||
|
||||
Every state of blocks with registered sources, plus untinted carried blocks,
|
||||
is checked at indices -1, 0, 1 and 2. Geometry and framebuffer are separate checks.
|
||||
"""
|
||||
import json
|
||||
try:
|
||||
from . import lab, oracle_harness as harness, test_section_mesh_parity as mesh
|
||||
except ImportError:
|
||||
import lab, oracle_harness as harness, test_section_mesh_parity as mesh
|
||||
|
||||
BLOCKS = """large_fern tall_grass fern short_grass potted_fern bush grass_block
|
||||
pink_petals wildflowers spruce_leaves birch_leaves oak_leaves jungle_leaves
|
||||
acacia_leaves dark_oak_leaves vine mangrove_leaves leaf_litter water_cauldron
|
||||
water bubble_column redstone_wire sugar_cane attached_melon_stem
|
||||
attached_pumpkin_stem melon_stem pumpkin_stem lily_pad stone poppy red_mushroom
|
||||
brown_mushroom crimson_fungus warped_roots carved_pumpkin""".split()
|
||||
|
||||
|
||||
def make_input(build):
|
||||
states = mesh.States(lab.RESOURCES)
|
||||
ids = sorted({state for name in BLOCKS for state in states.by_name["minecraft:" + name]})
|
||||
rows = [[state, index] for state in ids for index in [-1, 0, 1, 2]]
|
||||
path = build / "input.json"
|
||||
path.write_text(json.dumps(dict(resources=str(lab.RESOURCES), cases=rows)))
|
||||
return path, len(rows)
|
||||
|
||||
|
||||
def main():
|
||||
harness.differential("block-tint-parity", "VanillaBlockTintOracle", "block_tint_probe",
|
||||
make_input, "BlockTintSource.color(state), all states of tinted blocks and untinted carried blocks; per-face tint indices, no level/biome. Not model or framebuffer parity.",
|
||||
sources=[lab.ROOT / "tools/test_block_tint_parity.py", lab.ROOT / "crates/horizon-client/src/mesher.rs",
|
||||
lab.ROOT / "crates/horizon-client/src/item_render.rs"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user