diff --git a/bin/glitch_patterns.py b/bin/glitch_patterns.py index 42dc3c9d..5d699aad 100644 --- a/bin/glitch_patterns.py +++ b/bin/glitch_patterns.py @@ -31,6 +31,14 @@ class GlitchCategory(Enum): WATER_REFLECTION = "water_reflection" SKYBOX_SEAM = "skybox_seam" + # Three.js-specific categories (ref: timmy-config#543) + SHADER_FAILURE = "shader_failure" + TEXTURE_PLACEHOLDER = "texture_placeholder" + UV_MAPPING_ERROR = "uv_mapping_error" + FRUSTUM_CULLING = "frustum_culling" + SHADOW_MAP_ARTIFACT = "shadow_map_artifact" + BLOOM_OVERFLOW = "bloom_overflow" + @dataclass class GlitchPattern: @@ -241,6 +249,123 @@ MATRIX_GLITCH_PATTERNS: list[GlitchPattern] = [ ], confidence_threshold=0.45, ), + + # --- Three.js-Specific Glitch Patterns (ref: timmy-config#543) --- + GlitchPattern( + category=GlitchCategory.SHADER_FAILURE, + name="Shader Compilation Failure", + description="Three.js shader failed to compile, rendering the material as solid black. " + "Common when custom ShaderMaterial has syntax errors or missing uniforms.", + severity=GlitchSeverity.CRITICAL, + detection_prompts=[ + "Look for objects or surfaces rendered as pure black (#000000) that should have visible textures or materials.", + "Identify geometry that appears completely dark while surrounding objects are normally lit.", + "Check for objects where the material seems to 'absorb all light' — flat black with no shading gradient.", + ], + visual_indicators=[ + "solid black object with no shading", + "geometry rendered as silhouette", + "material appears to absorb light entirely", + "black patch inconsistent with scene lighting", + ], + confidence_threshold=0.7, + ), + GlitchPattern( + category=GlitchCategory.TEXTURE_PLACEHOLDER, + name="Three.js Texture Not Loaded", + description="Three.js failed to load the texture asset, rendering a 1x1 white pixel " + "stretched across the entire surface. Distinguished from missing-texture by " + "the uniform white/grey appearance rather than magenta.", + severity=GlitchSeverity.CRITICAL, + detection_prompts=[ + "Look for surfaces that are uniformly white or light grey with no texture detail, even on large geometry.", + "Identify objects where the texture appears as a single solid color stretched across complex UVs.", + "Check for surfaces that look 'blank' or 'unloaded' — flat white/grey where detail should exist.", + ], + visual_indicators=[ + "uniform white or light grey surface", + "no texture detail on large geometry", + "stretched single-color appearance", + "1x1 pixel placeholder stretched to fill UV space", + ], + confidence_threshold=0.65, + ), + GlitchPattern( + category=GlitchCategory.UV_MAPPING_ERROR, + name="BufferGeometry UV Mapping Error", + description="Three.js BufferGeometry has incorrect UV coordinates, causing textures to " + "appear stretched, compressed, or mapped to the wrong faces.", + severity=GlitchSeverity.HIGH, + detection_prompts=[ + "Look for textures that appear dramatically stretched in one direction on specific faces.", + "Identify surfaces where the texture pattern is distorted but other nearby surfaces look correct.", + "Check for faces where the texture seems 'smeared' or mapped with incorrect aspect ratio.", + ], + visual_indicators=[ + "texture stretching on specific faces", + "distorted pattern on geometry", + "smeared texture appearance", + "aspect ratio mismatch between texture and surface", + ], + confidence_threshold=0.6, + ), + GlitchPattern( + category=GlitchCategory.FRUSTUM_CULLING, + name="Frustum Culling Artifact", + description="Three.js frustum culling incorrectly marks objects as outside the camera " + "frustum, causing them to pop in/out of existence at screen edges.", + severity=GlitchSeverity.MEDIUM, + detection_prompts=[ + "Look for objects that are partially visible at the edge of the frame — half-rendered or cut off unnaturally.", + "Identify geometry that seems to 'pop' into existence as the view angle changes.", + "Check screen edges for objects that appear suddenly rather than smoothly entering the viewport.", + ], + visual_indicators=[ + "half-visible object at screen edge", + "object popping into frame", + "abrupt appearance of geometry", + "bounding box visible but mesh missing", + ], + confidence_threshold=0.55, + ), + GlitchPattern( + category=GlitchCategory.SHADOW_MAP_ARTIFACT, + name="Shadow Map Resolution Artifact", + description="Three.js shadow map has insufficient resolution, causing pixelated, " + "blocky shadows with visible texel edges instead of smooth shadow gradients.", + severity=GlitchSeverity.MEDIUM, + detection_prompts=[ + "Look for shadows with visible blocky or pixelated edges instead of smooth gradients.", + "Identify shadow maps where individual texels (texture pixels) are clearly visible.", + "Check for shadows that appear as jagged stair-stepped patterns rather than soft edges.", + ], + visual_indicators=[ + "blocky shadow edges", + "visible texel grid in shadows", + "stair-stepped shadow boundary", + "pixelated shadow gradient", + ], + confidence_threshold=0.55, + ), + GlitchPattern( + category=GlitchCategory.BLOOM_OVERFLOW, + name="Post-Processing Bloom Overflow", + description="Three.js UnrealBloomPass or similar post-processing bloom effect is too " + "intense, causing bright areas to bleed glow into surrounding geometry.", + severity=GlitchSeverity.LOW, + detection_prompts=[ + "Look for bright areas that have an unusually large, soft glow bleeding into adjacent surfaces.", + "Identify scenes where light sources appear to have a 'halo' that extends beyond physical plausibility.", + "Check for bright objects whose glow color bleeds onto nearby unrelated geometry.", + ], + visual_indicators=[ + "excessive glow bleeding from bright surfaces", + "halo around light sources", + "bloom color tinting adjacent geometry", + "glow bleeding beyond object boundaries", + ], + confidence_threshold=0.5, + ), ] @@ -289,6 +414,23 @@ def build_vision_prompt(patterns: list[GlitchPattern] | None = None) -> str: ) + +# Three.js-specific category set for filtering (ref: timmy-config#543) +THREEJS_CATEGORIES = { + GlitchCategory.SHADER_FAILURE, + GlitchCategory.TEXTURE_PLACEHOLDER, + GlitchCategory.UV_MAPPING_ERROR, + GlitchCategory.FRUSTUM_CULLING, + GlitchCategory.SHADOW_MAP_ARTIFACT, + GlitchCategory.BLOOM_OVERFLOW, +} + + +def get_threejs_patterns() -> list[GlitchPattern]: + """Return only Three.js-specific glitch patterns.""" + return [p for p in MATRIX_GLITCH_PATTERNS if p.category in THREEJS_CATEGORIES] + + if __name__ == "__main__": import json print(f"Loaded {len(MATRIX_GLITCH_PATTERNS)} glitch patterns:\n") diff --git a/bin/matrix_glitch_detector.py b/bin/matrix_glitch_detector.py index 03f16e55..4da11792 100644 --- a/bin/matrix_glitch_detector.py +++ b/bin/matrix_glitch_detector.py @@ -9,7 +9,7 @@ Usage: python matrix_glitch_detector.py [--angles 4] [--output report.json] python matrix_glitch_detector.py --demo # Run with synthetic test data -Ref: timmy-config#491 +Ref: timmy-config#491, timmy-config#543 """ import argparse @@ -33,6 +33,7 @@ from glitch_patterns import ( MATRIX_GLITCH_PATTERNS, build_vision_prompt, get_patterns_by_severity, + get_threejs_patterns, ) @@ -345,14 +346,17 @@ def _parse_vision_response( def _infer_severity(category: str, confidence: float) -> str: """Infer severity from category and confidence when not provided.""" - critical_cats = {"missing_textures", "clipping"} - high_cats = {"floating_assets", "broken_normals"} + critical_cats = {"missing_textures", "clipping", "shader_failure", "texture_placeholder"} + high_cats = {"floating_assets", "broken_normals", "uv_mapping_error"} + medium_cats = {"frustum_culling", "shadow_map_artifact"} cat_lower = category.lower() if any(c in cat_lower for c in critical_cats): return "critical" if confidence > 0.7 else "high" if any(c in cat_lower for c in high_cats): return "high" if confidence > 0.7 else "medium" + if any(c in cat_lower for c in medium_cats): + return "medium" if confidence > 0.6 else "low" return "medium" if confidence > 0.6 else "low" @@ -389,9 +393,9 @@ def build_report( ), }, metadata={ - "detector_version": "0.1.0", + "detector_version": "0.2.0", "pattern_count": len(MATRIX_GLITCH_PATTERNS), - "reference": "timmy-config#491", + "reference": "timmy-config#491, timmy-config#543", }, ) @@ -460,6 +464,30 @@ def run_demo(output_path: Optional[Path] = None) -> ScanResult: screenshot_index=3, screenshot_angle="left", ), + DetectedGlitch( + id=str(uuid.uuid4())[:8], + category="shader_failure", + name="Black Material on Portal Frame", + description="Portal frame rendered as solid black — shader compilation failed (missing uniform u_time)", + severity="critical", + confidence=0.91, + location_x=45.0, + location_y=30.0, + screenshot_index=0, + screenshot_angle="front", + ), + DetectedGlitch( + id=str(uuid.uuid4())[:8], + category="shadow_map_artifact", + name="Pixelated Character Shadow", + description="Character shadow shows visible texel grid — shadow map resolution too low (512x512)", + severity="medium", + confidence=0.78, + location_x=52.0, + location_y=75.0, + screenshot_index=1, + screenshot_angle="right", + ), ] print(f"[*] Detected {len(demo_glitches)} glitches") @@ -496,6 +524,11 @@ Examples: help="Minimum severity to include in report", ) parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") + parser.add_argument( + "--threejs", + action="store_true", + help="Focus on Three.js-specific glitch patterns only (shader, texture, UV, culling, shadow, bloom)", + ) args = parser.parse_args() @@ -525,9 +558,13 @@ Examples: screenshots = capture_screenshots(args.url, angles, screenshots_dir) print(f"[*] Captured {len(screenshots)} screenshots") - # Filter patterns by severity + # Filter patterns by severity and type min_sev = GlitchSeverity(args.min_severity) patterns = get_patterns_by_severity(min_sev) + if args.threejs: + threejs_patterns = get_threejs_patterns() + patterns = [p for p in patterns if p in threejs_patterns] + print(f"[*] Three.js-focused mode: {len(patterns)} patterns") # Analyze with vision AI print(f"[*] Analyzing with vision AI ({len(patterns)} patterns)...") diff --git a/tests/test_glitch_detector.py b/tests/test_glitch_detector.py index 970e529d..11750dea 100644 --- a/tests/test_glitch_detector.py +++ b/tests/test_glitch_detector.py @@ -19,9 +19,11 @@ from glitch_patterns import ( GlitchPattern, GlitchSeverity, MATRIX_GLITCH_PATTERNS, + THREEJS_CATEGORIES, build_vision_prompt, get_pattern_by_category, get_patterns_by_severity, + get_threejs_patterns, ) from matrix_glitch_detector import ( @@ -40,7 +42,7 @@ class TestGlitchPatterns(unittest.TestCase): def test_pattern_count(self): """Verify we have a reasonable number of defined patterns.""" - self.assertGreaterEqual(len(MATRIX_GLITCH_PATTERNS), 8) + self.assertGreaterEqual(len(MATRIX_GLITCH_PATTERNS), 14) # 10 generic + 6 Three.js def test_all_patterns_have_required_fields(self): """Every pattern must have category, name, description, severity, prompts.""" @@ -88,6 +90,9 @@ class TestGlitchPatterns(unittest.TestCase): self.assertIn("Floating Object", prompt) self.assertIn("Z-Fighting", prompt) self.assertIn("Missing", prompt) + # Three.js patterns should be included + self.assertIn("Shader Compilation Failure", prompt) + self.assertIn("Bloom Overflow", prompt) def test_build_vision_prompt_subset(self): """Vision prompt with subset should only include specified patterns.""" @@ -248,7 +253,7 @@ class TestGlitchDetector(unittest.TestCase): try: report = run_demo(output_path) - self.assertEqual(len(report.glitches), 4) + self.assertEqual(len(report.glitches), 6) # 4 original + 2 Three.js self.assertGreater(report.summary["total_glitches"], 0) self.assertTrue(output_path.exists()) @@ -260,6 +265,93 @@ class TestGlitchDetector(unittest.TestCase): output_path.unlink(missing_ok=True) +class TestThreeJsPatterns(unittest.TestCase): + """Tests for Three.js-specific glitch patterns (timmy-config#543).""" + + def test_get_threejs_patterns_returns_only_threejs(self): + """get_threejs_patterns() should return only Three.js categories.""" + patterns = get_threejs_patterns() + self.assertEqual(len(patterns), 6) + for p in patterns: + self.assertIn(p.category, THREEJS_CATEGORIES) + + def test_threejs_patterns_have_required_fields(self): + """All Three.js patterns must have valid fields.""" + for p in get_threejs_patterns(): + self.assertIsInstance(p.category, GlitchCategory) + self.assertTrue(p.name) + self.assertTrue(p.description) + self.assertIsInstance(p.severity, GlitchSeverity) + self.assertGreater(len(p.detection_prompts), 0) + self.assertGreater(len(p.visual_indicators), 0) + + def test_shader_failure_is_critical(self): + """Shader compilation failure should be CRITICAL severity.""" + p = get_pattern_by_category(GlitchCategory.SHADER_FAILURE) + self.assertIsNotNone(p) + self.assertEqual(p.severity, GlitchSeverity.CRITICAL) + + def test_texture_placeholder_is_critical(self): + """Texture placeholder (1x1 white) should be CRITICAL severity.""" + p = get_pattern_by_category(GlitchCategory.TEXTURE_PLACEHOLDER) + self.assertIsNotNone(p) + self.assertEqual(p.severity, GlitchSeverity.CRITICAL) + + def test_infer_severity_shader_failure(self): + """Shader failure should infer critical/high.""" + self.assertEqual(_infer_severity("shader_failure", 0.8), "critical") + self.assertEqual(_infer_severity("shader_failure", 0.5), "high") + + def test_infer_severity_texture_placeholder(self): + """Texture placeholder should infer critical/high.""" + self.assertEqual(_infer_severity("texture_placeholder", 0.8), "critical") + self.assertEqual(_infer_severity("texture_placeholder", 0.5), "high") + + def test_infer_severity_uv_mapping(self): + """UV mapping error should infer high/medium.""" + self.assertEqual(_infer_severity("uv_mapping_error", 0.8), "high") + self.assertEqual(_infer_severity("uv_mapping_error", 0.5), "medium") + + def test_infer_severity_frustum_culling(self): + """Frustum culling should infer medium/low.""" + self.assertEqual(_infer_severity("frustum_culling", 0.7), "medium") + self.assertEqual(_infer_severity("frustum_culling", 0.4), "low") + + def test_infer_severity_shadow_map(self): + """Shadow map artifact should infer medium/low.""" + self.assertEqual(_infer_severity("shadow_map_artifact", 0.7), "medium") + self.assertEqual(_infer_severity("shadow_map_artifact", 0.4), "low") + + def test_infer_severity_bloom_overflow(self): + """Bloom overflow should infer medium/low (default path).""" + self.assertEqual(_infer_severity("bloom_overflow", 0.7), "medium") + self.assertEqual(_infer_severity("bloom_overflow", 0.4), "low") + + def test_threejs_patterns_in_vision_prompt(self): + """Three.js patterns should appear in the composite vision prompt.""" + prompt = build_vision_prompt() + self.assertIn("shader_failure", prompt) + self.assertIn("texture_placeholder", prompt) + self.assertIn("uv_mapping_error", prompt) + self.assertIn("frustum_culling", prompt) + self.assertIn("shadow_map_artifact", prompt) + self.assertIn("bloom_overflow", prompt) + + def test_threejs_subset_prompt(self): + """Building prompt from Three.js-only patterns should work.""" + threejs = get_threejs_patterns() + prompt = build_vision_prompt(threejs) + self.assertIn("Shader Compilation Failure", prompt) + self.assertNotIn("Floating Object", prompt) # generic, not Three.js + + def test_report_metadata_version(self): + """Report metadata should reference both issues.""" + report = run_demo() + self.assertEqual(report.metadata["detector_version"], "0.2.0") + self.assertIn("543", report.metadata["reference"]) + + + class TestIntegration(unittest.TestCase): """Integration-level tests.""" @@ -276,6 +368,13 @@ class TestIntegration(unittest.TestCase): expected = {"floating_assets", "z_fighting", "missing_textures", "clipping", "broken_normals"} self.assertTrue(expected.issubset(category_values)) + def test_patterns_cover_threejs_themes(self): + """Patterns should cover Three.js-specific glitch themes (#543).""" + category_values = {p.category.value for p in MATRIX_GLITCH_PATTERNS} + threejs_expected = {"shader_failure", "texture_placeholder", "uv_mapping_error", + "frustum_culling", "shadow_map_artifact", "bloom_overflow"} + self.assertTrue(threejs_expected.issubset(category_values)) + if __name__ == "__main__": unittest.main()