1import * as THREE from "three"; 2import { colorLuminance, textureHasImage, prepareTexture } from "./three-helpers.js?viewer=materials-v45"; 3import { hashString } from "./utils.js?viewer=materials-v45"; 4 5export function fallbackMaterialColor(material) { 6 const name = String(material?.name || "").toLowerCase(); 7 if (/(hair|bang|tail|fur|fluff)/.test(name)) return new THREE.Color(0x2f3744); 8 if (/(eye|iris|pupil)/.test(name)) return new THREE.Color(0x75c7e8); 9 if (/(fang|tooth|teeth)/.test(name)) return new THREE.Color(0xe9dfd0); 10 if (/(nail|claw)/.test(name)) return new THREE.Color(0xc89ea8); 11 if (/(face|body|skin|arm|hand|leg|ear)/.test(name)) return new THREE.Color(0xd7a997); 12 if (/(short|fabric|cloth|shirt|dress|sock|fishnet|warmer)/.test(name)) return new THREE.Color(0x39425c); 13 if (/(metal|ring|zip|chain|buckle)/.test(name)) return new THREE.Color(0x8c9299); 14 const palette = [0xb68f7d, 0x6f8fb0, 0x9376a8, 0x7c9a8e, 0x9c7d61, 0x5f7088, 0x886b78]; 15 return new THREE.Color(palette[hashString(name || "material") % palette.length]); 16} 17 18export function shouldUseFallbackMaterialColor(material, hasColorMap, missingColorMap) { 19 const color = material?.color ? material.color.clone() : new THREE.Color(0xffffff); 20 if (missingColorMap) return true; 21 return !hasColorMap && colorLuminance(color) > 0.88; 22} 23 24export function materialColor(material, hasColorMap, missingColorMap) { 25 const color = material?.color ? material.color.clone() : new THREE.Color(0xffffff); 26 if (hasColorMap && colorLuminance(color) < 0.08) return new THREE.Color(0xffffff); 27 if (shouldUseFallbackMaterialColor(material, hasColorMap, missingColorMap)) return fallbackMaterialColor(material); 28 if (!hasColorMap && colorLuminance(color) < 0.035) return new THREE.Color(0x9da8b6); 29 return color; 30} 31 32export function applyMaterialTransparencyState(material, { transparent = material.transparent === true, alphaTest = material.alphaTest || 0, opacity = material.opacity ?? 1 } = {}) { 33 material.transparent = transparent === true; 34 material.opacity = opacity; 35 material.alphaTest = alphaTest; 36 material.depthTest = true; 37 material.depthWrite = !material.transparent; 38 material.premultipliedAlpha = material.premultipliedAlpha === true; 39 if ("forceSinglePass" in material) material.forceSinglePass = true; 40 material.needsUpdate = true; 41 return material; 42} 43 44export function applyTransparentSorting(object) { 45 object.traverse((node) => { 46 if (!node.isMesh || !node.material) return; 47 const mats = Array.isArray(node.material) ? node.material : [node.material]; 48 const hasTransparent = mats.some((mat) => mat?.transparent === true); 49 const hasAlphaTest = mats.some((mat) => (mat?.alphaTest || 0) > 0); 50 node.renderOrder = hasTransparent ? 20 : hasAlphaTest ? 10 : 0; 51 }); 52} 53 54export function createUnlitMaterial(material) { 55 const unlit = new THREE.MeshBasicMaterial({ 56 name: material.name || "", 57 color: material.color ? material.color.clone() : new THREE.Color(0xffffff), 58 map: material.map || null, 59 alphaMap: material.alphaMap || null, 60 aoMap: material.aoMap || null, 61 transparent: material.transparent === true, 62 opacity: material.opacity ?? 1, 63 alphaTest: material.alphaTest || 0, 64 depthWrite: material.depthWrite !== false, 65 depthTest: material.depthTest !== false, 66 premultipliedAlpha: material.premultipliedAlpha === true, 67 side: material.side || THREE.DoubleSide, 68 wireframe: material.wireframe === true, 69 vertexColors: material.vertexColors === true, 70 }); 71 unlit.userData.shadedMaterial = material.userData?.shadedMaterial || material; 72 unlit.userData.unlitMaterial = unlit; 73 unlit.userData.previewColor = material.userData?.previewColor; 74 material.userData.unlitMaterial = unlit; 75 return unlit; 76} 77 78export function attachMaterialModes(material) { 79 const unlit = createUnlitMaterial(material); 80 material.userData.shadedMaterial = material; 81 material.userData.unlitMaterial = unlit; 82 return material; 83} 84 85export function applyPreviewMaterials(object) { 86 const summary = { meshes: 0, materials: 0, mapped: 0, missingMaps: 0, fallbacked: 0, materialNames: [], fallbackColors: [] }; 87 object.traverse((node) => { 88 if (!node.isMesh) return; 89 summary.meshes += 1; 90 node.frustumCulled = false;
91 node.castShadow = false; 92 node.receiveShadow = false; 93 const materials = Array.isArray(node.material) ? node.material : [node.material]; 94 const mapped = materials.map((material) => { 95 summary.materials += 1; 96 if (material?.name) summary.materialNames.push(material.name); 97 if (!material) { 98 return attachMaterialModes(new THREE.MeshStandardMaterial({ 99 color: 0xb8c7db, 100 roughness: 0.88, 101 metalness: 0, 102 side: THREE.DoubleSide, 103 })); 104 } 105 const map = prepareTexture(material.map, true); 106 const emissiveMap = prepareTexture(material.emissiveMap, true); 107 const alphaMap = prepareTexture(material.alphaMap, false); 108 const normalMap = prepareTexture(material.normalMap, false); 109 const bumpMap = prepareTexture(material.bumpMap, false); 110 const roughnessMap = prepareTexture(material.roughnessMap, false); 111 const aoMap = prepareTexture(material.aoMap, false); 112 const missingColorMap = !!material.map && !map; 113 if (missingColorMap) summary.missingMaps += 1; 114 if (map) summary.mapped += 1; 115 116 const usedFallbackColor = shouldUseFallbackMaterialColor(material, !!map, missingColorMap); 117 const previewColor = materialColor(material, !!map, missingColorMap); 118 if (usedFallbackColor) { 119 summary.fallbacked += 1; 120 summary.fallbackColors.push({ name: material.name || "", color: `#${previewColor.getHexString()}` }); 121 } 122 const rawOpacity = material.opacity ?? 1; 123 const initialAlphaMap = rawOpacity < 0.05 && !map ? null : alphaMap; 124 const opacityLooksMisencoded = rawOpacity < 0.05 && !initialAlphaMap; 125 const safeOpacity = opacityLooksMisencoded ? 1 : rawOpacity; 126 const sourceAlphaTest = Number(material.alphaTest || 0); 127 const colorMapMayCarryAlpha = !!map && (material.transparent === true || sourceAlphaTest > 0); 128 const hasAlphaSource = !!initialAlphaMap || colorMapMayCarryAlpha; 129 const transparent = !opacityLooksMisencoded && sourceAlphaTest <= 0 && (safeOpacity < 1 || (hasAlphaSource && material.transparent === true)); 130 const alphaTest = sourceAlphaTest > 0 ? sourceAlphaTest : (!transparent && hasAlphaSource ? 0.28 : 0); 131 const standard = new THREE.MeshStandardMaterial({ 132 name: material.name || "", 133 color: previewColor, 134 map, 135 emissive: material.emissive ? material.emissive.clone() : new THREE.Color(0x000000), 136 emissiveMap, 137 emissiveIntensity: emissiveMap ? 0.45 : 0.0, 138 alphaMap: initialAlphaMap, 139 normalMap, 140 bumpMap, 141 roughnessMap, 142 aoMap, 143 transparent, 144 opacity: transparent || safeOpacity < 1 ? safeOpacity : 1, 145 alphaTest, 146 depthWrite: !transparent, 147 depthTest: true, 148 premultipliedAlpha: material.premultipliedAlpha === true, 149 side: THREE.DoubleSide, 150 roughness: map ? Math.min(Math.max(material.roughness ?? 0.6, 0.45), 0.78) : 0.62, 151 metalness: Math.min(material.metalness ?? 0, 0.05), 152 envMapIntensity: map ? 0.85 : 0.55, 153 vertexColors: !!node.geometry?.attributes?.color || material.vertexColors === true, 154 }); 155 standard.userData.previewColor = `#${previewColor.getHexString()}`; 156 applyMaterialTransparencyState(standard, { transparent, alphaTest, opacity: transparent || safeOpacity < 1 ? safeOpacity : 1 }); 157 return attachMaterialModes(standard); 158 }); 159 node.material = Array.isArray(node.material) ? mapped : mapped[0]; 160 }); 161 applyTransparentSorting(object); 162 summary.materialNames = [...new Set(summary.materialNames)].slice(0, 80); 163 window.__vrcModelViewerDebug = summary; 164 object.userData.viewerDebug = summary; 165} 166 167// Apply toon-family default shading (task 4.2): materials tagged isToonFamily by 168// applyDiscoveredTextures default to their unlit (albedo-dominant) variant, matching 169// how Unity's scene view displays Poiyomi/lilToon/UTS/MToon. Standard-family materials 170// keep the shaded PBR path. Called after texture discovery settles. 171export function applyToonFamilyDefaults(object) { 172 let swapped = 0; 173 object.traverse((node) => { 174 if (!node.isMesh || !node.material) return; 175 const swap = (material) => { 176 if (!material?.userData?.isToonFamily) return material; 177 const unlit = material.userData?.unlitMaterial; 178 if (!unlit) return material; 179 const wireframe = material.wireframe === true; 180 unlit.wireframe = wireframe; 181 swapped += 1; 182 return unlit; 183 }; 184 node.material = Array.isArray(node.material) 185 ? node.material.map(swap) 186 : swap(node.material); 187 }); 188 return swapped; 189} 190 191export function setObjectShading(object, enabled) { 192 object.traverse((node) => { 193 if (!node.isMesh || !node.material) return; 194 const swap = (material) => { 195 if (!material) return material; 196 const wireframe = material.wireframe === true; 197 const next = enabled 198 ? (material.userData?.shadedMaterial || material) 199 : (material.userData?.unlitMaterial || createUnlitMaterial(material)); 200 next.wireframe = wireframe; 201 return next; 202 }; 203 node.material = Array.isArray(node.material) 204 ? node.material.map(swap) 205 : swap(node.material); 206 }); 207}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.