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
BloomStrengthwas 0 (because shader usedBloomIntensitynotBloom_Strength), the generic "Color" param got applied as tint
Solution:
- Removed
ColorTintParam = "Color"fromDefaultProfile - Made color tint resolution strict: only apply if shader profile explicitly defines
ColorTintParam - Added
BloomIntensityas fallback parameter name forBloomStrength
Files Changed:
packages/extractor/LeagueConvert/IO/Skin/StaticMaterial.cs- Removed generic
ColorTintParamfromDefaultProfile - Made
ResolveForShader()only use explicit profileColorTintParam - Added
BloomIntensityfallback in bloom resolution
- Removed generic
Verification:
# Extract Soraka skin16 - baseColorFactor should be "NOT SET" for all materials
dotnet run --project LeagueConvert.CommandLine -- convert-wad Soraka -o /tmp/test --skins 162. 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:
- Added
Shaders/SkinnedMesh/Glassshader profile withNoDiffuseTexture = true - Generate 1x1 placeholder texture from
Glass_Color1parameter - Extract all glass parameters (
Glass_Roughness,Fresnel_Size_Inner,Fresnel_Size_Outer,Alpha_Bias, etc.) - Set
alphaMode: BLENDANDbaseColorFactoralpha fromAlpha_Bias - Frontend converts to
MeshPhysicalMaterialwith transmission, IOR, thickness
Files Changed:
packages/extractor/LeagueConvert/IO/Skin/StaticMaterial.cs- Added
NoDiffuseTexturetoShaderProfile - Added Glass shader profile
- Added
IsProceduralShader()method - Made
GetShaderProfile()public
- Added
packages/extractor/LeagueConvert/IO/Skin/Extensions/SkinExtensions.cs- Generate 1x1 placeholder texture for procedural shaders
- Extract and export
GlassParams - Set
isGlassflag inMaterialExtras
packages/extractor/LeagueConvert/IO/Skin/Skin.cs- Return
nullfor procedural shaders inFindTexture()
- Return
packages/extractor/SimpleGltf/Json/Material.cs- Added
GlassParamsrecord toMaterialExtras
- Added
packages/web/src/engine/index.ts- Convert glass materials to
MeshPhysicalMaterialwith transmission
- Convert glass materials to
Verification:
# Extract Renata skin0 - Weapon_Glass should have isGlass: true and glassParams
dotnet run --project LeagueConvert.CommandLine -- convert-wad Renata -o /tmp/test --skins 03. 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:
- Added
FlipbookSpeedproperty toStaticMaterial - Extract
flipbookspeedorflipbookanimspeedparameters - Export
flipbookSpeedinMaterialExtras - Frontend: Added
autoFlipbookMaterialsarray andupdateAutoFlipbooks()method - Frontend: Pause check - don't animate when user pauses
Files Changed:
packages/extractor/LeagueConvert/IO/Skin/StaticMaterial.cs- Added
FlipbookSpeedParamtoShaderProfile - Added
FlipbookSpeedproperty - Extract in
ResolveForShader()
- Added
packages/extractor/SimpleGltf/Json/Material.cs- Added
FlipbookSpeedtoMaterialExtras
- Added
packages/extractor/LeagueConvert/IO/Skin/Extensions/SkinExtensions.cs- Export
flipbookSpeedinMaterialExtras
- Export
packages/web/src/engine/index.ts- Added
autoFlipbookMaterialstracking - Added
updateAutoFlipbooks()for time-based animations - Added pause check
- Added
Verification:
# Extract Annie skin31 - Body should have flipbookSpeed: 6
dotnet run --project LeagueConvert.CommandLine -- convert-wad Annie -o /tmp/test --skins 314. 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.tshandleFlipbook(): 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 ofMagickColor(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:
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_StrengthvsBloomIntensityvsEmissive_Strengthflipbookspeedvsflipbookanimspeed
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
["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
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
// 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)
// 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
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
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
baseColorFactortinting - ✅ All materials have
baseColorFactor: NOT SET
Renata Skin0
- ✅
Weapon_GlasshasisGlass: true - ✅
Weapon_GlasshasglassParamswith all parameters - ✅
Canister_GlasshasisGlass: true - ✅ Glass materials have
alphaMode: BLENDandbaseColorFactoralpha
Annie Skin31
- ✅ Body has
flipbookSpeed: 6 - ✅ All flipbook materials have
flipbookSizeandflipbookSpeed
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
flipbookKeyframeswith 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.cspackages/extractor/LeagueConvert/IO/Skin/Extensions/SkinExtensions.cspackages/extractor/LeagueConvert/IO/Skin/Skin.cspackages/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
ClipAccessoryToReadwhich references animation clip accessories - TFT companions use keyframe drivers (
mDrivers) with direct animation hash → frame mappings - No
flipbookAccessoryHashwas 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:
- Detect keyframe-based flipbooks by checking for
mDriverscontainer (hash1929323817) - Extract animation hash → frame mappings from each driver
- Extract default frame from
mDefaultValue(hash3115772794) - Convert animation hashes to animation names using
_clipHashToAnimationNamemapping - Export as
isKeyframeFlipbook: truewithflipbookKeyframes: { "Idle": 7, "Dance": 2 } - 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
- Added
packages/extractor/LeagueConvert/IO/Skin/StaticMaterial.cs- Added
FlipbookDefaultFrameproperty - Added
IsKeyframeFlipbookproperty - Added
FlipbookKeyframesdictionary (uint → float)
- Added
packages/extractor/LeagueConvert/IO/Skin/Extensions/SkinExtensions.cs- Convert hash-based keyframes to name-based before export
- Prefer
FlipbookDefaultFrameover static param for keyframe flipbooks
packages/extractor/SimpleGltf/Json/Material.cs- Added
IsKeyframeFlipbooktoMaterialExtras - Added
FlipbookKeyframesasDictionary<string, float>(animation name → frame)
- Added
packages/web/src/engine/index.ts- Added
keyframeFlipbookMaterialsarray - Added
updateKeyframeFlipbooks()method (direct name lookup, no hashing) - Added
setFlipbookFrame()helper to reduce code duplication - Removed unused
baseRepeatX/baseRepeatYfrom flipbook materials
- Added
Output Example:
{
"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:
# Extract TFT Lee Sin - Mouth should have isKeyframeFlipbook: true
dotnet run --project LeagueConvert.CommandLine -- convert-wad Companions --alias petchibileesin -o /tmp/test --skins 1Pending Tasks
Rumble_Shield Transparency: Still unresolved. Texture has alpha but no explicit blend data. Need to investigate further.
Glass Emissive/Bloom: Implement
SunColorandLight_Intensityfor emissive/bloom effects on glass materials.Glass Color Gradient: Implement
Glass_Color2andGradient_Biasfor color blending on glass materials.Custom Glass Shader: Consider writing a custom Three.js shader for more accurate League glass rendering.
Notes for Future AIs
- Always check
common-pitfalls.mdbefore making material/shader changes - Test with known problematic skins (Soraka 16, Renata 0, Annie 31, Milio 0, PetChibiLeeSin 1)
- Never use generic parameter names without explicit shader profiles
- Always clone textures before modifying
repeatoroffset - Procedural shaders need placeholder textures - don't skip material creation
- Transparency needs both
alphaModeANDbaseColorFactoralpha - TFT companions use keyframe flipbooks - check for
mDriverscontainer, notClipAccessoryToRead - Keep hashing on backend - convert animation hashes to names before export, never hash on frontend