fix: apply opacity maps in preview
This commit is contained in:
@@ -69,6 +69,7 @@ Access the app at `http://localhost:3000`
|
|||||||
>
|
>
|
||||||
> Local 3D preview supports `model.gltf` folders by resolving dropped companion files such as `model.bin` and textures through local object URLs.
|
> Local 3D preview supports `model.gltf` folders by resolving dropped companion files such as `model.bin` and textures through local object URLs.
|
||||||
> The preview also shows a small model stats helper with estimated draw calls, meshes, triangles, materials, and texture count.
|
> The preview also shows a small model stats helper with estimated draw calls, meshes, triangles, materials, and texture count.
|
||||||
|
> Opacity helper textures can be named `opacity.png` for the whole model or `opacity_part-name.png` to target a mesh/material whose name contains `part-name`; alpha-channel PNGs are converted to alpha maps for the preview.
|
||||||
|
|
||||||
### Production (Coolify / Docker)
|
### Production (Coolify / Docker)
|
||||||
|
|
||||||
|
|||||||
+91
-17
@@ -4,8 +4,8 @@ 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, Object3D, Texture } from 'three'
|
import { CanvasTexture, Mesh, TextureLoader } from 'three'
|
||||||
import { TextureLoader } from 'three'
|
import type { Material, Object3D, Texture } from 'three'
|
||||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||||
|
|
||||||
export interface ModelStats {
|
export interface ModelStats {
|
||||||
@@ -21,6 +21,14 @@ interface OpacityMapEntry {
|
|||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AlphaMapMaterial extends Material {
|
||||||
|
alphaMap: Texture | null
|
||||||
|
transparent: boolean
|
||||||
|
alphaTest: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const alphaMapTextureCache = new WeakMap<Texture, Texture>()
|
||||||
|
|
||||||
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
|
||||||
@@ -43,19 +51,84 @@ function getOpacityMapEntries(assetUrls: Record<string, string>) {
|
|||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeMatchName(name: string) {
|
||||||
|
return name
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.replace(/[^a-z0-9]/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
function getMaterialName(material: Material) {
|
function getMaterialName(material: Material) {
|
||||||
return material.name.toLowerCase()
|
return normalizeMatchName(material.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getObjectName(mesh: Mesh) {
|
function getObjectName(mesh: Mesh) {
|
||||||
return mesh.name.toLowerCase()
|
return normalizeMatchName(mesh.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMesh(object: Object3D): object is Mesh {
|
||||||
|
return object instanceof Mesh
|
||||||
|
}
|
||||||
|
|
||||||
|
function supportsAlphaMap(material: Material): material is AlphaMapMaterial {
|
||||||
|
return 'alphaMap' in material
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAlphaMapTexture(texture: Texture) {
|
||||||
|
const cachedTexture = alphaMapTextureCache.get(texture)
|
||||||
|
if (cachedTexture) return cachedTexture
|
||||||
|
|
||||||
|
const image = texture.image as unknown
|
||||||
|
const isImageBitmap = typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap
|
||||||
|
|
||||||
|
if (!(image instanceof HTMLImageElement || image instanceof HTMLCanvasElement || isImageBitmap)) {
|
||||||
|
texture.flipY = false
|
||||||
|
alphaMapTextureCache.set(texture, texture)
|
||||||
|
return texture
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = image.width
|
||||||
|
canvas.height = image.height
|
||||||
|
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
if (!context) {
|
||||||
|
texture.flipY = false
|
||||||
|
alphaMapTextureCache.set(texture, texture)
|
||||||
|
return texture
|
||||||
|
}
|
||||||
|
|
||||||
|
context.drawImage(image, 0, 0)
|
||||||
|
|
||||||
|
const imageData = context.getImageData(0, 0, canvas.width, canvas.height)
|
||||||
|
const pixels = imageData.data
|
||||||
|
|
||||||
|
for (let i = 0; i < pixels.length; i += 4) {
|
||||||
|
const alpha = pixels[i + 3]
|
||||||
|
const luminance = Math.round((pixels[i] * 0.2126) + (pixels[i + 1] * 0.7152) + (pixels[i + 2] * 0.0722))
|
||||||
|
const opacity = alpha < 255 ? alpha : luminance
|
||||||
|
|
||||||
|
pixels[i] = opacity
|
||||||
|
pixels[i + 1] = opacity
|
||||||
|
pixels[i + 2] = opacity
|
||||||
|
pixels[i + 3] = 255
|
||||||
|
}
|
||||||
|
|
||||||
|
context.putImageData(imageData, 0, 0)
|
||||||
|
|
||||||
|
const alphaTexture = new CanvasTexture(canvas)
|
||||||
|
alphaTexture.flipY = false
|
||||||
|
alphaTexture.needsUpdate = true
|
||||||
|
alphaMapTextureCache.set(texture, alphaTexture)
|
||||||
|
|
||||||
|
return alphaTexture
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyAlphaMap(material: Material, texture: Texture) {
|
function applyAlphaMap(material: Material, texture: Texture) {
|
||||||
if (!('alphaMap' in material)) return
|
if (!supportsAlphaMap(material)) return
|
||||||
|
|
||||||
texture.flipY = false
|
material.alphaMap = createAlphaMapTexture(texture)
|
||||||
material.alphaMap = texture
|
|
||||||
material.transparent = true
|
material.transparent = true
|
||||||
material.alphaTest = 0.01
|
material.alphaTest = 0.01
|
||||||
material.needsUpdate = true
|
material.needsUpdate = true
|
||||||
@@ -91,14 +164,13 @@ function getModelStats(scene: Object3D, assetUrls: Record<string, string>): Mode
|
|||||||
let triangles = 0
|
let triangles = 0
|
||||||
|
|
||||||
scene.traverse((object) => {
|
scene.traverse((object) => {
|
||||||
const mesh = object as Mesh
|
if (!isMesh(object) || !object.geometry || !object.material) return
|
||||||
if (!mesh.isMesh || !mesh.geometry || !mesh.material) return
|
|
||||||
|
|
||||||
meshes += 1
|
meshes += 1
|
||||||
triangles += countGeometryTriangles(mesh)
|
triangles += countGeometryTriangles(object)
|
||||||
drawCalls += estimateMeshDrawCalls(mesh)
|
drawCalls += estimateMeshDrawCalls(object)
|
||||||
|
|
||||||
const meshMaterials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
const meshMaterials = Array.isArray(object.material) ? object.material : [object.material]
|
||||||
meshMaterials.forEach((material) => materials.add(material))
|
meshMaterials.forEach((material) => materials.add(material))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -120,7 +192,10 @@ function pickOpacityMap(
|
|||||||
const objectName = getObjectName(mesh)
|
const objectName = getObjectName(mesh)
|
||||||
const materialName = getMaterialName(material)
|
const materialName = getMaterialName(material)
|
||||||
const targetedIndex = entries.findIndex((entry) => (
|
const targetedIndex = entries.findIndex((entry) => (
|
||||||
entry.target && (objectName.includes(entry.target) || materialName.includes(entry.target))
|
entry.target && (
|
||||||
|
objectName.includes(normalizeMatchName(entry.target))
|
||||||
|
|| materialName.includes(normalizeMatchName(entry.target))
|
||||||
|
)
|
||||||
))
|
))
|
||||||
|
|
||||||
if (targetedIndex >= 0) return textures[targetedIndex]
|
if (targetedIndex >= 0) return textures[targetedIndex]
|
||||||
@@ -154,12 +229,11 @@ function Model({
|
|||||||
if (opacityMapEntries.length === 0) return
|
if (opacityMapEntries.length === 0) return
|
||||||
|
|
||||||
scene.traverse((object) => {
|
scene.traverse((object) => {
|
||||||
const mesh = object as Mesh
|
if (!isMesh(object) || !object.material) return
|
||||||
if (!mesh.isMesh || !mesh.material) return
|
|
||||||
|
|
||||||
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
const materials = Array.isArray(object.material) ? object.material : [object.material]
|
||||||
materials.forEach((material) => {
|
materials.forEach((material) => {
|
||||||
const opacityMap = pickOpacityMap(mesh, material, opacityMapEntries, opacityMaps)
|
const opacityMap = pickOpacityMap(object, material, opacityMapEntries, opacityMaps)
|
||||||
if (opacityMap) applyAlphaMap(material, opacityMap)
|
if (opacityMap) applyAlphaMap(material, opacityMap)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user