Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions objmc/assets/minecraft/shaders/include/objmc_main.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -191,20 +191,32 @@ 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 scale = 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 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,
0
);
posoffset = entityScale * rotate(trackedRotation + rotation) * posoffset;
}
//pure color rotation
else {
posoffset = rotate(rotation) * posoffset;
posoffset = entityScale * rotate(rotation) * posoffset;
}
}
}
Expand All @@ -223,4 +235,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);
//}
//}
Empty file added tests/__init__.py
Empty file.
142 changes: 142 additions & 0 deletions tests/test_autorotate_shader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import re
import unittest
from math import atan2, cos, pi, sin
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<body>.*?)\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\(",
)

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()