From 1b332c3edba87dd6ea5de104d88691a53aaa2a7e Mon Sep 17 00:00:00 2001 From: Ethanout <113243989+Ethanout@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:33:46 +0800 Subject: [PATCH] fix: restore autorotate axis locking --- .../minecraft/shaders/include/objmc_main.glsl | 15 ++- tests/test_autorotate_shader.py | 106 ++++++++++++++++++ 2 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 tests/test_autorotate_shader.py diff --git a/objmc/assets/minecraft/shaders/include/objmc_main.glsl b/objmc/assets/minecraft/shaders/include/objmc_main.glsl index 7f3f498..06e3c20 100644 --- a/objmc/assets/minecraft/shaders/include/objmc_main.glsl +++ b/objmc/assets/minecraft/shaders/include/objmc_main.glsl @@ -196,11 +196,18 @@ if (marker == ivec4(12,34,56,78)) { vec3 vPos0 = subgroupQuadBroadcast(Pos, 0); vec3 vPos1 = subgroupQuadBroadcast(Pos, 1); vec3 vPos2 = subgroupQuadBroadcast(Pos, 3); - float scale = distance(vPos0, vPos1); + float entityScale = distance(vPos0, vPos1); vPos1 = normalize(vPos0 - vPos1); vPos2 = normalize(vPos0 - vPos2); - mat3 fullRotation = mat3(vPos2, vPos1, cross(vPos2, vPos1)); - posoffset = scale * fullRotation * posoffset; + vec3 facing = cross(vPos2, vPos1); + float yaw = -atan(facing.x, facing.z); + float pitch = -atan(facing.y, length(facing.xz)); + vec3 trackedRotation = vec3( + pitch * autorotate.x, + yaw * autorotate.y, + 0 + ); + posoffset = entityScale * rotate(trackedRotation + rotation) * posoffset; } //pure color rotation else { @@ -223,4 +230,4 @@ if (marker == ivec4(12,34,56,78)) { // posoffset = vec3(gl_VertexID % 4 - 2, gl_VertexID % 4 / 2 * 2, -(gl_VertexID % 4) + 2 * 2); // Pos += posoffset; // vertexColor = vec4(1.0,0.0,0.0,1.0); -//} \ No newline at end of file +//} diff --git a/tests/test_autorotate_shader.py b/tests/test_autorotate_shader.py new file mode 100644 index 0000000..1e57669 --- /dev/null +++ b/tests/test_autorotate_shader.py @@ -0,0 +1,106 @@ +import re +import unittest +from pathlib import Path + + +SHADER_PATH = ( + Path(__file__).parents[1] + / "objmc" + / "assets" + / "minecraft" + / "shaders" + / "include" + / "objmc_main.glsl" +) +GENERATOR_PATH = Path(__file__).parents[1] / "objmc.py" + + +class AutorotateShaderTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.shader = SHADER_PATH.read_text(encoding="utf-8") + cls.generator = GENERATOR_PATH.read_text(encoding="utf-8") + match = re.search( + r"if \(any\(greaterThan\(autorotate,vec2\(0\)\)\)\) \{(?P.*?)\n\s*\}", + cls.shader, + re.DOTALL, + ) + if match is None: + raise AssertionError("autorotate shader block was not found") + cls.autorotate_body = match.group("body") + + decoder = re.search( + r"vec2 autorotate = vec2\(getb\(t\[6\]\.r, (\d), 1\), " + r"getb\(t\[6\]\.r, (\d), 1\)\);", + cls.shader, + ) + if decoder is None: + raise AssertionError("autorotate metadata decoder was not found") + cls.pitch_bit, cls.yaw_bit = map(int, decoder.groups()) + + encoder = re.search(r"autorotate << (\d)", cls.generator) + if encoder is None: + raise AssertionError("autorotate metadata encoder was not found") + cls.encoder_shift = int(encoder.group(1)) + + @classmethod + def decode_mode(cls, mode): + metadata = mode << cls.encoder_shift + return ( + (metadata >> cls.pitch_bit) & 1, + (metadata >> cls.yaw_bit) & 1, + ) + + def test_metadata_modes_decode_to_expected_axis_masks(self): + expected = { + 0: (0, 0), + 1: (0, 1), + 2: (1, 0), + 3: (1, 1), + } + self.assertEqual( + {mode: self.decode_mode(mode) for mode in expected}, + expected, + ) + + def test_axis_masks_lock_the_unselected_angle(self): + def tracked_angles(mode, pitch, yaw): + pitch_mask, yaw_mask = self.decode_mode(mode) + return pitch * pitch_mask, yaw * yaw_mask + + self.assertEqual(tracked_angles(1, 0.25, 1.0), tracked_angles(1, 0.75, 1.0)) + self.assertEqual(tracked_angles(2, 0.5, 0.25), tracked_angles(2, 0.5, 1.25)) + self.assertNotEqual(tracked_angles(3, 0.25, 1.0), tracked_angles(3, 0.75, 1.0)) + + def test_autorotate_derives_facing_from_the_quad_basis(self): + self.assertRegex( + self.autorotate_body, + r"vec3 facing\s*=\s*cross\(vPos2,\s*vPos1\)", + ) + self.assertNotRegex(self.autorotate_body, r"\bNormal\b") + + def test_autorotate_masks_pitch_and_yaw_independently(self): + self.assertRegex( + self.autorotate_body, + r"pitch\s*\*\s*autorotate\.x", + ) + self.assertRegex( + self.autorotate_body, + r"yaw\s*\*\s*autorotate\.y", + ) + + def test_autorotate_preserves_color_rotation(self): + self.assertRegex( + self.autorotate_body, + r"rotate\(trackedRotation\s*\+\s*rotation\)", + ) + + def test_autorotate_preserves_entity_scale(self): + self.assertRegex( + self.autorotate_body, + r"entityScale\s*\*\s*rotate\(", + ) + + +if __name__ == "__main__": + unittest.main()