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 1/2] 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() From 5252a36f2d2f0f9971584af9100d77748a914961 Mon Sep 17 00:00:00 2001 From: Ethanout <113243989+Ethanout@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:07:09 +0800 Subject: [PATCH 2/2] fix: stabilize autorotate scale and vertical yaw --- .../minecraft/shaders/include/objmc_main.glsl | 17 +++++---- tests/__init__.py | 0 tests/test_autorotate_shader.py | 36 +++++++++++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 tests/__init__.py diff --git a/objmc/assets/minecraft/shaders/include/objmc_main.glsl b/objmc/assets/minecraft/shaders/include/objmc_main.glsl index 06e3c20..c83aeba 100644 --- a/objmc/assets/minecraft/shaders/include/objmc_main.glsl +++ b/objmc/assets/minecraft/shaders/include/objmc_main.glsl @@ -191,17 +191,22 @@ if (marker == ivec4(12,34,56,78)) { posoffset = (vec4(posoffset,0) * ModelViewMat).xyz; } if (isHand + isGUI == 0) { + vec3 vPos0 = subgroupQuadBroadcast(Pos, 0); + vec3 vPos1 = subgroupQuadBroadcast(Pos, 1); + float entityScale = distance(vPos0, vPos1); if (any(greaterThan(autorotate,vec2(0)))) { //normal estimated rotation calculation from The Der Discohund - vec3 vPos0 = subgroupQuadBroadcast(Pos, 0); - vec3 vPos1 = subgroupQuadBroadcast(Pos, 1); vec3 vPos2 = subgroupQuadBroadcast(Pos, 3); - float entityScale = distance(vPos0, vPos1); vPos1 = normalize(vPos0 - vPos1); vPos2 = normalize(vPos0 - vPos2); vec3 facing = cross(vPos2, vPos1); - float yaw = -atan(facing.x, facing.z); - float pitch = -atan(facing.y, length(facing.xz)); + float horizontalLength = length(facing.xz); + // At vertical pitch the facing vector has no horizontal + // projection. Recover yaw from the quad's horizontal axis. + float yaw = horizontalLength > 0.000001 + ? -atan(facing.x, facing.z) + : atan(vPos2.z, vPos2.x); + float pitch = -atan(facing.y, horizontalLength); vec3 trackedRotation = vec3( pitch * autorotate.x, yaw * autorotate.y, @@ -211,7 +216,7 @@ if (marker == ivec4(12,34,56,78)) { } //pure color rotation else { - posoffset = rotate(rotation) * posoffset; + posoffset = entityScale * rotate(rotation) * posoffset; } } } diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_autorotate_shader.py b/tests/test_autorotate_shader.py index 1e57669..817fdda 100644 --- a/tests/test_autorotate_shader.py +++ b/tests/test_autorotate_shader.py @@ -1,5 +1,6 @@ import re import unittest +from math import atan2, cos, pi, sin from pathlib import Path @@ -101,6 +102,41 @@ def test_autorotate_preserves_entity_scale(self): r"entityScale\s*\*\s*rotate\(", ) + def test_off_and_autorotate_apply_the_same_entity_scale(self): + self.assertRegex( + self.shader, + r"float\s+entityScale\s*=\s*distance\(vPos0,\s*vPos1\);" + r"[\s\S]*?if\s*\(any\(greaterThan\(autorotate,vec2\(0\)\)\)\)" + r"[\s\S]*?posoffset\s*=\s*entityScale\s*\*\s*" + r"rotate\(trackedRotation\s*\+\s*rotation\)\s*\*\s*posoffset;" + r"[\s\S]*?else\s*\{\s*posoffset\s*=\s*entityScale\s*\*\s*" + r"rotate\(rotation\)\s*\*\s*posoffset;", + ) + + def test_vertical_facing_recovers_yaw_from_the_quad_axis(self): + self.assertRegex( + self.autorotate_body, + r"float\s+yaw\s*=\s*horizontalLength\s*>\s*0\.000001\s*" + r"\?\s*-atan\(facing\.x,\s*facing\.z\)\s*" + r":\s*atan\(vPos2\.z,\s*vPos2\.x\)", + ) + + def extract_yaw(pitch, yaw): + facing = ( + -cos(pitch) * sin(yaw), + -sin(pitch), + cos(pitch) * cos(yaw), + ) + right = (cos(yaw), 0, sin(yaw)) + horizontal_length = (facing[0] ** 2 + facing[2] ** 2) ** 0.5 + if horizontal_length > 0.000001: + return -atan2(facing[0], facing[2]) + return atan2(right[2], right[0]) + + for pitch in (-pi / 2, pi / 2): + for yaw in (-2.4, -0.75, 0.0, 1.2, 2.8): + self.assertAlmostEqual(extract_yaw(pitch, yaw), yaw) + if __name__ == "__main__": unittest.main()