2022-10-12 19:14:01 +00:00
|
|
|
import os
|
|
|
|
import rasterio
|
2022-10-20 02:01:21 +00:00
|
|
|
from rasterio.io import MemoryFile
|
2022-10-12 19:14:01 +00:00
|
|
|
import warnings
|
|
|
|
import numpy as np
|
|
|
|
import pygltflib
|
2023-01-25 18:08:59 +00:00
|
|
|
from opendm import system
|
|
|
|
from opendm import io
|
2022-10-12 19:14:01 +00:00
|
|
|
|
|
|
|
warnings.filterwarnings("ignore", category=rasterio.errors.NotGeoreferencedWarning)
|
|
|
|
|
2023-01-27 20:23:13 +00:00
|
|
|
|
2022-10-12 19:14:01 +00:00
|
|
|
def load_obj(obj_path, _info=print):
|
|
|
|
if not os.path.isfile(obj_path):
|
|
|
|
raise IOError("Cannot open %s" % obj_path)
|
|
|
|
|
|
|
|
obj_base_path = os.path.dirname(os.path.abspath(obj_path))
|
|
|
|
obj = {
|
|
|
|
'materials': {},
|
|
|
|
}
|
|
|
|
vertices = []
|
|
|
|
uvs = []
|
|
|
|
normals = []
|
|
|
|
|
|
|
|
faces = {}
|
|
|
|
current_material = "_"
|
|
|
|
|
|
|
|
with open(obj_path) as f:
|
|
|
|
_info("Loading %s" % obj_path)
|
|
|
|
|
|
|
|
for line in f:
|
|
|
|
if line.startswith("mtllib "):
|
|
|
|
# Materials
|
|
|
|
mtl_file = "".join(line.split()[1:]).strip()
|
|
|
|
obj['materials'].update(load_mtl(mtl_file, obj_base_path, _info=_info))
|
|
|
|
elif line.startswith("v "):
|
|
|
|
# Vertices
|
|
|
|
vertices.append(list(map(float, line.split()[1:4])))
|
|
|
|
elif line.startswith("vt "):
|
|
|
|
# UVs
|
|
|
|
uvs.append(list(map(float, line.split()[1:3])))
|
|
|
|
elif line.startswith("vn "):
|
|
|
|
normals.append(list(map(float, line.split()[1:4])))
|
|
|
|
elif line.startswith("usemtl "):
|
|
|
|
mtl_name = "".join(line.split()[1:]).strip()
|
|
|
|
if not mtl_name in obj['materials']:
|
|
|
|
raise Exception("%s material is missing" % mtl_name)
|
|
|
|
|
|
|
|
current_material = mtl_name
|
|
|
|
elif line.startswith("f "):
|
|
|
|
if current_material not in faces:
|
|
|
|
faces[current_material] = []
|
|
|
|
|
|
|
|
a,b,c = line.split()[1:]
|
|
|
|
|
|
|
|
if a.count("/") == 2:
|
|
|
|
av, at, an = map(int, a.split("/")[0:3])
|
|
|
|
bv, bt, bn = map(int, b.split("/")[0:3])
|
|
|
|
cv, ct, cn = map(int, c.split("/")[0:3])
|
|
|
|
|
|
|
|
faces[current_material].append((av - 1, bv - 1, cv - 1, at - 1, bt - 1, ct - 1, an - 1, bn - 1, cn - 1))
|
|
|
|
else:
|
|
|
|
av, at = map(int, a.split("/")[0:2])
|
|
|
|
bv, bt = map(int, b.split("/")[0:2])
|
|
|
|
cv, ct = map(int, c.split("/")[0:2])
|
|
|
|
faces[current_material].append((av - 1, bv - 1, cv - 1, at - 1, bt - 1, ct - 1))
|
|
|
|
|
|
|
|
obj['vertices'] = np.array(vertices, dtype=np.float32)
|
|
|
|
obj['uvs'] = np.array(uvs, dtype=np.float32)
|
|
|
|
obj['normals'] = np.array(normals, dtype=np.float32)
|
|
|
|
obj['faces'] = faces
|
|
|
|
|
2023-01-26 18:14:05 +00:00
|
|
|
obj['materials'] = convert_materials_to_jpeg(obj['materials'])
|
|
|
|
|
2022-10-12 19:14:01 +00:00
|
|
|
return obj
|
|
|
|
|
2023-01-26 18:14:05 +00:00
|
|
|
def convert_materials_to_jpeg(materials):
|
|
|
|
|
|
|
|
min_value = 0
|
|
|
|
value_range = 0
|
|
|
|
skip_conversion = False
|
|
|
|
|
|
|
|
for mat in materials:
|
|
|
|
image = materials[mat]
|
|
|
|
|
|
|
|
# Stop here, assuming all other materials are also uint8
|
|
|
|
if image.dtype == np.uint8:
|
|
|
|
skip_conversion = True
|
|
|
|
break
|
|
|
|
|
|
|
|
# Find common min/range values
|
|
|
|
try:
|
|
|
|
data_range = np.iinfo(image.dtype)
|
|
|
|
min_value = min(min_value, 0)
|
|
|
|
value_range = max(value_range, float(data_range.max) - float(data_range.min))
|
|
|
|
except ValueError:
|
|
|
|
# For floats use the actual range of the image values
|
|
|
|
min_value = min(min_value, float(image.min()))
|
|
|
|
value_range = max(value_range, float(image.max()) - min_value)
|
|
|
|
|
|
|
|
if value_range == 0:
|
|
|
|
value_range = 255 # Should never happen
|
|
|
|
|
|
|
|
for mat in materials:
|
|
|
|
image = materials[mat]
|
|
|
|
|
|
|
|
if not skip_conversion:
|
|
|
|
image = image.astype(np.float32)
|
|
|
|
image -= min_value
|
|
|
|
image *= 255.0 / value_range
|
|
|
|
np.around(image, out=image)
|
|
|
|
image[image > 255] = 255
|
|
|
|
image[image < 0] = 0
|
|
|
|
image = image.astype(np.uint8)
|
|
|
|
|
|
|
|
with MemoryFile() as memfile:
|
|
|
|
bands, h, w = image.shape
|
|
|
|
bands = min(3, bands)
|
|
|
|
with memfile.open(driver='JPEG', jpeg_quality=90, count=bands, width=w, height=h, dtype=rasterio.dtypes.uint8) as dst:
|
|
|
|
for b in range(1, min(3, bands) + 1):
|
|
|
|
dst.write(image[b - 1], b)
|
|
|
|
memfile.seek(0)
|
|
|
|
materials[mat] = memfile.read()
|
|
|
|
|
|
|
|
return materials
|
|
|
|
|
2022-10-12 19:14:01 +00:00
|
|
|
def load_mtl(mtl_file, obj_base_path, _info=print):
|
|
|
|
mtl_file = os.path.join(obj_base_path, mtl_file)
|
|
|
|
|
|
|
|
if not os.path.isfile(mtl_file):
|
|
|
|
raise IOError("Cannot open %s" % mtl_file)
|
|
|
|
|
|
|
|
mats = {}
|
|
|
|
current_mtl = ""
|
|
|
|
|
|
|
|
with open(mtl_file) as f:
|
|
|
|
for line in f:
|
|
|
|
if line.startswith("newmtl "):
|
|
|
|
current_mtl = "".join(line.split()[1:]).strip()
|
|
|
|
elif line.startswith("map_Kd ") and current_mtl:
|
|
|
|
map_kd_filename = "".join(line.split()[1:]).strip()
|
|
|
|
map_kd = os.path.join(obj_base_path, map_kd_filename)
|
|
|
|
if not os.path.isfile(map_kd):
|
|
|
|
raise IOError("Cannot open %s" % map_kd)
|
|
|
|
|
|
|
|
_info("Loading %s" % map_kd_filename)
|
2023-01-26 18:14:05 +00:00
|
|
|
with rasterio.open(map_kd, 'r') as src:
|
|
|
|
mats[current_mtl] = src.read()
|
2022-10-12 19:14:01 +00:00
|
|
|
return mats
|
|
|
|
|
2022-10-21 03:55:09 +00:00
|
|
|
def paddedBuffer(buf, boundary):
|
|
|
|
r = len(buf) % boundary
|
|
|
|
if r == 0:
|
|
|
|
return buf
|
|
|
|
pad = boundary - r
|
|
|
|
return buf + b'\x00' * pad
|
2022-10-12 19:14:01 +00:00
|
|
|
|
2023-01-25 18:08:59 +00:00
|
|
|
def obj2glb(input_obj, output_glb, rtc=(None, None), draco_compression=True, _info=print):
|
2022-10-21 03:55:09 +00:00
|
|
|
_info("Converting %s --> %s" % (input_obj, output_glb))
|
2022-10-12 19:14:01 +00:00
|
|
|
obj = load_obj(input_obj, _info=_info)
|
2022-10-21 03:55:09 +00:00
|
|
|
|
2022-10-12 19:14:01 +00:00
|
|
|
vertices = obj['vertices']
|
|
|
|
uvs = obj['uvs']
|
2022-10-21 03:55:09 +00:00
|
|
|
# Flip Y
|
|
|
|
uvs = (([0, 1] - (uvs * [0, 1])) + uvs * [1, 0]).astype(np.float32)
|
2022-10-20 02:01:21 +00:00
|
|
|
normals = obj['normals']
|
2022-10-12 19:14:01 +00:00
|
|
|
|
2022-10-21 03:55:09 +00:00
|
|
|
binary = b''
|
|
|
|
accessors = []
|
|
|
|
bufferViews = []
|
|
|
|
primitives = []
|
|
|
|
materials = []
|
|
|
|
textures = []
|
|
|
|
images = []
|
|
|
|
|
|
|
|
bufOffset = 0
|
|
|
|
def addBufferView(buf, target=None):
|
|
|
|
nonlocal bufferViews, bufOffset
|
|
|
|
bufferViews += [pygltflib.BufferView(
|
|
|
|
buffer=0,
|
|
|
|
byteOffset=bufOffset,
|
|
|
|
byteLength=len(buf),
|
|
|
|
target=target,
|
|
|
|
)]
|
|
|
|
bufOffset += len(buf)
|
|
|
|
return len(bufferViews) - 1
|
|
|
|
|
|
|
|
for material in obj['faces'].keys():
|
|
|
|
faces = obj['faces'][material]
|
|
|
|
faces = np.array(faces, dtype=np.uint32)
|
|
|
|
|
|
|
|
prim_vertices = vertices[faces[:,0:3].flatten()]
|
|
|
|
prim_uvs = uvs[faces[:,3:6].flatten()]
|
|
|
|
|
|
|
|
if faces.shape[1] == 9:
|
|
|
|
prim_normals = normals[faces[:,6:9].flatten()]
|
|
|
|
normals_blob = prim_normals.tobytes()
|
|
|
|
else:
|
|
|
|
prim_normals = None
|
|
|
|
normals_blob = None
|
|
|
|
|
|
|
|
vertices_blob = prim_vertices.tobytes()
|
|
|
|
uvs_blob = prim_uvs.tobytes()
|
|
|
|
|
|
|
|
binary += vertices_blob + uvs_blob
|
|
|
|
if normals_blob is not None:
|
|
|
|
binary += normals_blob
|
2022-10-21 04:09:19 +00:00
|
|
|
|
2022-10-21 03:55:09 +00:00
|
|
|
verticesBufferView = addBufferView(vertices_blob, pygltflib.ARRAY_BUFFER)
|
|
|
|
uvsBufferView = addBufferView(uvs_blob, pygltflib.ARRAY_BUFFER)
|
2022-10-21 04:09:19 +00:00
|
|
|
normalsBufferView = None
|
2022-10-21 03:55:09 +00:00
|
|
|
if normals_blob is not None:
|
|
|
|
normalsBufferView = addBufferView(normals_blob, pygltflib.ARRAY_BUFFER)
|
2022-10-21 04:09:19 +00:00
|
|
|
|
2022-10-21 03:55:09 +00:00
|
|
|
accessors += [
|
2022-10-12 19:14:01 +00:00
|
|
|
pygltflib.Accessor(
|
2022-10-21 03:55:09 +00:00
|
|
|
bufferView=verticesBufferView,
|
2022-10-12 19:14:01 +00:00
|
|
|
componentType=pygltflib.FLOAT,
|
2022-10-21 03:55:09 +00:00
|
|
|
count=len(prim_vertices),
|
2022-10-12 19:14:01 +00:00
|
|
|
type=pygltflib.VEC3,
|
2022-10-21 03:55:09 +00:00
|
|
|
max=prim_vertices.max(axis=0).tolist(),
|
|
|
|
min=prim_vertices.min(axis=0).tolist(),
|
2022-10-12 19:14:01 +00:00
|
|
|
),
|
|
|
|
pygltflib.Accessor(
|
2022-10-21 03:55:09 +00:00
|
|
|
bufferView=uvsBufferView,
|
2022-10-12 19:14:01 +00:00
|
|
|
componentType=pygltflib.FLOAT,
|
2022-10-21 03:55:09 +00:00
|
|
|
count=len(prim_uvs),
|
2022-10-12 19:14:01 +00:00
|
|
|
type=pygltflib.VEC2,
|
2022-10-21 03:55:09 +00:00
|
|
|
max=prim_uvs.max(axis=0).tolist(),
|
|
|
|
min=prim_uvs.min(axis=0).tolist(),
|
2022-10-12 19:14:01 +00:00
|
|
|
),
|
2022-10-21 03:55:09 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
if prim_normals is not None:
|
|
|
|
accessors += [
|
|
|
|
pygltflib.Accessor(
|
|
|
|
bufferView=normalsBufferView,
|
|
|
|
componentType=pygltflib.FLOAT,
|
|
|
|
count=len(prim_normals),
|
|
|
|
type=pygltflib.VEC3,
|
|
|
|
max=prim_normals.max(axis=0).tolist(),
|
|
|
|
min=prim_normals.min(axis=0).tolist(),
|
|
|
|
)
|
|
|
|
]
|
|
|
|
|
2022-10-21 04:09:19 +00:00
|
|
|
primitives += [pygltflib.Primitive(
|
|
|
|
attributes=pygltflib.Attributes(POSITION=verticesBufferView, TEXCOORD_0=uvsBufferView, NORMAL=normalsBufferView), material=len(primitives)
|
|
|
|
)]
|
|
|
|
|
|
|
|
for material in obj['faces'].keys():
|
|
|
|
texture_blob = paddedBuffer(obj['materials'][material], 4)
|
|
|
|
binary += texture_blob
|
|
|
|
textureBufferView = addBufferView(texture_blob)
|
|
|
|
|
2022-10-21 03:55:09 +00:00
|
|
|
images += [pygltflib.Image(bufferView=textureBufferView, mimeType="image/jpeg")]
|
|
|
|
textures += [pygltflib.Texture(source=len(images) - 1, sampler=0)]
|
2023-01-27 20:23:13 +00:00
|
|
|
|
|
|
|
mat = pygltflib.Material(pbrMetallicRoughness=pygltflib.PbrMetallicRoughness(baseColorTexture=pygltflib.TextureInfo(index=len(textures) - 1), metallicFactor=0, roughnessFactor=1),
|
2023-01-27 23:33:15 +00:00
|
|
|
alphaMode=pygltflib.OPAQUE)
|
2023-01-27 20:23:13 +00:00
|
|
|
mat.extensions = {
|
|
|
|
'KHR_materials_unlit': {}
|
|
|
|
}
|
|
|
|
materials += [mat]
|
2022-10-21 03:55:09 +00:00
|
|
|
|
|
|
|
gltf = pygltflib.GLTF2(
|
|
|
|
scene=0,
|
|
|
|
scenes=[pygltflib.Scene(nodes=[0])],
|
|
|
|
nodes=[pygltflib.Node(mesh=0)],
|
|
|
|
meshes=[pygltflib.Mesh(
|
|
|
|
primitives=primitives
|
|
|
|
)],
|
|
|
|
materials=materials,
|
|
|
|
textures=textures,
|
|
|
|
samplers=[pygltflib.Sampler(magFilter=pygltflib.LINEAR, minFilter=pygltflib.LINEAR)],
|
|
|
|
images=images,
|
|
|
|
accessors=accessors,
|
|
|
|
bufferViews=bufferViews,
|
|
|
|
buffers=[pygltflib.Buffer(byteLength=len(binary))],
|
2022-10-12 19:14:01 +00:00
|
|
|
)
|
|
|
|
|
2023-01-27 20:23:13 +00:00
|
|
|
gltf.extensionsRequired = ['KHR_materials_unlit']
|
2024-02-09 14:06:02 +00:00
|
|
|
gltf.extensionsUsed = ['KHR_materials_unlit']
|
2023-01-27 20:23:13 +00:00
|
|
|
|
2023-01-25 18:08:59 +00:00
|
|
|
if rtc != (None, None) and len(rtc) >= 2:
|
2024-02-09 14:06:02 +00:00
|
|
|
gltf.extensionsUsed.append('CESIUM_RTC')
|
2023-01-25 18:08:59 +00:00
|
|
|
gltf.extensions = {
|
|
|
|
'CESIUM_RTC': {
|
|
|
|
'center': [float(rtc[0]), float(rtc[1]), 0.0]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-20 02:01:21 +00:00
|
|
|
gltf.set_binary_blob(binary)
|
2022-10-12 19:14:01 +00:00
|
|
|
|
2022-10-21 03:55:09 +00:00
|
|
|
_info("Writing...")
|
2022-10-12 19:14:01 +00:00
|
|
|
gltf.save(output_glb)
|
2022-10-21 03:55:09 +00:00
|
|
|
_info("Wrote %s" % output_glb)
|
2022-10-12 19:14:01 +00:00
|
|
|
|
2023-01-25 18:08:59 +00:00
|
|
|
if draco_compression:
|
|
|
|
_info("Compressing with draco")
|
|
|
|
try:
|
|
|
|
compressed_glb = io.related_file_path(output_glb, postfix="_compressed")
|
|
|
|
system.run('draco_transcoder -i "{}" -o "{}" -qt 16 -qp 16'.format(output_glb, compressed_glb))
|
|
|
|
if os.path.isfile(compressed_glb) and os.path.isfile(output_glb):
|
|
|
|
os.remove(output_glb)
|
|
|
|
os.rename(compressed_glb, output_glb)
|
|
|
|
except Exception as e:
|
|
|
|
log.ODM_WARNING("Cannot compress GLB with draco: %s" % str(e))
|
|
|
|
|
|
|
|
|