main
Colby Knox Board GLB with the Gerber artwork (mask, copper, ENIG, silkscreen) baked into its top and bottom faces and real materia… f6f12e0 4d ago
#!/usr/bin/env python3
"""Bake the board's Gerber artwork and real finishes into the Altium board GLB, so any glTF viewer (the wiki's
3D viewer) shows what the renders show.

    bake_board_glb.py <board.glb> <top.png> <bottom.png> <out.glb>

- Materials: the STEP export leaves every material at glTF's default metallicFactor 1.0, so plastic, FR4 and
  silkscreen render as polished metal. Metal only for gold, tin and nickel tones (the render's rule), else
  metallic 0.05; roughness 0.35 for metal, 0.55 otherwise.
- Faces: top.png / bottom.png (textures.py: mask, copper, ENIG openings, silkscreen, holes from the Gerbers and
  drill file) as two alpha-masked quads, 12 um above the board's top face (z 0) and below its bottom face
  (z -0.4115 mm), over the 28 x 20 mm outline. bottom.png is already mirrored for viewing from below.
"""
import struct, sys
import numpy as np
from pygltflib import GLTF2, Image, Texture, Sampler, Material, PbrMetallicRoughness, TextureInfo, Mesh, Primitive, \
    Attributes, Node, Accessor, BufferView

src, top_png, bot_png, dst = sys.argv[1:5]
W, H, ZT, ZB, EPS = 0.028, 0.020, 0.0, -0.00041148, 0.000012
g = GLTF2().load(src)
blob = bytearray(g.binary_blob())

# materials: real finishes instead of all-metal
for m in g.materials:
    r, gg, b = m.pbrMetallicRoughness.baseColorFactor[:3]
    metal = (r > 0.55 and gg > 0.35 and b < 0.3) or (r > 0.5 and abs(r - gg) < 0.06 and abs(gg - b) < 0.08 and r < 0.8)
    m.pbrMetallicRoughness.metallicFactor = 0.85 if metal else 0.05
    m.pbrMetallicRoughness.roughnessFactor = 0.35 if metal else 0.55


def add(data, target=None):
    while len(blob) % 4:
        blob.append(0)
    off = len(blob); blob.extend(data)
    g.bufferViews.append(BufferView(buffer=0, byteOffset=off, byteLength=len(data), target=target))
    return len(g.bufferViews) - 1


def quad(z, normal_z, flip_u, img_bytes, name):
    # corners (x, y): u runs along x (reversed on the bottom, which the texture already mirrors), glTF v = 0 at the image top
    xy = [(0, 0), (W, 0), (W, H), (0, H)]
    pos = np.array([[x, y, z] for x, y in xy], np.float32)
    nrm = np.array([[0, 0, normal_z]] * 4, np.float32)
    uv = np.array([[(1 - x / W) if flip_u else x / W, 1 - y / H] for x, y in xy], np.float32)
    idx = np.array([0, 1, 2, 0, 2, 3] if normal_z > 0 else [0, 2, 1, 0, 3, 2], np.uint16)
    acc = []
    for arr, typ, ct, tgt in ((pos, "VEC3", 5126, 34962), (nrm, "VEC3", 5126, 34962), (uv, "VEC2", 5126, 34962), (idx, "SCALAR", 5123, 34963)):
        bv = add(arr.tobytes(), tgt)
        a = Accessor(bufferView=bv, componentType=ct, count=len(arr), type=typ)
        if arr is pos:
            a.min, a.max = pos.min(0).tolist(), pos.max(0).tolist()
        g.accessors.append(a); acc.append(len(g.accessors) - 1)
    g.images.append(Image(bufferView=add(img_bytes), mimeType="image/png", name=name))
    g.samplers = g.samplers or []
    if not g.samplers:
        g.samplers.append(Sampler(magFilter=9729, minFilter=9987, wrapS=33071, wrapT=33071))
    g.textures.append(Texture(source=len(g.images) - 1, sampler=0))
    g.materials.append(Material(name=name, alphaMode="MASK", alphaCutoff=0.5, doubleSided=False,
                                pbrMetallicRoughness=PbrMetallicRoughness(baseColorTexture=TextureInfo(index=len(g.textures) - 1),
                                                                          metallicFactor=0.0, roughnessFactor=0.32)))
    g.meshes.append(Mesh(name=name, primitives=[Primitive(attributes=Attributes(POSITION=acc[0], NORMAL=acc[1], TEXCOORD_0=acc[2]),
                                                          indices=acc[3], material=len(g.materials) - 1)]))
    g.nodes.append(Node(name=name, mesh=len(g.meshes) - 1))
    g.nodes[0].children.append(len(g.nodes) - 1)


g.images = g.images or []; g.textures = g.textures or []
quad(ZT + EPS, 1, False, open(top_png, "rb").read(), "Board top (mask, copper, silkscreen from the Gerbers)")
quad(ZB - EPS, -1, True, open(bot_png, "rb").read(), "Board bottom (mask, copper, silkscreen from the Gerbers)")
g.buffers[0].byteLength = len(blob)
g.set_binary_blob(bytes(blob))
g.save_binary(dst)
print(dst, len(blob), "bytes buffer;", len(g.materials), "materials,", len(g.meshes), "meshes")