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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Sources/VRMMetalKit/Core/StrictMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public struct RendererConfig {
public var strict: StrictLevel = .off

/// Color attachment pixel format for render pipelines (defaults to match MTKView default)
public var colorPixelFormat: MTLPixelFormat = .bgra8Unorm
public var colorPixelFormat: MTLPixelFormat = .bgra8Unorm_srgb

/// Enable Metal validation layers (debug builds only)
public var enableMetalValidation: Bool = true
Expand Down Expand Up @@ -77,7 +77,7 @@ public struct RendererConfig {
/// Higher values push coplanar surfaces further apart in depth buffer
public var depthBiasScale: Float = 1.0

public init(strict: StrictLevel = .off, colorPixelFormat: MTLPixelFormat = .bgra8Unorm, renderFilter: RenderFilter? = nil, drawUntil: Int? = nil, drawOnlyIndex: Int? = nil, testIdentityPalette: Int? = nil, sampleCount: Int = 1, depthBiasScale: Float = 1.0) {
public init(strict: StrictLevel = .off, colorPixelFormat: MTLPixelFormat = .bgra8Unorm_srgb, renderFilter: RenderFilter? = nil, drawUntil: Int? = nil, drawOnlyIndex: Int? = nil, testIdentityPalette: Int? = nil, sampleCount: Int = 1, depthBiasScale: Float = 1.0) {
self.strict = strict
self.colorPixelFormat = colorPixelFormat
self.renderFilter = renderFilter
Expand Down
8 changes: 4 additions & 4 deletions Sources/VRMMetalKit/Core/VRMTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -352,10 +352,10 @@ public struct VRM0MaterialProperty {
mtoon.shadingShiftFactor = shadingShiftFactor

// Shade texture from texture properties
// Skip when same as main texture - prevents blue padding artifacts
if let shadeTexIndex = textureProperties["_ShadeTexture"],
let mainTexIndex = textureProperties["_MainTex"],
shadeTexIndex != mainTexIndex {
// When _ShadeTexture == _MainTex (common in VRM 0.0), we still set it
// so the shader multiplies shadeColorFactor by the texture, matching
// Unity MToon behavior. The shader handles this with hasShadeMultiplyTexture.
if let shadeTexIndex = textureProperties["_ShadeTexture"] {
mtoon.shadeMultiplyTexture = shadeTexIndex
}

Expand Down
54 changes: 38 additions & 16 deletions Sources/VRMMetalKit/Renderer/VRMGeometry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,15 @@ public class VRMMaterial {
// _BlendMode: 0=Opaque, 1=Cutout, 2=Transparent, 3=TransparentWithZWrite
if let bm = vrm0Prop.floatProperties["_BlendMode"] {
blendMode = Int(bm)
// Map VRM 0.x blend mode to glTF alphaMode for pipeline selection.
// Without this, materials like AliciaSolid's bangs (_BlendMode=3)
// stay as glTF "OPAQUE" and render without alpha blending.
switch blendMode {
case 0: alphaMode = "OPAQUE"
case 1: alphaMode = "MASK"
case 2, 3: alphaMode = "BLEND"
default: alphaMode = "OPAQUE"
}
}
}
}
Expand All @@ -1271,14 +1280,27 @@ public class VRMMaterial {
return value <= 0.04045 ? value / 12.92 : pow((value + 0.055) / 1.055, 2.4)
}

/// Helper to extract Float array from AnyCodable-decoded values.
/// AnyCodable decodes JSON integers as `Int` and floats as `Double`,
/// so a simple `as? [Double]` cast fails for `[1, 1, 1]`.
private func floatArray(from value: Any, count: Int) -> [Float]? {
guard let array = value as? [Any], array.count >= count else { return nil }
return array.compactMap {
if let intVal = $0 as? Int { return Float(intVal) }
if let doubleVal = $0 as? Double { return Float(doubleVal) }
if let floatVal = $0 as? Float { return floatVal }
return nil
}
}

private func parseMToonExtension(_ mtoonExt: [String: Any], textures: [VRMTexture]) -> VRMMToonMaterial {
var mtoon = VRMMToonMaterial()

// Shade color factor
if let shadeColorFactor = mtoonExt["shadeColorFactor"] as? [Double], shadeColorFactor.count >= 3 {
mtoon.shadeColorFactor = SIMD3<Float>(Float(shadeColorFactor[0]),
Float(shadeColorFactor[1]),
Float(shadeColorFactor[2]))
if let shadeColorFactor = floatArray(from: mtoonExt["shadeColorFactor"], count: 3) {
mtoon.shadeColorFactor = SIMD3<Float>(shadeColorFactor[0],
shadeColorFactor[1],
shadeColorFactor[2])
}

// Shading properties
Expand All @@ -1295,17 +1317,17 @@ public class VRMMaterial {
}

// MatCap properties
if let matcapFactor = mtoonExt["matcapFactor"] as? [Double], matcapFactor.count >= 3 {
mtoon.matcapFactor = SIMD3<Float>(Float(matcapFactor[0]),
Float(matcapFactor[1]),
Float(matcapFactor[2]))
if let matcapFactor = floatArray(from: mtoonExt["matcapFactor"], count: 3) {
mtoon.matcapFactor = SIMD3<Float>(matcapFactor[0],
matcapFactor[1],
matcapFactor[2])
}

// Parametric rim lighting
if let parametricRimColorFactor = mtoonExt["parametricRimColorFactor"] as? [Double], parametricRimColorFactor.count >= 3 {
mtoon.parametricRimColorFactor = SIMD3<Float>(Float(parametricRimColorFactor[0]),
Float(parametricRimColorFactor[1]),
Float(parametricRimColorFactor[2]))
if let parametricRimColorFactor = floatArray(from: mtoonExt["parametricRimColorFactor"], count: 3) {
mtoon.parametricRimColorFactor = SIMD3<Float>(parametricRimColorFactor[0],
parametricRimColorFactor[1],
parametricRimColorFactor[2])
}
if let parametricRimFresnelPowerFactor = mtoonExt["parametricRimFresnelPowerFactor"] as? Double {
mtoon.parametricRimFresnelPowerFactor = Float(parametricRimFresnelPowerFactor)
Expand All @@ -1324,10 +1346,10 @@ public class VRMMaterial {
if let outlineWidthFactor = mtoonExt["outlineWidthFactor"] as? Double {
mtoon.outlineWidthFactor = Float(outlineWidthFactor)
}
if let outlineColorFactor = mtoonExt["outlineColorFactor"] as? [Double], outlineColorFactor.count >= 3 {
mtoon.outlineColorFactor = SIMD3<Float>(Float(outlineColorFactor[0]),
Float(outlineColorFactor[1]),
Float(outlineColorFactor[2]))
if let outlineColorFactor = floatArray(from: mtoonExt["outlineColorFactor"], count: 3) {
mtoon.outlineColorFactor = SIMD3<Float>(outlineColorFactor[0],
outlineColorFactor[1],
outlineColorFactor[2])
}
if let outlineLightingMixFactor = mtoonExt["outlineLightingMixFactor"] as? Double {
mtoon.outlineLightingMixFactor = Float(outlineLightingMixFactor)
Expand Down
8 changes: 8 additions & 0 deletions Sources/VRMMetalKit/Renderer/VRMRenderer+Pipeline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ extension VRMRenderer {
depthStencilStates["blend"] = state
}

// Blend with depth write (TransparentWithZWrite for non-face materials)
let blendZWriteDescriptor = MTLDepthStencilDescriptor()
blendZWriteDescriptor.depthCompareFunction = .lessEqual
blendZWriteDescriptor.isDepthWriteEnabled = true
if let state = device.makeDepthStencilState(descriptor: blendZWriteDescriptor) {
depthStencilStates["blendZWrite"] = state
}

// Kill switch test state (always pass, no depth write)
let alwaysDepthDescriptor = MTLDepthStencilDescriptor()
alwaysDepthDescriptor.depthCompareFunction = .always // Always pass depth test
Expand Down
29 changes: 21 additions & 8 deletions Sources/VRMMetalKit/Renderer/VRMRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ public final class VRMRenderer: NSObject, @unchecked Sendable {
let meshNameLower: String
let isFaceMaterial: Bool
let isEyeMaterial: Bool
let isTransparentWithZWrite: Bool

// OPTIMIZATION: Render order for single-array sorting (avoids concatenation)
var renderOrder: Int // 0=opaque, 1=faceSkin, 2=faceEyebrow, 3=faceEyeline, 4=mask, 5=faceEye, 6=faceHighlight, 7=blend
Expand Down Expand Up @@ -1519,6 +1520,9 @@ public final class VRMRenderer: NSObject, @unchecked Sendable {
meshNameLower: meshNameLower,
isFaceMaterial: isFaceMaterial,
isEyeMaterial: isEyeMaterial,
isTransparentWithZWrite: (primitive.materialIndex.flatMap { idx in
idx < model.materials.count ? model.materials[idx].isTransparentWithZWrite : false
} ?? false),
renderOrder: 0, // Will be set based on category
materialRenderQueue: materialRenderQueue,
primitiveIndex: globalPrimitiveIndex,
Expand Down Expand Up @@ -2371,9 +2375,9 @@ public final class VRMRenderer: NSObject, @unchecked Sendable {
mtoonUniforms.metallicFactor = material.metallicFactor
mtoonUniforms.roughnessFactor = material.roughnessFactor
mtoonUniforms.emissiveFactor = material.emissiveFactor

// LIGHTING FIX: Zero out emissive to prevent washout
mtoonUniforms.emissiveFactor = SIMD3<Float>(0, 0, 0)
// NOTE: Previously force-zeroed here. Removed to honor artist-authored
// emissive values per MToon spec. Visual impact depends on model;
// many VRoid/Unity-authored models do not rely on emissive for eyes.

// DEBUG: Log original baseColorFactor for all materials
#if DEBUG
Expand Down Expand Up @@ -2450,8 +2454,8 @@ public final class VRMRenderer: NSObject, @unchecked Sendable {
}
}

// LIGHTING FIX: Zero emissive AFTER MToon init to prevent washout
mtoonUniforms.emissiveFactor = SIMD3<Float>(0, 0, 0)
// NOTE: Previously force-zeroed emissive here. Removed for
// spec compliance (see mtoonUniforms.emissiveFactor above).

// ALPHA FIX: Restore effectiveAlphaMode AFTER MToon init
// MToon extension may have wrong alphaMode; use our detected/fixed value
Expand Down Expand Up @@ -2808,10 +2812,19 @@ public final class VRMRenderer: NSObject, @unchecked Sendable {
encoder.setDepthBias(baseBias, slopeScale: depthBiasCalculator.slopeScale, clamp: depthBiasCalculator.clamp)

case "blend":
if let blendDepthState = depthStencilStates["blend"] {
encoderStateCache.setDepthStencilState(encoder,blendDepthState)
// TransparentWithZWrite materials need depth write even in the non-face path
if item.isTransparentWithZWrite {
if let blendZWriteState = depthStencilStates["blendZWrite"] {
encoderStateCache.setDepthStencilState(encoder, blendZWriteState)
} else {
encoderStateCache.setDepthStencilState(encoder, depthStencilStates["opaque"])
}
} else {
encoderStateCache.setDepthStencilState(encoder,depthStencilStates["opaque"])
if let blendDepthState = depthStencilStates["blend"] {
encoderStateCache.setDepthStencilState(encoder, blendDepthState)
} else {
encoderStateCache.setDepthStencilState(encoder, depthStencilStates["opaque"])
}
}
encoderStateCache.setCullMode(encoder,.none)
// Apply base depth bias for BLEND materials
Expand Down
21 changes: 12 additions & 9 deletions Sources/VRMMetalKit/Renderer/VRMUniforms.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,18 @@ struct Uniforms {
// This provides front lighting for VRM 1.0 while not breaking VRM 0.0 eyebrows
var lightDirection_packed = SIMD4<Float>(-0.25, -0.5, -0.83, 0.0) // 16 bytes, offset 256 (xyz = direction)
var lightColor_packed = SIMD4<Float>(1.0, 1.0, 1.0, 1.732) // 16 bytes, offset 272 (xyz = color, w = intensity)
var ambientColor_packed = SIMD4<Float>(0.05, 0.05, 0.05, 0.0) // 16 bytes, offset 288 (SIMD3 + padding)

// Light 1 (fill light)
var light1Direction_packed = SIMD4<Float>(0, 0, 0, 0) // 16 bytes, offset 304 (xyz = direction)
var light1Color_packed = SIMD4<Float>(0, 0, 0, 0) // 16 bytes, offset 320 (xyz = color, w = intensity)

// Light 2 (rim/back light)
var light2Direction_packed = SIMD4<Float>(0, 0, 0, 0) // 16 bytes, offset 336 (xyz = direction)
var light2Color_packed = SIMD4<Float>(0, 0, 0, 0) // 16 bytes, offset 352 (xyz = color, w = intensity)
// Raised from 0.05 to 0.15 for hands-off consumers (GitHub #147).
// This is the most universally visible lighting fix.
var ambientColor_packed = SIMD4<Float>(0.15, 0.15, 0.15, 0.0) // 16 bytes, offset 288 (SIMD3 + padding)

// Light 1 (fill light) - soft cool fill from left-front to lift shadows
// Direction: from (-0.6, -0.2, 0.8)
var light1Direction_packed = SIMD4<Float>(-0.58, -0.19, 0.79, 0.0)
var light1Color_packed = SIMD4<Float>(0.45, 0.48, 0.55, 0.83) // intensity ~0.83

// Light 2 (rim/back light) - warm rim from upper-back-right
var light2Direction_packed = SIMD4<Float>(0.32, 0.85, -0.42, 0.0)
var light2Color_packed = SIMD4<Float>(0.55, 0.50, 0.45, 0.75) // intensity ~0.75

// Other fields
var viewportSize_packed = SIMD4<Float>(1280, 720, 0.0, 0.0) // 16 bytes, offset 368 (SIMD2 + padding)
Expand Down
Binary file modified Sources/VRMMetalKit/Resources/VRMMetalKitShaders.metallib
Binary file not shown.
6 changes: 4 additions & 2 deletions Sources/VRMMetalKit/Shaders/MToonShader.metal
Original file line number Diff line number Diff line change
Expand Up @@ -593,8 +593,10 @@ return float4(0.0, 0.0, 0.0, 1.0); // Black = no matcap
// Accumulate weighted contributions (manual normalization factor allows artistic control)
float3 litColor = (lit0 + lit1 + lit2) * uniforms.lightNormalizationFactor;

// Indirect diffuse: ambient always contributes at base color (matches three-vrm)
float3 indirectDiffuse = uniforms.ambientColor.xyz * baseColor.rgb;
// Indirect diffuse: ambient scaled by material giIntensityFactor (MToon spec).
// NOTE: On typical models with default giIntensityFactor (~0.05), this is visually
// subtle. The fix ensures spec compliance for models that rely on high GI intensity.
float3 indirectDiffuse = uniforms.ambientColor.xyz * baseColor.rgb * material.giIntensityFactor;
litColor += indirectDiffuse;

// Emissive
Expand Down
Loading
Loading