feat: support opacity maps in gltf preview
This commit is contained in:
@@ -1,11 +1,18 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Suspense } from 'react'
|
import { Suspense, useEffect } from 'react'
|
||||||
import { Canvas } from '@react-three/fiber'
|
import { Canvas } from '@react-three/fiber'
|
||||||
import { Stage, OrbitControls } from '@react-three/drei'
|
import { Stage, OrbitControls } from '@react-three/drei'
|
||||||
import { useLoader } from '@react-three/fiber'
|
import { useLoader } from '@react-three/fiber'
|
||||||
|
import type { Material, Mesh, Texture } from 'three'
|
||||||
|
import { TextureLoader } from 'three'
|
||||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||||
|
|
||||||
|
interface OpacityMapEntry {
|
||||||
|
target: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
function resolveAssetUrl(requestedUrl: string, assetUrls: Record<string, string>) {
|
function resolveAssetUrl(requestedUrl: string, assetUrls: Record<string, string>) {
|
||||||
if (requestedUrl.startsWith('blob:') || requestedUrl.startsWith('data:')) {
|
if (requestedUrl.startsWith('blob:') || requestedUrl.startsWith('data:')) {
|
||||||
return requestedUrl
|
return requestedUrl
|
||||||
@@ -17,10 +24,76 @@ function resolveAssetUrl(requestedUrl: string, assetUrls: Record<string, string>
|
|||||||
return filename ? assetUrls[filename] || requestedUrl : requestedUrl
|
return filename ? assetUrls[filename] || requestedUrl : requestedUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOpacityMapEntries(assetUrls: Record<string, string>) {
|
||||||
|
return Object.entries(assetUrls).reduce<OpacityMapEntry[]>((entries, [filename, url]) => {
|
||||||
|
const match = filename.toLowerCase().match(/^opacity(?:[_-](.+))?\.(png|jpe?g|webp)$/)
|
||||||
|
|
||||||
|
if (!match) return entries
|
||||||
|
|
||||||
|
entries.push({ target: match[1] || '', url })
|
||||||
|
return entries
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaterialName(material: Material) {
|
||||||
|
return material.name.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getObjectName(mesh: Mesh) {
|
||||||
|
return mesh.name.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAlphaMap(material: Material, texture: Texture) {
|
||||||
|
if (!('alphaMap' in material)) return
|
||||||
|
|
||||||
|
texture.flipY = false
|
||||||
|
material.alphaMap = texture
|
||||||
|
material.transparent = true
|
||||||
|
material.alphaTest = 0.01
|
||||||
|
material.needsUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickOpacityMap(
|
||||||
|
mesh: Mesh,
|
||||||
|
material: Material,
|
||||||
|
entries: OpacityMapEntry[],
|
||||||
|
textures: Texture[],
|
||||||
|
) {
|
||||||
|
const objectName = getObjectName(mesh)
|
||||||
|
const materialName = getMaterialName(material)
|
||||||
|
const targetedIndex = entries.findIndex((entry) => (
|
||||||
|
entry.target && (objectName.includes(entry.target) || materialName.includes(entry.target))
|
||||||
|
))
|
||||||
|
|
||||||
|
if (targetedIndex >= 0) return textures[targetedIndex]
|
||||||
|
|
||||||
|
const genericIndex = entries.findIndex((entry) => entry.target === '')
|
||||||
|
if (genericIndex >= 0) return textures[genericIndex]
|
||||||
|
|
||||||
|
return entries.length === 1 ? textures[0] : undefined
|
||||||
|
}
|
||||||
|
|
||||||
function Model({ url, assetUrls }: { url: string; assetUrls: Record<string, string> }) {
|
function Model({ url, assetUrls }: { url: string; assetUrls: Record<string, string> }) {
|
||||||
const { scene } = useLoader(GLTFLoader, url, (loader) => {
|
const { scene } = useLoader(GLTFLoader, url, (loader) => {
|
||||||
loader.manager.setURLModifier((requestedUrl) => resolveAssetUrl(requestedUrl, assetUrls))
|
loader.manager.setURLModifier((requestedUrl) => resolveAssetUrl(requestedUrl, assetUrls))
|
||||||
})
|
})
|
||||||
|
const opacityMapEntries = getOpacityMapEntries(assetUrls)
|
||||||
|
const opacityMaps = useLoader(TextureLoader, opacityMapEntries.map((entry) => entry.url)) as Texture[]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (opacityMapEntries.length === 0) return
|
||||||
|
|
||||||
|
scene.traverse((object) => {
|
||||||
|
const mesh = object as Mesh
|
||||||
|
if (!mesh.isMesh || !mesh.material) return
|
||||||
|
|
||||||
|
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
||||||
|
materials.forEach((material) => {
|
||||||
|
const opacityMap = pickOpacityMap(mesh, material, opacityMapEntries, opacityMaps)
|
||||||
|
if (opacityMap) applyAlphaMap(material, opacityMap)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [scene, opacityMapEntries, opacityMaps])
|
||||||
|
|
||||||
return <primitive object={scene} />
|
return <primitive object={scene} />
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user