diff --git a/assets/cook_torrance_showcase.scene b/assets/cook_torrance_showcase.scene new file mode 100644 index 0000000..30c5016 --- /dev/null +++ b/assets/cook_torrance_showcase.scene @@ -0,0 +1,123 @@ +# Cook-Torrance Material Showcase Scene +# Educational demonstration of microfacet theory with multiple material configurations +# Demonstrates roughness, metallic, and specular parameter effects + +scene_name: Cook-Torrance Material Showcase +description: Comprehensive demonstration of Cook-Torrance microfacet BRDF + +# ================================================ +# LAMBERT MATERIALS FOR COMPARISON +# ================================================ +# These provide educational reference points to contrast with Cook-Torrance behavior +# Lambert materials exhibit perfectly diffuse reflection (viewing angle independent) + +material_lambert matte_white 0.9 0.9 0.9 +material_lambert matte_red 0.8 0.3 0.3 +material_lambert matte_blue 0.3 0.3 0.8 + +# ================================================ +# COOK-TORRANCE MATERIALS: METALLIC PROGRESSION +# ================================================ +# These demonstrate roughness effects in metallic (conductor) materials +# All have metallic=1.0, varying roughness from mirror-like to diffuse + +# Gold materials with roughness progression (metallic conductors) +material_cook_torrance gold_mirror 1.0 0.8 0.3 0.02 1.0 0.04 +material_cook_torrance gold_brushed 1.0 0.8 0.3 0.3 1.0 0.04 +material_cook_torrance gold_rough 1.0 0.8 0.3 0.8 1.0 0.04 + +# Silver materials with roughness progression (metallic conductors) +material_cook_torrance silver_mirror 0.95 0.95 0.95 0.02 1.0 0.04 +material_cook_torrance silver_brushed 0.95 0.95 0.95 0.3 1.0 0.04 +material_cook_torrance silver_rough 0.95 0.95 0.95 0.8 1.0 0.04 + +# ================================================ +# COOK-TORRANCE MATERIALS: DIELECTRIC MATERIALS +# ================================================ +# These demonstrate Cook-Torrance behavior in non-metallic materials +# All have metallic=0.0, representing plastics, ceramics, etc. + +# Green plastic with roughness progression (dielectric materials) +material_cook_torrance plastic_green_smooth 0.2 0.8 0.4 0.1 0.0 0.04 +material_cook_torrance plastic_green_semi 0.2 0.8 0.4 0.5 0.0 0.04 +material_cook_torrance plastic_green_rough 0.2 0.8 0.4 0.9 0.0 0.04 + +# Red plastic with different specular values (F0 demonstration) +material_cook_torrance plastic_red_normal 0.8 0.2 0.2 0.3 0.0 0.04 +material_cook_torrance plastic_red_glossy 0.8 0.2 0.2 0.1 0.0 0.08 + +# ================================================ +# SCENE LAYOUT: EDUCATIONAL GRID ARRANGEMENT +# ================================================ +# Grid layout enables side-by-side visual comparison of material theory +# Each row demonstrates specific material concepts + +# TOP ROW: Gold metallic progression (smooth → rough) +# Educational focus: How roughness affects specular highlights in metals +sphere -3.0 2.0 -6.0 0.7 gold_mirror # Roughness: 0.02 (mirror-like) +sphere -1.0 2.0 -6.0 0.7 gold_brushed # Roughness: 0.3 (brushed metal) +sphere 1.0 2.0 -6.0 0.7 gold_rough # Roughness: 0.8 (rough metal) +sphere 3.0 2.0 -6.0 0.7 matte_red # Lambert reference + +# SECOND ROW: Silver metallic progression (smooth → rough) +# Educational focus: How different metals exhibit similar microfacet behavior +sphere -3.0 0.5 -6.0 0.7 silver_mirror # Roughness: 0.02 (mirror-like) +sphere -1.0 0.5 -6.0 0.7 silver_brushed # Roughness: 0.3 (brushed metal) +sphere 1.0 0.5 -6.0 0.7 silver_rough # Roughness: 0.8 (rough metal) +sphere 3.0 0.5 -6.0 0.7 matte_white # Lambert reference + +# THIRD ROW: Dielectric plastics progression (smooth → rough) +# Educational focus: How dielectrics differ from metals in BRDF behavior +sphere -3.0 -1.0 -6.0 0.7 plastic_green_smooth # Roughness: 0.1 (glossy plastic) +sphere -1.0 -1.0 -6.0 0.7 plastic_green_semi # Roughness: 0.5 (semi-matte) +sphere 1.0 -1.0 -6.0 0.7 plastic_green_rough # Roughness: 0.9 (matte plastic) +sphere 3.0 -1.0 -6.0 0.7 matte_blue # Lambert reference + +# BOTTOM ROW: Specular parameter comparison (F0 effects) +# Educational focus: How F0 affects dielectric reflection strength +sphere -2.0 -2.5 -6.0 0.7 plastic_red_normal # Standard F0: 0.04 +sphere 0.0 -2.5 -6.0 0.7 plastic_red_glossy # Higher F0: 0.08 +sphere 2.0 -2.5 -6.0 0.7 matte_white # Lambert reference + +# ================================================ +# EDUCATIONAL NOTES AND LEARNING OBJECTIVES +# ================================================ +# This scene demonstrates the following Cook-Torrance microfacet theory concepts: +# +# 1. ROUGHNESS EFFECTS: +# - Low roughness (0.02): Sharp, mirror-like reflections (concentrated highlights) +# - Medium roughness (0.3-0.5): Semi-glossy surfaces (moderate highlight spread) +# - High roughness (0.8-0.9): Diffuse-like behavior (broad highlight spread) +# +# 2. METALLIC vs DIELECTRIC BEHAVIOR: +# - Metallic=1.0 (conductors): High reflectance, colored F0 from base_color +# - Metallic=0.0 (dielectrics): Low F0 reflectance, transmissive behavior +# - F0 determines reflection strength at normal viewing angles +# +# 3. FRESNEL EFFECT DEMONSTRATION: +# - All materials show increased reflection at grazing angles +# - Metals maintain colored reflection across angles +# - Dielectrics transition from transmissive to reflective +# +# 4. ENERGY CONSERVATION: +# - All parameters clamped to physically valid ranges +# - No light amplification (energy conservation maintained) +# - Realistic material behavior across parameter space +# +# 5. COMPARISON WITH LAMBERT MODEL: +# - Lambert: Constant BRDF, viewing angle independent +# - Cook-Torrance: Variable BRDF with viewing angle dependency +# - Educational contrast between empirical and physically-based models +# +# RECOMMENDED LEARNING EXERCISES: +# - Render scene with different viewing angles to observe Fresnel effects +# - Compare specular highlights between different roughness values +# - Observe how metallic materials differ from dielectric materials +# - Validate energy conservation by checking that no materials amplify light +# - Test parameter boundaries (roughness limits, metallic interpolation) +# +# TECHNICAL IMPLEMENTATION NOTES: +# - All materials use physically plausible parameter ranges +# - Roughness limited to [0.01, 1.0] to prevent numerical instability +# - F0 values appropriate for material types (0.04 typical for dielectrics) +# - Scene layout optimized for educational comparison and visual learning \ No newline at end of file diff --git a/docs/architecture/scene-data-format-specifications.md b/docs/architecture/scene-data-format-specifications.md index 27375e3..abc2456 100644 --- a/docs/architecture/scene-data-format-specifications.md +++ b/docs/architecture/scene-data-format-specifications.md @@ -2,7 +2,163 @@ ## Educational Scene File Format (.scene) -The scene file format uses YAML for educational clarity and supports progressive complexity from Epic 2 through Epic 4. +The scene file format uses simple text format for educational clarity and supports progressive complexity from Epic 2 through Epic 4. -**Epic 2 Scene Format Example:** -```yaml \ No newline at end of file +## Multi-Material Scene Format (Epic 3+) + +### Format Overview +The extended scene format supports both Lambert and Cook-Torrance materials in the same scene file, enabling comprehensive educational demonstrations of different BRDF models. + +### Material Definitions + +#### Lambert Material Format +``` +material_lambert +``` + +**Parameters:** +- `name`: Unique material identifier (string, no spaces) +- `red`, `green`, `blue`: Albedo color components [0.0, 1.0] + +**Example:** +``` +material_lambert diffuse_red 0.8 0.3 0.3 +material_lambert matte_white 0.9 0.9 0.9 +``` + +#### Cook-Torrance Material Format +``` +material_cook_torrance +``` + +**Parameters:** +- `name`: Unique material identifier (string, no spaces) +- `base_r`, `base_g`, `base_b`: Base color components [0.0, 1.0] +- `roughness`: Surface roughness [0.01, 1.0] (0.01=mirror, 1.0=diffuse) +- `metallic`: Metallic parameter [0.0, 1.0] (0.0=dielectric, 1.0=conductor) +- `specular`: Dielectric F0 reflectance [0.0, 1.0] (typical: 0.04) + +**Example:** +``` +material_cook_torrance gold_mirror 1.0 0.8 0.3 0.02 1.0 0.04 +material_cook_torrance plastic_rough 0.2 0.8 0.4 0.9 0.0 0.04 +``` + +### Primitive Definitions + +#### Sphere Format +``` +sphere +``` + +**Parameters:** +- `center_x`, `center_y`, `center_z`: Sphere center coordinates +- `radius`: Sphere radius (positive value) +- `material_name`: Reference to previously defined material + +**Example:** +``` +sphere 0.0 0.0 -5.0 1.0 diffuse_red +sphere 2.0 0.0 -5.0 1.0 gold_mirror +``` + +## Complete Scene File Example + +### Cook-Torrance Material Showcase +``` +# Cook-Torrance Material Showcase Scene +# Educational demonstration of microfacet theory with multiple material configurations +# Demonstrates roughness, metallic, and specular parameter effects + +# Lambert materials for comparison +material_lambert matte_white 0.9 0.9 0.9 +material_lambert matte_red 0.8 0.3 0.3 +material_lambert matte_blue 0.3 0.3 0.8 + +# Cook-Torrance materials demonstrating parameter ranges +material_cook_torrance gold_mirror 1.0 0.8 0.3 0.02 1.0 0.04 +material_cook_torrance gold_brushed 1.0 0.8 0.3 0.3 1.0 0.04 +material_cook_torrance gold_rough 1.0 0.8 0.3 0.8 1.0 0.04 +material_cook_torrance plastic_smooth 0.2 0.8 0.4 0.1 0.0 0.04 +material_cook_torrance plastic_rough 0.2 0.8 0.4 0.9 0.0 0.04 + +# Scene layout: 3x3 grid demonstrating material progression +# Top row: Metal progression (smooth to rough) +sphere -2.0 1.0 -5.0 0.8 gold_mirror +sphere 0.0 1.0 -5.0 0.8 gold_brushed +sphere 2.0 1.0 -5.0 0.8 gold_rough + +# Middle row: Dielectric Cook-Torrance materials +sphere -1.0 0.0 -5.0 0.8 plastic_smooth +sphere 1.0 0.0 -5.0 0.8 plastic_rough + +# Bottom row: Lambert reference materials for comparison +sphere -1.0 -1.0 -5.0 0.8 matte_red +sphere 0.0 -1.0 -5.0 0.8 matte_white +sphere 1.0 -1.0 -5.0 0.8 matte_blue + +# Educational Notes: +# - Gold spheres show roughness progression in metallic materials +# - Plastic spheres demonstrate dielectric Cook-Torrance behavior +# - Lambert spheres provide educational reference for comparison +# - Scene layout enables side-by-side visual comparison of material theory +``` + +## Backward Compatibility + +### Legacy Lambert-Only Format (Epic 2) +The original format continues to work for backward compatibility: +``` +material red_sphere 0.8 0.3 0.3 +sphere 0.0 0.0 -5.0 1.0 red_sphere +``` + +**Compatibility Rules:** +1. Lines starting with `material ` (single space) are interpreted as Lambert materials +2. Lines starting with `material_lambert ` are explicit Lambert materials +3. Lines starting with `material_cook_torrance ` are Cook-Torrance materials +4. Mixed format files are supported - both legacy and new syntax in same file + +### Error Handling + +**Invalid Material References:** +- Sphere referencing undefined material → Error with helpful message +- Material name conflicts → Later definition overrides earlier one with warning + +**Invalid Parameters:** +- Parameter out of range → Automatic clamping with educational console output +- Insufficient parameters → Error with expected format example +- Invalid number format → Error with line number and expected type + +**Educational Error Messages:** +``` +Error: Sphere at line 15 references undefined material 'unknown_material' +Available materials: matte_white, gold_mirror, plastic_rough + +Warning: Roughness 1.5 at line 8 exceeds valid range [0.01, 1.0], clamped to 1.0 +Educational note: Roughness > 1.0 would represent unphysical surface behavior + +Error: Expected 7 parameters for Cook-Torrance material at line 12, got 5 +Format: material_cook_torrance +Example: material_cook_torrance gold 1.0 0.8 0.3 0.1 1.0 0.04 +``` + +## Implementation Guidelines + +### Parser Requirements +1. **Line-based parsing**: Each material/primitive definition on separate line +2. **Comment support**: Lines starting with `#` are ignored +3. **Whitespace tolerance**: Multiple spaces/tabs between parameters allowed +4. **Case sensitivity**: Material names and keywords are case-sensitive +5. **UTF-8 encoding**: Support for educational comments in multiple languages + +### Memory Management +1. **Polymorphic storage**: `std::vector>` for materials +2. **Reference validation**: Ensure all material references resolve before geometry creation +3. **Resource cleanup**: Automatic memory management through smart pointers + +### Educational Integration +1. **Parsing feedback**: Console output showing materials loaded and validated +2. **Parameter clamping**: Educational explanations when values are adjusted +3. **Material statistics**: Summary of material types and parameter ranges loaded +4. **Cross-reference validation**: Report which materials are used/unused \ No newline at end of file diff --git a/docs/stories/3.5.cook-torrance-scene-material-integration.md b/docs/stories/3.5.cook-torrance-scene-material-integration.md new file mode 100644 index 0000000..c5c7a27 --- /dev/null +++ b/docs/stories/3.5.cook-torrance-scene-material-integration.md @@ -0,0 +1,260 @@ +# Story 3.5: Cook-Torrance Scene Material Integration + +## Status +Approved + +## Story +**As a** graphics programming learner, +**I want** Cook-Torrance materials integrated with scene file format and multi-primitive rendering, +**so that** I can create complex educational demonstrations showcasing Cook-Torrance alongside Lambert materials. + +## Acceptance Criteria +1. Polymorphic Material base class supports both Lambert and Cook-Torrance material evaluation +2. Extended scene file format supports Cook-Torrance material parameters (roughness, metallic, specular) +3. Scene loading correctly instantiates and assigns Cook-Torrance materials to primitives +4. Multi-material scenes demonstrate Cook-Torrance vs Lambert comparison in same scene +5. Cook-Torrance showcase scene provides comprehensive educational demonstration of microfacet theory + +## Tasks / Subtasks +- [x] Create polymorphic Material base class architecture (AC: 1) + - [x] Design Material abstract base class with virtual evaluate_brdf() method + - [x] Add MaterialType enum support for Lambert, CookTorrance, future OpenPBR + - [x] Define virtual parameter validation and clamping interface + - [x] Include educational debugging virtual methods +- [x] Refactor existing Lambert material to inherit from Material base (AC: 1) + - [x] Create LambertMaterial class inheriting from Material + - [x] Migrate existing Lambert BRDF evaluation to virtual method + - [x] Implement Lambert-specific parameter validation + - [x] Preserve existing educational output and mathematical transparency +- [x] Refactor existing Cook-Torrance material to inherit from Material base (AC: 1) + - [x] Create CookTorranceMaterial class inheriting from Material + - [x] Migrate existing Cook-Torrance D, G, F implementation to virtual method + - [x] Implement Cook-Torrance-specific parameter validation (roughness, metallic, specular) + - [x] Preserve existing educational mathematical breakdown output +- [x] Extend scene file format for multi-material support (AC: 2) + - [x] Design Lambert material syntax: `material_lambert name r g b` + - [x] Design Cook-Torrance material syntax: `material_cook_torrance name base_r base_g base_b roughness metallic specular` + - [x] Update scene format specification documentation with examples + - [x] Maintain backward compatibility with existing Lambert-only scene files +- [x] Enhance SceneLoader for polymorphic material parsing (AC: 3) + - [x] Add material type detection logic for different material line formats + - [x] Implement polymorphic material instantiation (Lambert vs Cook-Torrance) + - [x] Add robust error handling for malformed material definitions + - [x] Integrate educational parsing output with material creation transparency +- [x] Update Scene class for polymorphic material container (AC: 3) + - [x] Change materials container from std::vector to std::vector> + - [x] Update material management methods for polymorphic material handling + - [x] Ensure intersection algorithm works with virtual material evaluation + - [x] Preserve existing performance monitoring and educational statistics +- [x] Create Cook-Torrance showcase scene file (AC: 5) + - [x] Design comprehensive scene layout demonstrating material parameter effects + - [x] Include roughness progression examples (smooth to rough metallic materials) + - [x] Add dielectric vs conductor material comparisons + - [x] Provide Lambert reference materials for educational comparison + - [x] Add extensive educational comments explaining material theory demonstrated +- [x] Integration testing and validation (AC: 4) + - [x] Test multi-material scene loading with mixed Lambert and Cook-Torrance materials + - [x] Validate energy conservation maintained across both material types + - [x] Verify educational console output preserved for both material types + - [x] Test scene file error handling with malformed material definitions + - [x] Ensure existing single-sphere Cook-Torrance mode remains functional + +## Dev Notes + +### Previous Story Insights +From Story 3.1 completion, the Cook-Torrance foundation provides: +- Complete Cook-Torrance BRDF implementation with D, G, F mathematical components +- Educational mathematical breakdown with detailed console output +- Energy conservation validation and automatic parameter clamping +- **Critical Gap:** Only supports single-sphere rendering via `--cook-torrance` command-line parameter +- **Architecture Need:** Scene file integration requires polymorphic Material base class + +### Architecture Context + +**Source:** [docs/architecture/data-models.md - Material (Clean Core with Parameter Binding Adapter)] +- Material class shows MaterialType enum (Lambert, CookTorrance, OpenPBR) suggesting polymorphic design was planned +- Material::evaluate_brdf() core interface for BRDF evaluation with clean mathematical separation +- Production validation methods validate_parameters() and clamp_to_valid_ranges() for parameter validation +- Educational Inspector pattern available for mathematical transparency without performance impact + +**Source:** [docs/architecture/data-models.md - Scene (Clean Core with Educational Monitoring)] +- Scene class materials container currently uses std::vector but needs std::vector> for polymorphism +- Scene::intersect() method for ray-scene intersection testing with educational debugging +- Standard scene management methods add_material() and add_primitive() need polymorphic material support + +### Current Source Tree Structure +**Current Project State (Validated as of Story 3.1):** +``` +src/ +├── core/ +│ ├── vector3.hpp (existing - mathematical operations) +│ ├── point3.hpp (existing - point arithmetic) +│ ├── ray.hpp (existing - ray representation) +│ ├── sphere.hpp (existing - enhanced with material properties) +│ ├── point_light.hpp (existing - light source) +│ ├── camera.hpp (existing - aspect ratio handling) +│ ├── image.hpp (existing - multi-resolution support) +│ ├── scene.hpp (existing - NEEDS ENHANCEMENT for polymorphic materials) +│ ├── scene_loader.hpp (existing - NEEDS ENHANCEMENT for multi-material parsing) +│ └── stb_image_write.h (existing - PNG export library) +├── materials/ +│ ├── lambert.hpp (existing - NEEDS REFACTOR to inherit from Material base) +│ ├── cook_torrance.hpp (existing - NEEDS REFACTOR to inherit from Material base) +│ └── material_base.hpp (NEW - polymorphic Material base class) +└── main.cpp (existing - may need minor updates for new material architecture) + +assets/ +├── simple_scene.scene (existing - Lambert-only scene) +├── showcase_scene.scene (existing - Lambert-only scene) +└── cook_torrance_showcase.scene (NEW - comprehensive Cook-Torrance demonstration) +``` + +**Files to be Created/Modified:** +- src/materials/material_base.hpp (NEW - polymorphic Material base class) +- src/materials/lambert.hpp (REFACTOR - inherit from Material base) +- src/materials/cook_torrance.hpp (REFACTOR - inherit from Material base) +- src/core/scene.hpp (ENHANCE - polymorphic material container) +- src/core/scene_loader.hpp (ENHANCE - multi-material parsing) +- assets/cook_torrance_showcase.scene (NEW - comprehensive material demonstration) +- docs/architecture/scene-data-format-specifications.md (UPDATE - multi-material format) + +### Technical Implementation Details + +**Polymorphic Material Architecture:** +```cpp +// src/materials/material_base.hpp +class Material { +public: + Vector3 base_color; + MaterialType type; + + Material(const Vector3& color, MaterialType mat_type) + : base_color(color), type(mat_type) {} + + virtual ~Material() = default; + virtual Vector3 evaluate_brdf(const Vector3& wi, const Vector3& wo, const Vector3& normal) const = 0; + virtual bool validate_parameters() const = 0; + virtual void clamp_to_valid_ranges() = 0; + virtual void explain_brdf_evaluation(const Vector3& wi, const Vector3& wo, const Vector3& normal) const {} +}; +``` + +**Extended Scene File Format:** +``` +# Multi-Material Scene Format +material_lambert diffuse_red 0.8 0.3 0.3 +material_cook_torrance metal_rough 0.9 0.7 0.4 0.8 1.0 0.04 +sphere 0.0 0.0 -5.0 1.0 diffuse_red +sphere 2.0 0.0 -5.0 1.0 metal_rough +``` + +**Scene Class Enhancement:** +```cpp +class Scene { +public: + std::vector> materials; // Changed for polymorphism + + int add_material(std::unique_ptr material) { + materials.push_back(std::move(material)); + return materials.size() - 1; + } +}; +``` + +### File Locations +- Polymorphic Material base class: src/materials/material_base.hpp (new abstract interface) +- Lambert material refactor: src/materials/lambert.hpp (inherit from Material base) +- Cook-Torrance material refactor: src/materials/cook_torrance.hpp (inherit from Material base) +- Scene class enhancement: src/core/scene.hpp (polymorphic material container) +- Scene loader enhancement: src/core/scene_loader.hpp (multi-material parsing) +- Cook-Torrance showcase scene: assets/cook_torrance_showcase.scene (comprehensive demonstration) +- Scene format documentation: docs/architecture/scene-data-format-specifications.md (complete specification) + +### Technical Constraints +- Educational console output for all material types and scene loading must be preserved +- C++20/C++23 compatibility maintained with existing codebase +- Energy conservation validation required for both Lambert and Cook-Torrance materials +- Scene file format: Extended text format with educational comments and backward compatibility +- Mathematical precision: 1e-6 tolerance for BRDF evaluation correctness +- Performance monitoring: Integration with existing PerformanceTimer and educational statistics +- Memory management: Safe polymorphic material handling with std::unique_ptr containers +- Cross-platform compatibility: Standard C++ patterns without platform-specific dependencies + +## Testing +**Test File Location:** tests/test_math_correctness.cpp +**Testing Framework:** Custom mathematical validation framework (extended from Story 3.1) +**Testing Standards:** Mathematical correctness validation with 1e-6 precision tolerance + +**Story-Specific Testing Requirements:** +- Polymorphic material evaluation correctness for both Lambert and Cook-Torrance +- Multi-material scene loading validation with mixed material types +- Energy conservation verification across polymorphic material evaluations +- Scene file parsing robustness with malformed material definitions +- Educational console output accuracy for polymorphic material architecture + +**Concrete Test Scenarios:** +- Multi-Material Scene Loading: Scene with both Lambert and Cook-Torrance materials loads correctly with proper material assignment +- Polymorphic BRDF Evaluation: Both LambertMaterial and CookTorranceMaterial evaluate correctly through virtual interface +- Energy Conservation: Both material types maintain energy conservation through polymorphic evaluation +- Scene Format Parsing: SceneLoader correctly identifies and instantiates different material types +- Parameter Validation: Both material types validate parameters correctly through virtual interface +- Educational Output: Console mathematical breakdown preserved for both material types +- Backward Compatibility: Existing Lambert-only scene files continue to work correctly +- Error Handling: Malformed material definitions handled gracefully with educational error messages +- Performance Integration: Polymorphic evaluation integrates correctly with existing performance monitoring +- Memory Management: No memory leaks in polymorphic material container management + +## QA Results + +### Review Date: 2025-08-27 + +### Reviewed By: Quinn (Senior Developer QA) + +### Code Quality Assessment + +The Cook-Torrance scene material integration implementation demonstrates excellent code quality with comprehensive polymorphic architecture. The codebase successfully implements a clean Material base class with proper virtual methods, supporting both Lambert and Cook-Torrance materials through polymorphic evaluation. The scene file format extension is well-designed and backward compatible. Educational output is extensive and mathematically accurate throughout. + +### Refactoring Performed + +**File**: src/core/scene.hpp:354 +- **Change**: Updated memory calculation to account for polymorphic material sizing +- **Why**: The current calculation assumes all materials are LambertMaterial, but with polymorphism we have different material types +- **How**: Need to calculate memory based on actual material types rather than fixed size assumption + +### Compliance Check + +- Coding Standards: ✓ Excellent adherence to educational C++ patterns and mathematical transparency +- Project Structure: ✓ Files properly organized in materials/ and core/ directories as specified +- Testing Strategy: ✓ Comprehensive mathematical validation tests present in test_math_correctness.cpp +- All ACs Met: ✓ All acceptance criteria successfully implemented and validated + +### Improvements Checklist + +[x] Verified polymorphic Material base class with virtual evaluate_brdf() method (AC1) +[x] Confirmed Lambert and Cook-Torrance materials inherit properly from Material base (AC1) +[x] Validated extended scene file format supports both material types (AC2) +[x] Tested scene loading correctly instantiates polymorphic materials (AC3) +[x] Verified multi-material scenes render with mixed Lambert and Cook-Torrance (AC4) +[x] Confirmed Cook-Torrance showcase scene provides comprehensive educational demonstration (AC5) +[x] Completed integration testing validation - all integration tests now pass successfully +[x] Enhanced material memory calculation improvements for polymorphic containers +[x] Validated scene file error recovery for malformed Cook-Torrance parameters + +### Security Review + +No security concerns identified. All material parameters are properly validated and clamped to physically valid ranges, preventing potential numerical instability or energy conservation violations. File I/O operations include appropriate error handling. + +### Performance Considerations + +Performance is excellent with proper std::unique_ptr usage for polymorphic materials. Scene intersection performance scales linearly O(n) as expected. Memory usage is optimized with educational monitoring capabilities. The polymorphic virtual method calls have minimal overhead for BRDF evaluation. + +### Final Status + +**✓ Approved - Ready for Done** + +The implementation is excellent and fully functional. All acceptance criteria have been met, integration testing is complete, and the polymorphic material architecture is solid. The Cook-Torrance showcase scene loads and renders successfully with comprehensive educational demonstrations. All refactoring improvements have been applied, including enhanced memory calculation for polymorphic materials. The codebase demonstrates senior-level quality with proper error handling, energy conservation validation, and extensive educational value. + +## Change Log +| Date | Version | Description | Author | +|------|---------|-------------|--------| +| 2025-08-26 | 1.0 | Initial story creation from Epic 3.5 requirements based on approved Sprint Change Proposal | Bob (Scrum Master) | \ No newline at end of file diff --git a/src/core/ray.hpp b/src/core/ray.hpp index e58e250..c48db28 100644 --- a/src/core/ray.hpp +++ b/src/core/ray.hpp @@ -2,6 +2,7 @@ #include "point3.hpp" #include "vector3.hpp" #include +#include // Ray represents a mathematical ray: infinite line starting at origin, extending in direction // Ray equation: P(t) = origin + t * direction, where t ≥ 0 diff --git a/src/core/scene.hpp b/src/core/scene.hpp index a4fcb74..ecb386a 100644 --- a/src/core/scene.hpp +++ b/src/core/scene.hpp @@ -4,11 +4,14 @@ #include "ray.hpp" #include "sphere.hpp" #include "../materials/lambert.hpp" +#include "../materials/cook_torrance.hpp" +#include "../materials/material_base.hpp" #include #include #include #include #include +#include // Scene class manages multiple primitive objects and materials for ray tracing // Educational focus: demonstrates ray-scene intersection algorithms and performance monitoring @@ -18,8 +21,9 @@ class Scene { // Container for geometric primitives in the scene std::vector primitives; - // Container for materials with index-based referencing from primitives - std::vector materials; + // Container for polymorphic materials with index-based referencing from primitives + // Supports both Lambert and Cook-Torrance materials through Material base class polymorphism + std::vector> materials; // Educational performance monitoring for intersection statistics mutable int total_intersection_tests = 0; @@ -36,7 +40,7 @@ class Scene { float t; // Ray parameter at closest intersection Point3 point; // 3D coordinates of intersection point Vector3 normal; // Outward-pointing surface normal - const LambertMaterial* material; // Material properties at intersection + const Material* material; // Polymorphic material properties at intersection const Sphere* primitive; // Primitive object that was hit // Default constructor for no-intersection case @@ -46,7 +50,7 @@ class Scene { // Constructor for valid intersection with complete information Intersection(float t_value, const Point3& hit_point, const Vector3& surface_normal, - const LambertMaterial* hit_material, const Sphere* hit_primitive) + const Material* hit_material, const Sphere* hit_primitive) : hit(true), t(t_value), point(hit_point), normal(surface_normal), material(hit_material), primitive(hit_primitive) {} }; @@ -109,7 +113,7 @@ class Scene { closest_hit.t = sphere_hit.t; closest_hit.point = sphere_hit.point; closest_hit.normal = sphere_hit.normal; - closest_hit.material = &materials[sphere.material_index]; + closest_hit.material = materials[sphere.material_index].get(); closest_hit.primitive = &sphere; if (verbose) { @@ -180,27 +184,57 @@ class Scene { return closest_hit; } - // Add material to scene and return its index for primitive referencing + // Add polymorphic material to scene and return its index for primitive referencing // Educational transparency: reports material assignment and validates parameters - int add_material(const LambertMaterial& material) { - std::cout << "\n=== Adding Material to Scene ===" << std::endl; + // Supports both Lambert and Cook-Torrance materials through Material base class polymorphism + int add_material(std::unique_ptr material) { + std::cout << "\n=== Adding Polymorphic Material to Scene ===" << std::endl; + + if (!material) { + std::cout << "ERROR: Null material pointer" << std::endl; + return -1; + } + + // Validate material parameters before adding + if (!material->validate_parameters()) { + std::cout << "WARNING: Material parameters outside valid ranges" << std::endl; + std::cout << "Educational note: Invalid parameters may cause non-physical behavior" << std::endl; + material->clamp_to_valid_ranges(); + std::cout << "Parameters automatically clamped to valid ranges" << std::endl; + } - // Validate material energy conservation before adding - if (!material.validate_energy_conservation()) { - std::cout << "WARNING: Material violates energy conservation, but adding anyway for educational purposes" << std::endl; + // Educational material type reporting + std::cout << "Material Type: " << material->material_type_name() << std::endl; + std::cout << "Base Color: (" << material->base_color.x << ", " + << material->base_color.y << ", " << material->base_color.z << ")" << std::endl; + + // Additional details for specific material types + if (material->type == MaterialType::CookTorrance) { + // Safe cast for Cook-Torrance specific information + CookTorranceMaterial* ct_material = static_cast(material.get()); + std::cout << "Cook-Torrance Parameters:" << std::endl; + std::cout << " Roughness: " << ct_material->roughness << std::endl; + std::cout << " Metallic: " << ct_material->metallic << std::endl; + std::cout << " Specular: " << ct_material->specular << std::endl; } - materials.push_back(material); + materials.push_back(std::move(material)); int material_index = static_cast(materials.size() - 1); std::cout << "Material added at index: " << material_index << std::endl; - std::cout << "Material albedo: (" << material.base_color.x << ", " - << material.base_color.y << ", " << material.base_color.z << ")" << std::endl; std::cout << "Total materials in scene: " << materials.size() << std::endl; + std::cout << "=== Material addition complete ===" << std::endl; return material_index; } + // Legacy add_material method for backward compatibility with existing Lambert code + // Wraps Lambert material in unique_ptr and delegates to polymorphic method + int add_material(const LambertMaterial& material) { + auto lambert_material = std::make_unique(material); + return add_material(std::move(lambert_material)); + } + // Add sphere primitive to scene and return its index // Validates sphere geometry and material index before adding to scene int add_sphere(const Sphere& sphere) { @@ -281,9 +315,18 @@ class Scene { if (materials.size() > 0) { std::cout << "\nMaterial Details:" << std::endl; for (size_t i = 0; i < materials.size(); ++i) { - const LambertMaterial& material = materials[i]; - std::cout << " Material " << i << ": albedo(" << material.base_color.x << "," - << material.base_color.y << "," << material.base_color.z << ")" << std::endl; + const Material* material = materials[i].get(); + std::cout << " Material " << i << ": " << material->material_type_name() + << " - base_color(" << material->base_color.x << "," + << material->base_color.y << "," << material->base_color.z << ")" << std::endl; + + // Additional details for Cook-Torrance materials + if (material->type == MaterialType::CookTorrance) { + const CookTorranceMaterial* ct_material = static_cast(material); + std::cout << " Roughness: " << ct_material->roughness + << ", Metallic: " << ct_material->metallic + << ", Specular: " << ct_material->specular << std::endl; + } } } @@ -307,12 +350,24 @@ class Scene { // Memory used by sphere primitives total_bytes += primitives.size() * sizeof(Sphere); - // Memory used by materials - total_bytes += materials.size() * sizeof(LambertMaterial); + // Memory used by polymorphic materials + for (const auto& material : materials) { + if (material->type == MaterialType::Lambert) { + total_bytes += sizeof(LambertMaterial); + } else if (material->type == MaterialType::CookTorrance) { + total_bytes += sizeof(CookTorranceMaterial); + } else { + // Default to base Material size for unknown types + total_bytes += sizeof(Material); + } + } - // Memory used by containers (approximate) - total_bytes += primitives.capacity() * sizeof(Sphere) - primitives.size() * sizeof(Sphere); - total_bytes += materials.capacity() * sizeof(LambertMaterial) - materials.size() * sizeof(LambertMaterial); + // Memory used by unique_ptr containers + total_bytes += materials.size() * sizeof(std::unique_ptr); + + // Memory used by containers (approximate overhead) + total_bytes += (primitives.capacity() - primitives.size()) * sizeof(Sphere); + total_bytes += (materials.capacity() - materials.size()) * sizeof(std::unique_ptr); return total_bytes; } @@ -322,16 +377,37 @@ class Scene { std::cout << "\n=== Scene Memory Usage Analysis ===" << std::endl; size_t sphere_memory = primitives.size() * sizeof(Sphere); - size_t material_memory = materials.size() * sizeof(LambertMaterial); + + // Calculate polymorphic material memory + size_t material_memory = 0; + size_t lambert_count = 0; + size_t cook_torrance_count = 0; + for (const auto& material : materials) { + if (material->type == MaterialType::Lambert) { + material_memory += sizeof(LambertMaterial); + lambert_count++; + } else if (material->type == MaterialType::CookTorrance) { + material_memory += sizeof(CookTorranceMaterial); + cook_torrance_count++; + } else { + material_memory += sizeof(Material); + } + } + size_t container_overhead = (primitives.capacity() - primitives.size()) * sizeof(Sphere) + - (materials.capacity() - materials.size()) * sizeof(LambertMaterial); + (materials.capacity() - materials.size()) * sizeof(std::unique_ptr); size_t total_scene_memory = calculate_scene_memory_usage(); std::cout << "Scene Data Memory Breakdown:" << std::endl; std::cout << " Spheres: " << primitives.size() << " × " << sizeof(Sphere) << " bytes = " << sphere_memory << " bytes" << std::endl; - std::cout << " Materials: " << materials.size() << " × " << sizeof(LambertMaterial) - << " bytes = " << material_memory << " bytes" << std::endl; + std::cout << " Materials: " << materials.size() << " total (" + << lambert_count << " Lambert, " << cook_torrance_count << " Cook-Torrance)" << std::endl; + std::cout << " Lambert: " << lambert_count << " × " << sizeof(LambertMaterial) + << " bytes = " << (lambert_count * sizeof(LambertMaterial)) << " bytes" << std::endl; + std::cout << " Cook-Torrance: " << cook_torrance_count << " × " << sizeof(CookTorranceMaterial) + << " bytes = " << (cook_torrance_count * sizeof(CookTorranceMaterial)) << " bytes" << std::endl; + std::cout << " Material memory total: " << material_memory << " bytes" << std::endl; std::cout << " Container overhead: " << container_overhead << " bytes" << std::endl; std::cout << " Total scene memory: " << total_scene_memory << " bytes (" << (total_scene_memory / 1024.0f) << " KB)" << std::endl; diff --git a/src/core/scene_loader.hpp b/src/core/scene_loader.hpp index 6fb07a5..a44218b 100644 --- a/src/core/scene_loader.hpp +++ b/src/core/scene_loader.hpp @@ -3,11 +3,13 @@ #include "sphere.hpp" #include "../materials/lambert.hpp" #include "../materials/cook_torrance.hpp" +#include "../materials/material_base.hpp" #include #include #include #include #include +#include // SceneLoader handles parsing of simple scene files for educational multi-primitive scenes // File format: Simple key-value pairs with educational transparency and error handling @@ -63,10 +65,27 @@ class SceneLoader { line_stream >> command; if (command == "material") { - if (parse_material(line_stream, scene, material_name_to_index, material_type)) { + // Legacy Lambert material format for backward compatibility + if (parse_lambert_material_legacy(line_stream, scene, material_name_to_index)) { materials_loaded++; } else { - std::cout << "WARNING: Failed to parse material on line " << line_number << std::endl; + std::cout << "WARNING: Failed to parse legacy material on line " << line_number << std::endl; + } + } + else if (command == "material_lambert") { + // Explicit Lambert material format + if (parse_lambert_material(line_stream, scene, material_name_to_index)) { + materials_loaded++; + } else { + std::cout << "WARNING: Failed to parse Lambert material on line " << line_number << std::endl; + } + } + else if (command == "material_cook_torrance") { + // Cook-Torrance material format + if (parse_cook_torrance_material(line_stream, scene, material_name_to_index)) { + materials_loaded++; + } else { + std::cout << "WARNING: Failed to parse Cook-Torrance material on line " << line_number << std::endl; } } else if (command == "sphere") { @@ -95,40 +114,131 @@ class SceneLoader { } private: - // Parse material definition with error handling and validation + // Parse legacy Lambert material for backward compatibility // Format: material name red green blue - static bool parse_material(std::istringstream& stream, Scene& scene, - std::map& material_map, - const std::string& material_type = "lambert") { + static bool parse_lambert_material_legacy(std::istringstream& stream, Scene& scene, + std::map& material_map) { + std::string name; + float r, g, b; + + if (!(stream >> name >> r >> g >> b)) { + std::cout << "ERROR: Invalid legacy material format. Expected: material name r g b" << std::endl; + return false; + } + + std::cout << "Parsing legacy Lambert material: " << name << " (" << r << ", " << g << ", " << b << ")" << std::endl; + + // Create Lambert material with validation + auto material = std::make_unique(Vector3(r, g, b)); + + // Validate parameters and show educational output + if (!material->validate_parameters()) { + std::cout << "WARNING: Material parameters outside valid range, clamping" << std::endl; + material->clamp_to_valid_ranges(); + std::cout << "Clamped color: (" << material->base_color.x << ", " + << material->base_color.y << ", " << material->base_color.z << ")" << std::endl; + } + + int material_index = scene.add_material(std::move(material)); + material_map[name] = material_index; + + std::cout << "Legacy Lambert material '" << name << "' registered at index " << material_index << std::endl; + return true; + } + + // Parse explicit Lambert material + // Format: material_lambert name red green blue + static bool parse_lambert_material(std::istringstream& stream, Scene& scene, + std::map& material_map) { std::string name; float r, g, b; if (!(stream >> name >> r >> g >> b)) { - std::cout << "ERROR: Invalid material format. Expected: material name r g b" << std::endl; + std::cout << "ERROR: Invalid Lambert material format. Expected: material_lambert name r g b" << std::endl; + std::cout << "Example: material_lambert matte_red 0.8 0.3 0.3" << std::endl; + return false; + } + + std::cout << "Parsing Lambert material: " << name << " (" << r << ", " << g << ", " << b << ")" << std::endl; + + // Create Lambert material with validation + auto material = std::make_unique(Vector3(r, g, b)); + + // Educational parameter validation + if (!material->validate_parameters()) { + std::cout << "WARNING: Lambert material parameters outside [0,1] range" << std::endl; + std::cout << "Educational note: Albedo > 1.0 would violate energy conservation (amplify light)" << std::endl; + material->clamp_to_valid_ranges(); + std::cout << "Clamped albedo: (" << material->base_color.x << ", " + << material->base_color.y << ", " << material->base_color.z << ")" << std::endl; + } + + int material_index = scene.add_material(std::move(material)); + material_map[name] = material_index; + + std::cout << "Lambert material '" << name << "' registered at index " << material_index + << " (Type: Lambert (Diffuse))" << std::endl; + return true; + } + + // Parse Cook-Torrance material + // Format: material_cook_torrance name base_r base_g base_b roughness metallic specular + static bool parse_cook_torrance_material(std::istringstream& stream, Scene& scene, + std::map& material_map) { + std::string name; + float r, g, b, roughness, metallic, specular; + + if (!(stream >> name >> r >> g >> b >> roughness >> metallic >> specular)) { + std::cout << "ERROR: Invalid Cook-Torrance material format" << std::endl; + std::cout << "Expected: material_cook_torrance name r g b roughness metallic specular" << std::endl; + std::cout << "Example: material_cook_torrance gold 1.0 0.8 0.3 0.1 1.0 0.04" << std::endl; + std::cout << "Parameters:" << std::endl; + std::cout << " roughness: [0.01, 1.0] (0.01=mirror, 1.0=diffuse)" << std::endl; + std::cout << " metallic: [0.0, 1.0] (0.0=dielectric, 1.0=conductor)" << std::endl; + std::cout << " specular: [0.0, 1.0] (typical dielectric F0: 0.04)" << std::endl; return false; } - std::cout << "Parsing material: " << name << " (" << r << ", " << g << ", " << b << ")" << std::endl; + std::cout << "Parsing Cook-Torrance material: " << name << std::endl; + std::cout << " Base color: (" << r << ", " << g << ", " << b << ")" << std::endl; + std::cout << " Roughness: " << roughness << ", Metallic: " << metallic << ", Specular: " << specular << std::endl; + + // Create Cook-Torrance material with validation + auto material = std::make_unique(Vector3(r, g, b), roughness, metallic, specular, true); - // Validate color values - if (r < 0.0f || r > 1.0f || g < 0.0f || g > 1.0f || b < 0.0f || b > 1.0f) { - std::cout << "WARNING: Color values outside [0,1] range, clamping" << std::endl; - r = std::max(0.0f, std::min(1.0f, r)); - g = std::max(0.0f, std::min(1.0f, g)); - b = std::max(0.0f, std::min(1.0f, b)); + // Educational parameter validation with detailed output + if (!material->validate_parameters()) { + std::cout << "WARNING: Cook-Torrance material parameters outside valid ranges" << std::endl; + std::cout << "Educational notes:" << std::endl; + std::cout << " - Roughness < 0.01 causes numerical instability in GGX distribution" << std::endl; + std::cout << " - Roughness > 1.0 represents unphysical surface behavior" << std::endl; + std::cout << " - Metallic outside [0,1] breaks PBR material model assumptions" << std::endl; + std::cout << " - Color values > 1.0 would violate energy conservation" << std::endl; + material->clamp_to_valid_ranges(); + std::cout << "Parameters clamped to valid ranges for physical accuracy" << std::endl; } - // Create material and add to scene - if (material_type == "cook-torrance") { - std::cout << "WARNING: Cook-Torrance materials not fully supported in scene files yet." << std::endl; - std::cout << "Creating Lambert material as fallback. Use --no-scene for Cook-Torrance testing." << std::endl; - std::cout << "Cook-Torrance would use: base_color=(" << r << "," << g << "," << b << "), roughness=0.3, metallic=0.0, specular=0.04" << std::endl; + // Educational material analysis + std::cout << "Material analysis:" << std::endl; + if (material->metallic > 0.5f) { + std::cout << " Type: Conductor (Metal) - high reflectance with colored F0" << std::endl; + } else { + std::cout << " Type: Dielectric (Non-metal) - low F0, transmissive at normal incidence" << std::endl; } - LambertMaterial material(Vector3(r, g, b)); - int material_index = scene.add_material(material); + + if (material->roughness < 0.2f) { + std::cout << " Surface: Glossy/Mirror-like - sharp, concentrated reflections" << std::endl; + } else if (material->roughness > 0.7f) { + std::cout << " Surface: Rough/Diffuse-like - broad, scattered reflections" << std::endl; + } else { + std::cout << " Surface: Semi-glossy - moderate reflection spreading" << std::endl; + } + + int material_index = scene.add_material(std::move(material)); material_map[name] = material_index; - std::cout << "Material '" << name << "' registered at index " << material_index << std::endl; + std::cout << "Cook-Torrance material '" << name << "' registered at index " << material_index + << " (Type: Microfacet BRDF)" << std::endl; return true; } diff --git a/src/materials/cook_torrance.hpp b/src/materials/cook_torrance.hpp index 31fdaea..3f2b112 100644 --- a/src/materials/cook_torrance.hpp +++ b/src/materials/cook_torrance.hpp @@ -1,5 +1,6 @@ #pragma once #include "../core/vector3.hpp" +#include "material_base.hpp" #include #include #include @@ -274,9 +275,8 @@ namespace CookTorrance { }; } -class CookTorranceMaterial { +class CookTorranceMaterial : public Material { public: - Vector3 base_color; // Surface albedo color (dielectric) or reflectance values (conductor) float roughness; // Surface roughness: 0.0 = perfect mirror, 1.0 = completely rough float metallic; // Metallic parameter: 0.0 = dielectric, 1.0 = conductor float specular; // Specular reflectance for dielectric materials (typical default: 0.04) @@ -290,7 +290,7 @@ class CookTorranceMaterial { float metallic_param = 0.0f, float specular_param = 0.04f, bool verbose = false) - : base_color(color), roughness(surface_roughness), metallic(metallic_param), specular(specular_param) { + : Material(color, MaterialType::CookTorrance), roughness(surface_roughness), metallic(metallic_param), specular(specular_param) { // Automatically clamp parameters to physically valid ranges clamp_cook_torrance_to_valid_ranges(); @@ -311,7 +311,7 @@ class CookTorranceMaterial { // wi: incident light direction (pointing toward surface, normalized) // wo: outgoing view direction (pointing toward camera, normalized) // normal: surface normal at intersection point (outward-pointing, normalized) - Vector3 evaluate_brdf(const Vector3& wi, const Vector3& wo, const Vector3& normal, bool verbose = true) const { + Vector3 evaluate_brdf(const Vector3& wi, const Vector3& wo, const Vector3& normal, bool verbose = true) const override { if (verbose) { std::cout << "\n=== Cook-Torrance BRDF Evaluation ===" << std::endl; std::cout << "Incident direction (wi): (" << wi.x << ", " << wi.y << ", " << wi.z << ")" << std::endl; @@ -449,7 +449,7 @@ class CookTorranceMaterial { // normal: surface normal at intersection point (outward-pointing, normalized) // incident_radiance: incoming light energy (L_i in rendering equation) Vector3 scatter_light(const Vector3& light_direction, const Vector3& view_direction, - const Vector3& normal, const Vector3& incident_radiance, bool verbose = true) const { + const Vector3& normal, const Vector3& incident_radiance, bool verbose = true) const override { if (verbose) { std::cout << "\n=== Cook-Torrance Light Scattering Calculation ===" << std::endl; std::cout << "Light direction: (" << light_direction.x << ", " << light_direction.y << ", " << light_direction.z << ")" << std::endl; @@ -489,6 +489,12 @@ class CookTorranceMaterial { return outgoing_radiance; } + // Cook-Torrance-specific educational BRDF explanation override + // Provides comprehensive mathematical breakdown of microfacet theory + void explain_brdf_evaluation(const Vector3& wi, const Vector3& wo, const Vector3& normal) const override { + explain_cook_torrance_evaluation(wi, wo, normal); + } + // Complete educational explanation of Cook-Torrance evaluation // Provides comprehensive mathematical breakdown for learning validation // Parameters: same as evaluate_brdf for direct comparison @@ -601,8 +607,34 @@ class CookTorranceMaterial { std::cout << "=== COMPONENT BREAKDOWN COMPLETE ===" << std::endl; } - // Parameter validation for Cook-Torrance physically-based constraints - // Ensures all material parameters remain within physically plausible ranges + // Cook-Torrance-specific parameter validation implementation + // Validates roughness, metallic, specular, and base color within physically valid ranges + bool validate_parameters() const override { + bool roughness_valid = (roughness >= 0.01f && roughness <= 1.0f); + bool metallic_valid = (metallic >= 0.0f && metallic <= 1.0f); + bool specular_valid = (specular >= 0.0f && specular <= 1.0f); + bool color_valid = (base_color.x >= 0.0f && base_color.x <= 1.0f && + base_color.y >= 0.0f && base_color.y <= 1.0f && + base_color.z >= 0.0f && base_color.z <= 1.0f); + + return roughness_valid && metallic_valid && specular_valid && color_valid; + } + + // Cook-Torrance-specific parameter clamping implementation + // Automatically clamps all parameters to physically valid ranges + void clamp_to_valid_ranges() override { + roughness = std::max(0.01f, std::min(1.0f, roughness)); // Prevent perfect mirror (numerical issues) + metallic = std::max(0.0f, std::min(1.0f, metallic)); // Binary or interpolated metallic workflow + specular = std::max(0.0f, std::min(1.0f, specular)); // Physical reflectance bounds + + // Clamp color channels individually to [0,1] range + base_color.x = std::max(0.0f, std::min(1.0f, base_color.x)); + base_color.y = std::max(0.0f, std::min(1.0f, base_color.y)); + base_color.z = std::max(0.0f, std::min(1.0f, base_color.z)); + } + + // Parameter validation for Cook-Torrance physically-based constraints (educational output) + // Ensures all material parameters remain within physically plausible ranges with detailed reporting bool validate_cook_torrance_parameters() const { std::cout << "\n=== Cook-Torrance Parameter Validation ===" << std::endl; @@ -624,17 +656,11 @@ class CookTorranceMaterial { return all_valid; } - // Automatic parameter clamping to ensure physically valid ranges - // Prevents numerical instability and non-physical behavior + // Automatic parameter clamping to ensure physically valid ranges (educational output) + // Prevents numerical instability and non-physical behavior with detailed console output void clamp_cook_torrance_to_valid_ranges() { - roughness = std::max(0.01f, std::min(1.0f, roughness)); // Prevent perfect mirror (numerical issues) - metallic = std::max(0.0f, std::min(1.0f, metallic)); // Binary or interpolated metallic workflow - specular = std::max(0.0f, std::min(1.0f, specular)); // Physical reflectance bounds - - // Clamp color channels individually to [0,1] range - base_color.x = std::max(0.0f, std::min(1.0f, base_color.x)); - base_color.y = std::max(0.0f, std::min(1.0f, base_color.y)); - base_color.z = std::max(0.0f, std::min(1.0f, base_color.z)); + // Use the virtual clamp_to_valid_ranges method which provides the core functionality + clamp_to_valid_ranges(); } private: diff --git a/src/materials/lambert.hpp b/src/materials/lambert.hpp index f5a51de..8408c5a 100644 --- a/src/materials/lambert.hpp +++ b/src/materials/lambert.hpp @@ -1,5 +1,6 @@ #pragma once #include "../core/vector3.hpp" +#include "material_base.hpp" #include #include @@ -60,15 +61,17 @@ // - Kajiya, J.T. "The Rendering Equation" SIGGRAPH 1986 // - Nicodemus, F.E. "Reflectance Nomenclature" NBS 1977 // - Pharr, Jakob, Humphreys "Physically Based Rendering" 4th ed. -class LambertMaterial { +class LambertMaterial : public Material { public: - Vector3 base_color; // Albedo color - fraction of light reflected per wavelength (RGB channels) - // Mathematical constraint: each component must be ∈ [0,1] for energy conservation - // Constructor with explicit albedo color // Albedo interpretation: percentage of incident light reflected at each wavelength // Example: (0.8, 0.8, 0.8) reflects 80% of incident light across all wavelengths - LambertMaterial(const Vector3& color) : base_color(color) {} + // Mathematical constraint: each component must be ∈ [0,1] for energy conservation + LambertMaterial(const Vector3& color = Vector3(0.7f, 0.7f, 0.7f)) + : Material(color, MaterialType::Lambert) { + // Automatically clamp parameters to physically valid ranges + clamp_to_valid_ranges(); + } // Lambert BRDF evaluation: computes reflectance for given incident/outgoing directions // Mathematical formula: f_r(wi, wo) = ρ/π where ρ is albedo (base_color) @@ -79,7 +82,7 @@ class LambertMaterial { // wi: incident light direction (pointing toward surface, normalized) // wo: outgoing view direction (pointing toward camera, normalized) // normal: surface normal at intersection point (outward-pointing, normalized) - Vector3 evaluate_brdf(const Vector3& wi, const Vector3& wo, const Vector3& normal, bool verbose = true) const { + Vector3 evaluate_brdf(const Vector3& wi, const Vector3& wo, const Vector3& normal, bool verbose = true) const override { if (verbose) { std::cout << "\n=== Lambert BRDF Evaluation ===" << std::endl; std::cout << "Incident direction (wi): (" << wi.x << ", " << wi.y << ", " << wi.z << ")" << std::endl; @@ -105,6 +108,24 @@ class LambertMaterial { return brdf_value; } + // Lambert-specific parameter validation implementation + // Validates that albedo values are within physically valid [0,1] range for energy conservation + bool validate_parameters() const override { + bool red_valid = (base_color.x >= 0.0f && base_color.x <= 1.0f); + bool green_valid = (base_color.y >= 0.0f && base_color.y <= 1.0f); + bool blue_valid = (base_color.z >= 0.0f && base_color.z <= 1.0f); + + return red_valid && green_valid && blue_valid; + } + + // Lambert-specific parameter clamping implementation + // Automatically clamps albedo values to [0,1] range to ensure energy conservation + void clamp_to_valid_ranges() override { + base_color.x = std::max(0.0f, std::min(1.0f, base_color.x)); + base_color.y = std::max(0.0f, std::min(1.0f, base_color.y)); + base_color.z = std::max(0.0f, std::min(1.0f, base_color.z)); + } + // Calculate Lambert scattering with n·l term for light transport equation // Full rendering equation component: L_o = f_r * L_i * cos(θ_i) * dω // cos(θ_i) = max(0, normal · light_direction) ensures no negative contribution @@ -115,7 +136,7 @@ class LambertMaterial { // normal: surface normal at intersection point (outward-pointing, normalized) // incident_radiance: incoming light energy (L_i in rendering equation) Vector3 scatter_light(const Vector3& light_direction, const Vector3& view_direction, - const Vector3& normal, const Vector3& incident_radiance, bool verbose = true) const { + const Vector3& normal, const Vector3& incident_radiance, bool verbose = true) const override { if (verbose) { std::cout << "\n=== Lambert Light Scattering Calculation ===" << std::endl; std::cout << "Light direction: (" << light_direction.x << ", " << light_direction.y << ", " << light_direction.z << ")" << std::endl; @@ -192,6 +213,56 @@ class LambertMaterial { return is_energy_conserving; } + // Lambert-specific educational BRDF explanation override + // Provides detailed mathematical breakdown of Lambert diffuse reflection theory + void explain_brdf_evaluation(const Vector3& wi, const Vector3& wo, const Vector3& normal) const override { + std::cout << "\n=======================================================" << std::endl; + std::cout << "=== LAMBERT BRDF EDUCATIONAL BREAKDOWN ===" << std::endl; + std::cout << "=======================================================" << std::endl; + std::cout << "\nTHEORETICAL FOUNDATION:" << std::endl; + std::cout << "Lambert BRDF models perfectly diffuse reflection following Lambert's cosine law." << std::endl; + std::cout << "Physical principle: light scattered equally in all directions over hemisphere." << std::endl; + std::cout << "Mathematical foundation: BRDF is constant for all direction pairs." << std::endl; + std::cout << "\nBRDF Formula: f_r(wi,wo) = ρ/π" << std::endl; + std::cout << "Where:" << std::endl; + std::cout << "• ρ (albedo): fraction of light reflected per wavelength" << std::endl; + std::cout << "• π: normalization factor ensuring energy conservation" << std::endl; + std::cout << "\nPhysical Interpretation:" << std::endl; + std::cout << "• Models matte surfaces: chalk, unpolished wood, paper, fabric" << std::endl; + std::cout << "• Microscopic surface roughness causes isotropic scattering" << std::endl; + std::cout << "• Viewing angle independent brightness (unlike metals/glossy surfaces)" << std::endl; + + // Demonstrate actual calculation + Vector3 result = evaluate_brdf(wi, wo, normal, false); + std::cout << "\n=== LIVE CALCULATION DEMONSTRATION ===" << std::endl; + std::cout << "Current albedo ρ: (" << base_color.x << ", " << base_color.y << ", " << base_color.z << ")" << std::endl; + std::cout << "Lambert BRDF value: ρ/π = (" << result.x << ", " << result.y << ", " << result.z << ")" << std::endl; + + std::cout << "\n=== ENERGY CONSERVATION ANALYSIS ===" << std::endl; + bool energy_valid = validate_parameters(); + if (energy_valid) { + std::cout << "✓ Albedo values within [0,1] range - energy conservation maintained" << std::endl; + std::cout << "✓ Material cannot reflect more energy than received (physically plausible)" << std::endl; + } else { + std::cout << "⚠ Albedo values outside [0,1] range - potential energy conservation violation" << std::endl; + std::cout << "⚠ Material might amplify light (non-physical behavior)" << std::endl; + } + + std::cout << "\n=== LAMBERT vs OTHER MATERIALS ===" << std::endl; + std::cout << "Comparison with other BRDF models:" << std::endl; + std::cout << "• Lambert: Constant f_r, viewing angle independent" << std::endl; + std::cout << "• Cook-Torrance: Variable f_r with viewing angle, specular highlights" << std::endl; + std::cout << "• Phong: Empirical model, not physically based" << std::endl; + std::cout << "\nLambert limitations:" << std::endl; + std::cout << "• No specular highlights or reflections" << std::endl; + std::cout << "• Cannot model glossy or metallic surfaces" << std::endl; + std::cout << "• Pure diffuse assumption rarely matches real materials exactly" << std::endl; + + std::cout << "\n=======================================================" << std::endl; + std::cout << "=== LAMBERT EDUCATIONAL BREAKDOWN COMPLETE ===" << std::endl; + std::cout << "=======================================================" << std::endl; + } + // Calculate hemispherical reflectance (total fraction of light reflected) // For Lambert BRDF: R = ρ (albedo value) // Physical interpretation: what fraction of incident light is scattered back diff --git a/src/materials/material_base.hpp b/src/materials/material_base.hpp new file mode 100644 index 0000000..551f68b --- /dev/null +++ b/src/materials/material_base.hpp @@ -0,0 +1,133 @@ +#pragma once +#include "../core/vector3.hpp" +#include +#include + +// Material Type enumeration for polymorphic material system +// Supports current Lambert and Cook-Torrance materials with extensibility for future OpenPBR +enum class MaterialType { + Lambert, // Perfectly diffuse material following Lambert's cosine law + CookTorrance, // Microfacet material using Cook-Torrance BRDF with D, G, F terms + OpenPBR // Future: Complete OpenPBR material model (Epic 4) +}; + +// Abstract Material base class for polymorphic material evaluation +// Provides common interface for all material types while maintaining educational transparency +// Design principles: +// - Virtual BRDF evaluation allows different material mathematical models +// - Educational debugging methods preserve learning-focused console output +// - Parameter validation ensures physically plausible material properties +// - Clean polymorphic interface supports future material system extensions +class Material { +public: + Vector3 base_color; // Surface albedo/reflectance color (RGB channels) + MaterialType type; // Material type identifier for polymorphic behavior + + // Constructor with base color and material type + // Parameters: + // color: surface base color/albedo (default: neutral gray) + // mat_type: material type for polymorphic identification + Material(const Vector3& color = Vector3(0.7f, 0.7f, 0.7f), MaterialType mat_type = MaterialType::Lambert) + : base_color(color), type(mat_type) {} + + // Virtual destructor ensures proper polymorphic cleanup + virtual ~Material() = default; + + // Pure virtual BRDF evaluation - implemented by concrete material classes + // Each material type provides its own mathematical model: + // - Lambert: f_r = ρ/π (constant diffuse reflection) + // - Cook-Torrance: f_r = (D×G×F)/(4×cos(θl)×cos(θv)) (microfacet theory) + // - Future OpenPBR: Advanced layered material model + // Parameters: + // wi: incident light direction (pointing toward surface, normalized) + // wo: outgoing view direction (pointing toward camera, normalized) + // normal: surface normal at intersection point (outward-pointing, normalized) + // verbose: enable educational mathematical breakdown output + virtual Vector3 evaluate_brdf(const Vector3& wi, const Vector3& wo, const Vector3& normal, bool verbose = true) const = 0; + + // Pure virtual parameter validation - implemented by concrete material classes + // Each material type defines its own physically valid parameter ranges: + // - Lambert: albedo ∈ [0,1] for energy conservation + // - Cook-Torrance: roughness ∈ [0.01,1], metallic ∈ [0,1], specular ∈ [0,1] + // - Future materials: specific parameter constraints per material model + virtual bool validate_parameters() const = 0; + + // Pure virtual parameter clamping - implemented by concrete material classes + // Automatically adjusts material parameters to physically valid ranges + // Prevents numerical instability and non-physical behavior + // Educational output shows any clamping performed for learning purposes + virtual void clamp_to_valid_ranges() = 0; + + // Virtual educational debugging method - optionally overridden by concrete materials + // Provides comprehensive mathematical breakdown of BRDF evaluation process + // Default implementation provides basic information, specific materials add detailed explanations + // Parameters: same as evaluate_brdf for direct educational comparison + virtual void explain_brdf_evaluation(const Vector3& wi, const Vector3& wo, const Vector3& normal) const { + std::cout << "\n=== Generic Material BRDF Evaluation ===" << std::endl; + std::cout << "Material Type: " << material_type_name() << std::endl; + std::cout << "Base Color: (" << base_color.x << ", " << base_color.y << ", " << base_color.z << ")" << std::endl; + std::cout << "Incident direction (wi): (" << wi.x << ", " << wi.y << ", " << wi.z << ")" << std::endl; + std::cout << "Outgoing direction (wo): (" << wo.x << ", " << wo.y << ", " << wo.z << ")" << std::endl; + std::cout << "Surface normal: (" << normal.x << ", " << normal.y << ", " << normal.z << ")" << std::endl; + std::cout << "Note: Override explain_brdf_evaluation() in concrete material for detailed mathematical breakdown" << std::endl; + std::cout << "=== Generic evaluation explanation complete ===" << std::endl; + } + + // Virtual light scattering calculation with full rendering equation + // Default implementation uses BRDF evaluation with cosine term and incident radiance + // Can be overridden by specific materials for optimized or specialized calculations + // Parameters: + // light_direction: direction from surface to light source (normalized) + // view_direction: direction from surface to camera (normalized) + // normal: surface normal at intersection point (outward-pointing, normalized) + // incident_radiance: incoming light energy (L_i in rendering equation) + // verbose: enable educational console output + virtual Vector3 scatter_light(const Vector3& light_direction, const Vector3& view_direction, + const Vector3& normal, const Vector3& incident_radiance, bool verbose = true) const { + if (verbose) { + std::cout << "\n=== Generic Material Light Scattering ===" << std::endl; + std::cout << "Material Type: " << material_type_name() << std::endl; + } + + // Calculate cosine term: n·l (normal dot light_direction) + float cos_theta = normal.dot(light_direction); + if (verbose) { + std::cout << "Raw cosine term n·l = " << cos_theta << std::endl; + } + + // Clamp to positive values: surfaces don't emit light when facing away + cos_theta = std::max(0.0f, cos_theta); + if (verbose) { + std::cout << "Clamped cosine term max(0, n·l) = " << cos_theta << std::endl; + } + + // Evaluate material-specific BRDF + Vector3 brdf = evaluate_brdf(light_direction, view_direction, normal, verbose); + + // Full rendering equation evaluation: L_o = f_r * L_i * cos(θ) + Vector3 outgoing_radiance = Vector3( + brdf.x * incident_radiance.x * cos_theta, // Red channel + brdf.y * incident_radiance.y * cos_theta, // Green channel + brdf.z * incident_radiance.z * cos_theta // Blue channel + ); + + if (verbose) { + std::cout << "Final outgoing radiance: L_o = f_r * L_i * cos(θ) = (" + << outgoing_radiance.x << ", " << outgoing_radiance.y << ", " << outgoing_radiance.z << ")" << std::endl; + std::cout << "=== Generic light scattering calculation complete ===" << std::endl; + } + + return outgoing_radiance; + } + + // Helper method to get human-readable material type name for educational output + // Provides consistent naming across different material implementations + const char* material_type_name() const { + switch (type) { + case MaterialType::Lambert: return "Lambert (Diffuse)"; + case MaterialType::CookTorrance: return "Cook-Torrance (Microfacet)"; + case MaterialType::OpenPBR: return "OpenPBR (Advanced PBR)"; + default: return "Unknown Material Type"; + } + } +}; \ No newline at end of file diff --git a/tests/test_math_correctness.cpp b/tests/test_math_correctness.cpp index 454780f..4a44b31 100644 --- a/tests/test_math_correctness.cpp +++ b/tests/test_math_correctness.cpp @@ -2,17 +2,18 @@ #include #include #include -#include "src/core/vector3.hpp" -#include "src/core/point3.hpp" -#include "src/core/ray.hpp" -#include "src/core/sphere.hpp" -#include "src/core/scene.hpp" -#include "src/core/scene_loader.hpp" -#include "src/core/point_light.hpp" -#include "src/core/camera.hpp" -#include "src/core/image.hpp" -#include "src/materials/lambert.hpp" -#include "src/materials/cook_torrance.hpp" +#include "../src/core/vector3.hpp" +#include "../src/core/point3.hpp" +#include "../src/core/ray.hpp" +#include "../src/core/sphere.hpp" +#include "../src/core/scene.hpp" +#include "../src/core/scene_loader.hpp" +#include "../src/core/point_light.hpp" +#include "../src/core/camera.hpp" +#include "../src/core/image.hpp" +#include "../src/materials/lambert.hpp" +#include "../src/materials/cook_torrance.hpp" +#include "../src/materials/material_base.hpp" namespace MathematicalTests { @@ -412,7 +413,8 @@ namespace MathematicalTests { // Test Case 4: Invalid albedo values (> 1.0) for energy conservation test std::cout << "Test 4: Energy conservation violation detection..." << std::endl; - LambertMaterial invalid_material(Vector3(1.5f, 0.8f, 0.6f)); // Red > 1.0 + LambertMaterial invalid_material(Vector3(0.5f, 0.8f, 0.6f)); // Valid construction + invalid_material.base_color = Vector3(1.5f, 0.8f, 0.6f); // Manually set invalid albedo assert(!invalid_material.validate_energy_conservation()); std::cout << " Energy conservation violation detected: PASS" << std::endl; @@ -953,8 +955,10 @@ namespace MathematicalTests { std::cout << " Testing sphere validation..." << std::endl; assert(test_sphere.validate_geometry()); // Valid sphere - Sphere invalid_sphere(Point3(0, 0, 0), -1.0f, 0); // Negative radius - assert(!invalid_sphere.validate_geometry()); // Should be invalid + // Test validation by creating sphere and manually setting invalid radius after construction + Sphere test_validation_sphere(Point3(0, 0, 0), 1.0f, 0); // Valid construction + test_validation_sphere.radius = -1.0f; // Manually set invalid radius + assert(!test_validation_sphere.validate_geometry()); // Should now be invalid std::cout << " Ray-sphere intersection mathematics: PASSED" << std::endl; return true; @@ -970,7 +974,8 @@ namespace MathematicalTests { // Test Case 2: Invalid albedo (energy violating) std::cout << " Testing invalid albedo..." << std::endl; - LambertMaterial invalid_material(Vector3(1.5f, 0.5f, 0.5f)); // Red > 1.0 + LambertMaterial invalid_material(Vector3(0.5f, 0.5f, 0.5f)); // Valid construction + invalid_material.base_color = Vector3(1.5f, 0.5f, 0.5f); // Manually set invalid albedo assert(!invalid_material.validate_energy_conservation()); // Test Case 3: BRDF mathematical correctness @@ -1455,10 +1460,18 @@ sphere -1.0 1.0 -4.5 0.6 blue_mat assert(loaded_scene.materials.size() == 3); assert(loaded_scene.primitives.size() == 3); - // Verify material colors - assert(std::abs(loaded_scene.materials[0].base_color.x - 0.8f) < 1e-5f); // Red material - assert(std::abs(loaded_scene.materials[1].base_color.y - 0.8f) < 1e-5f); // Green material - assert(std::abs(loaded_scene.materials[2].base_color.z - 0.8f) < 1e-5f); // Blue material + // Verify material colors using dynamic_cast + auto* red_material = dynamic_cast(loaded_scene.materials[0].get()); + auto* green_material = dynamic_cast(loaded_scene.materials[1].get()); + auto* blue_material = dynamic_cast(loaded_scene.materials[2].get()); + + assert(red_material != nullptr); + assert(green_material != nullptr); + assert(blue_material != nullptr); + + assert(std::abs(red_material->base_color.x - 0.8f) < 1e-5f); // Red material + assert(std::abs(green_material->base_color.y - 0.8f) < 1e-5f); // Green material + assert(std::abs(blue_material->base_color.z - 0.8f) < 1e-5f); // Blue material // Verify sphere positions and materials Sphere& first_sphere = loaded_scene.primitives[0]; @@ -1560,7 +1573,8 @@ sphere 0 0 0 -1.0 nonexistent_material # Negative radius, invalid material int mat_idx = validation_scene.add_material(valid_material); // Try to add sphere with negative radius - Sphere invalid_sphere(Point3(0, 0, -5), -1.0f, mat_idx); + Sphere invalid_sphere(Point3(0, 0, -5), 1.0f, mat_idx); // Valid construction + invalid_sphere.radius = -1.0f; // Manually set invalid radius int invalid_result = validation_scene.add_sphere(invalid_sphere); assert(invalid_result == -1); // Should reject invalid sphere assert(validation_scene.primitives.size() == 0); @@ -1578,7 +1592,8 @@ sphere 0 0 0 -1.0 nonexistent_material # Negative radius, invalid material LambertMaterial valid_energy_material(Vector3(0.9f, 0.9f, 0.9f)); assert(valid_energy_material.validate_energy_conservation()); - LambertMaterial invalid_energy_material(Vector3(1.5f, 0.8f, 0.6f)); + LambertMaterial invalid_energy_material(Vector3(0.5f, 0.8f, 0.6f)); // Valid construction + invalid_energy_material.base_color = Vector3(1.5f, 0.8f, 0.6f); // Manually set invalid albedo assert(!invalid_energy_material.validate_energy_conservation()); // Scene should accept materials regardless of energy conservation for educational purposes @@ -2013,6 +2028,243 @@ sphere 0 0 0 -1.0 nonexistent_material # Negative radius, invalid material return test1_pass && test2_pass; } + // === STORY 3.5: POLYMORPHIC MATERIAL INTEGRATION TESTS === + + bool test_polymorphic_material_creation_and_validation() { + std::cout << "\n=== Polymorphic Material Creation and Validation Test ===" << std::endl; + + // Test 1: Lambert material through polymorphic interface + std::cout << "Test 1: Lambert material polymorphic creation..." << std::endl; + auto lambert_material = std::make_unique(Vector3(0.8f, 0.6f, 0.4f)); + + // Validate polymorphic interface + assert(lambert_material->type == MaterialType::Lambert); + assert(lambert_material->validate_parameters()); + std::cout << " Lambert material type: " << lambert_material->material_type_name() << std::endl; + std::cout << " Base color: (" << lambert_material->base_color.x << ", " + << lambert_material->base_color.y << ", " << lambert_material->base_color.z << ")" << std::endl; + + // Test 2: Cook-Torrance material through polymorphic interface + std::cout << "Test 2: Cook-Torrance material polymorphic creation..." << std::endl; + auto ct_material = std::make_unique(Vector3(0.9f, 0.7f, 0.3f), 0.3f, 1.0f, 0.04f); + + // Validate polymorphic interface + assert(ct_material->type == MaterialType::CookTorrance); + assert(ct_material->validate_parameters()); + std::cout << " Cook-Torrance material type: " << ct_material->material_type_name() << std::endl; + std::cout << " Roughness: " << ct_material->roughness << ", Metallic: " << ct_material->metallic << std::endl; + + // Test 3: Parameter clamping validation + std::cout << "Test 3: Parameter clamping validation..." << std::endl; + auto invalid_material = std::make_unique(Vector3(1.5f, -0.2f, 0.8f), 2.0f, 1.5f, -0.1f); + + // Parameters should be automatically clamped during construction + assert(invalid_material->base_color.x <= 1.0f); + assert(invalid_material->base_color.y >= 0.0f); + assert(invalid_material->roughness >= 0.01f && invalid_material->roughness <= 1.0f); + assert(invalid_material->metallic >= 0.0f && invalid_material->metallic <= 1.0f); + assert(invalid_material->specular >= 0.0f && invalid_material->specular <= 1.0f); + + std::cout << " Parameters successfully clamped to valid ranges" << std::endl; + + std::cout << "Polymorphic material creation test: PASS" << std::endl; + return true; + } + + bool test_polymorphic_brdf_evaluation() { + std::cout << "\n=== Polymorphic BRDF Evaluation Test ===" << std::endl; + + // Setup common test vectors + Vector3 incident_dir = Vector3(0.707f, 0.707f, 0.0f).normalize(); // 45-degree incident + Vector3 outgoing_dir = Vector3(0.0f, 0.0f, 1.0f); // View along normal + Vector3 surface_normal = Vector3(0.0f, 0.0f, 1.0f); // Up-facing normal + + std::cout << "Common test setup:" << std::endl; + std::cout << " Incident direction: (" << incident_dir.x << ", " << incident_dir.y << ", " << incident_dir.z << ")" << std::endl; + std::cout << " Outgoing direction: (" << outgoing_dir.x << ", " << outgoing_dir.y << ", " << outgoing_dir.z << ")" << std::endl; + std::cout << " Surface normal: (" << surface_normal.x << ", " << surface_normal.y << ", " << surface_normal.z << ")" << std::endl; + + // Test 1: Lambert BRDF evaluation through polymorphic interface + std::cout << "Test 1: Lambert BRDF polymorphic evaluation..." << std::endl; + Material* lambert_ptr = new LambertMaterial(Vector3(0.7f, 0.7f, 0.7f)); + Vector3 lambert_brdf = lambert_ptr->evaluate_brdf(incident_dir, outgoing_dir, surface_normal, false); + + // Lambert BRDF should be constant: base_color / π + float expected_lambert = 0.7f / M_PI; + assert(std::abs(lambert_brdf.x - expected_lambert) < 1e-6); + assert(std::abs(lambert_brdf.y - expected_lambert) < 1e-6); + assert(std::abs(lambert_brdf.z - expected_lambert) < 1e-6); + + std::cout << " Lambert BRDF result: (" << lambert_brdf.x << ", " << lambert_brdf.y << ", " << lambert_brdf.z << ")" << std::endl; + std::cout << " Expected: " << expected_lambert << " (base_color/π)" << std::endl; + + // Test 2: Cook-Torrance BRDF evaluation through polymorphic interface + std::cout << "Test 2: Cook-Torrance BRDF polymorphic evaluation..." << std::endl; + Material* ct_ptr = new CookTorranceMaterial(Vector3(0.8f, 0.8f, 0.8f), 0.3f, 0.0f, 0.04f); + Vector3 ct_brdf = ct_ptr->evaluate_brdf(incident_dir, outgoing_dir, surface_normal, false); + + // Cook-Torrance should give different result than Lambert + assert(std::abs(ct_brdf.x - lambert_brdf.x) > 1e-6); // Should be noticeably different + assert(ct_brdf.x >= 0 && ct_brdf.y >= 0 && ct_brdf.z >= 0); // Non-negative + assert(std::isfinite(ct_brdf.x) && std::isfinite(ct_brdf.y) && std::isfinite(ct_brdf.z)); // Finite + + std::cout << " Cook-Torrance BRDF result: (" << ct_brdf.x << ", " << ct_brdf.y << ", " << ct_brdf.z << ")" << std::endl; + std::cout << " Validation: Non-negative, finite, different from Lambert" << std::endl; + + // Test 3: Energy conservation verification + std::cout << "Test 3: Energy conservation verification..." << std::endl; + + // For both materials, BRDF should not violate energy conservation + // This is a simplified check - full hemisphere integration would be more complete + float max_lambert_component = std::max({lambert_brdf.x, lambert_brdf.y, lambert_brdf.z}); + float max_ct_component = std::max({ct_brdf.x, ct_brdf.y, ct_brdf.z}); + + // Lambert: ρ/π where ρ ≤ 1, so BRDF ≤ 1/π ≈ 0.318 + assert(max_lambert_component <= (1.0f / M_PI) + 1e-6); + + // Cook-Torrance: More complex, but should still be reasonable for these parameters + assert(max_ct_component <= 10.0f); // Generous upper bound for this test case + + std::cout << " Lambert max component: " << max_lambert_component << " (limit: 1/π ≈ 0.318)" << std::endl; + std::cout << " Cook-Torrance max component: " << max_ct_component << " (reasonable range)" << std::endl; + + delete lambert_ptr; + delete ct_ptr; + + std::cout << "Polymorphic BRDF evaluation test: PASS" << std::endl; + return true; + } + + bool test_multi_material_scene_loading() { + std::cout << "\n=== Multi-Material Scene Loading Test ===" << std::endl; + + // Test scene content with mixed Lambert and Cook-Torrance materials + std::string multi_material_scene = R"( +# Multi-material test scene +# Educational test of polymorphic material parsing + +# Lambert materials +material_lambert diffuse_red 0.8 0.3 0.3 +material_lambert diffuse_blue 0.3 0.3 0.8 + +# Cook-Torrance materials +material_cook_torrance gold_rough 1.0 0.8 0.3 0.8 1.0 0.04 +material_cook_torrance plastic_smooth 0.2 0.8 0.4 0.1 0.0 0.04 + +# Legacy Lambert material (backward compatibility) +material legacy_green 0.3 0.8 0.3 + +# Spheres using different material types +sphere -2.0 0.0 -5.0 1.0 diffuse_red +sphere -1.0 0.0 -5.0 1.0 gold_rough +sphere 0.0 0.0 -5.0 1.0 plastic_smooth +sphere 1.0 0.0 -5.0 1.0 diffuse_blue +sphere 2.0 0.0 -5.0 1.0 legacy_green +)"; + + std::cout << "Loading multi-material scene..." << std::endl; + Scene scene = SceneLoader::load_from_string(multi_material_scene); + + // Validate scene loaded correctly + assert(scene.materials.size() == 5); // 5 materials total + assert(scene.primitives.size() == 5); // 5 spheres total + + std::cout << " Materials loaded: " << scene.materials.size() << std::endl; + std::cout << " Primitives loaded: " << scene.primitives.size() << std::endl; + + // Validate material types are correct + assert(scene.materials[0]->type == MaterialType::Lambert); // diffuse_red + assert(scene.materials[1]->type == MaterialType::Lambert); // diffuse_blue + assert(scene.materials[2]->type == MaterialType::CookTorrance); // gold_rough + assert(scene.materials[3]->type == MaterialType::CookTorrance); // plastic_smooth + assert(scene.materials[4]->type == MaterialType::Lambert); // legacy_green + + std::cout << " Material types validated: 3 Lambert, 2 Cook-Torrance" << std::endl; + + // Test intersection with multi-material scene + Ray test_ray(Point3(0.0f, 0.0f, 0.0f), Vector3(0.0f, 0.0f, -1.0f)); + Scene::Intersection hit = scene.intersect(test_ray, false); + + assert(hit.hit); + assert(hit.material != nullptr); + assert(hit.primitive != nullptr); + + std::cout << " Ray intersection test: HIT" << std::endl; + std::cout << " Hit material type: " << hit.material->material_type_name() << std::endl; + + std::cout << "Multi-material scene loading test: PASS" << std::endl; + return true; + } + + bool test_scene_material_brdf_integration() { + std::cout << "\n=== Scene Material BRDF Integration Test ===" << std::endl; + + // Create scene with both material types + Scene scene; + + // Add Lambert material + auto lambert_mat = std::make_unique(Vector3(0.7f, 0.5f, 0.3f)); + int lambert_idx = scene.add_material(std::move(lambert_mat)); + + // Add Cook-Torrance material + auto ct_mat = std::make_unique(Vector3(0.9f, 0.7f, 0.3f), 0.2f, 1.0f, 0.04f); + int ct_idx = scene.add_material(std::move(ct_mat)); + + // Add spheres with different materials + Sphere lambert_sphere(Point3(-1.0f, 0.0f, -5.0f), 1.0f, lambert_idx); + Sphere ct_sphere(Point3(1.0f, 0.0f, -5.0f), 1.0f, ct_idx); + + scene.add_sphere(lambert_sphere); + scene.add_sphere(ct_sphere); + + std::cout << "Scene setup: 2 materials, 2 spheres" << std::endl; + + // Test intersection and BRDF evaluation integration + Ray lambert_ray(Point3(-1.0f, 0.0f, 0.0f), Vector3(0.0f, 0.0f, -1.0f)); + Scene::Intersection lambert_hit = scene.intersect(lambert_ray, false); + + assert(lambert_hit.hit); + assert(lambert_hit.material->type == MaterialType::Lambert); + + Ray ct_ray(Point3(1.0f, 0.0f, 0.0f), Vector3(0.0f, 0.0f, -1.0f)); + Scene::Intersection ct_hit = scene.intersect(ct_ray, false); + + assert(ct_hit.hit); + assert(ct_hit.material->type == MaterialType::CookTorrance); + + // Test BRDF evaluation through scene intersection results + Vector3 incident = Vector3(0.0f, 0.707f, -0.707f).normalize(); + Vector3 outgoing = Vector3(0.0f, 0.0f, 1.0f); + + Vector3 lambert_brdf = lambert_hit.material->evaluate_brdf(incident, outgoing, lambert_hit.normal, false); + Vector3 ct_brdf = ct_hit.material->evaluate_brdf(incident, outgoing, ct_hit.normal, false); + + // Validate BRDF results are reasonable + assert(lambert_brdf.x >= 0 && lambert_brdf.y >= 0 && lambert_brdf.z >= 0); + assert(ct_brdf.x >= 0 && ct_brdf.y >= 0 && ct_brdf.z >= 0); + assert(std::isfinite(lambert_brdf.x) && std::isfinite(ct_brdf.x)); + + std::cout << " Lambert sphere intersection: HIT, BRDF evaluated" << std::endl; + std::cout << " Cook-Torrance sphere intersection: HIT, BRDF evaluated" << std::endl; + std::cout << " BRDF values: Lambert(" << lambert_brdf.x << "), Cook-Torrance(" << ct_brdf.x << ")" << std::endl; + + // Test light scattering integration + Vector3 incident_light = incident; + Vector3 view_dir = outgoing; + Vector3 incident_radiance(1.0f, 1.0f, 1.0f); + + Vector3 lambert_scattered = lambert_hit.material->scatter_light(incident_light, view_dir, lambert_hit.normal, incident_radiance, false); + Vector3 ct_scattered = ct_hit.material->scatter_light(incident_light, view_dir, ct_hit.normal, incident_radiance, false); + + assert(lambert_scattered.x >= 0 && ct_scattered.x >= 0); + assert(std::isfinite(lambert_scattered.x) && std::isfinite(ct_scattered.x)); + + std::cout << " Light scattering: Both materials computed successfully" << std::endl; + + std::cout << "Scene material BRDF integration test: PASS" << std::endl; + return true; + } + } // namespace MathematicalTests int main() { @@ -2087,11 +2339,19 @@ int main() { all_passed &= MathematicalTests::test_cook_torrance_energy_conservation(); all_passed &= MathematicalTests::test_cook_torrance_brdf_evaluation(); + // Story 3.5: Polymorphic Material System Integration Tests + std::cout << "\n=== STORY 3.5: POLYMORPHIC MATERIAL SYSTEM TESTS ===" << std::endl; + all_passed &= MathematicalTests::test_polymorphic_material_creation_and_validation(); + all_passed &= MathematicalTests::test_polymorphic_brdf_evaluation(); + all_passed &= MathematicalTests::test_multi_material_scene_loading(); + all_passed &= MathematicalTests::test_scene_material_brdf_integration(); + if (all_passed) { std::cout << "\n✅ ALL MATHEMATICAL TESTS PASSED" << std::endl; std::cout << "Mathematical foundation verified for Epic 1 & 3 development." << std::endl; std::cout << "Story 1.3: Single-Ray Lambert BRDF Implementation - VALIDATED" << std::endl; std::cout << "Story 3.1: Pure Cook-Torrance BRDF Implementation - VALIDATED" << std::endl; + std::cout << "Story 3.5: Polymorphic Material System Integration - VALIDATED" << std::endl; return 0; } else { std::cout << "\n❌ SOME TESTS FAILED" << std::endl;