110 lines
4.9 KiB
Python
110 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Load a GLB and render it three ways from vertex colour or a baked UV atlas.
|
|
Also prints structural stats (a sanity check that the glTF is well-formed).
|
|
|
|
python3 scripts/render_glb.py in.glb [out.png]
|
|
|
|
Runs anywhere with trimesh + numpy + Pillow (e.g. the t2tex container).
|
|
"""
|
|
import sys, numpy as np, trimesh
|
|
from PIL import Image
|
|
|
|
path = sys.argv[1]; out = sys.argv[2] if len(sys.argv) > 2 else "glb_render.png"
|
|
scene = trimesh.load(path, process=False)
|
|
mesh = scene if isinstance(scene, trimesh.Trimesh) else list(scene.geometry.values())[0]
|
|
|
|
V = np.asarray(mesh.vertices, np.float64)
|
|
F = np.asarray(mesh.faces, np.int64)
|
|
visual_attrs = getattr(mesh.visual, "vertex_attributes", {})
|
|
if "color" in visual_attrs:
|
|
raw = np.asarray(visual_attrs["color"])
|
|
scale = np.iinfo(raw.dtype).max if np.issubdtype(raw.dtype, np.integer) else 1.0
|
|
base_v = np.asarray(raw[:, :3], np.float64) / scale
|
|
# glTF COLOR_0 is already linear.
|
|
base_linear_v = np.clip(base_v, 0, 1)
|
|
custom = getattr(mesh, "vertex_attributes", {})
|
|
if "_METALLIC_ROUGHNESS" in custom:
|
|
mr = np.asarray(custom["_METALLIC_ROUGHNESS"])
|
|
mr_scale = np.iinfo(mr.dtype).max if np.issubdtype(mr.dtype, np.integer) else 1.0
|
|
metal_v = np.asarray(mr[:, 0], np.float64) / mr_scale
|
|
rough_v = np.asarray(mr[:, 1], np.float64) / mr_scale
|
|
else:
|
|
metal_v = np.zeros(len(V)); rough_v = np.full(len(V), 0.6)
|
|
print(f"GLB: {len(V):,} verts {len(F):,} faces COLOR_0 {raw.dtype} "
|
|
f"custom metalRough={'yes' if '_METALLIC_ROUGHNESS' in custom else 'no'}", flush=True)
|
|
else:
|
|
uv = np.asarray(mesh.visual.uv, np.float64)
|
|
mat = mesh.visual.material
|
|
base_img = np.asarray(mat.baseColorTexture.convert("RGB"), np.float64) / 255.0
|
|
try:
|
|
mr_img = np.asarray(mat.metallicRoughnessTexture.convert("RGB"), np.float64) / 255.0
|
|
except Exception:
|
|
mr_img = None
|
|
TH, TW = base_img.shape[:2]
|
|
def sample(img, u, v):
|
|
x = np.clip((u % 1.0) * (TW - 1), 0, TW - 1).astype(np.int32)
|
|
y = np.clip((v % 1.0) * (TH - 1), 0, TH - 1).astype(np.int32)
|
|
return img[y, x]
|
|
base_v = sample(base_img, uv[:, 0], uv[:, 1])
|
|
base_linear_v = np.clip(base_v, 0, 1) ** 2.2
|
|
if mr_img is not None:
|
|
rough_v = sample(mr_img, uv[:, 0], uv[:, 1])[:, 1]
|
|
metal_v = sample(mr_img, uv[:, 0], uv[:, 1])[:, 2]
|
|
else:
|
|
rough_v = np.full(len(V), 0.6); metal_v = np.zeros(len(V))
|
|
print(f"GLB: {len(V):,} verts {len(F):,} faces uv[{uv.min():.3f},{uv.max():.3f}] "
|
|
f"baseColor {base_img.shape} metalRough {None if mr_img is None else mr_img.shape}", flush=True)
|
|
|
|
N = np.zeros_like(V)
|
|
fn = np.cross(V[F[:, 1]] - V[F[:, 0]], V[F[:, 2]] - V[F[:, 0]])
|
|
for k in range(3): np.add.at(N, F[:, k], fn)
|
|
N /= np.linalg.norm(N, axis=1, keepdims=True) + 1e-9
|
|
|
|
SS = 2; W = H = 720 * SS
|
|
c = (V.max(0) + V.min(0)) / 2; Vc = V - c; Vc *= 0.92 / np.abs(Vc).max()
|
|
LC = [(np.array([.32, .55, .77]), np.array([1., .96, .88]), .85),
|
|
(np.array([-.65, .20, .45]), np.array([.70, .80, 1.]), .45),
|
|
(np.array([.10, -.75, .35]), np.array([.85, .85, .90]), .25)]
|
|
|
|
|
|
def Rmat(el, az):
|
|
a, e = np.radians(az), np.radians(el)
|
|
Ry = np.array([[np.cos(a), 0, np.sin(a)], [0, 1, 0], [-np.sin(a), 0, np.cos(a)]])
|
|
Rx = np.array([[1, 0, 0], [0, np.cos(e), -np.sin(e)], [0, np.sin(e), np.cos(e)]])
|
|
return Rx @ Ry
|
|
|
|
|
|
def render(el, az):
|
|
r = Rmat(el, az); P = Vc @ r.T; Nr = N @ r.T
|
|
Nr /= np.linalg.norm(Nr, axis=1, keepdims=True) + 1e-9
|
|
sx = ((P[:, 0]*.5+.5)*(W-1)).astype(np.int32); sy = ((.5-P[:, 1]*.5)*(H-1)).astype(np.int32)
|
|
z = P[:, 2]
|
|
view = np.array([0., 0., 1.]); nv_ = np.abs(Nr @ view)
|
|
F0 = 0.04*(1-metal_v)[:, None] + base_linear_v*metal_v[:, None]
|
|
Fr = F0 + (1-F0)*((1-nv_)[:, None]**5)
|
|
shin = 6.0 + 214.0*np.clip(1-rough_v, 0, 1)**1.5
|
|
sn = (shin+2.0)/(2*np.pi)
|
|
diffuse = np.zeros_like(base_v); specular = np.zeros_like(base_v)
|
|
for Ld, Lc, w in LC:
|
|
l = Ld/np.linalg.norm(Ld); ndl = np.abs(Nr @ l)
|
|
diffuse += Lc[None]*w*ndl[:, None]
|
|
h = l+view; h = h/np.linalg.norm(h); nh = np.abs(Nr @ h)
|
|
specular += Lc[None]*w*(nh[:, None]**shin[:, None])*sn[:, None]*ndl[:, None]
|
|
albedo = base_linear_v*(1-metal_v)[:, None]
|
|
hemi = np.abs(N[:, 1])*0.5+0.5
|
|
amb = (1-hemi)[:, None]*np.array([.20, .19, .22]) + hemi[:, None]*np.array([.55, .58, .66])
|
|
col = np.clip(albedo*(amb*0.55 + diffuse*0.75) + specular*Fr + Fr*0.25, 0, 1)
|
|
img = np.ones((H, W, 3))
|
|
idx = np.argsort(z); sxi, syi, coli = sx[idx], sy[idx], col[idx]
|
|
for dx in range(SS+1):
|
|
for dy in range(SS+1):
|
|
xx = sxi+dx; yy = syi+dy
|
|
m = (xx >= 0) & (xx < W) & (yy >= 0) & (yy < H)
|
|
img[yy[m], xx[m]] = coli[m]
|
|
return (img**(1/2.2)*255).reshape(H//SS, SS, W//SS, SS, 3).mean((1, 3)).astype(np.uint8)
|
|
|
|
|
|
row = np.concatenate([render(12, 25), render(12, 150), render(65, 20)], 1)
|
|
Image.fromarray(row).save(out)
|
|
print("wrote", out, flush=True)
|