Skip to content
Merged
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
9 changes: 9 additions & 0 deletions src/gltf/common.nim
Original file line number Diff line number Diff line change
Expand Up @@ -179,39 +179,48 @@ type
channels*: seq[AnimationChannel]

Material* = ref object
## Texture slots the source file left empty are filled with a 1x1
## constant image so renderers always have something to sample. Those
## fills are marked with the matching `*Placeholder` flag, since a 1x1
## image is otherwise indistinguishable from a real one that small.
name*: string
baseColor*: Image
baseColorKtx2*: string
baseColorName*: string
baseColorTransform*: TextureTransform
baseColorSampler*: TextureSampler
baseColorFactor*: Color
baseColorPlaceholder*: bool
metallicRoughness*: Image
metallicRoughnessKtx2*: string
metallicRoughnessName*: string
metallicRoughnessTransform*: TextureTransform
metallicRoughnessSampler*: TextureSampler
metallicFactor*: float32
roughnessFactor*: float32
metallicRoughnessPlaceholder*: bool
normal*: Image
normalKtx2*: string
normalName*: string
normalTransform*: TextureTransform
normalSampler*: TextureSampler
hasNormalTexture*: bool
normalScale*: float32
normalPlaceholder*: bool
occlusion*: Image
occlusionKtx2*: string
occlusionName*: string
occlusionTransform*: TextureTransform
occlusionSampler*: TextureSampler
occlusionStrength*: float32
occlusionPlaceholder*: bool
emissive*: Image
emissiveKtx2*: string
emissiveName*: string
emissiveTransform*: TextureTransform
emissiveSampler*: TextureSampler
emissiveFactor*: Color
emissivePlaceholder*: bool

alphaMode*: AlphaMode
alphaCutoff*: float32
Expand Down
10 changes: 10 additions & 0 deletions src/gltf/reader.nim
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,7 @@ proc defaultRuntimeMaterial(): Material =

