Skip to content

Daily Log - December 5, 2025

Summary

Fixed critical material rendering issues and implemented glass shader support. Key focus: preventing generic color parameter misuse and properly handling procedural shaders.

Issues Fixed

1. Generic Color Parameter Misuse (Soraka Skin16)

Problem: Soraka's body texture was incorrectly tinted dark red due to a generic "Color" parameter being applied as base tint.

Root Cause:

  • DefaultProfile.ColorTintParam = "Color" was too generic
  • Many shaders use "Color" for VFX effects (bloom, scrolling), not base tinting
  • When BloomStrength was 0 (because shader used BloomIntensity not Bloom_Strength), the generic "Color" param got applied as tint

Solution:

  1. Removed ColorTintParam = "Color" from DefaultProfile
  2. Made color tint resolution strict: only apply if shader profile explicitly defines ColorTintParam
  3. Added BloomIntensity as fallback parameter name for BloomStrength

Files Changed:

  • packages/extractor/LeagueConvert/IO/Skin/StaticMaterial.cs
    • Removed generic ColorTintParam from DefaultProfile
    • Made ResolveForShader() only use explicit profile ColorTintParam
    • Added BloomIntensity fallback in bloom resolution

Verification:

bash
# Extract Soraka skin16 - baseColorFactor should be "NOT SET" for all materials
dotnet run --project LeagueConvert.CommandLine -- convert-wad Soraka -o /tmp/test --skins 16

2. Glass Material Implementation (Renata Skin0)

Problem: Weapon_Glass and Canister_Glass materials were appearing black and opaque instead of transparent purple.

Root Cause:

  • Glass shader is procedural (no diffuse texture)
  • Initial fix skipped material creation if no texture found
  • Even with texture, material wasn't transparent

Solution:

  1. Added Shaders/SkinnedMesh/Glass shader profile with NoDiffuseTexture = true
  2. Generate 1x1 placeholder texture from Glass_Color1 parameter
  3. Extract all glass parameters (Glass_Roughness, Fresnel_Size_Inner, Fresnel_Size_Outer, Alpha_Bias, etc.)
  4. Set alphaMode: BLEND AND baseColorFactor alpha from Alpha_Bias
  5. Frontend converts to MeshPhysicalMaterial with transmission, IOR, thickness

Files Changed:

  • packages/extractor/LeagueConvert/IO/Skin/StaticMaterial.cs
    • Added NoDiffuseTexture to ShaderProfile
    • Added Glass shader profile
    • Added IsProceduralShader() method
    • Made GetShaderProfile() public
  • packages/extractor/LeagueConvert/IO/Skin/Extensions/SkinExtensions.cs
    • Generate 1x1 placeholder texture for procedural shaders
    • Extract and export GlassParams
    • Set isGlass flag in MaterialExtras
  • packages/extractor/LeagueConvert/IO/Skin/Skin.cs
    • Return null for procedural shaders in FindTexture()
  • packages/extractor/SimpleGltf/Json/Material.cs
    • Added GlassParams record to MaterialExtras
  • packages/web/src/engine/index.ts
    • Convert glass materials to MeshPhysicalMaterial with transmission

Verification:

bash
# Extract Renata skin0 - Weapon_Glass should have isGlass: true and glassParams
dotnet run --project LeagueConvert.CommandLine -- convert-wad Renata -o /tmp/test --skins 0

3. Flipbook Speed Extraction (Annie Skin31)

Problem: Annie's body flipbook wasn't animating - it's time-based auto-animated, not animation-synced.

Root Cause: Frontend only supported animation-synced flipbooks (driven by keyframes), not time-based ones.

Solution:

  1. Added FlipbookSpeed property to StaticMaterial
  2. Extract flipbookspeed or flipbookanimspeed parameters
  3. Export flipbookSpeed in MaterialExtras
  4. Frontend: Added autoFlipbookMaterials array and updateAutoFlipbooks() method
  5. Frontend: Pause check - don't animate when user pauses

Files Changed:

  • packages/extractor/LeagueConvert/IO/Skin/StaticMaterial.cs
    • Added FlipbookSpeedParam to ShaderProfile
    • Added FlipbookSpeed property
    • Extract in ResolveForShader()
  • packages/extractor/SimpleGltf/Json/Material.cs
    • Added FlipbookSpeed to MaterialExtras
  • packages/extractor/LeagueConvert/IO/Skin/Extensions/SkinExtensions.cs
    • Export flipbookSpeed in MaterialExtras
  • packages/web/src/engine/index.ts
    • Added autoFlipbookMaterials tracking
    • Added updateAutoFlipbooks() for time-based animations
    • Added pause check