result.baseColor = newImage(1, 1)
result.baseColor.fill(rgbx(255, 255, 255, 255))
result.baseColorPlaceholder = true
result.baseColorFactor = color(1, 1, 1, 1)
result.baseColorTransform = TextureTransform(
texCoord: 0,
Expand All @@ -1203,6 +1204,7 @@ proc defaultRuntimeMaterial(): Material =

result.metallicRoughness = newImage(1, 1)
result.metallicRoughness.fill(rgbx(255, 255, 255, 255))
result.metallicRoughnessPlaceholder = true
result.metallicFactor = 1.0
result.roughnessFactor = 1.0
result.metallicRoughnessTransform = TextureTransform(
Expand All @@ -1214,6 +1216,7 @@ proc defaultRuntimeMaterial(): Material =

result.normal = newImage(1, 1)
result.normal.fill(rgbx(128, 128, 255, 255))
result.normalPlaceholder = true
result.hasNormalTexture = false
result.normalScale = 1.0
result.normalTransform = TextureTransform(
Expand All @@ -1225,6 +1228,7 @@ proc defaultRuntimeMaterial(): Material =

result.occlusion = newImage(1, 1)
result.occlusion.fill(rgbx(255, 255, 255, 255))
result.occlusionPlaceholder = true
result.occlusionStrength = 1.0
result.occlusionTransform = TextureTransform(
texCoord: 0,
Expand All @@ -1235,6 +1239,7 @@ proc defaultRuntimeMaterial(): Material =

result.emissive = newImage(1, 1)
result.emissive.fill(rgbx(255, 255, 255, 255))
result.emissivePlaceholder = true
result.emissiveFactor = color(0, 0, 0, 1)
result.emissiveTransform = TextureTransform(
texCoord: 0,
Expand Down Expand Up @@ -1335,6 +1340,7 @@ proc loadPrimitive(
else:
result.material.baseColor = newImage(1, 1)
result.material.baseColor.fill(rgbx(255, 255, 255, 255))
result.material.baseColorPlaceholder = true
result.material.baseColorTransform = TextureTransform(
texCoord: pbr.baseColorTexture.texCoord,
offset: pbr.baseColorTexture.offset,
Expand All @@ -1353,6 +1359,7 @@ proc loadPrimitive(
else:
result.material.metallicRoughness = newImage(1, 1)
result.material.metallicRoughness.fill(rgbx(255, 255, 255, 255))
result.material.metallicRoughnessPlaceholder = true
result.material.metallicRoughnessTransform = TextureTransform(
texCoord: pbr.metallicRoughnessTexture.texCoord,
offset: pbr.metallicRoughnessTexture.offset,
Expand All @@ -1374,6 +1381,7 @@ proc loadPrimitive(
else:
result.material.normal = newImage(1, 1)
result.material.normal.fill(rgbx(128, 128, 255, 255))
result.material.normalPlaceholder = true
result.material.hasNormalTexture = false
result.material.normalScale = 1.0
result.material.normalTransform = TextureTransform(
Expand All @@ -1393,6 +1401,7 @@ proc loadPrimitive(
else:
result.material.occlusion = newImage(1, 1)
result.material.occlusion.fill(rgbx(255, 255, 255, 255))
result.material.occlusionPlaceholder = true
result.material.occlusionTransform = TextureTransform(
texCoord: material.occlusionTexture.texCoord,
offset: material.occlusionTexture.offset,
Expand All @@ -1411,6 +1420,7 @@ proc loadPrimitive(
else:
result.material.emissive = newImage(1, 1)
result.material.emissive.fill(rgbx(255, 255, 255, 255))
result.material.emissivePlaceholder = true
result.material.emissiveTransform = TextureTransform(
texCoord: material.emissiveTexture.texCoord,
offset: material.emissiveTexture.offset,
Expand Down
24 changes: 14 additions & 10 deletions src/gltf/writer.nim
Original file line number Diff line number Diff line change
Expand Up @@ -293,11 +293,12 @@ proc writeGLB*(
textureIds[key] = idx
idx

proc isPlaceholder(img: Image): bool =
## The reader substitutes 1x1 fill images for missing material maps;
## writing those back out would add meaningless textures (the factor
## values already carry the constant).
img == nil or (img.width <= 1 and img.height <= 1)
proc isPlaceholder(img: Image, placeholder: bool): bool =
## The reader substitutes 1x1 fill images for missing material maps and
## flags them; writing those back out would add meaningless textures (the
## factor values already carry the constant). The flag is what decides:
## a 1x1 image the caller supplied is a real texture and must be written.
img == nil or placeholder

proc materialIndex(mat: Material): int =
## Returns the output material index for a material.
Expand All @@ -321,13 +322,15 @@ proc writeGLB*(

# Images embedded in a .glb often carry no name; fall back to the
# semantic so unnamed textures still round-trip instead of vanishing.
if not isPlaceholder(mat.baseColor):
if not isPlaceholder(mat.baseColor, mat.baseColorPlaceholder):
let name =
if mat.baseColorName.len > 0: mat.baseColorName else: "baseColor"
let texIdx = textureIndex(mat.baseColor, name, tsColor)
pbr["baseColorTexture"] = %*{"index": texIdx}

if not isPlaceholder(mat.metallicRoughness):
if not isPlaceholder(
mat.metallicRoughness, mat.metallicRoughnessPlaceholder
):
let name =
if mat.metallicRoughnessName.len > 0:
mat.metallicRoughnessName
Expand All @@ -339,23 +342,24 @@ proc writeGLB*(
matNode["pbrMetallicRoughness"] = pbr
matNode["doubleSided"] = newJBool(mat.doubleSided)

if not isPlaceholder(mat.normal) and mat.hasNormalTexture:
if not isPlaceholder(mat.normal, mat.normalPlaceholder) and
mat.hasNormalTexture:
let name = if mat.normalName.len > 0: mat.normalName else: "normal"
let texIdx = textureIndex(mat.normal, name, tsNormal)
matNode["normalTexture"] = %*{
"index": texIdx,
"scale": mat.normalScale
}

if not isPlaceholder(mat.occlusion):
if not isPlaceholder(mat.occlusion, mat.occlusionPlaceholder):
let name = if mat.occlusionName.len > 0: mat.occlusionName else: "occlusion"
let texIdx = textureIndex(mat.occlusion, name, tsData)
matNode["occlusionTexture"] = %*{
"index": texIdx,
"strength": mat.occlusionStrength
}

if not isPlaceholder(mat.emissive):
if not isPlaceholder(mat.emissive, mat.emissivePlaceholder):
let name = if mat.emissiveName.len > 0: mat.emissiveName else: "emissive"
let texIdx = textureIndex(mat.emissive, name, tsColor)
matNode["emissiveTexture"] = %*{"index": texIdx}
Expand Down
58 changes: 58 additions & 0 deletions tests/tests.nim
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,64 @@ doAssert embeddedModel.root.nodes.len == 1
let embeddedPrimitive = embeddedModel.root.nodes[0].mesh.primitives[0]
doAssert embeddedPrimitive.material.baseColorName == "named_diffuse.png"

echo "Testing placeholder maps are not written back."
# The reader fills empty texture slots with 1x1 constant images; writing
# those out would add meaningless textures the source file never had.
let
placeholderDir = joinPath(tmpDir, "out_placeholder")
untexturedGltfPath = joinPath(placeholderDir, "untextured.gltf")
untexturedBufferPath = joinPath(placeholderDir, "untextured.bin")
placeholderPath = joinPath(placeholderDir, "placeholder.glb")
createDir(placeholderDir)
writeBytes(
untexturedBufferPath,
@[
0x00'u8, 0x00, 0x00, 0x00,
0x00'u8, 0x00, 0x00, 0x00,
0x00'u8, 0x00, 0x00, 0x00
]
)
writeFile(
untexturedGltfPath,
$(%*{
"asset": {"version": "2.0"},
"buffers": [{"byteLength": 12, "uri": "untextured.bin"}],
"bufferViews": [{"buffer": 0, "byteOffset": 0, "byteLength": 12}],
"accessors": [
{"bufferView": 0, "componentType": 5126, "count": 1, "type": "VEC3"}
],
"materials": [{"pbrMetallicRoughness": {}}],
"meshes": [
{"primitives": [{"attributes": {"POSITION": 0}, "material": 0}]}
],
"nodes": [{"name": "Untextured", "mesh": 0}],
"scenes": [{"nodes": [0]}],
"scene": 0
})
)

let untexturedModel = readGltfFile(untexturedGltfPath)
let untexturedMat =
untexturedModel.root["Untextured"].mesh.primitives[0].material
doAssert untexturedMat.baseColorPlaceholder
doAssert untexturedMat.metallicRoughnessPlaceholder
doAssert untexturedMat.normalPlaceholder
doAssert untexturedMat.occlusionPlaceholder
doAssert untexturedMat.emissivePlaceholder

writeGLB(untexturedModel.root, placeholderPath, iwmExternal)
doAssert fileExists(placeholderPath)
# A written texture would land next to the .glb as a sidecar image.
for kind, path in walkDir(placeholderDir):
doAssert path.extractFilename() in [
"untextured.gltf", "untextured.bin", "placeholder.glb"
], "placeholder written as a texture: " & path
let placeholderModel = readGltfFile(placeholderPath)
let placeholderMat =
placeholderModel.root["Untextured"].mesh.primitives[0].material
doAssert placeholderMat.baseColorPlaceholder
doAssert placeholderMat.emissivePlaceholder

echo "Testing EXT_texture_webp source selection."
let
webpOutDir = joinPath(tmpDir, "out_webp")
Expand Down