Verification:

bash
# Extract Annie skin31 - Body should have flipbookSpeed: 6
dotnet run --project LeagueConvert.CommandLine -- convert-wad Annie -o /tmp/test --skins 31

4. Shared Texture Instance Bug (Milio Skin0)

Problem: Flipbook textures were cascading - getting progressively smaller as multiple materials shared the same texture instance.

Root Cause: Modifying texture.repeat on shared instances caused cascading changes.

Solution: Clone texture before modifying repeat or offset.

Files Changed:

  • packages/web/src/engine/index.ts
    • handleFlipbook(): Clone texture before modifying

Key Learnings

1. Generic Parameter Names Are Dangerous

Lesson: Never use generic parameter names like "Color" as fallbacks. They're used for too many different purposes (VFX, bloom, tinting) and will cause incorrect behavior.

Rule: Only apply color tinting from explicit shader profiles. Unknown shaders should NOT get tinting.

2. Procedural Shaders Need Special Handling

Lesson: Procedural shaders (like Glass) don't have diffuse textures. You must:

  • Generate a 1x1 placeholder texture from color parameters
  • Always create the material (don't skip it)
  • Use MagickColor.FromRgb() instead of MagickColor(r, g, b, a) to avoid quantum depth issues

Rule: Check IsProceduralShader() before texture lookup. Return null from FindTexture() for procedural shaders.

3. Transparency Requires Both alphaMode AND baseColorFactor

Lesson: Setting alphaMode: BLEND alone isn't enough. You must also set the alpha component in baseColorFactor.

Rule: For transparent materials, always set both:

csharp
material.AlphaMode = nameof(AlphaMode.BLEND);
pbrMetallicRoughness.baseColorFactor = new float[] { 1f, 1f, 1f, opacity };

4. Parameter Name Variations

Lesson: Different shaders use different parameter names for the same concept:

  • Bloom_Strength vs BloomIntensity vs Emissive_Strength
  • flipbookspeed vs flipbookanimspeed

Rule: Always check multiple parameter name variations with fallbacks.

5. Texture Instance Sharing

Lesson: Multiple materials can share the same texture instance. Modifying properties like repeat or offset affects all materials using that texture.

Rule: Always clone textures before modifying repeat or offset.

6. Shader Hash Resolution

Lesson: Shader names can be stored as:

  • String paths: "Shaders/SkinnedMesh/Glass"
  • Numeric hashes: "2653475696" (FNV1a-32 hash of path)

Rule: GetShaderProfile() must handle both formats. Hash 2653475696 = Shaders/SkinnedMesh/Glass.

7. TFT Companions Use Different Flipbook System

Lesson: TFT companions (like PetChibiLeeSin) don't use ClipAccessoryToRead for flipbooks. They use keyframe drivers (mDrivers) with direct animation hash → frame mappings.

Rule: Always check for mDrivers container (hash 1929323817) before assuming standard flipbook system. Convert animation hashes to names in the backend - never hash on the frontend.

8. Keep Hashing on Backend

Lesson: Animation name hashing should be done in the extractor, not the frontend. Export resolved animation names directly.

Rule: Use _clipHashToAnimationName mapping to convert hashes before export. Frontend should only do direct string lookups.


Code Patterns Established

Shader Profile Definition

csharp
["Shaders/SkinnedMesh/Glass"] = new ShaderProfile
{
    ColorTintParam = "Glass_Color1",
    BloomIntensityParam = "Light_Intensity",
    NoDiffuseTexture = true,
    HasBloom = true,
    GlassRoughnessParam = "Glass_Roughness",
    FresnelInnerParam = "Fresnel_Size_Inner",
    FresnelOuterParam = "Fresnel_Size_Outer",
    AlphaBiasParam = "Alpha_Bias"
}

Procedural Shader Texture Generation

csharp
if (magickImage == null && staticMaterialForTexture != null && 
    staticMaterialForTexture.IsProceduralShader())
{
    var profile = staticMaterialForTexture.GetShaderProfile();
    float[] colorValue = null;
    if (!string.IsNullOrEmpty(profile.ColorTintParam))
    {
        staticMaterialForTexture.RawParams.TryGetValue(
            profile.ColorTintParam.ToLowerInvariant(), out colorValue);
    }
    byte r = colorValue != null && colorValue.Length >= 3 
        ? (byte)(Math.Clamp(colorValue[0], 0f, 1f) * 255) : (byte)255;
    byte g = colorValue != null && colorValue.Length >= 3 
        ? (byte)(Math.Clamp(colorValue[1], 0f, 1f) * 255) : (byte)255;
    byte b = colorValue != null && colorValue.Length >= 3 
        ? (byte)(Math.Clamp(colorValue[2], 0f, 1f) * 255) : (byte)255;
    magickImage = new MagickImage(MagickColor.FromRgb(r, g, b), 1, 1);
}

Strict Color Tint Resolution

csharp
// Only apply color tint from shader profile - NO generic fallbacks
if (ColorTintBase == null && !string.IsNullOrEmpty(profile.ColorTintParam))
{
    ColorTintBase = TryGetColorTint(profile.ColorTintParam);
}

Keyframe Flipbook Detection (Backend)

csharp
// In ParseDynamicMaterialParameterDef
foreach (var driverProp in driverProps)
{
    switch (driverProp.NameHash)
    {
        case 1929323817: // mDrivers - keyframe animation drivers (TFT companions)
            hasKeyframeDrivers = true;
            if (driverProp is BinTreeContainer driversContainer)
            {
                ExtractKeyframeFlipbookMappings(driversContainer, staticMaterial);
            }
            break;
        
        case 3115772794: // mDefaultValue - default value for keyframe drivers
            if (driverProp is BinTreeStructure defaultStruct)
            {
                foreach (var defaultProp in defaultStruct.Properties)
                {
                    if (defaultProp.NameHash == 619900041 && defaultProp is BinTreeFloat defaultFloat)
                    {
                        defaultValue = defaultFloat.Value;
                    }
                }
            }
            break;
    }
}

Keyframe Flipbook Frontend Update

typescript
private updateKeyframeFlipbooks(): void {
  if (this.keyframeFlipbookMaterials.length === 0 || !$currentAnimation.get()) return;
  
  const animationName = $currentAnimation.get().getClip().name;
  
  for (const { material, columns, rows, keyframes, defaultFrame } of this.keyframeFlipbookMaterials) {
    // Direct name lookup - no hashing needed!
    const frameIndex = keyframes[animationName] ?? defaultFrame;
    this.setFlipbookFrame(material, frameIndex, columns, rows);
  }
}

Glass Material Frontend Conversion

typescript
if (userData.isGlass) {
    const params = userData.glassParams;
    const opacity = params?.alphaBias !== undefined 
        ? Math.max(0, 1.0 + params.alphaBias) : 0.8;
    const transmission = Math.max(0.0, Math.min(1.0, opacity * 0.75));
    const roughness = params?.glassRoughness ?? 0.1;
    const fresnelOuter = params?.fresnelOuter ?? 0.5;
    const ior = Math.max(1.0, 1.0 + fresnelOuter * 1.0);
    const fresnelInner = params?.fresnelInner ?? 50;
    const thickness = Math.min(2.0, fresnelInner / 50);

    const glassMaterial = new MeshPhysicalMaterial({
        color: originalMaterial.color,
        map: originalMaterial.map,
        transparent: true,
        opacity: opacity,
        transmission: transmission,
        roughness: roughness,
        metalness: 0.0,
        ior: ior,
        thickness: thickness,
        side: originalMaterial.side,
        depthWrite: false,
    });
    object.material = glassMaterial;
}

Testing Performed

Soraka Skin16

  • ✅ No incorrect baseColorFactor tinting
  • ✅ All materials have baseColorFactor: NOT SET

Renata Skin0

  • Weapon_Glass has isGlass: true
  • Weapon_Glass has glassParams with all parameters
  • Canister_Glass has isGlass: true
  • ✅ Glass materials have alphaMode: BLEND and baseColorFactor alpha

Annie Skin31

  • ✅ Body has flipbookSpeed: 6
  • ✅ All flipbook materials have flipbookSize and flipbookSpeed

Milio Skin0

  • ✅ Flipbook textures no longer cascade
  • ✅ Each material has its own cloned texture instance

PetChibiLeeSin Skin1 (TFT Companion)

  • ✅ Mouth has isKeyframeFlipbook: true
  • ✅ Mouth has flipbookKeyframes with animation name mappings
  • ✅ No animation hashes in output - all converted to names
  • flipbookFrameIndex: 7 (default frame from mDefaultValue)

Files Modified

Backend (C#)

  • packages/extractor/LeagueConvert/IO/Skin/StaticMaterial.cs
  • packages/extractor/LeagueConvert/IO/Skin/Extensions/SkinExtensions.cs
  • packages/extractor/LeagueConvert/IO/Skin/Skin.cs
  • packages/extractor/SimpleGltf/Json/Material.cs

Frontend (TypeScript)

  • packages/web/src/engine/index.ts

5. TFT Companion Keyframe Flipbooks (PetChibiLeeSin)

Problem: TFT companion flipbook animations (like Lee Sin's mouth) weren't syncing with animations. They use a different system than standard flipbooks.

Root Cause:

  • Standard flipbooks (Milio) use ClipAccessoryToRead which references animation clip accessories
  • TFT companions use keyframe drivers (mDrivers) with direct animation hash → frame mappings
  • No flipbookAccessoryHash was being extracted because the data structure is completely different

Investigation:

Standard flipbook (Milio):
  dynamicMaterial.parameters[].driver.ClipAccessoryToRead = {hash}
  
TFT flipbook (Lee Sin):
  dynamicMaterial.parameters[].driver = {
    mDrivers: [ { animHash: X, frameValue: Y }, ... ],
    mDefaultValue: { value: 7 }
  }

Solution:

  1. Detect keyframe-based flipbooks by checking for mDrivers container (hash 1929323817)
  2. Extract animation hash → frame mappings from each driver
  3. Extract default frame from mDefaultValue (hash 3115772794)
  4. Convert animation hashes to animation names using _clipHashToAnimationName mapping
  5. Export as isKeyframeFlipbook: true with flipbookKeyframes: { "Idle": 7, "Dance": 2 }
  6. Frontend looks up current animation name directly (no hashing needed)

Files Changed:

  • packages/extractor/LeagueConvert/IO/Skin/Skin.cs
    • Added ResolveAnimationHash() method
    • Added ConvertKeyframesToAnimationNames() method
    • Added ExtractKeyframeFlipbookMappings() to parse mDrivers container
    • Added ExtractFlipbookFrameValue() to handle simple floats and keyframe graphs
  • packages/extractor/LeagueConvert/IO/Skin/StaticMaterial.cs
    • Added FlipbookDefaultFrame property
    • Added IsKeyframeFlipbook property
    • Added FlipbookKeyframes dictionary (uint → float)
  • packages/extractor/LeagueConvert/IO/Skin/Extensions/SkinExtensions.cs
    • Convert hash-based keyframes to name-based before export
    • Prefer FlipbookDefaultFrame over static param for keyframe flipbooks
  • packages/extractor/SimpleGltf/Json/Material.cs
    • Added IsKeyframeFlipbook to MaterialExtras
    • Added FlipbookKeyframes as Dictionary<string, float> (animation name → frame)
  • packages/web/src/engine/index.ts
    • Added keyframeFlipbookMaterials array
    • Added updateKeyframeFlipbooks() method (direct name lookup, no hashing)
    • Added setFlipbookFrame() helper to reduce code duplication
    • Removed unused baseRepeatX/baseRepeatY from flipbook materials

Output Example:

json
{
  "flipbookSize": [4, 4],
  "flipbookFrameIndex": 7,
  "isKeyframeFlipbook": true,
  "flipbookKeyframes": {
    "Cast_Cycle": 2,
    "Damage_Hurt": 7,
    "Dance_In": 7.001106,
    "Dance_Loop": 7.001106,
    "Death": 7,
    "Joke": 7,
    "Taunt": 7,
    "Cast_Animation": 7,
    "Cast_Damage": 7
  }
}

Verification:

bash
# Extract TFT Lee Sin - Mouth should have isKeyframeFlipbook: true
dotnet run --project LeagueConvert.CommandLine -- convert-wad Companions --alias petchibileesin -o /tmp/test --skins 1

Pending Tasks

  1. Rumble_Shield Transparency: Still unresolved. Texture has alpha but no explicit blend data. Need to investigate further.

  2. Glass Emissive/Bloom: Implement SunColor and Light_Intensity for emissive/bloom effects on glass materials.

  3. Glass Color Gradient: Implement Glass_Color2 and Gradient_Bias for color blending on glass materials.

  4. Custom Glass Shader: Consider writing a custom Three.js shader for more accurate League glass rendering.


Notes for Future AIs

  1. Always check common-pitfalls.md before making material/shader changes
  2. Test with known problematic skins (Soraka 16, Renata 0, Annie 31, Milio 0, PetChibiLeeSin 1)
  3. Never use generic parameter names without explicit shader profiles
  4. Always clone textures before modifying repeat or offset
  5. Procedural shaders need placeholder textures - don't skip material creation
  6. Transparency needs both alphaMode AND baseColorFactor alpha
  7. TFT companions use keyframe flipbooks - check for mDrivers container, not ClipAccessoryToRead
  8. Keep hashing on backend - convert animation hashes to names before export, never hash on frontend

Built for engineers and AI assistants working on Khada.