Initial commit
This commit is contained in:
commit
a437c068c8
64 changed files with 561683 additions and 0 deletions
6
scripts/.env.example
Normal file
6
scripts/.env.example
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
BLENDER_BIN=''
|
||||
# Optional override. If empty, scripts auto-detect the module root from this file location.
|
||||
SPATH=
|
||||
BACKUP_SETTINGS_PATH=/var/www/data/project/web/sites/default/settings.php
|
||||
RENDER_RESOLUTION='1024x1024x16'
|
||||
RENDER_SAMPLES='20'
|
||||
14
scripts/2gltf2/2gltf2.bat
Executable file
14
scripts/2gltf2/2gltf2.bat
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
ECHO OFF
|
||||
|
||||
IF "%1"=="" GOTO USAGE
|
||||
|
||||
"C:/Program Files/Blender Foundation/Blender 2.93/blender.exe" -b -P 2gltf2.py -- %1
|
||||
GOTO END
|
||||
|
||||
:USAGE
|
||||
ECHO To glTF 2.0 converter.
|
||||
ECHO Supported file formats: .abc .blend .dae .fbx. .obj .ply .stl .wrl .x3d
|
||||
ECHO.
|
||||
ECHO 2gltf2.bat [filename]
|
||||
|
||||
:END
|
||||
127
scripts/2gltf2/2gltf2.py
Executable file
127
scripts/2gltf2/2gltf2.py
Executable file
|
|
@ -0,0 +1,127 @@
|
|||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) since 2017 UX3D GmbH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
|
||||
#
|
||||
# Imports
|
||||
#
|
||||
|
||||
import bpy
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
if '--' in sys.argv:
|
||||
argv = sys.argv[sys.argv.index('--') + 1:]
|
||||
parser=argparse.ArgumentParser()
|
||||
parser.add_argument("--input", help="Input file path")
|
||||
parser.add_argument("--ext", help="Extenstion of imported file")
|
||||
parser.add_argument("--org_ext", help="Original extenstion of imported file")
|
||||
parser.add_argument("--output", help="Output file path")
|
||||
parser.add_argument("--is_archive", help="Importing archive flag")
|
||||
parser.add_argument("--resolution", help="Resolution preview images")
|
||||
parser.add_argument("--samples", help="Samples rendering quality")
|
||||
parser.add_argument("--compression", help="Compress object")
|
||||
parser.add_argument("--compression_level", help="Compress object level")
|
||||
args = parser.parse_known_args(argv)[0]
|
||||
|
||||
print('input: ', args.input)
|
||||
print('ext: ', args.ext)
|
||||
print('org_ext: ', args.org_ext)
|
||||
print('output: ', args.output)
|
||||
print('is_archive: ', args.is_archive)
|
||||
print('resolution: ', args.resolution)
|
||||
print('samples: ', args.samples)
|
||||
print('compression: ', args.compression)
|
||||
print('compression_level: ', args.compression_level)
|
||||
|
||||
#
|
||||
# Globals
|
||||
#
|
||||
|
||||
#
|
||||
# Functions
|
||||
#
|
||||
current_directory = os.getcwd()
|
||||
compression = "false"
|
||||
compression_level = 3
|
||||
|
||||
if args.ext:
|
||||
extension = args.ext
|
||||
if extension == "gltf":
|
||||
format = "GLTF_EMBEDDED"
|
||||
else:
|
||||
format = "GLB"
|
||||
|
||||
if args.org_ext:
|
||||
original_extension = args.ext
|
||||
|
||||
if args.compression:
|
||||
compression = args.compression
|
||||
|
||||
if args.compression_level:
|
||||
compression_level = int(args.compression_level)
|
||||
|
||||
root, current_extension = os.path.splitext(args.input)
|
||||
current_basename = os.path.basename(root)
|
||||
|
||||
if current_extension == ".abc" or current_extension == ".blend" or current_extension == ".dae" or current_extension == ".fbx" or current_extension == ".obj" or current_extension == ".ply" or current_extension == ".stl" or current_extension == ".wrl" or current_extension == ".x3d":
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
if current_extension == ".abc":
|
||||
bpy.ops.wm.alembic_import(filepath=args.input)
|
||||
|
||||
if current_extension == ".blend":
|
||||
bpy.ops.wm.open_mainfile(filepath=args.input)
|
||||
|
||||
if current_extension == ".dae":
|
||||
bpy.ops.wm.collada_import(filepath=args.input)
|
||||
|
||||
if current_extension == ".fbx":
|
||||
bpy.ops.import_scene.fbx(filepath=args.input)
|
||||
|
||||
if current_extension == ".obj":
|
||||
bpy.ops.wm.obj_import(filepath=args.input)
|
||||
|
||||
if current_extension == ".ply":
|
||||
bpy.ops.wm.ply_import(filepath=args.input)
|
||||
|
||||
if current_extension == ".stl":
|
||||
bpy.ops.import_mesh.stl(filepath=args.input)
|
||||
|
||||
if current_extension == ".wrl" or current_extension == ".x3d":
|
||||
bpy.ops.import_scene.x3d(filepath=args.input)
|
||||
|
||||
#
|
||||
if args.output:
|
||||
export_file = str(args.output)
|
||||
#export_file = root + current_basename + "." + extension
|
||||
else:
|
||||
root = root[::-1].replace(current_basename[::-1], "", 1)[::-1]
|
||||
export_file = root + "gltf/" + current_basename + "." + extension
|
||||
print("Writing: '" + export_file + "'")
|
||||
if compression == 'true':
|
||||
bpy.ops.export_scene.gltf(filepath=export_file,export_format=format,export_draco_mesh_compression_enable=True,export_draco_mesh_compression_level=compression_level)
|
||||
else:
|
||||
bpy.ops.export_scene.gltf(filepath=export_file,export_format=format)
|
||||
9
scripts/2gltf2/2gltf2.sh
Executable file
9
scripts/2gltf2/2gltf2.sh
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
#!/bin/bash
|
||||
#TODO PATH for Blender
|
||||
if [ "$#" -ge 1 ]
|
||||
then
|
||||
/var/lib/snapd/snap/blender/current/blender -b -P 2gltf2.py -- "$1" "$2" "$3"
|
||||
else
|
||||
echo Supported file formats: .abc .blend .dae .fbx. .obj .ply .stl .wrl .x3d
|
||||
echo 2gltf2.sh [filename]
|
||||
fi
|
||||
BIN
scripts/2gltf2/glTF.png
Executable file
BIN
scripts/2gltf2/glTF.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
837
scripts/CityGML2OBJv2/CityGML2OBJs.py
Normal file
837
scripts/CityGML2OBJv2/CityGML2OBJs.py
Normal file
|
|
@ -0,0 +1,837 @@
|
|||
##!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import config
|
||||
# The MIT License (MIT)
|
||||
|
||||
# This code is part of the CityGML2OBJs package
|
||||
|
||||
# Copyright (c) 2014
|
||||
# Filip Biljecki
|
||||
# Delft University of Technology
|
||||
# fbiljecki@gmail.com
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import markup3dmodule
|
||||
import polygon3dmodule
|
||||
import componentseparationmodule as csm
|
||||
from lxml import etree
|
||||
import os
|
||||
import argparse
|
||||
import glob
|
||||
import numpy as np
|
||||
import itertools
|
||||
import matplotlib.pyplot as plt
|
||||
import CityGMLTranslation as cgt
|
||||
from decimal import Decimal
|
||||
from config import setVersion
|
||||
import time
|
||||
|
||||
# -- ARGUMENTS
|
||||
# -i -- input directory (it will read and convert ALL CityGML files in a directory)
|
||||
# -o -- output directory (it will output the generated OBJs in that directory in the way that Delft.gml becomes Delft.obj)
|
||||
# -- SETTINGS of the converter (can be combined):
|
||||
# -s 0 (default) -- converts all geometries in one class in one file under the same object (plain OBJ file).
|
||||
# -s 1 -- differentiate between semantics, output each semantic class as one file, e.g. Delft-WallSurface.obj. Please note that in this case the grouped "plain" OBJ is not generated.
|
||||
# if no thematic boundaries are found, this option is ignored.
|
||||
# -g 0 (default) -- keeps all objects in the same bin.
|
||||
# -g 1 -- it creates one object for every building.
|
||||
# -v 1 -- validation
|
||||
# -p 1 -- skip triangulation and write polygons. Polys with interior not supported.
|
||||
# -t 1 -- translation (reduction) of coordinates so the smallest vertex (one with the minimum coordinates) is at (0, 0)
|
||||
# -a 1 or 2 or 3 -- this is a very custom setting for adding the texture based on attributes, here you can see the settings for my particular case of the solar radiation. By default it is off.
|
||||
|
||||
# -- Text to be printed at the beginning of each OBJ
|
||||
header = """# Converted from CityGML to OBJ with CityGML2OBJs.
|
||||
# Conversion tool developed by Filip Biljecki, TU Delft <fbiljecki@gmail.com>, see more at Github:
|
||||
# https://github.com/tudelft3d/CityGML2OBJs
|
||||
#
|
||||
"""
|
||||
|
||||
|
||||
def get_index(point, list_vertices, shift=0):
|
||||
"""Index the vertices.
|
||||
The third option is for incorporating a local index (building-level) to the global one (dataset-level)."""
|
||||
global vertices
|
||||
"""Unique identifier and indexer of vertices."""
|
||||
if point in list_vertices:
|
||||
return list_vertices.index(point) + 1 + shift, list_vertices
|
||||
else:
|
||||
list_vertices.append(point)
|
||||
return list_vertices.index(point) + 1 + shift, list_vertices
|
||||
|
||||
|
||||
def write_vertices(list_vertices, cla):
|
||||
"""Write the vertices in the OBJ format."""
|
||||
global vertices_output
|
||||
for each in list_vertices:
|
||||
vertices_output[cla].append("v" + " " + str(each[0]) + " " + str(each[1]) + " " + str(each[2]) + "\n")
|
||||
|
||||
|
||||
def remove_reccuring(list_vertices):
|
||||
"""Removes recurring vertices, which messes up the triangulation.
|
||||
Inspired by http://stackoverflow.com/a/1143432"""
|
||||
# last_point = list_vertices[-1]
|
||||
list_vertices_without_last = list_vertices[:-1]
|
||||
found = set()
|
||||
for item in list_vertices_without_last:
|
||||
if str(item) not in found:
|
||||
yield item
|
||||
found.add(str(item))
|
||||
|
||||
|
||||
def poly_to_obj(poly, cl, material=None):
|
||||
"""Main conversion function of one polygon to one or more faces in OBJ,
|
||||
in a specific semantic class. Supports assigning a material."""
|
||||
global local_vertices
|
||||
global vertices
|
||||
global face_output
|
||||
# -- Decompose the polygon into exterior and interior
|
||||
e, i = markup3dmodule.polydecomposer(poly)
|
||||
# -- Points forming the exterior LinearRing
|
||||
epoints = markup3dmodule.GMLpoints(e[0])
|
||||
# print(epoints)
|
||||
# -- Clean recurring points, except the last one
|
||||
last_ep = epoints[-1]
|
||||
epoints_clean = list(remove_reccuring(epoints))
|
||||
epoints_clean.append(last_ep)
|
||||
# print("epoints: ", epoints)
|
||||
# print("epoints_clean: ", epoints_clean)
|
||||
|
||||
# -- LinearRing(s) forming the interior
|
||||
irings = []
|
||||
for iring in i:
|
||||
ipoints = markup3dmodule.GMLpoints(iring)
|
||||
# -- Clean them in the same manner as the exterior ring
|
||||
last_ip = ipoints[-1]
|
||||
ipoints_clean = list(remove_reccuring(ipoints))
|
||||
ipoints_clean.append(last_ip)
|
||||
irings.append(ipoints_clean)
|
||||
# print("irings: ", irings)
|
||||
# -- If the polygon validation option is enabled
|
||||
if VALIDATION:
|
||||
# -- Check the polygon
|
||||
valid = polygon3dmodule.isPolyValid(epoints_clean, True)
|
||||
if valid:
|
||||
for iring in irings:
|
||||
if not polygon3dmodule.isPolyValid(iring, False):
|
||||
valid = False
|
||||
# -- If everything is valid send them to the Delaunay triangulation
|
||||
if valid:
|
||||
if SKIPTRI:
|
||||
# -- Triangulation is skipped, polygons are converted directly to faces
|
||||
# -- The last point is removed since it's equal to the first one
|
||||
t = [epoints_clean[:-1]]
|
||||
else:
|
||||
# -- Triangulate polys
|
||||
# t = polygon3dmodule.triangulation(epoints, irings)
|
||||
try:
|
||||
t = polygon3dmodule.triangulation(epoints_clean, irings)
|
||||
except:
|
||||
t = []
|
||||
# t = polygon3dmodule.triangulation(epoints_clean, irings)
|
||||
|
||||
# -- Process the triangles/polygons
|
||||
for tri in t:
|
||||
# -- Face marker
|
||||
f = "f "
|
||||
# -- For each point in the triangle/polygon (face) get the index "v" or add it to the index
|
||||
for ep in range(0, len(tri)):
|
||||
v, local_vertices[cl] = get_index(tri[ep], local_vertices[cl], len(vertices[cl]))
|
||||
f += str(v) + " "
|
||||
# -- Add the material if invoked
|
||||
if material:
|
||||
face_output[cl].append("usemtl " + str(mtl(material, min_value, max_value, res)) + str("\n"))
|
||||
# -- Store all together
|
||||
face_output[cl].append(f + "\n")
|
||||
else:
|
||||
# Get the gml:id of the Polygon if it exists
|
||||
polyid = poly.xpath("@g:id", namespaces={'g': ns_gml})
|
||||
if polyid:
|
||||
polyid = polyid[0]
|
||||
print("\t\t!! Detected an invalid polygon (%s). Skipping..." % polyid)
|
||||
else:
|
||||
print("\t\t!! Detected an invalid polygon. Skipping...")
|
||||
|
||||
else:
|
||||
# -- Do exactly the same, but without the validation
|
||||
try:
|
||||
if SKIPTRI:
|
||||
t = [epoints_clean[:-1]]
|
||||
else:
|
||||
t = polygon3dmodule.triangulation(epoints_clean, irings)
|
||||
|
||||
|
||||
except:
|
||||
|
||||
t = []
|
||||
|
||||
for tri in t:
|
||||
f = "f "
|
||||
for ep in range(0, len(tri)):
|
||||
v, local_vertices[cl] = get_index(tri[ep], local_vertices[cl], len(vertices[cl]))
|
||||
f += str(v) + " "
|
||||
|
||||
if material:
|
||||
face_output[cl].append("usemtl " + str(mtl(material, min_value, max_value, res)) + str("\n"))
|
||||
face_output[cl].append(f + "\n")
|
||||
|
||||
|
||||
# -- Parse command-line arguments
|
||||
PARSER = argparse.ArgumentParser(description='Convert a CityGML to OBJ.')
|
||||
PARSER.add_argument('-i', '--directory',
|
||||
help='Directory containing CityGML file(s).', required=True)
|
||||
PARSER.add_argument('-o', '--results',
|
||||
help='Directory where the OBJ file(s) should be written.', required=True)
|
||||
PARSER.add_argument('-s', '--semantics',
|
||||
help='Write one OBJ (0) or multiple OBJ per semantic class (1). 0 is default.', required=False)
|
||||
PARSER.add_argument('-g', '--grouping',
|
||||
help='Writes all buildings in one group (0) or multiple groups (1). 0 is default.', required=False)
|
||||
PARSER.add_argument('-a', '--attribute',
|
||||
help='Creates a texture regarding the value of an attribute of the surface. No material is default.',
|
||||
required=False)
|
||||
PARSER.add_argument('-v', '--validation',
|
||||
help='Validates polygons, and if they are not valid give a warning and skip them. No validation is default.',
|
||||
required=False)
|
||||
PARSER.add_argument('-t', '--translate',
|
||||
help='Translates all vertices, so that the smallest vertex is at zero. No translation is default.',
|
||||
required=False)
|
||||
PARSER.add_argument('-p', '--polypreserve',
|
||||
help='Skip the triangulation (preserve polygons). Triangulation is default.', required=False)
|
||||
|
||||
# Changes by Th_Fr: 2 New optional parameters added, see description for details!
|
||||
PARSER.add_argument('-tC', '--translateCityGML',
|
||||
help='Perform a Translation of the CityGML Dataset into a local CRS before further processing. No translation is default.',
|
||||
required=False)
|
||||
|
||||
PARSER.add_argument('-tCw', '--translateCityGMLwrite',
|
||||
help='Perform a Translation of the CityGML Dataset into a local CRS before further processing. The translation parameters are stored in a designated .txt file. No Translation is default ',
|
||||
required=False)
|
||||
|
||||
PARSER.add_argument('-sepC', '--separateComponents',
|
||||
help='Save each building component into an individual file with the filename serving as an identifier.',
|
||||
required=False)
|
||||
|
||||
PARSER.add_argument('-appW', '--approximateWindows',
|
||||
help='Approximate windows by their convex hulls to save some processing time.',
|
||||
required=False)
|
||||
|
||||
PARSER.add_argument('-addBB', '--addBoundingBox',
|
||||
help='Add small triangles defining the bounding box to each of the components.',
|
||||
required=False)
|
||||
|
||||
# Todo: Neue funktion muss noch fertig implementiert werden
|
||||
PARSER.add_argument('-importBB', '--importBoundingBox',
|
||||
help='Add small triangles defining an imported bounding box to each of the components.',
|
||||
required=False)
|
||||
|
||||
PARSER.add_argument('-addBBJSON', '--addBoundingBoxJSON',
|
||||
help='The bounding box of the building is additionally saved in a designated json-file',
|
||||
required=False)
|
||||
|
||||
PARSER.add_argument('-tbw', '--translateBuildingWise', # todo: implementation yet to be completed
|
||||
help='Translate into a local coordinate system building-wise.',
|
||||
required=False)
|
||||
|
||||
# End of changes by Th_Fr
|
||||
|
||||
ARGS = vars(PARSER.parse_args())
|
||||
DIRECTORY = os.path.join(ARGS['directory'], '')
|
||||
RESULT = os.path.join(ARGS['results'], '')
|
||||
|
||||
SEMANTICS = ARGS['semantics']
|
||||
if SEMANTICS == '1':
|
||||
SEMANTICS = True
|
||||
elif SEMANTICS == '0':
|
||||
SEMANTICS = False
|
||||
else:
|
||||
SEMANTICS = False
|
||||
|
||||
OBJECTS = ARGS['grouping']
|
||||
if OBJECTS == '1':
|
||||
OBJECTS = True
|
||||
elif OBJECTS == '0':
|
||||
OBJECTS = False
|
||||
else:
|
||||
OBJECTS = False
|
||||
|
||||
ATTRIBUTE = ARGS['attribute']
|
||||
if ATTRIBUTE == '1':
|
||||
ATTRIBUTE = 1
|
||||
elif ATTRIBUTE == '2':
|
||||
ATTRIBUTE = 2
|
||||
elif ATTRIBUTE == '3':
|
||||
ATTRIBUTE = 3
|
||||
elif ATTRIBUTE == '0':
|
||||
ATTRIBUTE = False
|
||||
else:
|
||||
ATTRIBUTE = False
|
||||
|
||||
VALIDATION = ARGS['validation']
|
||||
if VALIDATION == '1':
|
||||
VALIDATION = True
|
||||
elif VALIDATION == '0':
|
||||
VALIDATION = False
|
||||
else:
|
||||
VALIDATION = False
|
||||
|
||||
TRANSLATE = ARGS['translate']
|
||||
if TRANSLATE == '1':
|
||||
TRANSLATE = True
|
||||
elif TRANSLATE == '0':
|
||||
TRANSLATE = False
|
||||
else:
|
||||
TRANSLATE = False
|
||||
if TRANSLATE:
|
||||
global smallest_point
|
||||
|
||||
SKIPTRI = ARGS['polypreserve']
|
||||
if SKIPTRI == '1':
|
||||
SKIPTRI = True
|
||||
elif SKIPTRI == '0':
|
||||
SKIPTRI = False
|
||||
else:
|
||||
SKIPTRI = False
|
||||
|
||||
# Changes By Th_Fr:
|
||||
|
||||
TRANSLATECGML = ARGS['translateCityGML']
|
||||
if TRANSLATECGML == '1':
|
||||
TRANSLATECGML = True
|
||||
elif TRANSLATECGML == '0':
|
||||
TRANSLATECGML = False
|
||||
else:
|
||||
TRANSLATECGML = False
|
||||
|
||||
TRANSLATECGMLW = ARGS['translateCityGMLwrite']
|
||||
if TRANSLATECGMLW == '1':
|
||||
TRANSLATECGMLW = True
|
||||
elif TRANSLATECGMLW == '0':
|
||||
TRANSLATECGMLW = False
|
||||
else:
|
||||
TRANSLATECGMLW = False
|
||||
|
||||
SEPARATERCOMPONENTS = ARGS['separateComponents']
|
||||
if SEPARATERCOMPONENTS == '1':
|
||||
SEPARATERCOMPONENTS = True
|
||||
elif SEPARATERCOMPONENTS == '0':
|
||||
SEPARATERCOMPONENTS = False
|
||||
else:
|
||||
SEPARATERCOMPONENTS = False
|
||||
|
||||
APPROXIMATEWINDOWS = ARGS['approximateWindows']
|
||||
if APPROXIMATEWINDOWS == '1':
|
||||
APPROXIMATEWINDOWS = True
|
||||
elif APPROXIMATEWINDOWS == '0':
|
||||
APPROXIMATEWINDOWS = False
|
||||
else:
|
||||
APPROXIMATEWINDOWS = False
|
||||
|
||||
ADDBOUNDINGBOX = ARGS['addBoundingBox']
|
||||
if ADDBOUNDINGBOX == '1':
|
||||
ADDBOUNDINGBOX = True
|
||||
elif ADDBOUNDINGBOX == '0':
|
||||
ADDBOUNDINGBOX = False
|
||||
else:
|
||||
ADDBOUNDINGBOX = False
|
||||
|
||||
|
||||
IMPORTBOUNDINGBOX = ARGS['importBoundingBox']
|
||||
if ADDBOUNDINGBOX == True:
|
||||
IMPORTBOUNDINGBOX = None
|
||||
|
||||
ADDBOUNDINGBOXJSON = ARGS['addBoundingBoxJSON']
|
||||
if ADDBOUNDINGBOXJSON == '1':
|
||||
ADDBOUNDINGBOXJSON = True
|
||||
elif ADDBOUNDINGBOXJSON == '0' and IMPORTBOUNDINGBOX is not None:
|
||||
ADDBOUNDINGBOXJSON = False
|
||||
else:
|
||||
ADDBOUNDINGBOXJSON = False
|
||||
|
||||
if IMPORTBOUNDINGBOX is not None:
|
||||
ADDBOUNDINGBOXJSON = True
|
||||
|
||||
TRANSLATEBUILDINGS = ARGS['translateBuildingWise'] # todo: muss noch implementiert werden
|
||||
if TRANSLATEBUILDINGS == '1':
|
||||
TRANSLATEBUILDINGS = True
|
||||
elif TRANSLATEBUILDINGS == '0':
|
||||
TRANSLATEBUILDINGS = False
|
||||
else:
|
||||
TRANSLATEBUILDINGS = False
|
||||
|
||||
# End of Changes by Th_Fr
|
||||
# -----------------------------------------------------------------
|
||||
# -- Attribute stuff
|
||||
|
||||
# -- Number of classes (colours)
|
||||
res = 101
|
||||
# -- Configuration
|
||||
# -- Color the surfaces based on the normalised kWh/m^2 value <irradiation>. The plain OBJ will be coloured for the total irradiation.
|
||||
if ATTRIBUTE == 1:
|
||||
min_value = 350.0 # 234.591880403
|
||||
max_value = 1300.0 # 1389.97943395
|
||||
elif ATTRIBUTE == 2:
|
||||
min_value = 157.0136575
|
||||
max_value = 83371.4359245
|
||||
elif ATTRIBUTE == 3:
|
||||
min_value = 24925.0
|
||||
max_value = 103454.0
|
||||
|
||||
# -- Statistic parameter
|
||||
atts = []
|
||||
|
||||
|
||||
# -- Colouring function
|
||||
def mtl(att, min_value, max_value, res):
|
||||
"""Finds the corresponding material."""
|
||||
ar = np.linspace(0, 1, res).tolist()
|
||||
# -- Get rid of floating point errors
|
||||
for i in range(0, len(ar)):
|
||||
ar[i] = round(ar[i], 4)
|
||||
# -- Normalise the attribute
|
||||
v = float(att - min_value) / (max_value - min_value)
|
||||
# -- Get the material
|
||||
assigned_material = min(ar, key=lambda x: abs(x - v))
|
||||
return str(assigned_material)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Start time
|
||||
start_time = time.time()
|
||||
# -- Start of the program
|
||||
print("CityGML2OBJ. Searching for CityGML files...")
|
||||
global _VERSION
|
||||
# -- Find all CityGML files in the directory
|
||||
os.chdir(DIRECTORY)
|
||||
# -- Supported extensions
|
||||
# Old version: types = ('*.gml', '*.GML', '*.xml', '*.XML')
|
||||
types = ('*.gml', '*.xml')
|
||||
files_found = []
|
||||
for files in types:
|
||||
files_found.extend(glob.glob(files))
|
||||
for f in files_found:
|
||||
FILENAME = f[:f.rfind('.')]
|
||||
FULLPATH = os.path.join(DIRECTORY, f)
|
||||
|
||||
# -- Reading and parsing the CityGML file(s)
|
||||
CITYGML = etree.parse(FULLPATH)
|
||||
# -- Getting the root of the XML tree
|
||||
root = CITYGML.getroot()
|
||||
# -- Determine CityGML version
|
||||
# If 1.0
|
||||
if root.tag == "{http://www.opengis.net/citygml/1.0}CityModel":
|
||||
print("CityGML 1.0")
|
||||
config.setVersion(1)
|
||||
# -- Name spaces
|
||||
ns_citygml = "http://www.opengis.net/citygml/1.0"
|
||||
ns_gml = "http://www.opengis.net/gml"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/1.0"
|
||||
ns_tran = "http://www.opengis.net/citygml/transportation/1.0"
|
||||
ns_veg = "http://www.opengis.net/citygml/vegetation/1.0"
|
||||
ns_gen = "http://www.opengis.net/citygml/generics/1.0"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xsdschema:xAL:1.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/1.0"
|
||||
ns_frn = "http://www.opengis.net/citygml/cityfurniture/1.0"
|
||||
ns_tun = "http://www.opengis.net/citygml/tunnel/1.0"
|
||||
ns_wtr = "http://www.opengis.net/citygml/waterbody/1.0"
|
||||
ns_brid = "http://www.opengis.net/citygml/bridge/1.0"
|
||||
ns_app = "http://www.opengis.net/citygml/appearance/1.0"
|
||||
|
||||
# added by Th_Fr
|
||||
elif root.tag == "{http://www.opengis.net/citygml/3.0}CityModel":
|
||||
print("CityGML 3.0")
|
||||
config.setVersion(3)
|
||||
ns_citygml = "http://www.opengis.net/citygml/3.0"
|
||||
ns_con = "http://www.opengis.net/citygml/construction/3.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_gml = "http://www.opengis.net/gml/3.2"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/3.0"
|
||||
ns_app = "http://www.opengis.net/citygml/appearance/3.0"
|
||||
ns_pcl = "http://www.opengis.net/citygml/pointcloud/3.0"
|
||||
ns_gen = "http://www.opengis.net/citygml/generics/3.0"
|
||||
ns_gss = "http://www.isotc211.org/2005/gss"
|
||||
na_pfx0 = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||
ns_gsr = "http://www.isotc211.org/2005/gsr"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_gco = "http://www.isotc211.org/2005/gco"
|
||||
ns_tran = "http://www.opengis.net/citygml/transportation/3.0"
|
||||
ns_gmd = "http://www.isotc211.org/2005/gmd"
|
||||
ns_gts = "http://www.isotc211.org/2005/gts"
|
||||
ns_veg = "http://www.opengis.net/citygml/vegetation/3.0"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xal:3"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/3.0"
|
||||
ns_brid = "http://www.opengis.net/citygml/bridge/3.0"
|
||||
ns_frn = "http://www.opengis.net/citygml/cityfurniture/3.0"
|
||||
ns_tun = "http://www.opengis.net/citygml/tunnel/3.0"
|
||||
ns_wtr = "http://www.opengis.net/citygml/waterbody/3.0"
|
||||
|
||||
# -- Else probably means 2.0
|
||||
else:
|
||||
print("CityGML 2.0")
|
||||
config.setVersion(2)
|
||||
# -- Name spaces
|
||||
ns_citygml = "http://www.opengis.net/citygml/2.0"
|
||||
|
||||
ns_gml = "http://www.opengis.net/gml"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/2.0"
|
||||
ns_tran = "http://www.opengis.net/citygml/transportation/2.0"
|
||||
ns_veg = "http://www.opengis.net/citygml/vegetation/2.0"
|
||||
ns_gen = "http://www.opengis.net/citygml/generics/2.0"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/2.0"
|
||||
ns_frn = "http://www.opengis.net/citygml/cityfurniture/2.0"
|
||||
ns_tun = "http://www.opengis.net/citygml/tunnel/2.0"
|
||||
ns_wtr = "http://www.opengis.net/citygml/waterbody/2.0"
|
||||
ns_brid = "http://www.opengis.net/citygml/bridge/2.0"
|
||||
ns_app = "http://www.opengis.net/citygml/appearance/2.0"
|
||||
|
||||
nsmap = {
|
||||
None: ns_citygml,
|
||||
'gml': ns_gml,
|
||||
'bldg': ns_bldg,
|
||||
'tran': ns_tran,
|
||||
'veg': ns_veg,
|
||||
'gen': ns_gen,
|
||||
'xsi': ns_xsi,
|
||||
'xAL': ns_xAL,
|
||||
'xlink': ns_xlink,
|
||||
'dem': ns_dem,
|
||||
'frn': ns_frn,
|
||||
'tun': ns_tun,
|
||||
'brid': ns_brid,
|
||||
'app': ns_app
|
||||
|
||||
}
|
||||
|
||||
# Changes by Th_FR
|
||||
|
||||
if TRANSLATECGML:
|
||||
cgt.translateToLocalCRS(CITYGML, FILENAME, root, ns_bldg, ns_gml, ns_citygml, ns_frn, ns_veg, RESULT,
|
||||
write2file=False,
|
||||
applyHeight=Decimal("0")) # Todo: by TH_Fr: Diese Funktion ist noch nicht fertig
|
||||
|
||||
if TRANSLATECGMLW:
|
||||
cgt.translateToLocalCRS(CITYGML, FILENAME, root, ns_bldg, ns_gml, ns_citygml, ns_frn, ns_veg, RESULT,
|
||||
write2file=True, applyHeight=Decimal("0"))
|
||||
|
||||
# End of changes by Th_FR
|
||||
|
||||
# -- Empty lists for cityobjects and buildings
|
||||
cityObjects = []
|
||||
buildings = []
|
||||
other = []
|
||||
|
||||
# -- This denotes the dictionaries in which the surfaces are put.
|
||||
output = {}
|
||||
vertices_output = {}
|
||||
face_output = {}
|
||||
|
||||
# -- This denotes the dictionaries in which all surfaces are put. It is later ignored in the semantic option was invoked.
|
||||
output['All'] = []
|
||||
output['All'].append(header)
|
||||
if ATTRIBUTE:
|
||||
output['All'].append("mtllib colormap.mtl\n")
|
||||
vertices_output['All'] = []
|
||||
face_output['All'] = []
|
||||
|
||||
# -- If the semantic option was invoked, this part adds additional dictionaries.
|
||||
if SEMANTICS:
|
||||
# -- Easy to modify list of thematic boundaries
|
||||
semanticSurfaces = ['GroundSurface', 'WallSurface', 'RoofSurface', 'ClosureSurface', 'CeilingSurface',
|
||||
'InteriorWallSurface', 'FloorSurface', 'OuterCeilingSurface', 'OuterFloorSurface', 'Door',
|
||||
'Window']
|
||||
for semanticSurface in semanticSurfaces:
|
||||
output[semanticSurface] = []
|
||||
output[semanticSurface].append(header)
|
||||
# -- Add the material library
|
||||
if ATTRIBUTE:
|
||||
output[semanticSurface].append("mtllib colormap.mtl\n")
|
||||
vertices_output[semanticSurface] = []
|
||||
face_output[semanticSurface] = []
|
||||
|
||||
# -- Directory of vertices (indexing)
|
||||
vertices = {}
|
||||
vertices['All'] = []
|
||||
if SEMANTICS:
|
||||
for semanticSurface in semanticSurfaces:
|
||||
vertices[semanticSurface] = []
|
||||
vertices['Other'] = []
|
||||
face_output['Other'] = []
|
||||
output['Other'] = []
|
||||
|
||||
# -- Find all instances of cityObjectMember and put them in a list
|
||||
for obj in root.getiterator('{%s}cityObjectMember' % ns_citygml):
|
||||
cityObjects.append(obj)
|
||||
|
||||
print(FILENAME)
|
||||
|
||||
if len(cityObjects) > 0:
|
||||
# -- Report the progress and contents of the CityGML file
|
||||
print("\tThere are", len(cityObjects), "cityObject(s) in this CityGML file.")
|
||||
# -- Store each building separately
|
||||
for cityObject in cityObjects:
|
||||
for child in cityObject.getchildren():
|
||||
if child.tag == '{%s}Building' % ns_bldg:
|
||||
buildings.append(child)
|
||||
|
||||
for cityObject in cityObjects:
|
||||
for child in cityObject.getchildren():
|
||||
if child.tag == '{%s}Road' % ns_tran or child.tag == '{%s}PlantCover' % ns_veg or \
|
||||
child.tag == '{%s}GenericCityObject' % ns_gen or child.tag == '{%s}CityFurniture' % ns_frn or \
|
||||
child.tag == '{%s}Relief' % ns_dem or child.tag == '{%s}Tunnel' % ns_tun or \
|
||||
child.tag == '{%s}WaterBody' % ns_wtr or child.tag == '{%s}Bridge' % ns_brid:
|
||||
other.append(child)
|
||||
|
||||
print("\tAnalysing objects and extracting the geometry...")
|
||||
|
||||
# -- Count the buildings
|
||||
b_counter = 0
|
||||
b_total = len(buildings)
|
||||
print(" There are ", b_total, " buildings in the dataset")
|
||||
|
||||
# -- Do each building separately
|
||||
for b in buildings:
|
||||
# addd by th_fr
|
||||
if SEPARATERCOMPONENTS:
|
||||
json_filepath = RESULT + "index.json"
|
||||
# todo: mus snoch implementiert werde
|
||||
|
||||
csm.addCRSToJSON(root, json_filepath)
|
||||
csm.separateComponents(b, RESULT, APPROXIMATEWINDOWS=APPROXIMATEWINDOWS,
|
||||
ADDBOUNDINGBOX=ADDBOUNDINGBOX, ADDBOUNDINGBOXJSON=ADDBOUNDINGBOXJSON,
|
||||
TRANSLATEBUILDINGS=TRANSLATEBUILDINGS, IMPORTBOUNDINGBOX=IMPORTBOUNDINGBOX, b_counter=b_counter)
|
||||
# End time
|
||||
end_time = time.time()
|
||||
# Calculate elapsed time
|
||||
elapsed_time = end_time - start_time
|
||||
print(f"Elapsed time: {elapsed_time:.2f} seconds")
|
||||
b_counter += 1
|
||||
continue
|
||||
|
||||
# -- Build the local list of vertices to speed up the indexing
|
||||
local_vertices = {}
|
||||
local_vertices['All'] = []
|
||||
|
||||
if SEMANTICS:
|
||||
for semanticSurface in semanticSurfaces:
|
||||
local_vertices[semanticSurface] = []
|
||||
|
||||
# -- Increment the building counter
|
||||
b_counter += 1
|
||||
# -- If the object option is on, get the name for each building or create one
|
||||
if OBJECTS:
|
||||
ob = b.xpath("@g:id", namespaces={'g': ns_gml})
|
||||
if not ob:
|
||||
ob = b_counter
|
||||
else:
|
||||
ob = ob[0]
|
||||
|
||||
# -- Print progress for large files every 1000 buildings.
|
||||
if b_counter == 1000:
|
||||
print("\t1000... ", )
|
||||
elif b_counter % 1000 == 0 and b_counter == (b_total - b_total % 1000):
|
||||
print(str(b_counter) + "...")
|
||||
elif b_counter > 0 and b_counter % 1000 == 0:
|
||||
print(str(b_counter) + "... ", )
|
||||
|
||||
# -- Add the object identifier
|
||||
if OBJECTS:
|
||||
face_output['All'].append('o ' + str(ob) + '\n')
|
||||
|
||||
# -- Add the attribute for the building
|
||||
if ATTRIBUTE:
|
||||
for ch in b.getchildren():
|
||||
if ch.tag == "{%s}yearlyIrradiation" % ns_citygml:
|
||||
bAttVal = float(ch.text)
|
||||
|
||||
# -- OBJ with all surfaces in the same bin
|
||||
polys = markup3dmodule.polygonFinder(b)
|
||||
# -- Process each surface
|
||||
polycounter = 0
|
||||
for poly in polys:
|
||||
if ATTRIBUTE:
|
||||
poly_to_obj(poly, 'All', bAttVal)
|
||||
if ATTRIBUTE == 3:
|
||||
atts.append(bAttVal)
|
||||
else:
|
||||
# print etree.tostring(poly)
|
||||
poly_to_obj(poly, 'All')
|
||||
polycounter = polycounter + 1
|
||||
|
||||
# -- Semantic decomposition, with taking special care about the openings
|
||||
if SEMANTICS:
|
||||
# -- First take care about the openings since they can mix up
|
||||
openings = []
|
||||
openingpolygons = []
|
||||
for child in b.getiterator():
|
||||
if child.tag == '{%s}opening' % ns_bldg:
|
||||
openings.append(child)
|
||||
for o in child.findall('.//{%s}Polygon' % ns_gml):
|
||||
openingpolygons.append(o)
|
||||
|
||||
# -- Process each opening
|
||||
for o in openings:
|
||||
for child in o.getiterator():
|
||||
unique_identifier = child.xpath("@g:id", namespaces={'g': ns_gml})
|
||||
if child.tag == '{%s}Window' % ns_bldg or child.tag == '{%s}Door' % ns_bldg:
|
||||
# print(unique_identifier)
|
||||
if child.tag == '{%s}Window' % ns_bldg:
|
||||
t = 'Window'
|
||||
# print(t)
|
||||
else:
|
||||
t = 'Door'
|
||||
# print(t)
|
||||
polys = markup3dmodule.polygonFinder(o)
|
||||
for poly in polys:
|
||||
poly_to_obj(poly, t)
|
||||
|
||||
# -- Process other thematic boundaries
|
||||
for cl in output:
|
||||
cls = []
|
||||
for child in b.getiterator():
|
||||
if child.tag == '{%s}%s' % (ns_bldg, cl):
|
||||
cls.append(child)
|
||||
# -- Is this the first feature of this object?
|
||||
firstF = True
|
||||
for feature in cls:
|
||||
# -- If it is the first feature, print the object identifier
|
||||
unique_identifier = feature.xpath("@g:id", namespaces={
|
||||
'g': ns_gml})
|
||||
if OBJECTS and firstF:
|
||||
face_output[cl].append('o ' + str(ob) + "_" + str(unique_identifier) + '\n')
|
||||
|
||||
firstF = False
|
||||
# -- This is not supposed to happen, but just to be sure...
|
||||
if feature.tag == '{%s}Window' % ns_bldg or feature.tag == '{%s}Door' % ns_bldg:
|
||||
continue
|
||||
|
||||
# print(f"unigue identifier: {str(ob) + str(unique_identifier)}")
|
||||
|
||||
# -- Find all polygons in this semantic boundary hierarchy
|
||||
|
||||
for p in feature.findall('.//{%s}Polygon' % ns_gml):
|
||||
if ATTRIBUTE == 1 or ATTRIBUTE == 2:
|
||||
# -- Flush the previous value
|
||||
attVal = None
|
||||
if cl == 'RoofSurface':
|
||||
# print p.xpath("//@c:irradiation", namespaces={'c' : ns_citygml})
|
||||
# -- Silly way but it works, as I can't get the above xpath to work for some reason
|
||||
for ch in p.getchildren():
|
||||
if ATTRIBUTE == 1:
|
||||
if ch.tag == "{%s}irradiation" % ns_citygml:
|
||||
attVal = float(ch.text)
|
||||
atts.append(attVal)
|
||||
elif ATTRIBUTE == 2:
|
||||
if ch.tag == "{%s}totalIrradiation" % ns_citygml:
|
||||
attVal = float(ch.text)
|
||||
atts.append(attVal)
|
||||
elif ATTRIBUTE == 3:
|
||||
attVal = None
|
||||
if cl == 'RoofSurface':
|
||||
attVal = bAttVal
|
||||
else:
|
||||
# -- If the attribute option is off, pass no material
|
||||
attVal = None
|
||||
found_opening = False
|
||||
for optest in openingpolygons:
|
||||
if p == optest:
|
||||
found_opening = True
|
||||
break
|
||||
# -- If there is an opening skip it
|
||||
if found_opening:
|
||||
pass
|
||||
else:
|
||||
# -- Finally process the polygon
|
||||
poly_to_obj(p, cl, attVal)
|
||||
|
||||
# -- Merge the local list of vertices to the global
|
||||
for cl in local_vertices:
|
||||
for vertex in local_vertices[cl]:
|
||||
vertices[cl].append(vertex)
|
||||
|
||||
if len(other) > 0:
|
||||
vertices_output['Other'] = []
|
||||
local_vertices = {}
|
||||
local_vertices['Other'] = []
|
||||
for oth in other:
|
||||
# local_vertices = {}
|
||||
# local_vertices['All'] = []
|
||||
polys = markup3dmodule.polygonFinder(oth)
|
||||
# -- Process each surface
|
||||
for poly in polys:
|
||||
poly_to_obj(poly, 'Other')
|
||||
for vertex in local_vertices['Other']:
|
||||
vertices['Other'].append(vertex)
|
||||
|
||||
print("\tExtraction done. Sorting geometry and writing file(s).")
|
||||
|
||||
# -- Translate (convert) the vertices to a local coordinate system
|
||||
if TRANSLATE:
|
||||
print("\tTranslating the coordinates of vertices.")
|
||||
list_of_all_vertices = []
|
||||
for cl in output:
|
||||
if len(vertices[cl]) > 0:
|
||||
for vtx in vertices[cl]:
|
||||
list_of_all_vertices.append(vtx)
|
||||
smallest_vtx = polygon3dmodule.smallestPoint(list_of_all_vertices)
|
||||
dx = smallest_vtx[0]
|
||||
dy = smallest_vtx[1]
|
||||
dz = smallest_vtx[2]
|
||||
for cl in output:
|
||||
if len(vertices[cl]) > 0:
|
||||
for idx, vtx in enumerate(vertices[cl]):
|
||||
vertices[cl][idx][0] = vtx[0] - dx
|
||||
vertices[cl][idx][1] = vtx[1] - dy
|
||||
vertices[cl][idx][2] = vtx[2] - dz
|
||||
|
||||
# -- Write the OBJ(s)
|
||||
os.chdir(RESULT)
|
||||
# -- Theme by theme
|
||||
for cl in output:
|
||||
if len(vertices[cl]) > 0:
|
||||
write_vertices(vertices[cl], cl)
|
||||
output[cl].append("\n" + ''.join(vertices_output[cl]))
|
||||
output[cl].append("\n" + ''.join(face_output[cl]))
|
||||
if cl == 'All':
|
||||
adj_suffix = ""
|
||||
else:
|
||||
adj_suffix = "-" + str(cl)
|
||||
|
||||
with open(RESULT + FILENAME + str(adj_suffix) + ".obj", "w") as obj_file:
|
||||
obj_file.write(''.join(output[cl]))
|
||||
|
||||
print("\tOBJ file(s) written.")
|
||||
|
||||
# -- Print the range of attributes. Useful for defining the range of the colorbar.
|
||||
if ATTRIBUTE:
|
||||
print('\tRange of attributes:', min(atts), '--', max(atts))
|
||||
|
||||
else:
|
||||
print(
|
||||
"\tThere is a problem with this file: no cityObjects have been found. Please check if the file complies to CityGML.")
|
||||
|
||||
# End time
|
||||
end_time = time.time()
|
||||
# Calculate elapsed time
|
||||
elapsed_time = end_time - start_time
|
||||
print(f"Elapsed time: {elapsed_time:.2f} seconds")
|
||||
339
scripts/CityGML2OBJv2/CityGMLTranslation.py
Normal file
339
scripts/CityGML2OBJv2/CityGMLTranslation.py
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
##!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# The MIT License (MIT)
|
||||
|
||||
# This code is part of the CityGML2OBJs package
|
||||
|
||||
# Copyright (c) 2023
|
||||
# Thomas Fröch
|
||||
# Technische Universität München (TUM)
|
||||
# thomas.froech@tum.de
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import markup3dmodule as m3dm
|
||||
from decimal import Decimal, getcontext
|
||||
import numpy as np
|
||||
|
||||
# Setting the precision of Decimal
|
||||
getcontext().prec = 28
|
||||
|
||||
def performStableAddition(number1, number2):
|
||||
# Input: string number1, string number 2, output: string sum
|
||||
# Determine the number of positions after the comma.
|
||||
# print("Number 1 totally before: ", number1)
|
||||
# print("Number 2 totally before: ", number2)
|
||||
|
||||
try:
|
||||
n_after_comma_number1 = len(number1.split(".")[1])
|
||||
except:
|
||||
n_after_comma_number1 = 0
|
||||
try:
|
||||
n_after_comma_number2 = len(number2.split(".")[1])
|
||||
except:
|
||||
n_after_comma_number2 = 0
|
||||
lengths = [n_after_comma_number1, n_after_comma_number2]
|
||||
|
||||
# convert float numbers into integers by removing the comma
|
||||
# print("Number 1 before all: ", number1)
|
||||
# print("Number 2 before all: ", number2)
|
||||
number1 = number1.replace(".", "")
|
||||
number2 = number2.replace(".", "")
|
||||
|
||||
# print("Number 1 after: ", number1)
|
||||
# print("Number 2 after: ", number2)
|
||||
|
||||
# find the number with more positions after the comma
|
||||
abs_lengths = np.abs(lengths) # Erstellen eines Arrays mit den Beträgen
|
||||
max_index = np.argmax(abs_lengths)
|
||||
|
||||
# Distinguish the two different cases
|
||||
# Case 1: number one has more positions after the comma than number two
|
||||
if max_index == 0:
|
||||
# Find the difference of positions after the comma
|
||||
n_positions_difference = (lengths[0] - lengths[1])
|
||||
# print("n_positions_difference: ", n_positions_difference)
|
||||
# Fill up the missing digits of number 2 with zeros
|
||||
# print("number2 before: ", number2)
|
||||
for i in range(n_positions_difference):
|
||||
number2 = number2 + "0"
|
||||
# print("number2_after", number2)
|
||||
# print("Number1: ", number1)
|
||||
# convert both stings into integers
|
||||
number1_int = np.double(number1)
|
||||
number2_int = np.double(number2)
|
||||
|
||||
# Add both numbers
|
||||
number_sum = number1_int + number2_int
|
||||
|
||||
# convert back into string
|
||||
format_string = "{:.0f}"
|
||||
number_sum_string = format_string.format(number_sum)
|
||||
|
||||
# Insert the comma at the correct position
|
||||
length_of_number = len(number_sum_string.replace("-",
|
||||
""))
|
||||
if length_of_number >= lengths[max_index]:
|
||||
if n_after_comma_number1 != 0:
|
||||
number_sum_string = number_sum_string[:-(lengths[0])] + "." + number_sum_string[-(lengths[0]):]
|
||||
return number_sum_string
|
||||
else:
|
||||
number_sum_string = number_sum_string
|
||||
return number_sum_string
|
||||
elif length_of_number < n_after_comma_number1:
|
||||
new_string = ""
|
||||
for i in range(len(number_sum_string)):
|
||||
if number_sum_string[i] == "-":
|
||||
new_string += "0."
|
||||
else:
|
||||
new_string += number_sum_string[i]
|
||||
number_sum_string = new_string
|
||||
return number_sum_string
|
||||
else:
|
||||
number_sum_string = number_sum_string
|
||||
return number_sum_string
|
||||
|
||||
# Case 2: number two has more positions after the comma than number one
|
||||
else:
|
||||
# find the difference of positions after the comma
|
||||
n_positions_difference = lengths[1] - lengths[0]
|
||||
|
||||
# Fill up the missing digits of number 2 with zeros
|
||||
for i in range(n_positions_difference):
|
||||
number1 = number1 + "0"
|
||||
|
||||
# convert both stings into integers
|
||||
number1_int = Decimal(number1)
|
||||
number2_int = Decimal(number2)
|
||||
|
||||
# Add both numbers
|
||||
number_sum = number1_int + number2_int
|
||||
|
||||
# convert back into string
|
||||
format_string = "{:.0f}"
|
||||
number_sum_string = format_string.format(number_sum)
|
||||
|
||||
# Insert the comma at the correct position
|
||||
length_of_number = len(number_sum_string.replace("-",
|
||||
""))
|
||||
if length_of_number > n_after_comma_number2:
|
||||
if n_after_comma_number2 != 0:
|
||||
|
||||
number_sum_string = number_sum_string[:-(lengths[1])] + "." + number_sum_string[-(lengths[1]):]
|
||||
return number_sum_string
|
||||
|
||||
else:
|
||||
number_sum_string = number_sum_string
|
||||
return number_sum_string
|
||||
|
||||
elif length_of_number < n_after_comma_number2:
|
||||
new_string = ""
|
||||
for i in range(len(number_sum_string)):
|
||||
if number_sum_string[i] == "-":
|
||||
new_string += "0."
|
||||
else:
|
||||
new_string += number_sum_string[i]
|
||||
number_sum_string = new_string
|
||||
return number_sum_string
|
||||
else:
|
||||
number_sum_string = number_sum_string
|
||||
return number_sum_string
|
||||
|
||||
|
||||
# This function is used in order to extract all the envelopes from the CityGML-File
|
||||
# These envelopes are going to be used in order to determine the translation parameters later
|
||||
def getEnvelopes(root, ns_bldg, ns_gml, ns_citygml):
|
||||
envelopes = []
|
||||
for envelope in root.getiterator('{%s}Envelope' % ns_gml):
|
||||
envelopes.append(envelope)
|
||||
return envelopes
|
||||
|
||||
|
||||
# This function is used in order to calculate the translation parameters from the
|
||||
# envelopes that were previously extracted from the envelopes
|
||||
def getTranslationParameters(envelopes, ns_gml):
|
||||
# Setting up some initial values
|
||||
dx = Decimal("0")
|
||||
dy = Decimal("0")
|
||||
lowerCorner = []
|
||||
upperCorner = []
|
||||
|
||||
# Iterating through all the envelopes in the CityGML-File
|
||||
for envelope in envelopes:
|
||||
# print(" Envelope: ", envelope)
|
||||
# Finding the upper and the lower corner
|
||||
for child in envelope.getchildren():
|
||||
if child.tag == '{%s}lowerCorner' % ns_gml:
|
||||
lowerCorner.append(child.text)
|
||||
elif child.tag == '{%s}upperCorner' % ns_gml:
|
||||
upperCorner.append(child.text)
|
||||
# Converting into Decimal
|
||||
pointCounter = 0
|
||||
for point in lowerCorner:
|
||||
dy = dy + (Decimal(point.split(" ")[0]))
|
||||
dx = dx + (Decimal(point.split(" ")[1]))
|
||||
pointCounter = pointCounter + 1
|
||||
|
||||
dyret = -dy / pointCounter
|
||||
dxret = -dx / pointCounter
|
||||
|
||||
return [Decimal(str(int(dxret))), Decimal(str(int(dyret)))]
|
||||
|
||||
|
||||
# This function is used to Parse the text from the CityGML file to Decimal numbers and
|
||||
# to aplly the previously calculated translation parameters
|
||||
# Notice: this code only allows ONE "gml:PosList-Element" for each Interior and exterior of a Polygon
|
||||
# If there are more than just one,just the first one is going to be transformed
|
||||
def splitAndApplyTrafo(coordString, transParam):
|
||||
# Splitting the coordinate string by empty spaces
|
||||
split = coordString.split(" ")
|
||||
alalala = len(split)
|
||||
# Apply the Trafo
|
||||
if split[0] == '':
|
||||
split.pop(0)
|
||||
if split[-1] == "":
|
||||
split.pop(-1)
|
||||
counter = 0
|
||||
length = int(len(split))
|
||||
length_new = int(length / 3)
|
||||
for i in range(length_new):
|
||||
|
||||
|
||||
# print("y")
|
||||
split[counter] = performStableAddition(split[counter], str(transParam[1]))
|
||||
# print("x")
|
||||
split[counter + 1] = performStableAddition(split[counter + 1], str(transParam[0]))
|
||||
# print("z")
|
||||
split[counter + 2] = performStableAddition(split[counter + 2], str(transParam[2]))
|
||||
|
||||
counter += 3
|
||||
# converting back to a string
|
||||
translated = ""
|
||||
for i in split:
|
||||
if len(translated) == 0:
|
||||
translated = str(i)
|
||||
else:
|
||||
translated = translated + " " + str(i)
|
||||
return translated
|
||||
|
||||
|
||||
# This code is used in order to find all the coordinates that are defined in the CityGML-File
|
||||
# Please Notice: the search for coordinates here has the same limitations as the search for coordinates that
|
||||
# is used in the "CityGML2OBJ" functionality!
|
||||
def applyTranslationToCityGML(CITYGML, root, transParam, ns_citygml, ns_gml, ns_frn, ns_veg, filename):
|
||||
# Iterate over all the cityObjectMembers
|
||||
for obj in root.getiterator('{%s}cityObjectMember' % ns_citygml):
|
||||
# Iterate over all the children of cityObject Member
|
||||
for child in obj.getchildren():
|
||||
# Exclude all the implicitly referenced objects from the transformation
|
||||
if child.findall(
|
||||
'.//{%s}ImplicitGeometry' % ns_citygml) == []:
|
||||
polys = m3dm.polygonFinder(child)
|
||||
# Iterate over all the polygons of the children of cityObjectMember
|
||||
for poly in polys:
|
||||
# decompose all the polygons in the interior and exterior rings
|
||||
exter, inter = m3dm.polydecomposer(poly)
|
||||
# iterate over all the exterior rings
|
||||
for e in exter:
|
||||
# find all the coordinates that are stored as a "posList"
|
||||
if len(e.findall('.//{%s}posList' % ns_gml)) > 0:
|
||||
points_tmp = e.findall('.//{%s}posList' % ns_gml)[0].text
|
||||
points = points_tmp.replace('\n', ' ')
|
||||
translated = splitAndApplyTrafo(points, transParam)
|
||||
# print("Before: ", e.findall('.//{%s}posList' % ns_gml)[0].text)
|
||||
e.findall('.//{%s}posList' % ns_gml)[0].text = translated
|
||||
# print("Result: ", e.findall('.//{%s}posList' % ns_gml)[0].text)
|
||||
# find all the coordinates that are stored as "pos"
|
||||
elif len(e.findall('.//{%s}pos' % ns_gml)) > 0:
|
||||
points = e.findall('.//{%s}pos' % ns_gml)
|
||||
counter = 0
|
||||
for k in points:
|
||||
translated = splitAndApplyTrafo(k.text, transParam)
|
||||
e.findall('.//{%s}pos' % ns_gml)[counter].text = translated
|
||||
counter = counter + 1
|
||||
# iterate over all the interior rings
|
||||
for i in inter:
|
||||
# find all the coordinates that are stored as a "posList"
|
||||
if len(i.findall('.//{%s}posList' % ns_gml)) > 0:
|
||||
points_tmp = i.findall('.//{%s}posList' % ns_gml)[0].text
|
||||
points = points_tmp.replace('\n', ' ')
|
||||
translated = splitAndApplyTrafo(points, transParam)
|
||||
i.findall('.//{%s}posList' % ns_gml)[0].text = translated
|
||||
# find all the coordinates that are stored as "pos"
|
||||
elif len(i.findall('.//{%s}pos' % ns_gml)) > 0:
|
||||
points = i.findall('.//{%s}pos' % ns_gml)
|
||||
counter = 0
|
||||
for k in points:
|
||||
translated = splitAndApplyTrafo(k.text, transParam)
|
||||
i.findall('.//{%s}pos' % ns_gml)[counter].text = translated
|
||||
counter = counter + 1
|
||||
|
||||
else: # This condition is used in order to transform the reference points of the implicitly defined geometries
|
||||
# Step 1: find all the reference points:
|
||||
referencePoints = child.findall('.//{%s}referencePoint' % ns_citygml)
|
||||
for referencePoint in referencePoints:
|
||||
points_tmp = referencePoint.findall('.//{%s}pos' % ns_gml)
|
||||
points = points_tmp.replace('\n', ' ')
|
||||
counter = 0
|
||||
for l in points:
|
||||
translated = splitAndApplyTrafo(l.text, transParam)
|
||||
referencePoint.findall('.//{%s}pos' % ns_gml)[counter].text = translated
|
||||
counter = counter + 1
|
||||
|
||||
# Iterate over all the envelopes
|
||||
for envelope in root.getiterator('{%s}Envelope' % ns_gml):
|
||||
lowerCorner = envelope.findall('.//{%s}lowerCorner' % ns_gml)[0].text
|
||||
upperCorner = envelope.findall('.//{%s}upperCorner' % ns_gml)[0].text
|
||||
translatedLowerCorner = splitAndApplyTrafo(lowerCorner, transParam)
|
||||
translatedUpperCorner = splitAndApplyTrafo(upperCorner, transParam)
|
||||
envelope.findall('.//{%s}lowerCorner' % ns_gml)[0].text = translatedLowerCorner
|
||||
envelope.findall('.//{%s}upperCorner' % ns_gml)[0].text = translatedUpperCorner
|
||||
|
||||
CITYGML.write(filename + "_local_" + ".gml")
|
||||
return root
|
||||
|
||||
|
||||
# This function is used in order to write the previously calculated translation parameters to a
|
||||
# designated .txt file. The use of this functionality is optional and can be activated by setting the
|
||||
# optional "write2file" parameter to "True" when calling the "translateToLocalCRS" - function.
|
||||
def writeTransparam2File(filename, directory, transParam):
|
||||
textfileName = directory + filename + "_Translation_Parameters.txt"
|
||||
f = open(textfileName, "w")
|
||||
f.write("This file contains the translation parameters that were applied to the original CityGML file." + "\n" +
|
||||
"Conversion tool developed by Filip Biljecki, TU Delft <fbiljecki@gmail.com>" + "\n" +
|
||||
"Conversion tool extended by Thomas Fröch, TUM <thomas.froech@tum.de>" + "\n" +
|
||||
"see more at Github:" + "\n" +
|
||||
"https://github.com/tudelft3d/CityGML2OBJs" + "\n" + "\n")
|
||||
key = ['y', 'x', 'z']
|
||||
for i in range(len(transParam)):
|
||||
f.write(key[i] + ': ' + str(transParam[i]) + ' ')
|
||||
f.close()
|
||||
print("Translation parameters written to: " + textfileName)
|
||||
return 0
|
||||
|
||||
def translateToLocalCRS(CITYGML, file, root, ns_bldg, ns_gml, ns_citygml, ns_frn, ns_veg, directory, write2file=False,
|
||||
applyHeight=Decimal("0")):
|
||||
envelopes = getEnvelopes(root, ns_bldg, ns_gml, ns_citygml)
|
||||
transParam = getTranslationParameters(envelopes, ns_gml)
|
||||
transParam.append(applyHeight)
|
||||
if write2file == True:
|
||||
writeTransparam2File(file, directory, transParam)
|
||||
applyTranslationToCityGML(CITYGML, root, transParam, ns_citygml, ns_gml, ns_frn, ns_veg, file)
|
||||
return 0
|
||||
21
scripts/CityGML2OBJv2/LICENSE
Normal file
21
scripts/CityGML2OBJv2/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Chair of Geoinformatics, Technical University of Munich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
80
scripts/CityGML2OBJv2/README.md
Normal file
80
scripts/CityGML2OBJv2/README.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# :cityscape: CityGML2OBJ 2.0 :cityscape:
|
||||
Command line converter of **CityGML (.gml)** to **OBJ (.obj)** files, while maintaining the semantics
|
||||
|
||||

|
||||
|
||||
## :arrow_forward: How to run?
|
||||
The `CityGML2OBJs.py` represents the starting point of the code, choose this file when configuring the runtime and pass the following parameters:
|
||||
|
||||
`-i your-input-citygml-path-here`
|
||||
|
||||
`-o your-output-obj-path-here`
|
||||
|
||||
Please make sure to use the absolute paths to the respective directories.
|
||||
|
||||
and Bob's your uncle! :construction_worker:
|
||||
|
||||
### :wrench: Optional features
|
||||
|
||||
| Optional feature | specification |
|
||||
| -------- | -------- |
|
||||
| Semanitcs Option|`-s 1`|
|
||||
| Geometry Validation | `-v 1`|
|
||||
| Object Preservation | `-g 1`|
|
||||
| Skip the triangulation | `-p 1`|
|
||||
| Conversion of the resulting dataset into a local coordinate system | `-t 1`|
|
||||
| Translation of the CityGML dataset into a local coordinate system before further processing, without saving the translation parameters|`-tC 1`|
|
||||
| Translation of the CityGML dataset into a local coordinate system before further processing, with saving the translation parameters to a designated .txt file|`-tCw 1`|
|
||||
|
||||
|
||||
## :page_with_curl: Requirements
|
||||
|
||||
### Python packages:
|
||||
|
||||
+ [Numpy](http://docs.scipy.org/doc/numpy/user/install.html)
|
||||
+ [Triangle](http://dzhelil.info/triangle/)
|
||||
+ [lxml](http://lxml.de)
|
||||
+ [Shapely](https://github.com/Toblerity/Shapely)
|
||||
+ [Decimal](https://docs.python.org/3/library/decimal.html)
|
||||
|
||||
#### Optional:
|
||||
|
||||
+ [Matplotlib](http://matplotlib.org/users/installing.html)
|
||||
|
||||
### Tested:
|
||||
|
||||
Using Python 3.10 and Windows 10 OS
|
||||
|
||||
### CityGML Requirements:
|
||||
|
||||
#### Mandatory:
|
||||
|
||||
+ CityGML 1.0 or 2.0
|
||||
+ Files must end with `.gml`, `.GML`, `.xml`, or `.XML`
|
||||
+ Vertices in either `<gml:posList>` or `<gml:pos>`
|
||||
+ Your files must be valid (e.g., free check with [CityDoctor](https://transfer.hft-stuttgart.de/gitlab/citydoctor/citydoctor2)
|
||||
|
||||
#### Optional, but recommended:
|
||||
|
||||
+ `<gml:id>` for each `<bldg:Building>` and other types of city objects
|
||||
+ `<gml:id>` for each `<gml:Polygon>`
|
||||
|
||||
|
||||
## Limitations
|
||||
|
||||
Information on the limitations can be found in this [Wiki Page](https://github.com/tum-gis/citygml2obj-2.0/wiki/Limitations)
|
||||
|
||||
## :handshake: Credits
|
||||
We are indebted to [Filip Biljecki](https://github.com/fbiljecki), [Hugo Ledoux](https://github.com/hugoledoux) and [Ravi Peters](https://github.com/Ylannl) from [TU Delft](https://github.com/tudelft3d) for their initial version of the CityGML2OBJs converter. The archived version of the repo can still be found here: https://github.com/tudelft3d/CityGML2OBJs; the paper:
|
||||
|
||||
Biljecki, F., & Arroyo Ohori, K. (2015). Automatic semantic-preserving conversion between OBJ and CityGML. Eurographics Workshop on Urban Data Modelling and Visualisation 2015, pp. 25-30.
|
||||
|
||||
[[PDF]](https://filipbiljecki.com/publications/2015_udmv_citygml_obj.pdf) [[DOI]](http://doi.org/10.2312/udmv.20151345)
|
||||
|
||||
## :mailbox: Contact & Feedback
|
||||
|
||||
Feel free to open a discussion under Issues or write us an email
|
||||
|
||||
- [Thomas Froech](thomas.froech@tum.de)
|
||||
- [Benedikt Schwab](benedikt.schwab@tum.de)
|
||||
- [Olaf Wysocki](olaf.wysocki@tum.de)
|
||||
65857
scripts/CityGML2OBJv2/Sy_Olkieniki_GML.obj
Normal file
65857
scripts/CityGML2OBJv2/Sy_Olkieniki_GML.obj
Normal file
File diff suppressed because it is too large
Load diff
483630
scripts/CityGML2OBJv2/Sy_Olkieniki_GML.xml
Normal file
483630
scripts/CityGML2OBJv2/Sy_Olkieniki_GML.xml
Normal file
File diff suppressed because it is too large
Load diff
Binary file not shown.
BIN
scripts/CityGML2OBJv2/__pycache__/markup3dmodule.cpython-38.pyc
Normal file
BIN
scripts/CityGML2OBJv2/__pycache__/markup3dmodule.cpython-38.pyc
Normal file
Binary file not shown.
BIN
scripts/CityGML2OBJv2/__pycache__/polygon3dmodule.cpython-38.pyc
Normal file
BIN
scripts/CityGML2OBJv2/__pycache__/polygon3dmodule.cpython-38.pyc
Normal file
Binary file not shown.
756
scripts/CityGML2OBJv2/componentseparationmodule.py
Normal file
756
scripts/CityGML2OBJv2/componentseparationmodule.py
Normal file
|
|
@ -0,0 +1,756 @@
|
|||
import re
|
||||
import config
|
||||
import markup3dmodule as m3dm
|
||||
import polygon3dmodule as p3dm
|
||||
import json
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
import open3d.core as o3c
|
||||
import os
|
||||
|
||||
|
||||
# Function to create triangles at each corner in 3D
|
||||
def create_corner_triangles(box_points, triangle_size=1):
|
||||
triangles = []
|
||||
for i, point in enumerate(box_points):
|
||||
x, y, z = point
|
||||
if i == 0: # Bottom-left-front corner
|
||||
triangles.append([[x, y, z], [x + triangle_size, y, z], [x, y + triangle_size, z]])
|
||||
elif i == 1: # Bottom-right-front corner
|
||||
triangles.append([[x, y, z], [x - triangle_size, y, z], [x, y + triangle_size, z]])
|
||||
elif i == 2: # Top-left-front corner
|
||||
triangles.append([[x, y, z], [x + triangle_size, y, z], [x, y - triangle_size, z]])
|
||||
elif i == 3: # Bottom-left-back corner
|
||||
triangles.append([[x, y, z], [x + triangle_size, y, z], [x, y + triangle_size, z]])
|
||||
elif i == 4: # Top-right-back corner
|
||||
triangles.append([[x, y, z], [x - triangle_size, y, z], [x, y - triangle_size, z]])
|
||||
elif i == 5: # Top-left-back corner
|
||||
triangles.append([[x, y, z], [x + triangle_size, y, z], [x, y - triangle_size, z]])
|
||||
elif i == 6: # Bottom-right-back corner
|
||||
triangles.append([[x, y, z], [x - triangle_size, y, z], [x, y + triangle_size, z]])
|
||||
elif i == 7: # Top-right-front corner
|
||||
triangles.append([[x, y, z], [x - triangle_size, y, z], [x, y - triangle_size, z]])
|
||||
return triangles
|
||||
|
||||
|
||||
def addTranslationParameters(e, i, trans_param):
|
||||
# Convert lists to numpy arrays for easier manipulation
|
||||
e = np.array(e)
|
||||
# i = np.array(i)
|
||||
i_translated = []
|
||||
trans_param = np.array(trans_param)
|
||||
# case distinction for empty translation parameters
|
||||
if len(trans_param) > 0:
|
||||
# Subtract the translation parameters from each point
|
||||
if len(e) > 0:
|
||||
e_translated = e - trans_param
|
||||
elif len(e) == 0:
|
||||
e_translated = np.asarray([])
|
||||
# Iterate over all the different interior rings
|
||||
if len(i) > 0:
|
||||
for interior_ring in i:
|
||||
interior_ring = np.asarray(interior_ring)
|
||||
# Translate the interior ring
|
||||
interior_ring_translated = interior_ring - trans_param
|
||||
# Coollect the translated interior rings
|
||||
i_translated.append(interior_ring_translated.tolist())
|
||||
else:
|
||||
i_translated = i
|
||||
return e_translated.tolist(), i_translated
|
||||
else:
|
||||
return e.tolist(), i
|
||||
|
||||
def getBufferedBBoxPoints(b):
|
||||
# Schritt 1: identifying all wallsurfaces and roof surfaces of the building
|
||||
output = {}
|
||||
specifyVersion()
|
||||
# comprehensive list of semantic surfaces
|
||||
semanticSurfaces = ['GroundSurface', 'WallSurface', 'RoofSurface', 'ClosureSurface', 'CeilingSurface', ]
|
||||
|
||||
for semanticSurface in semanticSurfaces:
|
||||
output[semanticSurface] = []
|
||||
data = []
|
||||
for cl in output:
|
||||
cls = []
|
||||
for child in b.getiterator():
|
||||
if child.tag == '{%s}%s' % (ns_bldg, cl):
|
||||
cls.append(child)
|
||||
|
||||
for feature in cls:
|
||||
for p in feature.findall('.//{%s}Polygon' % ns_gml):
|
||||
e, i = m3dm.polydecomposer(p)
|
||||
epoints = m3dm.GMLpoints(e[0])
|
||||
# -- Clean recurring points, except the last one
|
||||
last_ep = epoints[-1]
|
||||
epoints_clean = list(remove_reccuring(epoints))
|
||||
epoints_clean.append(last_ep)
|
||||
for point in epoints_clean:
|
||||
data.append(point)
|
||||
|
||||
# Schritt 2: Idetify the Bounding volume
|
||||
# 2.1 creating an open3d pointcloud from all the idetified vertex points
|
||||
pcd = o3d.t.geometry.PointCloud(o3c.Tensor(data, o3c.float32))
|
||||
# 2.2 obtain the axis aligned boundign box of the point cloud
|
||||
axis_aligned_bb = pcd.get_axis_aligned_bounding_box()
|
||||
# Schritt 3: Construct small triangles that describe the boundign box sufficienly
|
||||
box_points = axis_aligned_bb.get_box_points().numpy().tolist()
|
||||
# Convert the list to a numpy array for easier manipulation
|
||||
box_points = np.array(box_points)
|
||||
# Calculate the min and max coordinates
|
||||
min_x, min_y, min_z = np.min(box_points, axis=0)
|
||||
max_x, max_y, max_z = np.max(box_points, axis=0)
|
||||
# Add a 3m buffer
|
||||
buffer = 3
|
||||
min_x -= buffer
|
||||
min_y -= buffer
|
||||
min_z -= buffer
|
||||
max_x += buffer
|
||||
max_y += buffer
|
||||
max_z += buffer
|
||||
# Define the buffered bounding box points
|
||||
buffered_box_points = np.array([
|
||||
[min_x, min_y, min_z],
|
||||
[max_x, min_y, min_z],
|
||||
[min_x, max_y, min_z],
|
||||
[min_x, min_y, max_z],
|
||||
[max_x, max_y, max_z],
|
||||
[min_x, max_y, max_z],
|
||||
[max_x, min_y, max_z],
|
||||
[max_x, max_y, min_z]
|
||||
])
|
||||
return buffered_box_points
|
||||
|
||||
def obtainSRSInfo(root):
|
||||
specifyVersion()
|
||||
# obtain the envelope object
|
||||
envelopes = []
|
||||
for envelope in root.getiterator('{%s}Envelope' % ns_gml):
|
||||
envelopes.append(envelope)
|
||||
|
||||
# Extracting the srsName attribute from each Envelope
|
||||
srs_names = [envelope.get('srsName') for envelope in envelopes]
|
||||
srs_Dimensions = [envelope.get('srsDimension') for envelope in envelopes]
|
||||
return srs_names, srs_Dimensions
|
||||
|
||||
|
||||
# This function is used to create a corresponding json file defining the bbox of an object for each corresponding obj file
|
||||
def writeBBOXJSON(b, overall_counter, path, b_counter, trans_param):
|
||||
if len(trans_param) > 0:
|
||||
translation_parameters = {
|
||||
"d_x": str(trans_param[0]),
|
||||
"d_y": str(trans_param[1]),
|
||||
"d_z": str(trans_param[2])
|
||||
}
|
||||
else:
|
||||
translation_parameters = {
|
||||
"d_x": str(0),
|
||||
"d_y": str(0),
|
||||
"d_z": str(0)
|
||||
}
|
||||
|
||||
buffered_box_points_global = getBufferedBBoxPoints(b)
|
||||
|
||||
# translate to the local coordinate system
|
||||
buffered_box_points, _ = addTranslationParameters(buffered_box_points_global, [], trans_param=trans_param)
|
||||
|
||||
# From this set of points, obtain the minimum set that is necessary to describe the bounding box.
|
||||
min_point = buffered_box_points[0]
|
||||
max_point = buffered_box_points[4]
|
||||
|
||||
# Construct the json file path
|
||||
json_file_path = str(path) + str(b_counter) + "_" + str(overall_counter) + "_bbox_" + ".json"
|
||||
|
||||
# Write the bbox to a designated json file
|
||||
# Prüfen, ob die JSON-Datei existiert und laden
|
||||
if os.path.exists(json_file_path):
|
||||
with open(json_file_path, 'r') as json_file:
|
||||
axis_aligned_bbox = json.load(json_file)
|
||||
|
||||
|
||||
else:
|
||||
axis_aligned_bbox = {}
|
||||
|
||||
# Neuen Identifier hinzufügen
|
||||
axis_aligned_bbox["axis_aligned_bbox"] = {
|
||||
"min_point": str(min_point),
|
||||
"max_point": str(max_point),
|
||||
"translation_parameters": translation_parameters
|
||||
|
||||
}
|
||||
|
||||
# Zuordnungen in JSON-Datei schreiben
|
||||
with open(json_file_path, 'w') as json_file:
|
||||
json.dump(axis_aligned_bbox, json_file, indent=4)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
# This function is used to add information about the used spatial reference system to the json file
|
||||
def addCRSToJSON(root, json_file_path):
|
||||
specifyVersion()
|
||||
# obtain the envelope object
|
||||
envelopes = []
|
||||
for envelope in root.getiterator('{%s}Envelope' % ns_gml):
|
||||
envelopes.append(envelope)
|
||||
if envelopes:
|
||||
# Extracting the srsName attribute from each Envelope
|
||||
srs_names = [envelope.get('srsName') for envelope in envelopes]
|
||||
srs_Dimensions = [envelope.get('srsDimension') for envelope in envelopes]
|
||||
elif not envelopes:
|
||||
srs_names = "Unkown"
|
||||
srs_Dimensions = "Unkown"
|
||||
|
||||
|
||||
used_srs = srs_names[0]
|
||||
# Prüfen, ob die JSON-Datei existiert und laden
|
||||
if os.path.exists(json_file_path):
|
||||
with open(json_file_path, 'r') as json_file:
|
||||
crs_info = json.load(json_file)
|
||||
else:
|
||||
crs_info = {}
|
||||
|
||||
# Neuen Identifier hinzufügen
|
||||
crs_info["CRS"] = {
|
||||
"srsName": used_srs,
|
||||
"srsDimensions": srs_Dimensions
|
||||
}
|
||||
|
||||
# Zuordnungen in JSON-Datei schreiben
|
||||
with open(json_file_path, 'w') as json_file:
|
||||
json.dump(crs_info, json_file, indent=4)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def claculateCornerTriangles(b, trans_param):
|
||||
buffered_box_points = getBufferedBBoxPoints(b)
|
||||
|
||||
# Translate the Bounding box into the local coordinate system
|
||||
# buffered_box_points, _ = addTranslationParameters(buffered_box_points_global, [], trans_param=trans_param)
|
||||
|
||||
# Create triangles at the corners of the buffered bounding box
|
||||
corner_triangles = create_corner_triangles(buffered_box_points)
|
||||
|
||||
# Convert the triangles to lists
|
||||
corner_triangles = [np.array(triangle).tolist() for triangle in corner_triangles]
|
||||
|
||||
print("\nCorner Triangles:")
|
||||
for i, triangle in enumerate(corner_triangles):
|
||||
print(f"Triangle {i + 1}: {triangle}")
|
||||
|
||||
return corner_triangles
|
||||
|
||||
|
||||
# diese funktion dient dazu ein JSON file zu schreiben um die meta informationen über die einzelnen objekte zuspeichern
|
||||
def add_identifier_to_json(filename, tag, parentID, gmlID, json_file_path):
|
||||
"""
|
||||
Adds the identifier information for one .obj file to a JSON file.
|
||||
|
||||
Parameters:
|
||||
- number (int): The number corresponding to the .obj file.
|
||||
- tag (str): The tag corresponding to the .obj file.
|
||||
- parentID (str): The parent ID corresponding to the .obj file.
|
||||
- gmlID (str): The gml ID corresponding to the .obj file.
|
||||
- json_file_path (str): Path to the JSON file where identifier information will be stored.
|
||||
"""
|
||||
|
||||
# Prüfen, ob die JSON-Datei existiert und laden
|
||||
if os.path.exists(json_file_path):
|
||||
with open(json_file_path, 'r') as json_file:
|
||||
identifiers = json.load(json_file)
|
||||
else:
|
||||
identifiers = {}
|
||||
|
||||
# Neuen Identifier hinzufügen
|
||||
identifiers[filename] = {
|
||||
'tag': tag,
|
||||
'parentID': parentID,
|
||||
'gmlID': gmlID
|
||||
}
|
||||
|
||||
# Zuordnungen in JSON-Datei schreiben
|
||||
with open(json_file_path, 'w') as json_file:
|
||||
json.dump(identifiers, json_file, indent=4)
|
||||
|
||||
print(f"Zuordnung für {filename} wurde gespeichert.")
|
||||
|
||||
|
||||
def perturb_points(points, perturbation_scale=1e-6):
|
||||
"""
|
||||
Perturb the points slightly to avoid degenerate cases.
|
||||
|
||||
Parameters:
|
||||
points (list of tuple of floats): A list where each element is a tuple (x, y, z) representing a 3D point.
|
||||
perturbation_scale (float): The maximum magnitude of the perturbation applied to each coordinate.
|
||||
|
||||
Returns:
|
||||
list of list of floats: Perturbed list of points.
|
||||
"""
|
||||
points_array = np.array(points)
|
||||
perturbation = np.random.uniform(-perturbation_scale, perturbation_scale, points_array.shape)
|
||||
perturbed_points = points_array + perturbation
|
||||
return perturbed_points.tolist()
|
||||
|
||||
|
||||
def write_obj_file(surfaces, filename, tag, parentid, gmlid, counter, path, tr_1, translation_parameters):
|
||||
for triangle in tr_1:
|
||||
triangle_local, _ = addTranslationParameters(triangle, [], trans_param=translation_parameters)
|
||||
surfaces.append(triangle_local)
|
||||
with open(filename, 'w') as file:
|
||||
vertex_index = 1
|
||||
for triangle in surfaces:
|
||||
for vertex in triangle:
|
||||
file.write(f"v {vertex[0]} {vertex[1]} {vertex[2]}\n")
|
||||
file.write(f"f {vertex_index} {vertex_index + 1} {vertex_index + 2}\n")
|
||||
vertex_index += 3
|
||||
add_identifier_to_json(filename, tag, parentid, gmlid, (path + "index.json"))
|
||||
|
||||
|
||||
def remove_reccuring(list_vertices):
|
||||
"""Removes recurring vertices, which messes up the triangulation.
|
||||
Inspired by http://stackoverflow.com/a/1143432"""
|
||||
# last_point = list_vertices[-1]
|
||||
list_vertices_without_last = list_vertices[:-1]
|
||||
found = set()
|
||||
for item in list_vertices_without_last:
|
||||
if str(item) not in found:
|
||||
yield item
|
||||
found.add(str(item))
|
||||
|
||||
|
||||
def separate_string(s):
|
||||
# Define the regex pattern
|
||||
pattern = r'\{([^}]*)\}(.*)'
|
||||
# Search for the pattern in the input string
|
||||
match = re.search(pattern, s)
|
||||
if match:
|
||||
# Extract the parts
|
||||
inside_braces = match.group(1)
|
||||
outside_braces = match.group(2)
|
||||
return inside_braces, outside_braces
|
||||
else:
|
||||
return None, None
|
||||
|
||||
|
||||
def specifyVersion():
|
||||
global ns_citygml
|
||||
global ns_gml
|
||||
global ns_bldg
|
||||
global ns_xsi
|
||||
global ns_xAL
|
||||
global ns_xlink
|
||||
global ns_dem
|
||||
global ns_con
|
||||
global ns_app
|
||||
global ns_pcl
|
||||
global ns_gen
|
||||
global ns_gss
|
||||
global ns_pfx0
|
||||
global ns_gsr
|
||||
global ns_tran
|
||||
global ns_gmd
|
||||
global ns_gts
|
||||
global ns_veg
|
||||
global ns_frn
|
||||
global ns_tun
|
||||
global ns_wtr
|
||||
global nsmap
|
||||
|
||||
if config.getVersion() == 1:
|
||||
# -- Name spaces for CityGML 2.0
|
||||
ns_citygml = "http://www.opengis.net/citygml/1.0"
|
||||
ns_gml = "http://www.opengis.net/gml"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/1.0"
|
||||
ns_tran = "http://www.opengis.net/citygml/transportation/1.0"
|
||||
ns_veg = "http://www.opengis.net/citygml/vegetation/1.0"
|
||||
ns_gen = "http://www.opengis.net/citygml/generics/1.0"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xsdschema:xAL:1.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/1.0"
|
||||
ns_frn = "http://www.opengis.net/citygml/cityfurniture/1.0"
|
||||
ns_tun = "http://www.opengis.net/citygml/tunnel/1.0"
|
||||
ns_wtr = "http://www.opengis.net/citygml/waterbody/1.0"
|
||||
ns_brid = "http://www.opengis.net/citygml/bridge/1.0"
|
||||
ns_app = "http://www.opengis.net/citygml/appearance/1.0"
|
||||
if config.getVersion() == 2:
|
||||
# -- Name spaces for CityGML 2.0
|
||||
ns_citygml = "http://www.opengis.net/citygml/2.0"
|
||||
ns_gml = "http://www.opengis.net/gml"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/2.0"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/2.0"
|
||||
elif config.getVersion() == 3:
|
||||
# -- Name spaces for CityGML 3.0
|
||||
ns_citygml = "http://www.opengis.net/citygml/3.0"
|
||||
ns_con = "http://www.opengis.net/citygml/construction/3.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_gml = "http://www.opengis.net/gml/3.2"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/3.0"
|
||||
ns_app = "http://www.opengis.net/citygml/appearance/3.0"
|
||||
ns_pcl = "http://www.opengis.net/citygml/pointcloud/3.0"
|
||||
ns_gen = "http://www.opengis.net/citygml/generics/3.0"
|
||||
ns_gss = "http://www.isotc211.org/2005/gss"
|
||||
ns_pfx0 = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||
ns_gsr = "http://www.isotc211.org/2005/gsr"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_tran = "http://www.opengis.net/citygml/transportation/3.0"
|
||||
ns_gmd = "http://www.isotc211.org/2005/gmd"
|
||||
ns_gts = "http://www.isotc211.org/2005/gts"
|
||||
ns_veg = "http://www.opengis.net/citygml/vegetation/3.0"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xal:3"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/3.0"
|
||||
ns_frn = "http://www.opengis.net/citygml/cityfurniture/3.0"
|
||||
ns_tun = "http://www.opengis.net/citygml/tunnel/3.0"
|
||||
ns_wtr = "http://www.opengis.net/citygml/waterbody/3.0"
|
||||
|
||||
nsmap = {
|
||||
None: ns_citygml,
|
||||
'gml': ns_gml,
|
||||
'bldg': ns_bldg,
|
||||
'xsi': ns_xsi,
|
||||
'xAL': ns_xAL,
|
||||
'xlink': ns_xlink,
|
||||
'dem': ns_dem
|
||||
}
|
||||
|
||||
|
||||
def compute_convex_hull(points):
|
||||
"""
|
||||
Computes the convex hull of a set of 3D points using Open3D, including triangulation of the hull's faces.
|
||||
|
||||
Parameters:
|
||||
points (list of tuple of floats): A list where each element is a tuple (x, y, z) representing a 3D point.
|
||||
|
||||
Returns:
|
||||
list: A list of faces, where each face is a list of vertex coordinates forming that face.
|
||||
"""
|
||||
# Convert the list of points to a NumPy array
|
||||
points_array = np.array(points)
|
||||
perturbed_points = perturb_points(points_array)
|
||||
|
||||
# Create an Open3D PointCloud object
|
||||
pcd = o3d.geometry.PointCloud()
|
||||
pcd.points = o3d.utility.Vector3dVector(perturbed_points)
|
||||
|
||||
# Compute the convex hull
|
||||
hull, triangles = pcd.compute_convex_hull()
|
||||
|
||||
# Extract vertices and faces (triangles)
|
||||
hull_vertices = np.asarray(hull.vertices)
|
||||
hull_triangles = np.asarray(hull.triangles)
|
||||
|
||||
# Prepare the faces in the desired format
|
||||
faces = []
|
||||
for triangle in hull_triangles:
|
||||
face = [list(hull_vertices[vertex]) for vertex in triangle]
|
||||
faces.append(face)
|
||||
return faces
|
||||
|
||||
|
||||
def process_polygon(p, trans_param):
|
||||
e = p[0]
|
||||
i = p[1]
|
||||
e_trans, i_trans = addTranslationParameters(e, i, trans_param=trans_param)
|
||||
t = p3dm.triangulation(e_trans, i_trans)
|
||||
# print(f"t: {t}")
|
||||
return t
|
||||
|
||||
|
||||
def process_polygons_parallel(polys, trans_param):
|
||||
data = []
|
||||
results = []
|
||||
for poly in polys:
|
||||
e, i = m3dm.polydecomposer(poly)
|
||||
epoints = m3dm.GMLpoints(e[0])
|
||||
# -- Clean recurring points, except the last one
|
||||
last_ep = epoints[-1]
|
||||
epoints_clean = list(remove_reccuring(epoints))
|
||||
epoints_clean.append(last_ep)
|
||||
# -- LinearRing(s) forming the interior
|
||||
irings = []
|
||||
for iring in i:
|
||||
ipoints = m3dm.GMLpoints(iring)
|
||||
# -- Clean them in the same manner as the exterior ring
|
||||
last_ip = ipoints[-1]
|
||||
ipoints_clean = list(remove_reccuring(ipoints))
|
||||
ipoints_clean.append(last_ip)
|
||||
irings.append(ipoints_clean)
|
||||
if len(epoints_clean) > 4:
|
||||
t = process_polygon([epoints_clean, irings], trans_param=trans_param)
|
||||
results.append(t)
|
||||
# data.append(poly_components)
|
||||
if len(epoints_clean) == 4:
|
||||
epoints_clean_translated, _ = addTranslationParameters(epoints_clean, [], trans_param=trans_param)
|
||||
results.append([epoints_clean_translated])
|
||||
|
||||
# t = process_polygon(poly)
|
||||
# results.append(t)
|
||||
|
||||
# cpu_cores = os.cpu_count()
|
||||
# print(f'Number of available CPU cores (using os): {cpu_cores}')
|
||||
# with ThreadPoolExecutor(max_workers=cpu_cores) as executor:
|
||||
# # Submitting all tasks
|
||||
# futures = [executor.submit(process_polygon, p) for p in data]
|
||||
# # Collecting results
|
||||
# for future in futures:
|
||||
# result = future.result() # This will re-raise any exception caught during the execution of the task
|
||||
# results.append(result)
|
||||
return results
|
||||
|
||||
|
||||
# this is an experimental method for parallelization
|
||||
def processOpening(o, path, buildingid, overall_counter, tr_1, trans_param, b_counter):
|
||||
for child in o.getiterator():
|
||||
unique_identifier = child.xpath("@g:id", namespaces={'g': ns_gml})
|
||||
if child.tag == '{%s}Window' % ns_bldg or child.tag == '{%s}Door' % ns_bldg:
|
||||
polys = m3dm.polygonFinder(o)
|
||||
t = process_polygons_parallel(polys, trans_param=trans_param)
|
||||
triangles = []
|
||||
for poly in t:
|
||||
for tr in poly:
|
||||
triangles.append(tr)
|
||||
filename = path + str(b_counter) + "_" + str(overall_counter) + ".obj"
|
||||
write_obj_file(triangles, filename, str(child.tag), buildingid, unique_identifier, overall_counter, path,
|
||||
tr_1, trans_param)
|
||||
|
||||
|
||||
def getAllExteriorPoints(polys):
|
||||
data = []
|
||||
for poly in polys:
|
||||
e, i = m3dm.polydecomposer(poly)
|
||||
epoints = m3dm.GMLpoints(e[0])
|
||||
# -- Clean recurring points, except the last one
|
||||
last_ep = epoints[-1]
|
||||
epoints_clean = list(remove_reccuring(epoints))
|
||||
epoints_clean.append(last_ep)
|
||||
for point in epoints_clean:
|
||||
data.append(point)
|
||||
return data
|
||||
|
||||
|
||||
def processWithApproximatedWindows(o, path, buildingid, overall_counter, tr_1, translation_parameters, b_counter):
|
||||
for child in o.getiterator():
|
||||
unique_identifier = child.xpath("@g:id", namespaces={'g': ns_gml})
|
||||
if child.tag == '{%s}Window' % ns_bldg or child.tag == '{%s}Door' % ns_bldg:
|
||||
polys = m3dm.polygonFinder(o)
|
||||
exterior_points = getAllExteriorPoints(polys)
|
||||
t_global = compute_convex_hull(exterior_points)
|
||||
_, t = addTranslationParameters(e=[], i=t_global, trans_param=translation_parameters)
|
||||
filename = path + str(b_counter) + "_" + str(overall_counter) + ".obj"
|
||||
write_obj_file(t, filename, str(child.tag), buildingid, unique_identifier, overall_counter, path, tr_1,
|
||||
translation_parameters=translation_parameters)
|
||||
|
||||
|
||||
# This function is used to im port a bounding box that is associated to the corresponding building component
|
||||
# It still has to be tested if it works
|
||||
def importBoundingBox(pathToBoundingBoxFile, trans_param):
|
||||
print("Hier")
|
||||
keys = ["_xmin", "_xmax", "_ymin", "_ymax", "_zmin", "_zmax"]
|
||||
values = {}
|
||||
|
||||
with open(pathToBoundingBoxFile, 'r') as file:
|
||||
for _ in range(15):
|
||||
line = file.readline()
|
||||
for key in keys:
|
||||
match = re.search(f'"{key}"\s*:\s*([-0-9.]+)', line)
|
||||
if match:
|
||||
values[key] = float(match.group(1))
|
||||
|
||||
if not all(k in values for k in keys):
|
||||
raise ValueError("Missing bounding box values in the first 15 lines")
|
||||
|
||||
min_x, max_x = values["_xmin"], values["_xmax"]
|
||||
min_y, max_y = values["_ymin"], values["_ymax"]
|
||||
min_z, max_z = values["_zmin"], values["_zmax"]
|
||||
|
||||
print(f"min x: {min_x} min y: {min_y} , min z: {min_z}")
|
||||
print(f"max x: {max_x} max y: {max_y} , max z: {max_z}")
|
||||
|
||||
box_points = np.array([
|
||||
[min_x, min_y, min_z],
|
||||
[max_x, min_y, min_z],
|
||||
[min_x, max_y, min_z],
|
||||
[min_x, min_y, max_z],
|
||||
[max_x, max_y, max_z],
|
||||
[min_x, max_y, max_z],
|
||||
[max_x, min_y, max_z],
|
||||
[max_x, max_y, min_z]
|
||||
])
|
||||
|
||||
# Create triangles at the corners of the buffered bounding box
|
||||
corner_triangles = create_corner_triangles(box_points)
|
||||
|
||||
# Convert the triangles to lists
|
||||
corner_triangles = [np.array(triangle).tolist() for triangle in corner_triangles]
|
||||
|
||||
print("\nCorner Triangles:")
|
||||
for i, triangle in enumerate(corner_triangles):
|
||||
print(f"Triangle {i + 1}: {triangle}")
|
||||
|
||||
return corner_triangles
|
||||
|
||||
|
||||
def separateComponents(b, path, APPROXIMATEWINDOWS, ADDBOUNDINGBOX, ADDBOUNDINGBOXJSON, TRANSLATEBUILDINGS,
|
||||
IMPORTBOUNDINGBOX, b_counter):
|
||||
if TRANSLATEBUILDINGS:
|
||||
# Step 1: Obtain the axis oriented bounding box of the building
|
||||
bounding_box_points = getBufferedBBoxPoints(b)
|
||||
|
||||
# Step 2 calculate the mean value of the points that the bbox points
|
||||
translation_parameters = np.mean(bounding_box_points, axis=0)
|
||||
|
||||
if not TRANSLATEBUILDINGS: # todo: nocheinmal üerlegen ob man hier nicht vielleicht besser elif oder so nehmen sollte
|
||||
translation_parameters = []
|
||||
|
||||
if IMPORTBOUNDINGBOX != None:
|
||||
ADDBOUNDINGBOX = False # make sure that the bounding box is not calculated
|
||||
|
||||
# Option to include the small triangles to mark the buffered bounding box
|
||||
if ADDBOUNDINGBOX:
|
||||
tr_1 = claculateCornerTriangles(b, trans_param=translation_parameters)
|
||||
elif not ADDBOUNDINGBOX:
|
||||
tr_1 = []
|
||||
global overall_counter
|
||||
overall_counter = 0
|
||||
output = {}
|
||||
specifyVersion()
|
||||
# comprehensive list of semantic surfaces
|
||||
semanticSurfaces = ['GroundSurface', 'WallSurface', 'RoofSurface', 'ClosureSurface', 'CeilingSurface',
|
||||
'InteriorWallSurface', 'FloorSurface', 'OuterCeilingSurface', 'OuterFloorSurface', 'Door',
|
||||
"outerBuildingInstallation",
|
||||
'Window', "BuildingInstallation", "BuildingConstructiveElement"]
|
||||
|
||||
for semanticSurface in semanticSurfaces:
|
||||
output[semanticSurface] = []
|
||||
|
||||
# get the building id for the building
|
||||
buildingid = b.xpath("@g:id", namespaces={'g': ns_gml})
|
||||
|
||||
# Handle the case when a building has no gml:id - This should however never occur...
|
||||
if not buildingid:
|
||||
buildingid = b_counter
|
||||
|
||||
# Todo: Muss noch implementiert werden
|
||||
# Import the bounding box
|
||||
if IMPORTBOUNDINGBOX != None:
|
||||
pathToBoundingBoxFile = IMPORTBOUNDINGBOX + "/" + str(buildingid) + ".json"
|
||||
tr_1 = importBoundingBox(pathToBoundingBoxFile=pathToBoundingBoxFile, trans_param=translation_parameters)
|
||||
|
||||
if config.getVersion() != 3:
|
||||
openings = []
|
||||
openingpolygons = []
|
||||
for child in b.getiterator():
|
||||
if child.tag == '{%s}opening' % ns_bldg:
|
||||
openings.append(child)
|
||||
for o in child.findall('.//{%s}Polygon' % ns_gml):
|
||||
openingpolygons.append(o)
|
||||
|
||||
for o in openings:
|
||||
# print("approximate windows: ", APPROXIMATEWINDOWS)
|
||||
if APPROXIMATEWINDOWS:
|
||||
processWithApproximatedWindows(o, path, buildingid, overall_counter, tr_1=tr_1,
|
||||
translation_parameters=translation_parameters, b_counter=b_counter)
|
||||
if not APPROXIMATEWINDOWS:
|
||||
processOpening(o, path, buildingid, overall_counter, tr_1, trans_param=translation_parameters,
|
||||
b_counter=b_counter)
|
||||
if ADDBOUNDINGBOXJSON:
|
||||
writeBBOXJSON(b, overall_counter=overall_counter, path=path, b_counter=b_counter,
|
||||
trans_param=translation_parameters)
|
||||
overall_counter += 1
|
||||
|
||||
if config.getVersion() == 3:
|
||||
openingpolygons = []
|
||||
print("Component separation for CityGML 3.0 is not implemented yet.")
|
||||
# todo: muss noch implementiert werden
|
||||
|
||||
# -- Process other thematic boundaries
|
||||
for cl in output:
|
||||
cls = []
|
||||
for child in b.getiterator():
|
||||
if child.tag == '{%s}%s' % (ns_bldg, cl):
|
||||
cls.append(child)
|
||||
|
||||
for feature in cls:
|
||||
# -- If it is the first feature, print the object identifier
|
||||
unique_identifier = feature.xpath("@g:id", namespaces={
|
||||
'g': ns_gml})
|
||||
if str(unique_identifier) != "[]" or str(unique_identifier) == "[]":
|
||||
cleaned_filename = str(unique_identifier)
|
||||
# -- This is not supposed to happen, but just to be sure...
|
||||
if feature.tag == '{%s}Window' % ns_bldg or feature.tag == '{%s}Door' % ns_bldg:
|
||||
continue
|
||||
tag = feature.tag
|
||||
_, cleaned_tag = separate_string(tag)
|
||||
# -- Find all polygons in this semantic boundary hierarchy
|
||||
poly_t = []
|
||||
t_ges = []
|
||||
number_of_polygons = len(feature.findall('.//{%s}Polygon' % ns_gml))
|
||||
pcounter = 0
|
||||
print(f"there are {number_of_polygons} polygons there!")
|
||||
for p in feature.findall('.//{%s}Polygon' % ns_gml):
|
||||
found_opening = False
|
||||
for optest in openingpolygons:
|
||||
if p == optest:
|
||||
found_opening = True
|
||||
break
|
||||
# -- If there is an opening skip it
|
||||
if found_opening:
|
||||
pass
|
||||
else:
|
||||
# -- Decompose the polygon into exterior and interior
|
||||
e, i = m3dm.polydecomposer(p)
|
||||
# -- Points forming the exterior LinearRing
|
||||
epoints = m3dm.GMLpoints(e[0])
|
||||
# -- Clean recurring points, except the last one
|
||||
last_ep = epoints[-1]
|
||||
epoints_clean = list(remove_reccuring(epoints))
|
||||
epoints_clean.append(last_ep)
|
||||
|
||||
# -- LinearRing(s) forming the interior
|
||||
irings = []
|
||||
for iring in i:
|
||||
ipoints = m3dm.GMLpoints(iring)
|
||||
# -- Clean them in the same manner as the exterior ring
|
||||
last_ip = ipoints[-1]
|
||||
ipoints_clean = list(remove_reccuring(ipoints))
|
||||
ipoints_clean.append(last_ip)
|
||||
irings.append(ipoints_clean)
|
||||
|
||||
# Applying the translation parameters
|
||||
e_trans, i_trans = addTranslationParameters(e=epoints_clean, i=irings,
|
||||
trans_param=translation_parameters)
|
||||
|
||||
try:
|
||||
if len(epoints_clean) > 4:
|
||||
t = p3dm.triangulation(e_trans, i_trans)
|
||||
# print("Nur drei Punkte")
|
||||
poly_t.append(t)
|
||||
if len(epoints_clean) == 4:
|
||||
t = e_trans[0:3]
|
||||
poly_t.append([t])
|
||||
if len(epoints_clean) < 3:
|
||||
t = []
|
||||
# print("Empty Surface!")
|
||||
except:
|
||||
t = []
|
||||
|
||||
for surfaces in poly_t:
|
||||
t_ges = t_ges + surfaces
|
||||
|
||||
pcounter += 1
|
||||
if pcounter % 100 == 0:
|
||||
print(pcounter)
|
||||
|
||||
filename = path + str(b_counter) + "_" + str(overall_counter) + ".obj"
|
||||
if ADDBOUNDINGBOXJSON:
|
||||
writeBBOXJSON(b, overall_counter=overall_counter, path=path, b_counter=b_counter,
|
||||
trans_param=translation_parameters)
|
||||
write_obj_file(t_ges, filename, str(feature.tag), buildingid, cleaned_filename, overall_counter, path,
|
||||
tr_1, translation_parameters=translation_parameters)
|
||||
overall_counter += 1
|
||||
|
||||
print("Segmentation finished!")
|
||||
return 0
|
||||
7
scripts/CityGML2OBJv2/config.py
Normal file
7
scripts/CityGML2OBJv2/config.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
def setVersion(version):
|
||||
global VERSION
|
||||
VERSION = version
|
||||
print(f"version: {VERSION}")
|
||||
|
||||
def getVersion():
|
||||
return VERSION
|
||||
52
scripts/CityGML2OBJv2/generateMTL.py
Normal file
52
scripts/CityGML2OBJv2/generateMTL.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# The MIT License (MIT)
|
||||
|
||||
# This code is part of the CityGML2OBJs package
|
||||
|
||||
# Copyright (c) 2014
|
||||
# Filip Biljecki
|
||||
# Delft University of Technology
|
||||
# fbiljecki@gmail.com
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.cm as cm
|
||||
|
||||
#-- Number of classes of the colormap
|
||||
no_values = 101
|
||||
|
||||
#-- Select the colormap and get its RGB values for each of the classes
|
||||
colormap = cm.get_cmap("afmhot", no_values) # http://matplotlib.org/examples/color/colormaps_reference.html
|
||||
colormap_vals = colormap(np.arange(no_values)).tolist()
|
||||
|
||||
#-- This is the MTL file
|
||||
mtlcontents = ""
|
||||
|
||||
#-- Class by class... MTL!
|
||||
for i in range(0, no_values):
|
||||
b = float(i)/100
|
||||
mtlcontents += "newmtl " + str(b) + "\n"
|
||||
mtlcontents += "Ka " + str(colormap_vals[i][0]) + " " + str(colormap_vals[i][1]) + " " + str(colormap_vals[i][2]) + "\n"
|
||||
mtlcontents += "Kd " + str(colormap_vals[i][0]) + " " + str(colormap_vals[i][1]) + " " + str(colormap_vals[i][2]) + "\n"
|
||||
#-- Write the MTL
|
||||
with open("colormap.mtl", "w") as mtl_file:
|
||||
mtl_file.write(mtlcontents)
|
||||
148
scripts/CityGML2OBJv2/markup3dmodule.py
Normal file
148
scripts/CityGML2OBJv2/markup3dmodule.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import config
|
||||
# The MIT License (MIT)
|
||||
|
||||
# This code is part of the CityGML2OBJs package
|
||||
|
||||
# Copyright (c) 2014
|
||||
# Filip Biljecki
|
||||
# Delft University of Technology
|
||||
# fbiljecki@gmail.com
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
from config import setVersion
|
||||
def specifyVersion():
|
||||
global ns_citygml
|
||||
global ns_gml
|
||||
global ns_bldg
|
||||
global ns_xsi
|
||||
global ns_xAL
|
||||
global ns_xlink
|
||||
global ns_dem
|
||||
global ns_con
|
||||
global ns_app
|
||||
global ns_pcl
|
||||
global ns_gen
|
||||
global ns_gss
|
||||
global ns_pfx0
|
||||
global ns_gsr
|
||||
global ns_tran
|
||||
global ns_gmd
|
||||
global ns_gts
|
||||
global ns_veg
|
||||
global ns_frn
|
||||
global ns_tun
|
||||
global ns_wtr
|
||||
global nsmap
|
||||
|
||||
#print("config.getVerision", config.getVersion())
|
||||
if config.getVersion() == 2 or config.getVersion() == 1:
|
||||
# -- Name spaces for CityGML 2.0
|
||||
ns_citygml = "http://www.opengis.net/citygml/2.0"
|
||||
ns_gml = "http://www.opengis.net/gml"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/2.0"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/2.0"
|
||||
elif config.getVersion() == 3:
|
||||
# -- Name spaces for CityGML 3.0
|
||||
ns_citygml = "http://www.opengis.net/citygml/3.0"
|
||||
ns_con = "http://www.opengis.net/citygml/construction/3.0"
|
||||
ns_xlink = "http://www.w3.org/1999/xlink"
|
||||
ns_gml = "http://www.opengis.net/gml/3.2"
|
||||
ns_bldg = "http://www.opengis.net/citygml/building/3.0"
|
||||
ns_app = "http://www.opengis.net/citygml/appearance/3.0"
|
||||
ns_pcl = "http://www.opengis.net/citygml/pointcloud/3.0"
|
||||
ns_gen = "http://www.opengis.net/citygml/generics/3.0"
|
||||
ns_gss = "http://www.isotc211.org/2005/gss"
|
||||
ns_pfx0 = "urn:oasis:names:tc:ciq:xsdschema:xAL:2.0"
|
||||
ns_gsr = "http://www.isotc211.org/2005/gsr"
|
||||
ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
ns_tran = "http://www.opengis.net/citygml/transportation/3.0"
|
||||
ns_gmd = "http://www.isotc211.org/2005/gmd"
|
||||
ns_gts = "http://www.isotc211.org/2005/gts"
|
||||
ns_veg = "http://www.opengis.net/citygml/vegetation/3.0"
|
||||
ns_xAL = "urn:oasis:names:tc:ciq:xal:3"
|
||||
ns_dem = "http://www.opengis.net/citygml/relief/3.0"
|
||||
ns_frn = "http://www.opengis.net/citygml/cityfurniture/3.0"
|
||||
ns_tun = "http://www.opengis.net/citygml/tunnel/3.0"
|
||||
ns_wtr = "http://www.opengis.net/citygml/waterbody/3.0"
|
||||
|
||||
nsmap = {
|
||||
None: ns_citygml,
|
||||
'gml': ns_gml,
|
||||
'bldg': ns_bldg,
|
||||
'xsi': ns_xsi,
|
||||
'xAL': ns_xAL,
|
||||
'xlink': ns_xlink,
|
||||
'dem': ns_dem
|
||||
}
|
||||
|
||||
|
||||
def polydecomposer(polygon):
|
||||
"""Extracts the <gml:exterior> and <gml:interior> of a <gml:Polygon>."""
|
||||
specifyVersion()
|
||||
exter = polygon.findall('.//{%s}exterior' % ns_gml)
|
||||
inter = polygon.findall('.//{%s}interior' % ns_gml)
|
||||
return exter, inter
|
||||
|
||||
|
||||
def polygonFinder(GMLelement):
|
||||
"""Find the <gml:polygon> element."""
|
||||
specifyVersion()
|
||||
#print("NSgml:", ns_gml)
|
||||
polygonsLocal = GMLelement.findall('.//{%s}Polygon' % ns_gml)
|
||||
#print(polygonsLocal)
|
||||
|
||||
#for polygon in polygonsLocal:
|
||||
# gml_id = polygon.get('{%s}id' % ns_gml)
|
||||
# print(gml_id)
|
||||
return polygonsLocal
|
||||
|
||||
|
||||
def GMLpoints(ring):
|
||||
specifyVersion()
|
||||
"Extract points from a <gml:LinearRing>."
|
||||
# -- List containing points
|
||||
listPoints = []
|
||||
# -- Read the <gml:posList> value and convert to string
|
||||
if len(ring.findall('.//{%s}posList' % ns_gml)) > 0:
|
||||
points = ring.findall('.//{%s}posList' % ns_gml)[0].text
|
||||
# -- List of coordinates
|
||||
coords = points.split()
|
||||
assert (len(coords) % 3 == 0)
|
||||
# -- Store the coordinate tuple
|
||||
for i in range(0, len(coords), 3):
|
||||
listPoints.append([float(coords[i]), float(coords[i + 1]), float(coords[i + 2])])
|
||||
elif len(ring.findall('.//{%s}pos' % ns_gml)) > 0:
|
||||
points = ring.findall('.//{%s}pos' % ns_gml)
|
||||
# -- Extract each point separately
|
||||
for p in points:
|
||||
coords = p.text.split()
|
||||
assert (len(coords) % 3 == 0)
|
||||
# -- Store the coordinate tuple
|
||||
for i in range(0, len(coords), 3):
|
||||
listPoints.append([float(coords[i]), float(coords[i + 1]), float(coords[i + 2])])
|
||||
else:
|
||||
return None
|
||||
|
||||
return listPoints
|
||||
77
scripts/CityGML2OBJv2/plotcolorbar.py
Normal file
77
scripts/CityGML2OBJv2/plotcolorbar.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# The MIT License (MIT)
|
||||
|
||||
# This code is part of the CityGML2OBJs package
|
||||
|
||||
# Copyright (c) 2014
|
||||
# Filip Biljecki
|
||||
# Delft University of Technology
|
||||
# fbiljecki@gmail.com
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import matplotlib as mpl
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
#-- Resource: http://matplotlib.org/examples/api/colorbar_only.html
|
||||
plt.rc('text', usetex=True)
|
||||
plt.rc('font', family='serif')
|
||||
#-- Make a figure and axes with dimensions as desired
|
||||
fig = plt.figure(figsize=(8, 1))
|
||||
ax1 = fig.add_axes([0.05, 0.80, 0.9, 0.15])
|
||||
|
||||
#-- Bounds
|
||||
vmin = 350
|
||||
vmax = 1300
|
||||
|
||||
#-- Colormap
|
||||
cmap = mpl.cm.afmhot
|
||||
norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
|
||||
|
||||
cb1 = mpl.colorbar.ColorbarBase(ax1, cmap=cmap,
|
||||
norm=norm,
|
||||
orientation='horizontal')
|
||||
|
||||
#-- Label on the axis
|
||||
cb1.set_label(r"Annual solar irradiation [kWh/m$^{2}$/year]")
|
||||
|
||||
|
||||
cmap = mpl.colors.ListedColormap(['r', 'g', 'b', 'c'])
|
||||
cmap.set_over('0.25')
|
||||
cmap.set_under('0.75')
|
||||
|
||||
bounds = [1, 2, 4, 7, 8]
|
||||
norm = mpl.colors.BoundaryNorm(bounds, cmap.N)
|
||||
|
||||
#-- Change the last tick label
|
||||
labels = [item.get_text() for item in cb1.ax.get_xticklabels()]
|
||||
li = 0
|
||||
for l in labels:
|
||||
# labels[li] = r"$"+str(l)+"$"
|
||||
labels[li] = r""+str(l)
|
||||
li += 1
|
||||
labels[-1] = r"$\geq "+str(vmax) + "$"
|
||||
cb1.ax.set_xticklabels(labels)
|
||||
|
||||
#-- Output
|
||||
plt.savefig('colorbar.pdf', transparent=True)
|
||||
plt.savefig('colorbar.png', dpi=600, transparent=True)
|
||||
plt.show()
|
||||
716
scripts/CityGML2OBJv2/polygon3dmodule.py
Normal file
716
scripts/CityGML2OBJv2/polygon3dmodule.py
Normal file
|
|
@ -0,0 +1,716 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# The MIT License (MIT)
|
||||
|
||||
# This code is part of the CityGML2OBJs package
|
||||
|
||||
# Copyright (c) 2014
|
||||
# Filip Biljecki
|
||||
# Delft University of Technology
|
||||
# fbiljecki@gmail.com
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import math
|
||||
import markup3dmodule
|
||||
from lxml import etree
|
||||
import copy
|
||||
import triangle
|
||||
import numpy as np
|
||||
import shapely
|
||||
from sklearn.decomposition import PCA
|
||||
|
||||
|
||||
def getAreaOfGML(poly, height=True):
|
||||
"""Function which reads <gml:Polygon> and returns its area.
|
||||
The function also accounts for the interior and checks for the validity of the polygon."""
|
||||
exteriorarea = 0.0
|
||||
interiorarea = 0.0
|
||||
# -- Decompose the exterior and interior boundary
|
||||
e, i = markup3dmodule.polydecomposer(poly)
|
||||
# -- Extract points in the <gml:LinearRing> of <gml:exterior>
|
||||
epoints = markup3dmodule.GMLpoints(e[0])
|
||||
if isPolyValid(epoints):
|
||||
if height:
|
||||
exteriorarea += get3DArea(epoints)
|
||||
else:
|
||||
exteriorarea += get2DArea(epoints)
|
||||
for idx, iring in enumerate(i):
|
||||
# -- Extract points in the <gml:LinearRing> of <gml:interior>
|
||||
ipoints = markup3dmodule.GMLpoints(iring)
|
||||
if isPolyValid(ipoints):
|
||||
if height:
|
||||
interiorarea += get3DArea(ipoints)
|
||||
else:
|
||||
interiorarea += get2DArea(ipoints)
|
||||
# -- Account for the interior
|
||||
area = exteriorarea - interiorarea
|
||||
# -- Area in dimensionless units (coordinate units)
|
||||
return area
|
||||
|
||||
|
||||
# -- Validity of a polygon ---------
|
||||
def isPolyValid(polypoints, output=True):
|
||||
"""Checks if a polygon is valid. Second option is to supress output."""
|
||||
# -- Number of points of the polygon (including the doubled first/last point)
|
||||
npolypoints = len(polypoints)
|
||||
# -- Assume that it is valid, and try to disprove the assumption
|
||||
valid = True
|
||||
# -- Check if last point equal
|
||||
if polypoints[0] != polypoints[-1]:
|
||||
if output:
|
||||
print("\t\tA degenerate polygon. First and last points do not match.")
|
||||
valid = False
|
||||
# -- Check if it has at least three points
|
||||
if npolypoints < 4: # -- Four because the first point is doubled as the last one in the ring
|
||||
if output:
|
||||
print("\t\tA degenerate polygon. The number of points is smaller than 3.")
|
||||
valid = False
|
||||
# -- Check if the points are planar
|
||||
if not isPolyPlanar(polypoints):
|
||||
if output:
|
||||
print("\t\tA degenerate polygon. The points are not planar.")
|
||||
valid = False
|
||||
# -- Check if some of the points are repeating
|
||||
for i in range(1, npolypoints):
|
||||
if polypoints[i] == polypoints[i - 1]:
|
||||
if output:
|
||||
print("\t\tA degenerate polygon. There are identical points.")
|
||||
valid = False
|
||||
# -- Check if the polygon does not have self-intersections
|
||||
# -- Disabled, something doesn't work here, will work on this later.
|
||||
# if not isPolySimple(polypoints):
|
||||
# print "A degenerate polygon. The edges are intersecting."
|
||||
# valid = False
|
||||
return valid
|
||||
|
||||
|
||||
def isPolyPlanar(polypoints):
|
||||
"""Checks if a polygon is planar."""
|
||||
# -- Normal of the polygon from the first three points
|
||||
try:
|
||||
normal = unit_normal(polypoints[0], polypoints[1], polypoints[2])
|
||||
except:
|
||||
return False
|
||||
# -- Number of points
|
||||
npolypoints = len(polypoints)
|
||||
# -- Tolerance
|
||||
eps = 0.01
|
||||
# -- Assumes planarity
|
||||
planar = True
|
||||
for i in range(3, npolypoints):
|
||||
vector = [polypoints[i][0] - polypoints[0][0], polypoints[i][1] - polypoints[0][1],
|
||||
polypoints[i][2] - polypoints[0][2]]
|
||||
if math.fabs(dot(vector, normal)) > eps:
|
||||
planar = False
|
||||
return planar
|
||||
|
||||
|
||||
def isPolySimple(polypoints): #todo: this function has to be adapted
|
||||
"""Checks if the polygon is simple, i.e. it does not have any self-intersections.
|
||||
Inspired by http://www.win.tue.nl/~vanwijk/2IV60/2IV60_exercise_3_answers.pdf"""
|
||||
npolypoints = len(polypoints)
|
||||
# -- Check if the polygon is vertical, i.e. a projection cannot be made.
|
||||
# -- First copy the list so the originals are not modified
|
||||
temppolypoints = copy.deepcopy(polypoints)
|
||||
newpolypoints = copy.deepcopy(temppolypoints)
|
||||
# -- If the polygon is vertical
|
||||
#if math.fabs(unit_normal(temppolypoints[0], temppolypoints[1], temppolypoints[2])[2]) < 10e-6:
|
||||
# vertical = True
|
||||
|
||||
#else:
|
||||
# vertical = False
|
||||
|
||||
normal = calculate_polygon_normal(temppolypoints)
|
||||
if math.fabs(normal[2]) < 10e-6:
|
||||
vertical = True
|
||||
print("The polygon is vertical 2")
|
||||
print("math.fabs(normal[2]): ", math.fabs(normal[2]))
|
||||
else:
|
||||
vertical = False
|
||||
print("Not vertical 2")
|
||||
|
||||
# -- We want to project the vertical polygon to the XZ plane
|
||||
# -- If a polygon is parallel with the YZ plane that will not be possible
|
||||
YZ = True
|
||||
for i in range(1, npolypoints):
|
||||
if temppolypoints[i][0] != temppolypoints[0][0]:
|
||||
YZ = False
|
||||
continue
|
||||
# -- Project the plane in the special case
|
||||
if YZ:
|
||||
for i in range(0, npolypoints):
|
||||
newpolypoints[i][0] = temppolypoints[i][1]
|
||||
newpolypoints[i][1] = temppolypoints[i][2]
|
||||
# -- Project the plane
|
||||
elif vertical:
|
||||
for i in range(0, npolypoints):
|
||||
newpolypoints[i][1] = temppolypoints[i][2]
|
||||
else:
|
||||
pass # -- No changes here
|
||||
# -- Check for the self-intersection edge by edge
|
||||
for i in range(0, npolypoints - 3):
|
||||
if i == 0:
|
||||
m = npolypoints - 3
|
||||
else:
|
||||
m = npolypoints - 2
|
||||
for j in range(i + 2, m):
|
||||
if intersection(newpolypoints[i], newpolypoints[i + 1], newpolypoints[j % npolypoints],
|
||||
newpolypoints[(j + 1) % npolypoints]):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def intersection(p, q, r, s):
|
||||
"""Check if two line segments (pq and rs) intersect. Computation is in 2D.
|
||||
Inspired by http://www.win.tue.nl/~vanwijk/2IV60/2IV60_exercise_3_answers.pdf"""
|
||||
|
||||
eps = 10e-6
|
||||
|
||||
V = [q[0] - p[0], q[1] - p[1]]
|
||||
W = [r[0] - s[0], r[1] - s[1]]
|
||||
|
||||
d = V[0] * W[1] - W[0] * V[1]
|
||||
|
||||
if math.fabs(d) < eps:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
# ------------------------------------------
|
||||
|
||||
def collinear(p0, p1, p2):
|
||||
# -- http://stackoverflow.com/a/9609069
|
||||
x1, y1 = p1[0] - p0[0], p1[1] - p0[1]
|
||||
x2, y2 = p2[0] - p0[0], p2[1] - p0[1]
|
||||
return x1 * y2 - x2 * y1 < 1e-12
|
||||
|
||||
|
||||
# -- Area and other handy computations
|
||||
def det(a):
|
||||
"""Determinant of matrix a."""
|
||||
return a[0][0] * a[1][1] * a[2][2] + a[0][1] * a[1][2] * a[2][0] + a[0][2] * a[1][0] * a[2][1] - a[0][2] * a[1][1] * \
|
||||
a[2][0] - a[0][1] * a[1][0] * a[2][2] - a[0][0] * a[1][2] * a[2][1]
|
||||
|
||||
|
||||
def unit_normal(a, b, c):
|
||||
"""Unit normal vector of plane defined by points a, b, and c."""
|
||||
x = det([[1, a[1], a[2]],
|
||||
[1, b[1], b[2]],
|
||||
[1, c[1], c[2]]])
|
||||
y = det([[a[0], 1, a[2]],
|
||||
[b[0], 1, b[2]],
|
||||
[c[0], 1, c[2]]])
|
||||
z = det([[a[0], a[1], 1],
|
||||
[b[0], b[1], 1],
|
||||
[c[0], c[1], 1]])
|
||||
magnitude = (x ** 2 + y ** 2 + z ** 2) ** .5
|
||||
if magnitude == 0.0:
|
||||
raise ValueError(
|
||||
"The normal of the polygon has no magnitude. Check the polygon. The most common cause for this are two identical sequential points or collinear points.")
|
||||
return (x / magnitude, y / magnitude, z / magnitude)
|
||||
|
||||
|
||||
def dot(a, b):
|
||||
"""Dot product of vectors a and b."""
|
||||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
|
||||
|
||||
def cross(a, b):
|
||||
"""Cross product of vectors a and b."""
|
||||
x = a[1] * b[2] - a[2] * b[1]
|
||||
y = a[2] * b[0] - a[0] * b[2]
|
||||
z = a[0] * b[1] - a[1] * b[0]
|
||||
return (x, y, z)
|
||||
|
||||
|
||||
def get3DArea(polypoints):
|
||||
"""Function which reads the list of coordinates and returns its area.
|
||||
The code has been borrowed from http://stackoverflow.com/questions/12642256/python-find-area-of-polygon-from-xyz-coordinates"""
|
||||
# -- Compute the area
|
||||
total = [0, 0, 0]
|
||||
for i in range(len(polypoints)):
|
||||
vi1 = polypoints[i]
|
||||
if i is len(polypoints) - 1:
|
||||
vi2 = polypoints[0]
|
||||
else:
|
||||
vi2 = polypoints[i + 1]
|
||||
prod = cross(vi1, vi2)
|
||||
total[0] += prod[0]
|
||||
total[1] += prod[1]
|
||||
total[2] += prod[2]
|
||||
result = dot(total, unit_normal(polypoints[0], polypoints[1], polypoints[2]))
|
||||
return math.fabs(result * .5)
|
||||
|
||||
|
||||
def get2DArea(polypoints):
|
||||
"""Reads the list of coordinates and returns its projected area (disregards z coords)."""
|
||||
flatpolypoints = copy.deepcopy(polypoints)
|
||||
for p in flatpolypoints:
|
||||
p[2] = 0.0
|
||||
return get3DArea(flatpolypoints)
|
||||
|
||||
|
||||
def getNormal(polypoints):
|
||||
"""Get the normal of the first three points of a polygon. Assumes planarity."""
|
||||
return unit_normal(polypoints[0], polypoints[1], polypoints[2])
|
||||
|
||||
|
||||
def getAngles(normal):
|
||||
"""Get the azimuth and altitude from the normal vector."""
|
||||
# -- Convert from polar system to azimuth
|
||||
azimuth = 90 - math.degrees(math.atan2(normal[1], normal[0]))
|
||||
if azimuth >= 360.0:
|
||||
azimuth -= 360.0
|
||||
elif azimuth < 0.0:
|
||||
azimuth += 360.0
|
||||
t = math.sqrt(normal[0] ** 2 + normal[1] ** 2)
|
||||
if t == 0:
|
||||
tilt = 0.0
|
||||
else:
|
||||
tilt = 90 - math.degrees(math.atan(normal[2] / t)) # 0 for flat roof, 90 for wall
|
||||
tilt = round(tilt, 3)
|
||||
|
||||
return azimuth, tilt
|
||||
|
||||
|
||||
def GMLstring2points(pointstring):
|
||||
"""Convert list of points in string to a list of points. Works for 3D points."""
|
||||
listPoints = []
|
||||
# -- List of coordinates
|
||||
coords = pointstring.split()
|
||||
# -- Store the coordinate tuple
|
||||
assert (len(coords) % 3 == 0)
|
||||
for i in range(0, len(coords), 3):
|
||||
listPoints.append([float(coords[i]), float(coords[i + 1]), float(coords[i + 2])])
|
||||
return listPoints
|
||||
|
||||
|
||||
def smallestPoint(list_of_points):
|
||||
"Finds the smallest point from a three-dimensional tuple list."
|
||||
smallest = []
|
||||
# -- Sort the points
|
||||
sorted_points = sorted(list_of_points, key=lambda x: (x[0], x[1], x[2]))
|
||||
# -- First one is the smallest one
|
||||
smallest = sorted_points[0]
|
||||
return smallest
|
||||
|
||||
|
||||
def highestPoint(list_of_points, a=None):
|
||||
"Finds the highest point from a three-dimensional tuple list."
|
||||
highest = []
|
||||
# -- Sort the points
|
||||
sorted_points = sorted(list_of_points, key=lambda x: (x[0], x[1], x[2]))
|
||||
# -- Last one is the highest one
|
||||
if a is not None:
|
||||
equalZ = True
|
||||
for i in range(-1, -1 * len(list_of_points), -1):
|
||||
if equalZ:
|
||||
highest = sorted_points[i]
|
||||
if highest[2] != a[2]:
|
||||
equalZ = False
|
||||
break
|
||||
else:
|
||||
break
|
||||
else:
|
||||
highest = sorted_points[-1]
|
||||
return highest
|
||||
|
||||
|
||||
def centroid(list_of_points):
|
||||
"""Returns the centroid of the list of points."""
|
||||
sum_x = 0
|
||||
sum_y = 0
|
||||
sum_z = 0
|
||||
n = float(len(list_of_points))
|
||||
for p in list_of_points:
|
||||
sum_x += float(p[0])
|
||||
sum_y += float(p[1])
|
||||
sum_z += float(p[2])
|
||||
return [sum_x / n, sum_y / n, sum_z / n]
|
||||
|
||||
# This function delivers very unsatisfying results for some reason.
|
||||
# The returned point lies on the contour of the polygon sometimes, wich then messes up the triangulation
|
||||
def point_inside(list_of_points):
|
||||
"""Returns a point that is guaranteed to be inside the polygon, thanks to Shapely."""
|
||||
# Th_Fr: new function that actually works
|
||||
representative_point_tmp = centroid(list_of_points)
|
||||
representative_point = shapely.geometry.Point(representative_point_tmp)
|
||||
# End of the changes by Th_Fr
|
||||
return representative_point.coords
|
||||
|
||||
|
||||
def plane(a, b, c):
|
||||
"""Returns the equation of a three-dimensional plane in space by entering the three coordinates of the plane."""
|
||||
p_a = (b[1] - a[1]) * (c[2] - a[2]) - (c[1] - a[1]) * (b[2] - a[2])
|
||||
p_b = (b[2] - a[2]) * (c[0] - a[0]) - (c[2] - a[2]) * (b[0] - a[0])
|
||||
p_c = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])
|
||||
p_d = -1 * (p_a * a[0] + p_b * a[1] + p_c * a[2])
|
||||
return p_a, p_b, p_c, p_d
|
||||
|
||||
# added by Th_Fr
|
||||
def planeAdjusted(points):
|
||||
"""
|
||||
Returns the equation of a plane in three dimensions using PCA.
|
||||
|
||||
Parameters:
|
||||
points: list of lists or numpy array of shape (n, 3)
|
||||
List of points in 3D space [x, y, z] through which the plane should pass.
|
||||
At least 3 points are required to define a plane uniquely.
|
||||
|
||||
Returns:
|
||||
p_a, p_b, p_c, p_d: float
|
||||
Parameters of the plane equation ax + by + cz + d = 0.
|
||||
"""
|
||||
# Convert points to numpy array for easier manipulation
|
||||
points = np.array(points)
|
||||
|
||||
# Check if at least 3 points are provided
|
||||
if points.shape[0] < 3:
|
||||
raise ValueError("At least 3 points are required to define a plane.")
|
||||
|
||||
# Use PCA to fit the plane
|
||||
pca = PCA(n_components=3)
|
||||
pca.fit(points)
|
||||
normal = pca.components_[2] # The normal vector to the plane
|
||||
|
||||
# Extract coefficients
|
||||
p_a, p_b, p_c = normal
|
||||
p_d = -np.dot(normal, pca.mean_) # Calculate d using the mean of points
|
||||
|
||||
return p_a, p_b, p_c, p_d
|
||||
|
||||
|
||||
def get_height(plane, x, y):
|
||||
"""Get the missing coordinate from the plane equation and the partial coordinates."""
|
||||
p_a, p_b, p_c, p_d = plane
|
||||
z = (-p_a * x - p_b * y - p_d) / p_c
|
||||
return z
|
||||
|
||||
|
||||
def get_y(plane, x, z):
|
||||
"""Get the missing coordinate from the plane equation and the partial coordinates."""
|
||||
p_a, p_b, p_c, p_d = plane
|
||||
y = (-p_a * x - p_c * z - p_d) / (p_b)
|
||||
return y
|
||||
|
||||
|
||||
def compare_normals(n1, n2):
|
||||
"""Compares if two normals are equal or opposite. Takes into account a small tolerance to overcome floating point errors."""
|
||||
tolerance = 10e-2
|
||||
# -- Assume equal and prove otherwise
|
||||
equal = True
|
||||
# -- i
|
||||
if math.fabs(n1[0] - n2[0]) > tolerance:
|
||||
equal = False
|
||||
# -- j
|
||||
elif math.fabs(n1[1] - n2[1]) > tolerance:
|
||||
equal = False
|
||||
# -- k
|
||||
elif math.fabs(n1[2] - n2[2]) > tolerance:
|
||||
equal = False
|
||||
return equal
|
||||
|
||||
|
||||
def reverse_vertices(vertices):
|
||||
"""Reverse vertices. Useful to reorient the normal of the polygon."""
|
||||
reversed_vertices = []
|
||||
nv = len(vertices)
|
||||
for i in range(nv - 1, -1, -1):
|
||||
reversed_vertices.append(vertices[i])
|
||||
return reversed_vertices
|
||||
|
||||
# Added by Th_FR, inspirde by https://pythonseminar.de/prufen-ob-die-liste-doppelte-elemente-enthalt-in-python/
|
||||
def has_duplicates(seq):
|
||||
seen = []
|
||||
unique_list = [x for x in seq if x not in seen and not seen.append(x)]
|
||||
return len(seq) != len(unique_list)
|
||||
# End of changes
|
||||
|
||||
# Added by Th_Fr
|
||||
def weighted_centroid(vertices):
|
||||
"""
|
||||
Calculate the weighted centroid of a polygon defined by vertices.
|
||||
|
||||
Arguments:
|
||||
vertices (numpy array): Array of vertices of the polygon.
|
||||
|
||||
Returns:
|
||||
numpy array: Weighted centroid [x, y, z].
|
||||
"""
|
||||
total_area = 0.0
|
||||
centroid = np.zeros(3)
|
||||
num_vertices = len(vertices)
|
||||
|
||||
for i in range(num_vertices):
|
||||
j = (i + 1) % num_vertices
|
||||
cross = np.cross(vertices[i], vertices[j])
|
||||
area = np.linalg.norm(cross)
|
||||
centroid += (vertices[i] + vertices[j]) * area
|
||||
total_area += area
|
||||
|
||||
|
||||
return centroid / (3 * total_area)
|
||||
|
||||
def calculate_polygon_normal_old(poly):
|
||||
"""
|
||||
Calculate the normal vector of a polygon using a weighted centroid and cross product approach.
|
||||
|
||||
Arguments:
|
||||
poly (list of lists): List of vertices of the polygon, where each vertex is [x, y, z].
|
||||
|
||||
Returns:
|
||||
numpy array: Normal vector (nx, ny, nz) of the polygon's plane.
|
||||
"""
|
||||
vertices = np.array(poly)
|
||||
num_vertices = len(vertices)
|
||||
|
||||
# Calculate weighted centroid
|
||||
#print("here b")
|
||||
centroid1 = centroid(vertices)
|
||||
|
||||
# Compute the normal vector using cross product of edges
|
||||
normal = np.zeros(3)
|
||||
for i in range(num_vertices):
|
||||
j = (i + 1) % num_vertices
|
||||
vi = vertices[i]
|
||||
vj = vertices[j]
|
||||
normal[0] += (vi[1] - centroid1[1]) * (vj[2] - centroid1[2]) - (vi[2] - centroid1[2]) * (vj[1] - centroid1[1])
|
||||
normal[1] += (vi[2] - centroid1[2]) * (vj[0] - centroid1[0]) - (vi[0] - centroid1[0]) * (vj[2] - centroid1[2])
|
||||
normal[2] += (vi[0] - centroid1[0]) * (vj[1] - centroid1[1]) - (vi[1] - centroid1[1]) * (vj[0] - centroid1[0])
|
||||
|
||||
norm = np.linalg.norm(normal)
|
||||
if norm != 0:
|
||||
normal /= norm
|
||||
else:
|
||||
normal = np.array([0.0, 0.0, 0.0])
|
||||
|
||||
return normal
|
||||
|
||||
|
||||
def calculate_polygon_normal(polygon):
|
||||
"""
|
||||
Calculate the surface normal of a polygon.
|
||||
|
||||
Parameters:
|
||||
polygon (list): A list of vertices, where each vertex is a list of three coordinates [x, y, z].
|
||||
|
||||
Returns:
|
||||
np.array: A normalized vector representing the surface normal.
|
||||
"""
|
||||
|
||||
normal = np.array([0.0, 0.0, 0.0])
|
||||
num_verts = len(polygon)
|
||||
|
||||
for i in range(num_verts):
|
||||
current = np.array(polygon[i])
|
||||
next_vert = np.array(polygon[(i + 1) % num_verts])
|
||||
|
||||
normal[0] += (current[1] - next_vert[1]) * (current[2] + next_vert[2])
|
||||
normal[1] += (current[2] - next_vert[2]) * (current[0] + next_vert[0])
|
||||
normal[2] += (current[0] - next_vert[0]) * (current[1] + next_vert[1])
|
||||
|
||||
normal = normalize(normal)
|
||||
return normal
|
||||
|
||||
|
||||
def normalize(vector):
|
||||
"""
|
||||
Normalize a vector.
|
||||
|
||||
Parameters:
|
||||
vector (np.array): A vector to normalize.
|
||||
|
||||
Returns:
|
||||
np.array: A normalized vector.
|
||||
"""
|
||||
norm = np.linalg.norm(vector)
|
||||
if norm == 0:
|
||||
return vector
|
||||
return vector / norm
|
||||
|
||||
|
||||
def triangulation(e, i):
|
||||
"""Triangulate the polygon with the exterior and interior list of points. Works only for convex polygons.
|
||||
Assumes planarity. Projects to a 2D plane and goes back to 3D."""
|
||||
vertices = []
|
||||
holes = []
|
||||
segments = []
|
||||
index_point = 0
|
||||
# -- Slope computation points
|
||||
a = [[], [], []]
|
||||
b = [[], [], []]
|
||||
for ip in range(len(e) - 1):
|
||||
vertices.append(e[ip])
|
||||
if a == [[], [], []] and index_point == 0:
|
||||
a = [e[ip][0], e[ip][1], e[ip][2]]
|
||||
if index_point > 0 and (e[ip] != e[ip - 1]):
|
||||
if b == [[], [], []]:
|
||||
b = [e[ip][0], e[ip][1], e[ip][2]]
|
||||
if ip == len(e) - 2:
|
||||
segments.append([index_point, 0])
|
||||
else:
|
||||
segments.append([index_point, index_point + 1])
|
||||
index_point += 1
|
||||
|
||||
for hole in i:
|
||||
first_point_in_hole = index_point
|
||||
for p in range(len(hole) - 1):
|
||||
if p == len(hole) - 2:
|
||||
segments.append([index_point, first_point_in_hole])
|
||||
else:
|
||||
segments.append([index_point, index_point + 1])
|
||||
index_point += 1
|
||||
vertices.append(hole[p])
|
||||
# -- A more robust way to get the point inside the hole, should work for non-convex interior polygons
|
||||
# alt: holes.append(point_inside(hole[:-1]))
|
||||
# -- Alternative, use centroid
|
||||
holes.append(centroid(hole[:-1])) # This should be useful!
|
||||
# -- Project to 2D since the triangulation cannot be done in 3D with the library that is used
|
||||
npolypoints = len(vertices)
|
||||
nholes = len(holes)
|
||||
# -- Check if the polygon is vertical, i.e. a projection cannot be made.
|
||||
# -- First copy the list so the originals are not modified
|
||||
temppolypoints = copy.deepcopy(vertices)
|
||||
newpolypoints = copy.deepcopy(vertices)
|
||||
tempholes = copy.deepcopy(holes)
|
||||
newholes = copy.deepcopy(holes)
|
||||
|
||||
# -- Compute the normal of the polygon for detecting vertical polygons and
|
||||
# -- for the correct orientation of the new triangulated faces
|
||||
# -- If the polygon is vertical
|
||||
#normal = unit_normal(temppolypoints[0], temppolypoints[1], temppolypoints[2])
|
||||
normal = calculate_polygon_normal(temppolypoints)
|
||||
|
||||
if math.fabs(normal[2]) < 10e-2:
|
||||
vertical = True
|
||||
#print("The polygon is vertical")
|
||||
#print("math.fabs(normal[2]): ", math.fabs(normal[2]))
|
||||
else:
|
||||
vertical = False
|
||||
#print("Not vertical")
|
||||
# -- We want to project the vertical polygon to the XZ plane
|
||||
# -- If a polygon is parallel with the YZ plane that will not be possible
|
||||
|
||||
|
||||
YZ = True
|
||||
for i in range(1, npolypoints):
|
||||
if temppolypoints[i][0] != temppolypoints[0][0]:
|
||||
YZ = False
|
||||
continue
|
||||
|
||||
|
||||
# -- Project the plane in the special case
|
||||
if YZ == True:
|
||||
for i in range(0, npolypoints):
|
||||
newpolypoints[i][0] = temppolypoints[i][1]
|
||||
newpolypoints[i][1] = temppolypoints[i][2]
|
||||
for i in range(0, nholes):
|
||||
newholes[i][0] = tempholes[i][1]
|
||||
newholes[i][1] = tempholes[i][2]
|
||||
# -- Project the plane
|
||||
elif vertical == True:
|
||||
for i in range(0, npolypoints):
|
||||
newpolypoints[i][1] = temppolypoints[i][2]
|
||||
for i in range(0, nholes):
|
||||
newholes[i][1] = tempholes[i][2]
|
||||
else:
|
||||
pass # -- No changes here
|
||||
|
||||
# -- Drop the last point (identical to first)
|
||||
for p in newpolypoints:
|
||||
# print("p: ", p)
|
||||
p.pop(-1)
|
||||
# print("p: ", p)
|
||||
# -- If there are no holes
|
||||
if len(newholes) == 0:
|
||||
newholes = None
|
||||
else:
|
||||
counter = 0
|
||||
for h in newholes:
|
||||
counter = counter + 1
|
||||
h = h.pop(-1)
|
||||
|
||||
# -- Plane information (assumes planarity) #todo: hier muss noch etwas angepasst werden; erledigt?
|
||||
a = e[0]
|
||||
b = e[1]
|
||||
c = e[2]
|
||||
# -- Construct the plane
|
||||
pl = planeAdjusted(e)
|
||||
|
||||
# -- Prepare the polygon to be triangulated
|
||||
# Change by Th_Fr: Distinguishing different cases!
|
||||
# There are two cases distinguished here: 1. A Polygon without holes, 2. A polygon with holes
|
||||
if newholes == None:
|
||||
poly = {'vertices': np.array(newpolypoints), 'segments': np.array(segments)}
|
||||
# For some reason this if.case sometimes fails, this is why there is a second version of the
|
||||
# Trinangulation without the optional 'pQjz' parameter
|
||||
if has_duplicates(newpolypoints) == False:
|
||||
t = triangle.triangulate(poly, 'pQjz')
|
||||
else:
|
||||
t = triangle.triangulate(poly)
|
||||
|
||||
else:
|
||||
poly = {'vertices': np.array(newpolypoints), 'segments': np.array(segments), 'holes': np.array(newholes)}
|
||||
t = triangle.triangulate(poly, 'pQjz')
|
||||
|
||||
# End of changes by Th_Fr
|
||||
|
||||
# -- Get the triangles and their vertices
|
||||
try:
|
||||
tris = t['triangles']
|
||||
except:
|
||||
print("strange error")
|
||||
tris = []
|
||||
|
||||
try:
|
||||
vert = t['vertices'].tolist()
|
||||
except:
|
||||
vert = []
|
||||
# -- Store the vertices of each triangle in a list
|
||||
tri_points = []
|
||||
for tri in tris:
|
||||
tri_points_tmp = []
|
||||
for v in tri.tolist():
|
||||
vert_adj = [[], [], []]
|
||||
if YZ:
|
||||
vert_adj[0] = temppolypoints[0][0]
|
||||
vert_adj[1] = vert[v][0]
|
||||
vert_adj[2] = vert[v][1]
|
||||
elif vertical:
|
||||
vert_adj[0] = vert[v][0]
|
||||
vert_adj[2] = vert[v][1]
|
||||
vert_adj[1] = get_y(pl, vert_adj[0], vert_adj[2])
|
||||
else:
|
||||
vert_adj[0] = vert[v][0]
|
||||
vert_adj[1] = vert[v][1]
|
||||
vert_adj[2] = get_height(pl, vert_adj[0], vert_adj[1])
|
||||
tri_points_tmp.append(vert_adj)
|
||||
try:
|
||||
tri_normal = unit_normal(tri_points_tmp[0], tri_points_tmp[1], tri_points_tmp[2])
|
||||
except:
|
||||
continue
|
||||
if compare_normals(normal, tri_normal):
|
||||
tri_points.append(tri_points_tmp)
|
||||
else:
|
||||
tri_points_tmp = reverse_vertices(tri_points_tmp)
|
||||
tri_points.append(tri_points_tmp)
|
||||
return tri_points
|
||||
73
scripts/CityGML2OBJv2/requirements.txt
Normal file
73
scripts/CityGML2OBJv2/requirements.txt
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
asttokens==3.0.1
|
||||
attrs==25.4.0
|
||||
blinker==1.9.0
|
||||
certifi==2026.1.4
|
||||
charset-normalizer==3.4.4
|
||||
click==8.3.1
|
||||
colorama==0.4.6
|
||||
comm==0.2.3
|
||||
ConfigArgParse==1.7.1
|
||||
contourpy==1.3.2
|
||||
cycler==0.12.1
|
||||
dash==4.0.0
|
||||
dash-core-components==2.0.0
|
||||
dash-html-components==2.0.0
|
||||
dash-table==5.0.0
|
||||
decorator==5.2.1
|
||||
exceptiongroup==1.3.1
|
||||
executing==2.2.1
|
||||
fastjsonschema==2.21.2
|
||||
Flask==3.1.3
|
||||
fonttools==4.61.1
|
||||
idna==3.15
|
||||
importlib_metadata==8.7.1
|
||||
ipython==8.38.0
|
||||
ipywidgets==8.1.8
|
||||
itsdangerous==2.2.0
|
||||
jedi==0.19.2
|
||||
Jinja2==3.1.6
|
||||
joblib==1.5.3
|
||||
jsonschema==4.26.0
|
||||
jsonschema-specifications==2025.9.1
|
||||
jupyter_core==5.9.1
|
||||
jupyterlab_widgets==3.0.16
|
||||
kiwisolver==1.4.9
|
||||
lxml==6.1.0
|
||||
MarkupSafe==3.0.3
|
||||
matplotlib==3.10.8
|
||||
matplotlib-inline==0.2.1
|
||||
narwhals==2.16.0
|
||||
nbformat==5.10.4
|
||||
nest-asyncio==1.6.0
|
||||
numpy==2.2.6
|
||||
open3d==0.19.0
|
||||
packaging==26.0
|
||||
parso==0.8.6
|
||||
pillow==12.2.0
|
||||
platformdirs==4.9.2
|
||||
plotly==6.5.2
|
||||
prompt_toolkit==3.0.52
|
||||
pure_eval==0.2.3
|
||||
Pygments==2.20.0
|
||||
pyparsing==3.3.2
|
||||
python-dateutil==2.9.0.post0
|
||||
pywin32==311
|
||||
referencing==0.37.0
|
||||
requests==2.33.0
|
||||
retrying==1.4.2
|
||||
rpds-py==0.30.0
|
||||
scikit-learn==1.7.2
|
||||
scipy==1.15.3
|
||||
shapely==2.1.2
|
||||
six==1.17.0
|
||||
stack-data==0.6.3
|
||||
tenacity==9.1.4
|
||||
threadpoolctl==3.6.0
|
||||
traitlets==5.14.3
|
||||
triangle==20250106
|
||||
typing_extensions==4.15.0
|
||||
urllib3==2.7.0
|
||||
wcwidth==0.6.0
|
||||
Werkzeug==3.1.6
|
||||
widgetsnbextension==4.0.15
|
||||
zipp==3.23.0
|
||||
BIN
scripts/IfcConvert
Executable file
BIN
scripts/IfcConvert
Executable file
Binary file not shown.
36
scripts/backup/backup_daily.sh
Executable file
36
scripts/backup/backup_daily.sh
Executable file
|
|
@ -0,0 +1,36 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# == func log ===================================================================================================================
|
||||
warning() {
|
||||
echo "Warning: $1"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo "Error: $1" && exit 1
|
||||
}
|
||||
|
||||
REPONAME="3drepository"
|
||||
BPATH="/var/www/data/backups/daily"
|
||||
BDPATH="/var/www/data/backups/daily/db"
|
||||
WEBSITE="www.dfg-repository.wisski.cloud"
|
||||
|
||||
# == init script ================================================================================================================
|
||||
CURRENT_DATE=$(date +%d%m%y%H%M)
|
||||
TGZ_FILE="${BPATH}/backup_${WEBSITE}_${CURRENT_DATE}.tgz"
|
||||
SQL_FILE="${BDPATH}/${WEBSITE}_databases_${CURRENT_DATE}.sql"
|
||||
BSOURCE="/var/www/html/${REPONAME}"
|
||||
|
||||
mkdir -p "$BPATH"
|
||||
|
||||
# == main =======================================================================================================================
|
||||
mkdir -p "${BDPATH}"
|
||||
|
||||
source read_settings.sh
|
||||
#mysqldump -h ${DATABASE_HOST} -u ${DATABASE_USER} -p${DATABASE_PASS} ${DATABASE} --hex-blob --skip-lock-tables --single-transaction > "$SQL_FILE"
|
||||
|
||||
tar --exclude="${BPATH:1}" --warning=none -czpf "${TGZ_FILE}" -C / "$BSOURCE" var/www/data
|
||||
|
||||
#scp "$TGZ_FILE" "${USER}@example.work:/mnt/home/${USER}"
|
||||
|
||||
find ${BPATH}/*.tgz -mtime +7 -exec rm {} \;
|
||||
find ${BDPATH}/*.tgz -mtime +7 -exec rm {} \;
|
||||
63
scripts/backup/backup_monthly.sh
Executable file
63
scripts/backup/backup_monthly.sh
Executable file
|
|
@ -0,0 +1,63 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
error() {
|
||||
echo -e "Error: $1\nExiting..." && exit 1
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo "Warning: $1 Skipping..."
|
||||
}
|
||||
|
||||
MONTHLY_BACKUPS_PATH="/var/www/data/backups/monthly"
|
||||
WEEKLY_BACKUPS_PATH="/var/www/data/backups/weekly"
|
||||
|
||||
clean_old_monthly_backups() {
|
||||
find ${MONTHLY_BACKUPS_PATH}/*.tgz -mtime +181 -exec rm {} \;
|
||||
}
|
||||
|
||||
clean_old_monthly_db_backups() {
|
||||
find ${MONTHLY_BACKUPS_PATH}/db/*.tgz -mtime +181 -exec rm {} \;
|
||||
}
|
||||
|
||||
sync_from_weekly() {
|
||||
# shellcheck disable=SC2010
|
||||
ffd="$(ls -pt | grep -v / | head -1)"
|
||||
if [[ -n "$ffd" ]]; then
|
||||
rsync -dtz --ignore-existing "${ffd}" "$1"
|
||||
else
|
||||
warning "No files found to sync!"
|
||||
fi
|
||||
}
|
||||
|
||||
process_with_dbs() {
|
||||
if [[ -d "${MONTHLY_BACKUPS_PATH}/" ]]; then
|
||||
clean_old_monthly_backups
|
||||
|
||||
if ! cd "${WEEKLY_BACKUPS_PATH}/"; then
|
||||
warning "Failed to cd into ${WEEKLY_BACKUPS_PATH}/"
|
||||
else
|
||||
# clean old weekly backups
|
||||
find ${WEEKLY_BACKUPS_PATH}/*.tgz -mtime +28 -exec rm {} \;
|
||||
|
||||
sync_from_weekly "${MONTHLY_BACKUPS_PATH}/"
|
||||
fi
|
||||
|
||||
if [[ -d "${MONTHLY_BACKUPS_PATH}/db/" ]]; then
|
||||
clean_old_monthly_db_backups
|
||||
|
||||
if ! cd "${WEEKLY_BACKUPS_PATH}/db/"; then
|
||||
warning "Failed to cd into ${WEEKLY_BACKUPS_PATH}/db/"
|
||||
else
|
||||
# clean old weekly db backups
|
||||
find ${WEEKLY_BACKUPS_PATH}/db/*.tgz -mtime +28 -exec rm {} \;
|
||||
|
||||
sync_from_weekly "${MONTHLY_BACKUPS_PATH}/db/"
|
||||
fi
|
||||
else
|
||||
warning "Cannot locate ${MONTHLY_BACKUPS_PATH}/db/"
|
||||
fi
|
||||
else
|
||||
warning "Cannot locate ${MONTHLY_BACKUPS_PATH}/"
|
||||
fi
|
||||
}
|
||||
|
||||
64
scripts/backup/backup_weekly.sh
Executable file
64
scripts/backup/backup_weekly.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# == func log ===================================================================================================================
|
||||
warning() {
|
||||
echo "Warning: $1"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo "Error: $1" && exit 1
|
||||
}
|
||||
|
||||
BDPATH="/var/www/data/backups/daily/db"
|
||||
WEBSITE="www.dfg-repository.wisski.cloud"
|
||||
|
||||
DAILY_BACKUPS_PATH="/var/www/data/backups/daily"
|
||||
WEEKLY_BACKUPS_PATH="/var/www/data/backups/weekly"
|
||||
|
||||
sync_from_daily() {
|
||||
ffd="$(ls -pt | grep -v / | head -1)"
|
||||
if [[ -n "$ffd" ]]; then
|
||||
rsync -dtz --ignore-existing "${ffd}" "$1"
|
||||
else
|
||||
warning "No files found to sync!"
|
||||
fi
|
||||
}
|
||||
|
||||
process_with_dbs() {
|
||||
path="${DAILY_BACKUPS_PATH}"
|
||||
|
||||
if [[ -d "${WEEKLY_BACKUPS_PATH}/" ]]; then
|
||||
# clean old weekly backups
|
||||
find ${WEEKLY_BACKUPS_PATH}/*.tgz -mtime +28 -exec rm {} \;
|
||||
|
||||
if ! cd "${path}"; then
|
||||
warning "Failed to cd into ${path}"
|
||||
else
|
||||
# clean old daily backups
|
||||
find "${path}"/*.tgz -mtime +6 -exec rm {} \;
|
||||
|
||||
sync_from_daily "${WEEKLY_BACKUPS_PATH}/"
|
||||
fi
|
||||
|
||||
path+="/db"
|
||||
if [[ -d "${WEEKLY_BACKUPS_PATH}/db/" ]]; then
|
||||
# clean old weekly db backups
|
||||
find ${WEEKLY_BACKUPS_PATH}/db/*.tgz -mtime +28 -exec rm {} \;
|
||||
|
||||
if ! cd "${path}"; then
|
||||
warning "Failed to cd into ${path}"
|
||||
else
|
||||
# clean old daily db backups
|
||||
find "${path}"/*.tgz -mtime +6 -exec rm {} \;
|
||||
|
||||
sync_from_daily "${WEEKLY_BACKUPS_PATH}/db/"
|
||||
fi
|
||||
else
|
||||
warning "Cannot locate ${WEEKLY_BACKUPS_PATH}/db/"
|
||||
fi
|
||||
else
|
||||
warning "Cannot locate ${WEEKLY_BACKUPS_PATH}/"
|
||||
fi
|
||||
}
|
||||
|
||||
process_with_dbs
|
||||
10
scripts/backup/read_settings.sh
Executable file
10
scripts/backup/read_settings.sh
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
FILENAME="/var/www/html/3drepository/sites/default/settings.php"
|
||||
|
||||
test -f ../.env && source "$_"
|
||||
|
||||
DATABASE=$(php -r "error_reporting(0); \$filename = '$FILENAME'; error_reporting(0); (require(\$filename)); echo \$databases[\"default\"][\"default\"][\"database\"];")
|
||||
DATABASE_HOST=$(php -r "error_reporting(0); \$filename = '$FILENAME'; error_reporting(0); (require(\$filename)); echo \$databases[\"default\"][\"default\"][\"host\"];")
|
||||
DATABASE_PASS=$(php -r "error_reporting(0); \$filename = '$FILENAME'; error_reporting(0); (require(\$filename)); echo \$databases[\"default\"][\"default\"][\"password\"];")
|
||||
DATABASE_USER=$(php -r "error_reporting(0); \$filename = '$FILENAME'; error_reporting(0); (require(\$filename)); echo \$databases[\"default\"][\"default\"][\"username\"];")
|
||||
9
scripts/convert-blender-to-gltf.py
Normal file
9
scripts/convert-blender-to-gltf.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import bpy
|
||||
import os
|
||||
import sys
|
||||
|
||||
print("Converting: '" + sys.argv[6] + "'")
|
||||
print("Saving to : '" + sys.argv[7] + "'")
|
||||
|
||||
bpy.ops.wm.open_mainfile(filepath=sys.argv[6])
|
||||
bpy.ops.export_scene.gltf(filepath=sys.argv[7], export_format="GLB")
|
||||
414
scripts/convert.sh
Executable file
414
scripts/convert.sh
Executable file
|
|
@ -0,0 +1,414 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Ubuntu way
|
||||
#apt install xvfb
|
||||
#apt install libxkbcommon0
|
||||
#apt install blender python3-pip
|
||||
#apt install python3-lxml python3-shapely python3-matplotlib (for CityGML converter)
|
||||
#apt install libxi6 libgconf-2-4
|
||||
#OR
|
||||
# Debian way
|
||||
#wget https://ftp.halifax.rwth-aachen.de/blender/release/Blender4.4/blender-4.4.3-linux-x64.tar.xz
|
||||
#tar -xvf blender-4.4.3-linux-x64.tar.xz
|
||||
#change .env BLENDER_PATH or make symlink to it `ln -s PATH_TO_YOUR_UNCOMPRESSED_BLENDER/blender-4.4.3-linux64/blender /usr/local/bin/blender`
|
||||
#pip install numpy or apt install python3-numpy
|
||||
#pip install triangle
|
||||
#usage: ./convert.sh -c COMPRESS -cl COMPRESSION_LEVEL -i 'INPUT' -o 'OUTPUT' -b BINARY -f FORCE_OVERRIDE
|
||||
|
||||
|
||||
#apt install -y libxi6 libxrender1 libxrandr2 libxinerama1 libxcursor1 libxcomposite1 libxdamage1 libxtst6 libglib2.0-0 libsm6 libice6 libgl1 libxkbcommon0
|
||||
#TESTING:
|
||||
# sudo blender -b -P ./scripts/2gltf2/2gltf2.py -- --input "/opt/drupal/web/sites/default/files/{NAME}" --ext "$GLTF" --compression "true" --compression_level "3" --output "/opt/drupal/web/sites/default/files/2025-05/test-TEST.glb"
|
||||
# xvfb-run --auto-servernum --server-args="-screen 0 512x512x16" sudo blender -b -P ./scripts/render.py -- --input "/var/www/html/sites/default/files/{NAME}.glb" --ext "glb" --org_ext "glb" --output "/var/www/html/sites/default/files/views/" --is_archive false --resolution 512x512x16 --samples 20 -E BLENDER_EEVEE -f 1
|
||||
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MODULE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
if [[ -f "$SCRIPT_DIR/.env" ]]; then
|
||||
source "$SCRIPT_DIR/.env"
|
||||
fi
|
||||
BLENDER_BIN="${BLENDER_BIN:-}"
|
||||
if [[ -z "$BLENDER_BIN" ]]; then
|
||||
BLENDER_BIN="blender"
|
||||
fi
|
||||
SPATH="${SPATH:-$MODULE_ROOT}"
|
||||
if [[ ! -d "$SPATH/scripts" ]]; then
|
||||
SPATH="$MODULE_ROOT"
|
||||
fi
|
||||
|
||||
#Defaults:
|
||||
COMPRESSION=false
|
||||
COMPRESSION_LEVEL=3
|
||||
GLTF="gltf"
|
||||
FORCE=false
|
||||
isOutput=false
|
||||
IS_ARCHIVE=false
|
||||
LIGHTWEIGHT=false
|
||||
INPUT=""
|
||||
OUTPUT=""
|
||||
OUTPUTPATH=""
|
||||
|
||||
resolve_blender_bin () {
|
||||
local candidate="$1"
|
||||
|
||||
if [[ -z "$candidate" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$candidate" == /* ]]; then
|
||||
[[ -f "$candidate" && -x "$candidate" ]] || return 1
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -f "$SCRIPT_DIR/$candidate" && -x "$SCRIPT_DIR/$candidate" ]]; then
|
||||
printf '%s\n' "$SCRIPT_DIR/$candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -f "$SPATH/$candidate" && -x "$SPATH/$candidate" ]]; then
|
||||
printf '%s\n' "$SPATH/$candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v "$candidate" &> /dev/null; then
|
||||
command -v "$candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
check_blender () {
|
||||
if ! BLENDER_BIN="$(resolve_blender_bin "$BLENDER_BIN")"; then
|
||||
echo "Blender doesn't exist, install it by 'apt install blender python3-pip' then 'pip install numpy' or set BLENDER_BIN in scripts/.env"
|
||||
return 1
|
||||
elif [[ ! -x "$BLENDER_BIN" ]]; then
|
||||
echo "Configured BLENDER_BIN is not executable: $BLENDER_BIN"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -n "${BLENDER_BIN:-}" ]]; then
|
||||
echo "Blender exists and be used for next steps..."
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
check_scripts () {
|
||||
if [ ! -d "${SPATH}/scripts" ]; then
|
||||
echo "Can't find dependencies directory. Resolved SPATH=${SPATH}. SCRIPT_DIR=${SCRIPT_DIR}. MODULE_ROOT=${MODULE_ROOT}. Did you change your SPATH value in scripts/.env?"
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
check_blender
|
||||
|
||||
check_scripts
|
||||
|
||||
show_usage () {
|
||||
echo "-c=compress -l=compression level -i=input path -o=output path -b=binary -f=force override existing file"
|
||||
cat <<EOF
|
||||
Usage: convert.sh [options]
|
||||
|
||||
Options:
|
||||
-c, --compression true|false
|
||||
-l, --compression-level 0-9
|
||||
-i, --input FILE
|
||||
-o, --output FILE
|
||||
-b, --binary true|false (true = glb, false = gltf)
|
||||
-f, --force true|false
|
||||
-t, --lightweight true|false
|
||||
-a, --archive true|false
|
||||
-h, --help
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
######################################
|
||||
# Helpers
|
||||
######################################
|
||||
die() {
|
||||
echo "Error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
bool() {
|
||||
[[ "$1" == "true" || "$1" == "false" ]] || die "Value must be true/false (is: $1)"
|
||||
}
|
||||
|
||||
file_exists() {
|
||||
[[ -f "$1" ]] || die "File not found: $1"
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
|
||||
}
|
||||
|
||||
######################################
|
||||
# Parsing
|
||||
######################################
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-c|--compression)
|
||||
bool "$2"
|
||||
COMPRESSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
-l|--compression-level)
|
||||
[[ "$2" =~ ^[0-9]$ ]] || die "compression-level must be 0–9"
|
||||
COMPRESSION_LEVEL="$2"
|
||||
shift 2
|
||||
;;
|
||||
-i|--input)
|
||||
INPUT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-o|--output)
|
||||
OUTPUT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-b|--binary)
|
||||
bool "$2"
|
||||
[[ "$2" == "true" ]] && GLTF="glb" || GLTF="gltf"
|
||||
shift 2
|
||||
;;
|
||||
-f|--force)
|
||||
bool "$2"
|
||||
FORCE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-t|--lightweight)
|
||||
bool "$2"
|
||||
LIGHTWEIGHT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-a|--archive)
|
||||
bool "$2"
|
||||
IS_ARCHIVE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
die "Error parsing arguments"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
check_status () {
|
||||
if [ ! -f "$1.off" ]; then
|
||||
touch "$1.off"
|
||||
else
|
||||
rm -rf "$1.off"
|
||||
fi;
|
||||
}
|
||||
|
||||
cleanup_main_lock() {
|
||||
flock -u 200 2>/dev/null || true
|
||||
exec 200>&- 2>/dev/null || true
|
||||
if [[ -n "${MAIN_LOCKFILE:-}" ]]; then
|
||||
rm -f "$MAIN_LOCKFILE"
|
||||
fi
|
||||
}
|
||||
|
||||
create_dirs () {
|
||||
mkdir -p "$INPATH"/gltf/
|
||||
mkdir -p "$INPATH"/metadata/
|
||||
}
|
||||
|
||||
create_flock () {
|
||||
INPATH=$1
|
||||
FILENAME=$2
|
||||
MAIN_LOCKFILE="$INPATH/${FILENAME}.lock"
|
||||
exec 200>"$MAIN_LOCKFILE" || exit 1
|
||||
flock -n 200 || {
|
||||
echo "Process already running for $FILENAME"
|
||||
exit 0
|
||||
}
|
||||
trap cleanup_main_lock EXIT
|
||||
}
|
||||
|
||||
handle_file () {
|
||||
INPATH=$1
|
||||
FILENAME=$2
|
||||
NAME=$3
|
||||
EXT=$4
|
||||
OUTPUT=$5
|
||||
OUTPUTPATH=$6
|
||||
|
||||
create_flock "$INPATH" "$FILENAME"
|
||||
|
||||
if [[ "$isOutput" = false ]]; then
|
||||
"$BLENDER_BIN" -b -P "${SPATH}/scripts/2gltf2/2gltf2.py" -- --input "$INPATH/$FILENAME" --ext "$GLTF" --compression "$COMPRESSION" --compression_level "$COMPRESSION_LEVEL" #> /dev/null 2>&1
|
||||
else
|
||||
"$BLENDER_BIN" -b -P "${SPATH}/scripts/2gltf2/2gltf2.py" -- --input "$INPATH/$FILENAME" --ext "$GLTF" --compression "$COMPRESSION" --compression_level "$COMPRESSION_LEVEL" --output "$OUTPUT$OUTPUTPATH" #> /dev/null 2>&1
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
handle_unsupported_file () {
|
||||
INPATH=$1
|
||||
FILENAME=$2
|
||||
NAME=$3
|
||||
EXT=$4
|
||||
OUTPUT=$5
|
||||
OUTPUTPATH=$6
|
||||
|
||||
#touch "${INPATH}/gltf/${NAME}.glb.off"
|
||||
}
|
||||
|
||||
handle_ifc_file () {
|
||||
INPATH=$1
|
||||
FILENAME=$2
|
||||
NAME=$3
|
||||
EXT=$4
|
||||
OUTPUT=$5
|
||||
OUTPUTPATH=$6
|
||||
|
||||
create_flock "$INPATH" "$FILENAME"
|
||||
|
||||
create_dirs
|
||||
|
||||
${SPATH}/scripts/IfcConvert "$INPATH/$FILENAME" "$INPATH/gltf/$NAME.glb" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
handle_blend_file () {
|
||||
INPATH=$1
|
||||
FILENAME=$2
|
||||
NAME=$3
|
||||
EXT=$4
|
||||
|
||||
create_flock "$INPATH" "$FILENAME"
|
||||
|
||||
create_dirs
|
||||
|
||||
"$BLENDER_BIN" -b -P "${SPATH}/scripts/convert-blender-to-gltf.py" -- "$INPATH/$FILENAME" "$INPATH/gltf/$NAME.glb" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
handle_gml_file () {
|
||||
INPATH=$1
|
||||
FILENAME=$2
|
||||
NAME=$3
|
||||
EXT=$4
|
||||
OUTPUT=$5
|
||||
OUTPUTPATH=$6
|
||||
|
||||
create_flock "$INPATH" "$FILENAME"
|
||||
|
||||
require_cmd python3
|
||||
GLB_PATH="${INPATH}/${NAME}_GLB"
|
||||
|
||||
mkdir -p "$GLB_PATH"
|
||||
cp -rf "$INPATH/$FILENAME" "$GLB_PATH/"
|
||||
python3 "${SPATH}/scripts/CityGML2OBJv2/CityGML2OBJs.py" -i "$GLB_PATH" -o "$GLB_PATH" > /dev/null 2>&1
|
||||
create_dirs
|
||||
"$BLENDER_BIN" -b -P "${SPATH}/scripts/2gltf2/2gltf2.py" -- "$GLB_PATH/${NAME}.obj" "$GLTF" "$COMPRESSION" "$COMPRESSION_LEVEL" "$INPATH/gltf/$NAME.glb" > /dev/null 2>&1
|
||||
rm -rf "$GLB_PATH"
|
||||
|
||||
}
|
||||
|
||||
printf "\n"
|
||||
echo "======== Parameters ========"
|
||||
echo " INPUT: $INPUT"
|
||||
echo " OUTPUT: $OUTPUT"
|
||||
echo " COMPRESSION: $COMPRESSION"
|
||||
echo " LEVEL: $COMPRESSION_LEVEL"
|
||||
echo " FORMAT: $GLTF"
|
||||
echo " FORCE: $FORCE"
|
||||
echo " LIGHTWEIGHT: $LIGHTWEIGHT"
|
||||
echo " ARCHIVE: $IS_ARCHIVE"
|
||||
echo "==========================="
|
||||
printf "\n"
|
||||
|
||||
if [[ ! -z "$INPUT" && -f $INPUT ]]; then
|
||||
FILENAME=${INPUT##*/}
|
||||
NAME="${FILENAME%.*}"
|
||||
EXT=${FILENAME//*.}
|
||||
INPATH=${INPUT%/*}
|
||||
|
||||
if [[ $FILENAME = $INPATH ]]; then
|
||||
INPATH="."
|
||||
fi
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
OUTPUT=`echo ${INPATH}/gltf`
|
||||
else
|
||||
#echo $OUTPUT
|
||||
OUTFILENAME=${OUTPUT%/*} # trim everything past the last /
|
||||
OUTFILENAME=${OUTFILENAME##*/}
|
||||
OUTFILENAME=$(printf '%s' "$OUTFILENAME" | sed -E 's/_(ZIP|RAR|TAR|XZ|GZ)$//I')
|
||||
OUTPUTPATH=`echo $OUTFILENAME.$GLTF`
|
||||
OUTPUT=`echo ${OUTPUT}gltf/`
|
||||
isOutput=true
|
||||
fi
|
||||
if [[ "$EXT" != "$FILENAME" ]]; then
|
||||
EXT="${EXT,,}"
|
||||
if [[ ! -d "$OUTPUT" ]]; then
|
||||
mkdir "$OUTPUT"
|
||||
fi
|
||||
if [[ ! -f $OUTPUT/$NAME.$GLTF || $FORCE ]]; then
|
||||
start=`date +%s`
|
||||
case $EXT in
|
||||
abc|dae|fbx|obj|ply|stl|wrl|x3d)
|
||||
echo "Converting $EXT file..."
|
||||
handle_file "$INPATH" "$FILENAME" "$NAME" $EXT "$OUTPUT" "$OUTPUTPATH"
|
||||
end=`date +%s`
|
||||
echo "File $FILENAME compressed successfully. Runtime: $((end-start))s."
|
||||
exit 0;
|
||||
;;
|
||||
ifc)
|
||||
echo "Converting $EXT file..."
|
||||
handle_ifc_file "$INPATH" "$FILENAME" "$NAME" $EXT "$OUTPUT" "$OUTPUTPATH"
|
||||
end=`date +%s`
|
||||
echo "File $FILENAME compressed successfully. Runtime: $((end-start))s."
|
||||
exit 0;
|
||||
;;
|
||||
blend)
|
||||
echo "Converting $EXT file..."
|
||||
handle_blend_file "$INPATH" "$FILENAME" "$NAME" $EXT
|
||||
end=`date +%s`
|
||||
echo "File $FILENAME compressed successfully. Runtime: $((end-start))s."
|
||||
exit 0;
|
||||
;;
|
||||
gml)
|
||||
echo "Converting $EXT file..."
|
||||
handle_gml_file "$INPATH" "$FILENAME" "$NAME" $EXT "$OUTPUT" "$OUTPUTPATH"
|
||||
end=`date +%s`
|
||||
echo "File $FILENAME compressed successfully. Runtime: $((end-start))s."
|
||||
exit 0;
|
||||
;;
|
||||
glb)
|
||||
end=`date +%s`
|
||||
echo "Given file was already compressed."
|
||||
exit 0;
|
||||
;;
|
||||
|
||||
*)
|
||||
handle_unsupported_file "$INPATH" "$FILENAME" "$NAME" $EXT "$OUTPUT" "$OUTPUTPATH"
|
||||
#echo "File extension $EXT is not supported for conversion yet."
|
||||
exit 0;
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo "Compressed file $OUTPUT/$NAME.$GLTF already exists."
|
||||
exit 1;
|
||||
fi
|
||||
else
|
||||
echo "No extension found on $FILENAME";
|
||||
exit 2;
|
||||
fi
|
||||
elif [[ -z "$INPUT" ]] && die "No --input argument provided"; then
|
||||
file_exists "$INPUT"
|
||||
elif [[ -n "$OUTPUT" && -f "$OUTPUT" && "$FORCE" != "true" ]]; then
|
||||
die "Output file already exists (use --force true)"
|
||||
else
|
||||
echo "Given file '$INPUT' not found"
|
||||
show_usage
|
||||
fi
|
||||
1
scripts/convert_files.txt
Normal file
1
scripts/convert_files.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
/opt/drupal/web/sites/default/files/2024-12/MAINZ_3D_Impressao_3D.glb
|
||||
76
scripts/convert_multiple.sh
Normal file
76
scripts/convert_multiple.sh
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
|
||||
######################################
|
||||
# Config
|
||||
######################################
|
||||
LOG_FILE="convert_$(date +%Y-%m-%d_%H-%M-%S).log"
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
FILES_LIST="$SCRIPT_DIR/convert_files.txt"
|
||||
|
||||
[[ -f $FILES_LIST ]] || { echo "Missing file: $FILES_LIST"; exit 1; }
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
FILES+=("$line")
|
||||
done < "$FILES_LIST"
|
||||
|
||||
[[ ${#FILES[@]} -eq 0 ]] && {
|
||||
echo "No files to process."
|
||||
exit 1
|
||||
}
|
||||
|
||||
######################################
|
||||
# Logging helper
|
||||
######################################
|
||||
log() {
|
||||
echo "[$(date '+%F %T')] $*"
|
||||
}
|
||||
|
||||
######################################
|
||||
# Start
|
||||
######################################
|
||||
echo "Conversion started: $(date)" | tee -a "$LOG_FILE"
|
||||
echo "Log: $LOG_FILE" | tee -a "$LOG_FILE"
|
||||
echo | tee -a "$LOG_FILE"
|
||||
|
||||
FAILED=()
|
||||
SUCCESS=()
|
||||
|
||||
for file in "${FILES[@]}"; do
|
||||
START=$(date +%s)
|
||||
log "Start: $file" | tee -a "$LOG_FILE"
|
||||
|
||||
if "$SCRIPT_DIR/convert.sh" \
|
||||
--input "$file" \
|
||||
--compression true \
|
||||
--compression-level 3 \
|
||||
--binary true \
|
||||
--force false \
|
||||
2>&1 | tee -a "$LOG_FILE"
|
||||
then
|
||||
log "OK: $file" | tee -a "$LOG_FILE"
|
||||
SUCCESS+=("$file")
|
||||
else
|
||||
log "FAIL: $file" | tee -a "$LOG_FILE"
|
||||
FAILED+=("$file")
|
||||
fi
|
||||
END=$(date +%s)
|
||||
log "$((END-START))s"
|
||||
|
||||
echo | tee -a "$LOG_FILE"
|
||||
done
|
||||
|
||||
######################################
|
||||
# Summary
|
||||
######################################
|
||||
echo "================ SUMMARY ================" | tee -a "$LOG_FILE"
|
||||
log "Success: ${#SUCCESS[@]}" | tee -a "$LOG_FILE"
|
||||
log "Errors: ${#FAILED[@]}" | tee -a "$LOG_FILE"
|
||||
|
||||
if (( ${#FAILED[@]} )); then
|
||||
echo "Failed files:" | tee -a "$LOG_FILE"
|
||||
printf ' - %s\n' "${FAILED[@]}" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
|
||||
echo "End: $(date)" | tee -a "$LOG_FILE"
|
||||
157
scripts/convert_test.sh
Executable file
157
scripts/convert_test.sh
Executable file
|
|
@ -0,0 +1,157 @@
|
|||
#!/bin/bash
|
||||
|
||||
#apt install blender python3-pip
|
||||
#pip install numpy
|
||||
#usage: ./convert.sh -c COMPRESS -cl COMPRESSION_LEVEL -i INPUT -o OUTPUT -b BINARY -f FORCE_OVERRIDE
|
||||
|
||||
BLENDER_PATH=''
|
||||
#BLENDER_PATH='/var/lib/snapd/snap/blender/current/'
|
||||
|
||||
#Defaults:
|
||||
COMPRESSION=false
|
||||
COMPRESSION_LEVEL=3
|
||||
GLTF="gltf"
|
||||
FORCE="false"
|
||||
isOutput=false
|
||||
IS_ARCHIVE=false
|
||||
|
||||
while getopts ":c:l:o:i:b:f:" flag; do
|
||||
case "${flag}" in
|
||||
c) COMPRESSION=${OPTARG};;
|
||||
l) COMPRESSION_LEVEL=${OPTARG};;
|
||||
i) INPUT="${OPTARG}";;
|
||||
o) OUTPUT="${OPTARG}";;
|
||||
f) FORCE="${OPTARG}";;
|
||||
a) IS_ARCHIVE="${OPTARG}";;
|
||||
b) if [[ "${OPTARG}" = "true" ]]; then GLTF="glb"; else GLTF="gltf"; fi;;
|
||||
esac
|
||||
done
|
||||
|
||||
render_preview () {
|
||||
if [[ ! -d "$INPATH/views" ]]; then
|
||||
mkdir "$INPATH/views/"
|
||||
fi
|
||||
if [[ "$EXT" = "glb" ]]; then
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 512x512x16" sudo ${BLENDER_PATH}blender -b -P /var/www/html/3drepository/modules/dfg_3dviewer/scripts/render.py -- "$INPATH/$NAME.glb" "glb" $1 "$INPATH/views/" $IS_ARCHIVE -E BLENDER_EEVEE -f 1 > /dev/null 2>&1
|
||||
else
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 512x512x16" sudo ${BLENDER_PATH}blender -b -P /var/www/html/3drepository/modules/dfg_3dviewer/scripts/render.py -- "$INPATH/gltf/$NAME.glb" "glb" $1 "$INPATH/views/" $IS_ARCHIVE -E BLENDER_EEVEE -f 1 > /dev/null 2>&1
|
||||
fi;
|
||||
}
|
||||
|
||||
handle_file () {
|
||||
INPATH=$1
|
||||
FILENAME=$2
|
||||
NAME=$3
|
||||
EXT=$4
|
||||
OUTPUT=$5
|
||||
OUTPUTPATH=$6
|
||||
|
||||
if [[ "$isOutput" = false ]]; then
|
||||
${BLENDER_PATH}blender -b -P /var/www/html/3drepository/modules/dfg_3dviewer/scripts/2gltf2/2gltf2.py -- "$INPATH/$FILENAME" "$GLTF" "$COMPRESSION" "$COMPRESSION_LEVEL" > /dev/null 2>&1
|
||||
else
|
||||
${BLENDER_PATH}blender -b -P /var/www/html/3drepository/modules/dfg_3dviewer/scripts/2gltf2/2gltf2.py -- "$INPATH/$FILENAME" "$GLTF" "$COMPRESSION" "$COMPRESSION_LEVEL" "$OUTPUT$OUTPUTPATH" > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
#if [[ -f "$INPATH/gltf/$NAME.glb" ]]; then
|
||||
# render_preview $EXT
|
||||
#else
|
||||
# render_preview "$INPATH/$NAME.$EXT"
|
||||
#fi;
|
||||
}
|
||||
|
||||
handle_unsupported_file () {
|
||||
INPATH=$1
|
||||
FILENAME=$2
|
||||
NAME=$3
|
||||
EXT=$4
|
||||
OUTPUT=$5
|
||||
OUTPUTPATH=$6
|
||||
|
||||
touch $INPATH/gltf/$NAME.glb.off
|
||||
}
|
||||
|
||||
handle_ifc_file () {
|
||||
INPATH=$1
|
||||
FILENAME=$2
|
||||
NAME=$3
|
||||
EXT=$4
|
||||
OUTPUT=$5
|
||||
OUTPUTPATH=$6
|
||||
|
||||
if [[ ! -d "$INPATH"/gltf/ ]]; then
|
||||
mkdir "$INPATH"/gltf/
|
||||
fi
|
||||
/var/www/html/3drepository/modules/dfg_3dviewer/scripts/IfcConvert "$INPATH/$FILENAME" "$INPATH/gltf/$NAME.glb" > /dev/null 2>&1
|
||||
render_preview $EXT
|
||||
}
|
||||
|
||||
if [[ ! -z "$INPUT" && -f $INPUT ]]; then
|
||||
FILENAME=${INPUT##*/}
|
||||
NAME="${FILENAME%.*}"
|
||||
EXT=${FILENAME//*.}
|
||||
INPATH=${INPUT%/*}
|
||||
|
||||
if [[ $FILENAME = $INPATH ]]; then
|
||||
INPATH="."
|
||||
fi
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
OUTPUT=`echo $INPATH/\gltf`
|
||||
else
|
||||
echo $OUTPUT
|
||||
OUTFILENAME=${OUTPUT%/*} # trim everything past the last /
|
||||
OUTFILENAME=${OUTFILENAME##*/}
|
||||
OUTFILENAME=${OUTFILENAME/"_ZIP"/""}
|
||||
OUTFILENAME=${OUTFILENAME/"_RAR"/""}
|
||||
OUTFILENAME=${OUTFILENAME/"_TAR"/""}
|
||||
OUTFILENAME=${OUTFILENAME/"_XZ"/""}
|
||||
OUTFILENAME=${OUTFILENAME/"_GZ"/""}
|
||||
OUTPUTPATH=`echo $OUTFILENAME.$GLTF`
|
||||
OUTPUT=`echo ${OUTPUT}gltf/`
|
||||
isOutput=true
|
||||
fi
|
||||
if [[ "$EXT" != "$filename" ]]; then
|
||||
EXT="${EXT,,}"
|
||||
if [[ ! -d "$OUTPUT" ]]; then
|
||||
mkdir "$OUTPUT"
|
||||
fi
|
||||
if [[ ! -f $OUTPUT/$NAME.$GLTF || $FORCE ]]; then
|
||||
start=`date +%s`
|
||||
case $EXT in
|
||||
abc|blend|dae|fbx|obj|ply|stl|wrl|x3d)
|
||||
handle_file "$INPATH" "$FILENAME" "$NAME" $EXT "$OUTPUT" "$OUTPUTPATH"
|
||||
end=`date +%s`
|
||||
echo "File $FILENAME compressed successfully. Runtime: $((end-start))s."
|
||||
exit 0;
|
||||
;;
|
||||
ifc)
|
||||
handle_ifc_file "$INPATH" "$FILENAME" "$NAME" $EXT "$OUTPUT" "$OUTPUTPATH"
|
||||
end=`date +%s`
|
||||
echo "File $FILENAME compressed successfully. Runtime: $((end-start))s."
|
||||
exit 0;
|
||||
;;
|
||||
glb)
|
||||
render_preview $EXT
|
||||
end=`date +%s`
|
||||
echo "Given file was already compressed."
|
||||
exit 0;
|
||||
;;
|
||||
|
||||
*)
|
||||
handle_unsupported_file "$INPATH" "$FILENAME" "$NAME" $EXT "$OUTPUT" "$OUTPUTPATH"
|
||||
echo "Flie extension $EXT is not supported for conversion yet."
|
||||
exit 1;
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo "Compressed file $OUTPUT/$NAME.$GLTF already exists."
|
||||
exit 1;
|
||||
fi
|
||||
else
|
||||
echo "No extension found on $FILENAME";
|
||||
exit 2;
|
||||
fi
|
||||
else
|
||||
echo "No file $INPUT or 0 arguments given."
|
||||
echo "Usage: ./convert.sh -c true/false -cl [0-6] -i INPUT -o OUTPUT -b true/false -f true/false"
|
||||
echo "-c=compress -cl=compression level -i=input path -o=output path -b=binary -f=force override existing file"
|
||||
fi
|
||||
98
scripts/pack-drupal-module.js
Normal file
98
scripts/pack-drupal-module.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const stagingDir = path.join(root, '.pack-drupal-staging', 'dfg_3dviewer');
|
||||
const outFile = path.join(root, 'dfg_3dviewer-drupal.zip');
|
||||
|
||||
const moduleFiles = [
|
||||
'dfg_3dviewer.info.yml',
|
||||
'dfg_3dviewer.libraries.yml',
|
||||
'dfg_3dviewer.links.menu.yml',
|
||||
'dfg_3dviewer.module',
|
||||
'dfg_3dviewer.permissions.yml',
|
||||
'dfg_3dviewer.routing.yml',
|
||||
'dfg_3dviewer.services.yml',
|
||||
'dfg_3dviewer.translation.yml',
|
||||
'README.md',
|
||||
];
|
||||
|
||||
const moduleDirs = ['src', 'config'];
|
||||
|
||||
const scriptExclude = new Set([
|
||||
'pack-drupal-module.js',
|
||||
'.env',
|
||||
]);
|
||||
|
||||
function copyRecursive(src, dest) {
|
||||
fs.cpSync(src, dest, { recursive: true });
|
||||
}
|
||||
|
||||
function stageScripts(targetScriptsDir) {
|
||||
fs.mkdirSync(targetScriptsDir, { recursive: true });
|
||||
for (const entry of fs.readdirSync(path.join(root, 'scripts'), { withFileTypes: true })) {
|
||||
if (entry.isFile() && scriptExclude.has(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
const src = path.join(root, 'scripts', entry.name);
|
||||
const dest = path.join(targetScriptsDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
copyRecursive(src, dest);
|
||||
} else {
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stageModule() {
|
||||
if (fs.existsSync(path.dirname(stagingDir))) {
|
||||
fs.rmSync(path.dirname(stagingDir), { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(stagingDir, { recursive: true });
|
||||
|
||||
for (const file of moduleFiles) {
|
||||
const src = path.join(root, file);
|
||||
if (!fs.existsSync(src)) {
|
||||
console.warn(`[pack-drupal] skip missing file: ${file}`);
|
||||
continue;
|
||||
}
|
||||
fs.copyFileSync(src, path.join(stagingDir, file));
|
||||
}
|
||||
|
||||
for (const dir of moduleDirs) {
|
||||
copyRecursive(path.join(root, dir), path.join(stagingDir, dir));
|
||||
}
|
||||
|
||||
stageScripts(path.join(stagingDir, 'scripts'));
|
||||
}
|
||||
|
||||
stageModule();
|
||||
|
||||
if (fs.existsSync(outFile)) {
|
||||
fs.rmSync(outFile);
|
||||
}
|
||||
|
||||
const zipProcess = spawnSync('zip', ['-rq', outFile, 'dfg_3dviewer'], {
|
||||
cwd: path.dirname(stagingDir),
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
fs.rmSync(path.dirname(stagingDir), { recursive: true, force: true });
|
||||
|
||||
if (zipProcess.error) {
|
||||
if (zipProcess.error.code === 'ENOENT') {
|
||||
console.error('`zip` command not found. Install it before running pack.');
|
||||
process.exit(1);
|
||||
}
|
||||
throw zipProcess.error;
|
||||
}
|
||||
|
||||
if (zipProcess.status !== 0) {
|
||||
process.exit(zipProcess.status ?? 1);
|
||||
}
|
||||
|
||||
console.log(`Created ${outFile} (${fs.statSync(outFile).size} bytes)`);
|
||||
33
scripts/progress.js
Normal file
33
scripts/progress.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
(function () {
|
||||
|
||||
const progressWrapper = document.querySelector('[data-3d-progress]');
|
||||
|
||||
if (!progressWrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entityId = progressWrapper.dataset.entityId;
|
||||
|
||||
function checkProgress() {
|
||||
|
||||
fetch('/dfg-3dviewer/progress/' + entityId)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
|
||||
const bar = document.querySelector('#progress-bar');
|
||||
const label = document.querySelector('#progress-label');
|
||||
|
||||
if (!bar || !label) return;
|
||||
|
||||
bar.style.width = data.progress + '%';
|
||||
label.innerText = data.progress + '%';
|
||||
|
||||
if (data.status === 'ready') {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(checkProgress, 3000);
|
||||
|
||||
})();
|
||||
439
scripts/render.py
Executable file
439
scripts/render.py
Executable file
|
|
@ -0,0 +1,439 @@
|
|||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) since 2017 UX3D GmbH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
|
||||
#
|
||||
# Imports
|
||||
#
|
||||
|
||||
import bpy
|
||||
import os
|
||||
import sys
|
||||
import numpy as np
|
||||
import math
|
||||
from mathutils import Matrix, Vector
|
||||
import itertools
|
||||
from math import radians
|
||||
import argparse
|
||||
import time
|
||||
|
||||
if '--' in sys.argv:
|
||||
argv = sys.argv[sys.argv.index('--') + 1:]
|
||||
parser=argparse.ArgumentParser()
|
||||
parser.add_argument("--input", help="Input file path")
|
||||
parser.add_argument("--ext", help="Extenstion of imported file")
|
||||
parser.add_argument("--org_ext", help="Original extenstion of imported file")
|
||||
parser.add_argument("--output", help="Output file path")
|
||||
parser.add_argument("--is_archive", help="Importing archive flag")
|
||||
parser.add_argument("--resolution", help="Resolution preview images")
|
||||
parser.add_argument("--samples", help="Samples rendering quality")
|
||||
args = parser.parse_known_args(argv)[0]
|
||||
|
||||
def rotation_matrix(axis, theta):
|
||||
"""
|
||||
Return the rotation matrix associated with counterclockwise rotation about
|
||||
the given axis by theta radians.
|
||||
"""
|
||||
axis = np.asarray(axis)
|
||||
axis = axis / math.sqrt(np.dot(axis, axis))
|
||||
a = math.cos(theta / 2.0)
|
||||
b, c, d = -axis * math.sin(theta / 2.0)
|
||||
aa, bb, cc, dd = a * a, b * b, c * c, d * d
|
||||
bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d
|
||||
return np.array([[aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)],
|
||||
[2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)],
|
||||
[2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc]])
|
||||
|
||||
def rotate(point, angle_degrees, axis=(0,1,0)):
|
||||
theta_degrees = angle_degrees
|
||||
theta_radians = math.radians(theta_degrees)
|
||||
|
||||
rotated_point = np.dot(rotation_matrix(axis, theta_radians), point)
|
||||
return rotated_point
|
||||
|
||||
""" get_min
|
||||
- (bound_box) bound_box
|
||||
utilized bound_box
|
||||
>>> (Vector) (x,y,z)
|
||||
get_min estimates the minimal x, y, z values
|
||||
"""
|
||||
def get_min(bound_box):
|
||||
min_x = min([bound_box[i][0] for i in range(0, 8)])
|
||||
min_y = min([bound_box[i][1] for i in range(0, 8)])
|
||||
min_z = min([bound_box[i][2] for i in range(0, 8)])
|
||||
return Vector((min_x, min_y, min_z))
|
||||
|
||||
|
||||
""" get_max
|
||||
- (bound_box) bound_box
|
||||
utilized bound_box
|
||||
>>> (Vector) (x,y,z)
|
||||
get_max estimates the maximal x, y, z values
|
||||
"""
|
||||
def get_max(bound_box):
|
||||
max_x = max([bound_box[i][0] for i in range(0, 8)])
|
||||
max_y = max([bound_box[i][1] for i in range(0, 8)])
|
||||
max_z = max([bound_box[i][2] for i in range(0, 8)])
|
||||
return Vector((max_x, max_y, max_z))
|
||||
|
||||
|
||||
def get_origin(v1, v2):
|
||||
return v1 + 0.5 * (v2 - v1)
|
||||
|
||||
max_model_dim = 10
|
||||
|
||||
def scale_scene():
|
||||
pmin = Vector((float("inf"), float("inf"), float("inf")))
|
||||
pmax = Vector((float("-inf"), float("-inf"), float("-inf")))
|
||||
for o in bpy.data.objects:
|
||||
if o.type == 'MESH':
|
||||
mat = o.matrix_world
|
||||
for v in o.bound_box:
|
||||
v = mat @ Vector(v)
|
||||
if v[0] < pmin[0]: pmin[0] = v[0]
|
||||
if v[1] < pmin[1]: pmin[1] = v[1]
|
||||
if v[2] < pmin[2]: pmin[2] = v[2]
|
||||
if v[0] > pmax[0]: pmax[0] = v[0]
|
||||
if v[1] > pmax[1]: pmax[1] = v[1]
|
||||
if v[2] > pmax[2]: pmax[2] = v[2]
|
||||
|
||||
root = bpy.data.objects.new("scaled_root", None)
|
||||
for obj in bpy.context.scene.objects:
|
||||
if not obj.parent:
|
||||
obj.parent = root
|
||||
bpy.context.scene.collection.objects.link(root)
|
||||
|
||||
center = (pmin + pmax) / 2
|
||||
scale = max_model_dim / (pmax-pmin).length
|
||||
root.matrix_world = Matrix.Diagonal((scale,) * 3).to_4x4() @ Matrix.Translation(-center)
|
||||
|
||||
pmin = root.matrix_world @ pmin
|
||||
pmax = root.matrix_world @ pmax
|
||||
bounds = [
|
||||
pmin[0], pmin[1], pmin[2], # left front bottom
|
||||
pmin[0], pmin[1], pmax[2], # left front top
|
||||
pmin[0], pmax[1], pmax[2], # left back top
|
||||
pmin[0], pmax[1], pmin[2], # left back bottom
|
||||
pmax[0], pmin[1], pmin[2], # right front bottom
|
||||
pmax[0], pmin[1], pmax[2], # right front top
|
||||
pmax[0], pmax[1], pmax[2], # right back top
|
||||
pmax[0], pmax[1], pmin[2] # right back bottom
|
||||
]
|
||||
return bounds
|
||||
|
||||
|
||||
#
|
||||
# Globals
|
||||
#
|
||||
bpy.context.scene.render.resolution_percentage = 70
|
||||
bpy.context.scene.render.resolution_x = 1024
|
||||
bpy.context.scene.render.resolution_y = 1024
|
||||
bpy.context.scene.cycles.samples = 20
|
||||
|
||||
if args.resolution:
|
||||
resolution = args.resolution.split('x', 2)
|
||||
bpy.context.scene.render.resolution_x = int(resolution[0])
|
||||
bpy.context.scene.render.resolution_y = int(resolution[1])
|
||||
if args.samples:
|
||||
bpy.context.scene.cycles.samples = int(args.samples)
|
||||
#
|
||||
# Functions
|
||||
#
|
||||
current_directory = os.getcwd()
|
||||
|
||||
extension = "glb"
|
||||
original_extension = "glb"
|
||||
|
||||
if args.ext:
|
||||
extension = args.ext
|
||||
if extension == "gltf":
|
||||
format = "GLTF_EMBEDDED"
|
||||
else:
|
||||
format = "GLB"
|
||||
|
||||
if args.org_ext:
|
||||
original_extension = args.org_ext
|
||||
|
||||
is_archive = args.is_archive
|
||||
|
||||
print("Converting: '" + original_extension + "'")
|
||||
|
||||
root, current_extension = os.path.splitext(args.input)
|
||||
current_basename = os.path.basename(root)
|
||||
|
||||
if current_extension == ".abc" or current_extension == ".blend" or current_extension == ".dae" or current_extension == ".fbx" or current_extension == ".gltf" or current_extension == ".glb" or current_extension == ".obj" or current_extension == ".ply" or current_extension == ".stl" or current_extension == ".wrl" or current_extension == ".x3d":
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
if current_extension == ".abc":
|
||||
bpy.ops.wm.alembic_import(filepath=args.input)
|
||||
|
||||
if current_extension == ".blend":
|
||||
bpy.ops.wm.open_mainfile(filepath=args.input)
|
||||
|
||||
if current_extension == ".dae":
|
||||
bpy.ops.wm.collada_import(filepath=args.input)
|
||||
|
||||
if current_extension == ".fbx":
|
||||
bpy.ops.import_scene.fbx(filepath=args.input)
|
||||
|
||||
if current_extension == ".obj":
|
||||
object=bpy.ops.import_scene.obj(filepath=args.input)
|
||||
|
||||
if current_extension == ".ply":
|
||||
bpy.ops.import_mesh.ply(filepath=args.input)
|
||||
|
||||
if current_extension == ".stl":
|
||||
bpy.ops.import_mesh.stl(filepath=args.input)
|
||||
|
||||
if current_extension == ".wrl" or current_extension == ".x3d":
|
||||
bpy.ops.import_scene.x3d(filepath=args.input)
|
||||
|
||||
if current_extension == ".gltf" or current_extension == ".glb":
|
||||
bpy.ops.import_scene.gltf(filepath=args.input)
|
||||
|
||||
scene = bpy.context.scene
|
||||
context = bpy.context
|
||||
render = scene.render
|
||||
|
||||
# --------------------------------------------------
|
||||
# UTILS
|
||||
# --------------------------------------------------
|
||||
|
||||
def np_matmul_coords(coords, matrix):
|
||||
M = matrix.transposed()
|
||||
ones = np.ones((coords.shape[0], 1))
|
||||
coords4d = np.hstack((coords, ones))
|
||||
return np.dot(coords4d, M)[:, :-1]
|
||||
|
||||
|
||||
def get_scene_bounds():
|
||||
coords = np.vstack(
|
||||
tuple(
|
||||
np_matmul_coords(np.array(o.bound_box), o.matrix_world.copy())
|
||||
for o in scene.objects if o.type == 'MESH'
|
||||
)
|
||||
)
|
||||
bfl = coords.min(axis=0)
|
||||
tbr = coords.max(axis=0)
|
||||
size = Vector(tbr - bfl)
|
||||
center = Vector((bfl + tbr) * 0.5)
|
||||
return center, size
|
||||
|
||||
|
||||
def fit_camera_to_bounds(cam, center, size, margin=1.2):
|
||||
# aspect ratio of render
|
||||
render = bpy.context.scene.render
|
||||
aspect = render.resolution_x / render.resolution_y
|
||||
|
||||
ratio = size.x / size.z
|
||||
print(f"Camera fit ratio: {ratio:.2f}")
|
||||
|
||||
if ratio > 6.0:
|
||||
cam.data.type = 'ORTHO'
|
||||
|
||||
# ORTHO: skala = wysokość kadru
|
||||
ortho_height = size.z * 1.2
|
||||
ortho_width = size.x * 1.2 / aspect
|
||||
|
||||
cam.data.ortho_scale = max(ortho_height, ortho_width)
|
||||
|
||||
# clipping – MUST HAVE
|
||||
cam.data.clip_start = 0.01
|
||||
cam.data.clip_end = max(size) * 10
|
||||
|
||||
else:
|
||||
cam.data.type = 'PERSP'
|
||||
cam.data.clip_start = 0.01
|
||||
cam.data.clip_end = max(size) * 10
|
||||
|
||||
# FOV vertical and horizontal
|
||||
fov_x = cam_data.angle
|
||||
fov_y = 2 * math.atan(math.tan(fov_x / 2) / aspect)
|
||||
|
||||
# required distance to fit bounds in view
|
||||
dist_x = (size.x * 0.5) / math.tan(fov_x * 0.5)
|
||||
dist_z = (size.z * 0.5) / math.tan(fov_y * 0.5)
|
||||
|
||||
distance = max(dist_x, dist_z) * margin
|
||||
|
||||
cam.location = center + Vector((0, -distance, 0))
|
||||
|
||||
if args.output:
|
||||
export_file = args.output
|
||||
else:
|
||||
root = root[::-1].replace(current_basename[::-1], "", 1)[::-1]
|
||||
export_file = root + "_" + extension
|
||||
|
||||
if is_archive:
|
||||
mainfilepath=export_file+current_basename
|
||||
else:
|
||||
mainfilepath=export_file+current_basename+"."+original_extension
|
||||
|
||||
# --------------------------------------------------
|
||||
# RENDER / CYCLES
|
||||
# --------------------------------------------------
|
||||
|
||||
render.engine = 'CYCLES'
|
||||
render.film_transparent = True
|
||||
render.resolution_x = int(resolution[0])
|
||||
render.resolution_y = int(resolution[1])
|
||||
render.resolution_percentage = 100
|
||||
|
||||
render.image_settings.file_format = 'PNG'
|
||||
render.image_settings.color_mode = 'RGBA'
|
||||
render.image_settings.color_depth = '16'
|
||||
render.image_settings.color_management = 'FOLLOW_SCENE'
|
||||
render.image_settings.view_settings.view_transform = 'Standard'
|
||||
|
||||
scene.render.use_compositing = True
|
||||
|
||||
scene.cycles.device = 'CPU'
|
||||
scene.cycles.samples = 256
|
||||
scene.cycles.use_adaptive_sampling = True
|
||||
scene.cycles.adaptive_threshold = 0.03
|
||||
scene.cycles.adaptive_min_samples = 16
|
||||
|
||||
scene.cycles.use_denoising = True
|
||||
scene.cycles.denoiser = 'OPENIMAGEDENOISE'
|
||||
scene.cycles.denoising_input_passes = 'RGB_ALBEDO_NORMAL'
|
||||
scene.cycles.denoising_prefilter = 'ACCURATE'
|
||||
|
||||
scene.cycles.max_bounces = 6
|
||||
scene.cycles.diffuse_bounces = 3
|
||||
scene.cycles.glossy_bounces = 3
|
||||
scene.cycles.transparent_max_bounces = 4
|
||||
scene.cycles.transmission_bounces = 4
|
||||
|
||||
scene.cycles.sample_clamp_indirect = 20
|
||||
scene.cycles.light_sampling_threshold = 0.03
|
||||
|
||||
scene.view_settings.exposure = 1.0
|
||||
scene.view_settings.gamma = 1.0
|
||||
|
||||
# CUDA OFF (no warnings)
|
||||
prefs = bpy.context.preferences
|
||||
prefs.addons['cycles'].preferences.compute_device_type = 'NONE'
|
||||
|
||||
# --------------------------------------------------
|
||||
# VIEW LAYER PASSES (CLI SAFE)
|
||||
# --------------------------------------------------
|
||||
|
||||
view_layer = scene.view_layers["ViewLayer"]
|
||||
view_layer.use_pass_normal = True
|
||||
view_layer.use_pass_diffuse_color = True
|
||||
view_layer.use_pass_object_index = True
|
||||
|
||||
# --------------------------------------------------
|
||||
# CAMERA
|
||||
# --------------------------------------------------
|
||||
|
||||
center, size = get_scene_bounds()
|
||||
cam_data = bpy.data.cameras.new("Camera")
|
||||
cam_data.lens = 50 # product look
|
||||
cam_data.sensor_width = 36
|
||||
|
||||
cam = bpy.data.objects.new("Camera", cam_data)
|
||||
scene.collection.objects.link(cam)
|
||||
scene.camera = cam
|
||||
|
||||
cam_empty = bpy.data.objects.new("CamTarget", None)
|
||||
cam_empty.location = center
|
||||
scene.collection.objects.link(cam_empty)
|
||||
|
||||
cam.parent = cam_empty
|
||||
|
||||
constraint = cam.constraints.new(type='TRACK_TO')
|
||||
constraint.target = cam_empty
|
||||
constraint.track_axis = 'TRACK_NEGATIVE_Z'
|
||||
constraint.up_axis = 'UP_Y'
|
||||
constraint.owner_space = 'WORLD'
|
||||
constraint.target_space = 'WORLD'
|
||||
|
||||
# --------------------------------------------------
|
||||
# BASE CAMERA FIT
|
||||
# --------------------------------------------------
|
||||
|
||||
fit_camera_to_bounds(cam, center, size, margin=1.45)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# LIGHT
|
||||
# --------------------------------------------------
|
||||
|
||||
max_size = max(size)
|
||||
|
||||
sun_data = bpy.data.lights.new('SunMain', type='SUN')
|
||||
sun_data.energy = 5.0 # 4.0–6.0 sweet spot
|
||||
|
||||
sun = bpy.data.objects.new('SunMain', sun_data)
|
||||
scene.collection.objects.link(sun)
|
||||
|
||||
sun.rotation_euler = (
|
||||
math.radians(50),
|
||||
0.0,
|
||||
math.radians(30)
|
||||
)
|
||||
|
||||
fill_data = bpy.data.lights.new('SunFill', type='SUN')
|
||||
fill_data.energy = 0.5 # 10% głównego
|
||||
|
||||
fill = bpy.data.objects.new('SunFill', fill_data)
|
||||
scene.collection.objects.link(fill)
|
||||
|
||||
fill.rotation_euler = (
|
||||
math.radians(75),
|
||||
0.0,
|
||||
math.radians(-120)
|
||||
)
|
||||
|
||||
# --------------------------------------------------
|
||||
# RENDERS
|
||||
# --------------------------------------------------
|
||||
t0 = time.perf_counter()
|
||||
|
||||
print("Starting rendering...")
|
||||
def render_angle(angle_deg, suffix):
|
||||
print(f"Rendering angle {angle_deg}")
|
||||
cam_empty.rotation_euler = (0, 0, math.radians(angle_deg))
|
||||
scene.render.filepath = f"{mainfilepath}_{suffix}.png"
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
# sides
|
||||
for a in [0, 90, 180, 270]:
|
||||
render_angle(a, f"side{a}")
|
||||
|
||||
# hero angles
|
||||
for a in [45, 135, 225, 315]:
|
||||
render_angle(a, f"side{a}")
|
||||
|
||||
# top
|
||||
cam.location = center + Vector((0, 0, max(size.x, size.y) * 1.3))
|
||||
cam.rotation_euler = (0, 0, 0)
|
||||
scene.render.filepath = f"{mainfilepath}_top.png"
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
t1 = time.perf_counter()
|
||||
|
||||
print(f"Rendering done (took: {t1 - t0:.3f} s)")
|
||||
# --------------------------------------------------
|
||||
194
scripts/render.sh
Executable file
194
scripts/render.sh
Executable file
|
|
@ -0,0 +1,194 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
if [[ -f "${SCRIPT_DIR}/.env" ]]; then
|
||||
source "${SCRIPT_DIR}/.env"
|
||||
fi
|
||||
BLENDER_BIN="${BLENDER_BIN:-}"
|
||||
if [[ -z "$BLENDER_BIN" ]]; then
|
||||
BLENDER_BIN="blender"
|
||||
fi
|
||||
SPATH="${SPATH:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
||||
|
||||
IS_ARCHIVE=false
|
||||
INPUT=""
|
||||
GLB_INPUT=""
|
||||
|
||||
die() {
|
||||
echo "Error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
bool() {
|
||||
[[ "$1" == "true" || "$1" == "false" ]] || die "Value must be true/false (is: $1)"
|
||||
}
|
||||
|
||||
resolve_blender_bin() {
|
||||
local candidate="$1"
|
||||
|
||||
if [[ -z "$candidate" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$candidate" == /* ]]; then
|
||||
[[ -f "$candidate" && -x "$candidate" ]] || return 1
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -f "$SCRIPT_DIR/$candidate" && -x "$SCRIPT_DIR/$candidate" ]]; then
|
||||
printf '%s\n' "$SCRIPT_DIR/$candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -f "$SPATH/$candidate" && -x "$SPATH/$candidate" ]]; then
|
||||
printf '%s\n' "$SPATH/$candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v "$candidate" &> /dev/null; then
|
||||
command -v "$candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! BLENDER_BIN="$(resolve_blender_bin "$BLENDER_BIN")"; then
|
||||
BLENDER_BIN=""
|
||||
fi
|
||||
|
||||
check_blender() {
|
||||
if ! BLENDER_BIN="$(resolve_blender_bin "$BLENDER_BIN")"; then
|
||||
echo "Blender doesn't exist, install it by 'apt install blender python3-pip' then 'pip install numpy' or set BLENDER_BIN in scripts/.env"
|
||||
return 1
|
||||
elif [[ ! -f "$BLENDER_BIN" || ! -x "$BLENDER_BIN" ]]; then
|
||||
echo "Configured BLENDER_BIN is not executable: $BLENDER_BIN"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -n "${BLENDER_BIN:-}" ]]; then
|
||||
echo "Blender exists and be used for next steps..."
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
check_xvfb_run() {
|
||||
if ! command -v xvfb-run &> /dev/null; then
|
||||
echo "xvfb-run doesn't exist, install it by 'apt install xvfb'"
|
||||
return 1
|
||||
else
|
||||
echo "xvfb-run exists and be used for next steps..."
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
check_scripts() {
|
||||
if [ ! -d "${SPATH}/scripts" ]; then
|
||||
echo "Can't find dependencies directory. Did you change your SPATH value in scripts/.env?"
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
show_usage() {
|
||||
cat <<EOF
|
||||
Usage: render.sh [options]
|
||||
|
||||
Options:
|
||||
-i, --input FILE Original input model path.
|
||||
-g, --glb-input FILE Optional explicit GLB path.
|
||||
-a, --archive true|false
|
||||
-h, --help
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-i|--input)
|
||||
INPUT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-g|--glb-input)
|
||||
GLB_INPUT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-a|--archive)
|
||||
bool "$2"
|
||||
IS_ARCHIVE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
show_usage
|
||||
;;
|
||||
*)
|
||||
die "Error parsing arguments"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$INPUT" ]] || die "No --input argument provided"
|
||||
[[ -f "$INPUT" ]] || die "Input file not found: $INPUT"
|
||||
|
||||
FILENAME=${INPUT##*/}
|
||||
NAME="${FILENAME%.*}"
|
||||
EXT=${FILENAME//*.}
|
||||
EXT="${EXT,,}"
|
||||
INPATH=${INPUT%/*}
|
||||
if [[ $FILENAME = $INPATH ]]; then
|
||||
INPATH="."
|
||||
fi
|
||||
|
||||
INPUT_GLTF_PATH="$INPATH/$NAME.glb"
|
||||
ORG_EXT="$EXT"
|
||||
|
||||
if [[ "$EXT" != "glb" ]]; then
|
||||
INPUT_GLTF_PATH="$INPATH/gltf/$NAME.glb"
|
||||
fi
|
||||
|
||||
if [[ -n "$GLB_INPUT" ]]; then
|
||||
INPUT_GLTF_PATH="$GLB_INPUT"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$INPUT_GLTF_PATH" ]]; then
|
||||
die "Render input not found: $INPUT_GLTF_PATH"
|
||||
fi
|
||||
|
||||
mkdir -p "$INPATH/views"
|
||||
RENDER_LOCKFILE="$INPATH/views/${NAME}.lock"
|
||||
|
||||
exec 201>"$RENDER_LOCKFILE" || exit 1
|
||||
flock -n 201 || {
|
||||
echo "Render already running for $NAME"
|
||||
exec 201>&- 2>/dev/null || true
|
||||
exit 0
|
||||
}
|
||||
trap 'flock -u 201 2>/dev/null || true; exec 201>&- 2>/dev/null || true; rm -f "$RENDER_LOCKFILE"' EXIT
|
||||
|
||||
check_blender || die "Blender validation failed"
|
||||
check_xvfb_run || die "xvfb-run validation failed"
|
||||
check_scripts || die "Dependency scripts directory not found"
|
||||
|
||||
if [[ -z "$RENDER_RESOLUTION" ]]; then
|
||||
RENDER_RESOLUTION='1024x1024x16'
|
||||
fi
|
||||
if [[ -z "$RENDER_SAMPLES" ]]; then
|
||||
RENDER_SAMPLES='20'
|
||||
fi
|
||||
|
||||
echo "Rendering thumbnails..."
|
||||
xvfb-run --auto-servernum \
|
||||
--server-args="-screen 0 ${RENDER_RESOLUTION}" \
|
||||
"$BLENDER_BIN" -b -P "$SPATH/scripts/render.py" -- \
|
||||
--input "$INPUT_GLTF_PATH" \
|
||||
--ext glb \
|
||||
--org_ext "$ORG_EXT" \
|
||||
--output "$INPATH/views/" \
|
||||
--is_archive "$IS_ARCHIVE" \
|
||||
--resolution "$RENDER_RESOLUTION" \
|
||||
--samples "$RENDER_SAMPLES" \
|
||||
-E BLENDER_EEVEE -f 1
|
||||
echo "Blender exit code: $?"
|
||||
415
scripts/render2.py
Normal file
415
scripts/render2.py
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) since 2017 UX3D GmbH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
|
||||
#
|
||||
# Imports
|
||||
#
|
||||
|
||||
import bpy
|
||||
import os
|
||||
import sys
|
||||
import numpy as np
|
||||
import math
|
||||
from mathutils import Matrix, Vector
|
||||
import itertools
|
||||
from math import radians
|
||||
|
||||
bpy.context.scene.render.resolution_percentage = 50
|
||||
bpy.context.scene.render.resolution_x = 1280
|
||||
bpy.context.scene.render.resolution_y = 960
|
||||
bpy.context.scene.cycles.samples = 20
|
||||
|
||||
def rotation_matrix(axis, theta):
|
||||
"""
|
||||
Return the rotation matrix associated with counterclockwise rotation about
|
||||
the given axis by theta radians.
|
||||
"""
|
||||
axis = np.asarray(axis)
|
||||
axis = axis / math.sqrt(np.dot(axis, axis))
|
||||
a = math.cos(theta / 2.0)
|
||||
b, c, d = -axis * math.sin(theta / 2.0)
|
||||
aa, bb, cc, dd = a * a, b * b, c * c, d * d
|
||||
bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d
|
||||
return np.array([[aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)],
|
||||
[2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)],
|
||||
[2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc]])
|
||||
|
||||
def rotate(point, angle_degrees, axis=(0,1,0)):
|
||||
theta_degrees = angle_degrees
|
||||
theta_radians = math.radians(theta_degrees)
|
||||
|
||||
rotated_point = np.dot(rotation_matrix(axis, theta_radians), point)
|
||||
return rotated_point
|
||||
|
||||
""" get_min
|
||||
- (bound_box) bound_box
|
||||
utilized bound_box
|
||||
>>> (Vector) (x,y,z)
|
||||
get_min estimates the minimal x, y, z values
|
||||
"""
|
||||
def get_min(bound_box):
|
||||
min_x = min([bound_box[i][0] for i in range(0, 8)])
|
||||
min_y = min([bound_box[i][1] for i in range(0, 8)])
|
||||
min_z = min([bound_box[i][2] for i in range(0, 8)])
|
||||
return Vector((min_x, min_y, min_z))
|
||||
|
||||
|
||||
""" get_max
|
||||
- (bound_box) bound_box
|
||||
utilized bound_box
|
||||
>>> (Vector) (x,y,z)
|
||||
get_max estimates the maximal x, y, z values
|
||||
"""
|
||||
def get_max(bound_box):
|
||||
max_x = max([bound_box[i][0] for i in range(0, 8)])
|
||||
max_y = max([bound_box[i][1] for i in range(0, 8)])
|
||||
max_z = max([bound_box[i][2] for i in range(0, 8)])
|
||||
return Vector((max_x, max_y, max_z))
|
||||
|
||||
|
||||
def get_origin(v1, v2):
|
||||
return v1 + 0.5 * (v2 - v1)
|
||||
|
||||
max_model_dim = 10
|
||||
|
||||
def scale_scene():
|
||||
pmin = Vector((float("inf"), float("inf"), float("inf")))
|
||||
pmax = Vector((float("-inf"), float("-inf"), float("-inf")))
|
||||
for o in bpy.data.objects:
|
||||
if o.type == 'MESH':
|
||||
mat = o.matrix_world
|
||||
for v in o.bound_box:
|
||||
v = mat @ Vector(v)
|
||||
if v[0] < pmin[0]: pmin[0] = v[0]
|
||||
if v[1] < pmin[1]: pmin[1] = v[1]
|
||||
if v[2] < pmin[2]: pmin[2] = v[2]
|
||||
if v[0] > pmax[0]: pmax[0] = v[0]
|
||||
if v[1] > pmax[1]: pmax[1] = v[1]
|
||||
if v[2] > pmax[2]: pmax[2] = v[2]
|
||||
|
||||
root = bpy.data.objects.new("scaled_root", None)
|
||||
for obj in bpy.context.scene.objects:
|
||||
if not obj.parent:
|
||||
obj.parent = root
|
||||
bpy.context.scene.collection.objects.link(root)
|
||||
|
||||
center = (pmin + pmax) / 2
|
||||
scale = max_model_dim / (pmax-pmin).length
|
||||
root.matrix_world = Matrix.Diagonal((scale,) * 3).to_4x4() @ Matrix.Translation(-center)
|
||||
|
||||
pmin = root.matrix_world @ pmin
|
||||
pmax = root.matrix_world @ pmax
|
||||
bounds = [
|
||||
pmin[0], pmin[1], pmin[2], # left front bottom
|
||||
pmin[0], pmin[1], pmax[2], # left front top
|
||||
pmin[0], pmax[1], pmax[2], # left back top
|
||||
pmin[0], pmax[1], pmin[2], # left back bottom
|
||||
pmax[0], pmin[1], pmin[2], # right front bottom
|
||||
pmax[0], pmin[1], pmax[2], # right front top
|
||||
pmax[0], pmax[1], pmax[2], # right back top
|
||||
pmax[0], pmax[1], pmin[2] # right back bottom
|
||||
]
|
||||
return bounds
|
||||
|
||||
#
|
||||
# Globals
|
||||
#
|
||||
|
||||
#
|
||||
# Functions
|
||||
#
|
||||
current_directory = os.getcwd()
|
||||
|
||||
if sys.argv[6:]:
|
||||
extension = sys.argv[6]
|
||||
if extension == "gltf":
|
||||
format = "GLTF_EMBEDDED"
|
||||
else:
|
||||
format = "GLB"
|
||||
|
||||
force_continue = True
|
||||
for current_argument in sys.argv:
|
||||
|
||||
if force_continue:
|
||||
if current_argument == '--':
|
||||
force_continue = False
|
||||
continue
|
||||
|
||||
#
|
||||
|
||||
root, current_extension = os.path.splitext(current_argument)
|
||||
current_basename = os.path.basename(root)
|
||||
|
||||
if current_extension != ".abc" and current_extension != ".blend" and current_extension != ".dae" and current_extension != ".fbx" and current_extension != ".gltf" and current_extension != ".glb" and current_extension != ".obj" and current_extension != ".ply" and current_extension != ".stl" and current_extension != ".wrl" and current_extension != ".x3d":
|
||||
continue
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
#print("Converting: '" + current_argument + "'")
|
||||
|
||||
#
|
||||
|
||||
if current_extension == ".abc":
|
||||
bpy.ops.wm.alembic_import(filepath=current_argument)
|
||||
|
||||
if current_extension == ".blend":
|
||||
bpy.ops.wm.open_mainfile(filepath=current_argument)
|
||||
|
||||
if current_extension == ".dae":
|
||||
bpy.ops.wm.collada_import(filepath=current_argument)
|
||||
|
||||
if current_extension == ".fbx":
|
||||
bpy.ops.import_scene.fbx(filepath=current_argument)
|
||||
|
||||
if current_extension == ".obj":
|
||||
object=bpy.ops.import_scene.obj(filepath=current_argument)
|
||||
|
||||
if current_extension == ".ply":
|
||||
bpy.ops.import_mesh.ply(filepath=current_argument)
|
||||
|
||||
if current_extension == ".stl":
|
||||
bpy.ops.import_mesh.stl(filepath=current_argument)
|
||||
|
||||
if current_extension == ".wrl" or current_extension == ".x3d":
|
||||
bpy.ops.import_scene.x3d(filepath=current_argument)
|
||||
|
||||
if current_extension == ".gltf" or current_extension == ".glb":
|
||||
bpy.ops.import_scene.gltf(filepath=current_argument)
|
||||
|
||||
scene = bpy.context.scene
|
||||
context = bpy.context
|
||||
render = bpy.context.scene.render
|
||||
bounds = scale_scene()
|
||||
|
||||
item='MESH'
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
bpy.ops.object.select_by_type(type=item)
|
||||
|
||||
# multiply 3d coord list by matrix
|
||||
def np_matmul_coords(coords, matrix, space=None):
|
||||
M = (space @ matrix @ space.inverted()
|
||||
if space else matrix).transposed()
|
||||
ones = np.ones((coords.shape[0], 1))
|
||||
coords4d = np.hstack((coords, ones))
|
||||
|
||||
return np.dot(coords4d, M)[:,:-1]
|
||||
return coords4d[:,:-1]
|
||||
|
||||
# get the global coordinates of all object bounding box corners
|
||||
coords = np.vstack(
|
||||
tuple(np_matmul_coords(np.array(o.bound_box), o.matrix_world.copy())
|
||||
for o in
|
||||
bpy.context.scene.objects
|
||||
if o.type == 'MESH'
|
||||
)
|
||||
)
|
||||
print("#" * 72)
|
||||
# bottom front left (all the mins)
|
||||
bfl = coords.min(axis=0)
|
||||
# top back right
|
||||
tbr = coords.max(axis=0)
|
||||
G = np.array((bfl, tbr)).T
|
||||
# bound box coords ie the 8 combinations of bfl tbr.
|
||||
bbc = [i for i in itertools.product(*G)]
|
||||
print(np.array(bbc))
|
||||
bb_sides = get_min(bbc) - get_max(bbc)
|
||||
bb_sides = (abs(bb_sides[0]), abs(bb_sides[1]), abs(bb_sides[2]))
|
||||
print("#" * 72)
|
||||
print('BBOX')
|
||||
print(bb_sides)
|
||||
|
||||
group = bpy.data.collections.new("MainGroup")
|
||||
bpy.context.scene.collection.children.link(group)
|
||||
#for ob in context.selected_objects: # or whichever list of objects desired
|
||||
# group.objects.link(ob)
|
||||
#print("Moving objects into origin (0, 0, 0)")
|
||||
#group.location = (0, 0, 0)
|
||||
#for obj in context.selected_objects:
|
||||
# obj.location = (0, 0, 0)
|
||||
render.engine = "CYCLES"
|
||||
render.film_transparent = True
|
||||
scene.cycles.device = "CPU"
|
||||
scene.cycles.samples = 128 # default 128
|
||||
scene.cycles.use_adaptive_sampling = True
|
||||
scene.cycles.adaptive_threshold = 0.1
|
||||
scene.cycles.adaptive_min_samples = 1
|
||||
scene.cycles.use_denoising = True
|
||||
scene.cycles.seed = 0 # default
|
||||
scene.cycles.use_animated_seed = True
|
||||
scene.cycles.min_light_bounces = 0 # default
|
||||
scene.cycles.min_transparent_bounces = 0 # default
|
||||
scene.cycles.light_sampling_threshold = 0.01 # default
|
||||
scene.cycles.max_bounces = 5
|
||||
scene.cycles.sample_clamp_direct = 0 # default
|
||||
scene.cycles.sample_clamp_indirect = 10 # default
|
||||
scene.cycles.blur_glossy = 1 # default
|
||||
scene.cycles.caustics_reflective = False
|
||||
scene.cycles.caustics_refractive = False
|
||||
|
||||
#render.engine = 'BLENDER_EEVEE'
|
||||
#render.engine = 'CYCLES'
|
||||
#render.engine = 'BLENDER_WORKBENCH'
|
||||
render.image_settings.color_mode = 'RGBA'
|
||||
render.image_settings.color_depth = '16'
|
||||
render.image_settings.file_format = 'PNG'
|
||||
render.resolution_x = 512
|
||||
render.resolution_y = 512
|
||||
render.resolution_percentage = 100
|
||||
render.film_transparent = True
|
||||
#scene.render.engine = 'CYCLES'
|
||||
scene.render.use_freestyle = False
|
||||
scene.use_nodes = True
|
||||
scene.view_layers["View Layer"].use_pass_normal = True
|
||||
scene.view_layers["View Layer"].use_pass_diffuse_color = True
|
||||
scene.view_layers["View Layer"].use_pass_object_index = True
|
||||
|
||||
#
|
||||
#print("ROOT" + root)
|
||||
if sys.argv[7:]:
|
||||
export_file = str(sys.argv[7])
|
||||
else:
|
||||
root = root[::-1].replace(current_basename[::-1], "", 1)[::-1]
|
||||
export_file = root + "_" + extension
|
||||
|
||||
multiplier=10
|
||||
|
||||
levels=3
|
||||
density=5
|
||||
r_offset=0.2
|
||||
z_offset=0.2
|
||||
|
||||
#target_obj = bpy.context.selected_objects[0]
|
||||
#target_origin = target_obj.location
|
||||
# get bounding box side lengths
|
||||
#bb_sides = get_min(target_obj.bound_box) - get_max(target_obj.bound_box)
|
||||
(dist_x, dist_y, dist_z) = tuple([abs(c) for c in bb_sides])
|
||||
originated_dist_y = .5 * dist_y
|
||||
radius = 0.5 * max(dist_x, dist_z)
|
||||
max_size = max(dist_x, dist_y, dist_z)
|
||||
|
||||
light_data = bpy.data.lights.new('light', type='AREA')
|
||||
sun = bpy.data.objects.new('light', light_data)
|
||||
sun.data.energy=max_size*5000.0
|
||||
sun.data.size = max_size*2
|
||||
#sun.location = (3, 4, -5)
|
||||
#sun.location = (dist_x*1.4, dist_y*1.4, dist_z*1.4)
|
||||
sun.location = (0,0,0)
|
||||
bpy.context.collection.objects.link(sun)
|
||||
sun_bottom = bpy.data.objects.new('light_bottom', light_data)
|
||||
sun_bottom.data.energy=max_size*5000.0
|
||||
sun_bottom.data.size = max_size*2
|
||||
sun_bottom.location = (-dist_x*1.4, dist_y*1.4, dist_z*1.4)
|
||||
#sun_bottom.location = (0, 0, dist_z/8)
|
||||
sun_bottom.rotation_euler = (2, 0.3, 0.3)
|
||||
bpy.context.collection.objects.link(sun_bottom)
|
||||
|
||||
scene.render.image_settings.file_format='PNG'
|
||||
scene.render.filepath=export_file+current_basename+'.png'
|
||||
print("Rendering: " + scene.render.filepath)
|
||||
cam_data = bpy.data.cameras.new('camera')
|
||||
cam = bpy.data.objects.new('camera', cam_data)
|
||||
cam.data.lens = 35
|
||||
cam.data.sensor_width = 32
|
||||
cam_constraint = cam.constraints.new(type='TRACK_TO')
|
||||
cam_constraint.track_axis = 'TRACK_NEGATIVE_Z'
|
||||
cam_constraint.up_axis = 'UP_Y'
|
||||
cam_empty = bpy.data.objects.new("Empty", None)
|
||||
#cam_empty.location = (0, 0, 0)
|
||||
cam_empty.location = (0, 0, 0)
|
||||
cam.parent = cam_empty
|
||||
|
||||
scene.collection.objects.link(cam_empty)
|
||||
context.view_layer.objects.active = cam_empty
|
||||
cam_constraint.target = cam_empty
|
||||
|
||||
print(dist_x, dist_y, dist_z)
|
||||
#cam.location=Vector((dist_x, dist_y, dist_z))
|
||||
bpy.context.collection.objects.link(cam)
|
||||
|
||||
"""
|
||||
# get the current object
|
||||
for _obj in scene.objects:
|
||||
if (_obj.type == 'MESH'):
|
||||
current_obj = _obj
|
||||
# set geometry to origin
|
||||
bpy.ops.object.origin_set(type="GEOMETRY_ORIGIN")
|
||||
zverts = []
|
||||
# get all z coordinates of the vertices
|
||||
for face in current_obj.data.polygons:
|
||||
verts_in_face = face.vertices[:]
|
||||
for vert in verts_in_face:
|
||||
local_point = current_obj.data.vertices[vert].co
|
||||
world_point = current_obj.matrix_world @ local_point
|
||||
zverts.append(world_point[2])
|
||||
|
||||
scene.cursor.location = (0, 0, min(zverts))
|
||||
bpy.ops.object.origin_set(type="ORIGIN_CURSOR")
|
||||
# set the object to (0,0,0)
|
||||
current_obj.location = (0,0,0)
|
||||
# reset the cursor
|
||||
scene.cursor.location = (0,0,0)
|
||||
"""
|
||||
#bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')
|
||||
|
||||
#bpy.ops.export_scene.fbx(filepath=export_file+current_basename+'.fbx')
|
||||
|
||||
scene.camera=cam
|
||||
cam.location = (0, dist_y*2.5, dist_z*0.9)
|
||||
sun.location = cam.location
|
||||
sun_bottom.location = cam.location
|
||||
#cam.location=Vector((dist_x/multiplier, dist_y/multiplier, dist_z/multiplier))
|
||||
|
||||
#scene.render.filepath=export_file+current_basename+'_org.png'
|
||||
#bpy.ops.render.render(write_still=True)
|
||||
|
||||
#cam.location = rotate(cam.location, 45, axis=(0, 0, 1))
|
||||
|
||||
for angle in range(0, 360, 90):
|
||||
sun.location = rotate(sun.location, 80, axis=(0, 0, 1))
|
||||
sun_bottom.location = rotate(sun_bottom.location, 80, axis=(0, 0, 1))
|
||||
scene.render.filepath=export_file+current_basename+'_side'+str(angle)+'.png'
|
||||
bpy.ops.render.render(write_still=True)
|
||||
cam.location = rotate(cam.location, 90, axis=(0, 0, 1))
|
||||
|
||||
cam.location = (0, dist_y*2.9, dist_z*1.7)
|
||||
cam.location = rotate(cam.location, 45, axis=(0, 0, 1))
|
||||
for angle in range(45, 360, 90):
|
||||
sun.location = rotate(sun.location, 80, axis=(0, 0, 1))
|
||||
sun_bottom.location = rotate(sun_bottom.location, 80, axis=(0, 0, 1))
|
||||
scene.render.filepath=export_file+current_basename+'_side'+str(angle)+'.png'
|
||||
bpy.ops.render.render(write_still=True)
|
||||
cam.location = rotate(cam.location, 90, axis=(0, 0, 1))
|
||||
|
||||
|
||||
#top
|
||||
cam.location=Vector((0, 0, dist_z*5))
|
||||
sun.location = cam.location
|
||||
scene.render.filepath=export_file+current_basename+'_top.png'
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
#bottom
|
||||
#cam.location=Vector((0, 0, -dist_z*5))
|
||||
#sun.location = cam.location
|
||||
#scene.render.filepath=export_file+current_basename+'_bottom.png'
|
||||
#bpy.ops.render.render(write_still=True)
|
||||
print("Rendering done")
|
||||
98
scripts/uncompress.sh
Executable file
98
scripts/uncompress.sh
Executable file
|
|
@ -0,0 +1,98 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Ubuntu way
|
||||
#apt install unrar-free
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TYPE=""
|
||||
INPUT=""
|
||||
OUTPUT=""
|
||||
NAME=""
|
||||
FORCE=""
|
||||
|
||||
while getopts ":t:o:i:f:n:" flag; do
|
||||
case "${flag}" in
|
||||
t) TYPE="${OPTARG}" ;;
|
||||
i) INPUT="${OPTARG}" ;;
|
||||
o) OUTPUT="${OPTARG}" ;;
|
||||
n) NAME="${OPTARG}" ;;
|
||||
f) FORCE="${OPTARG}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -f "$SCRIPT_DIR/.env" ]]; then
|
||||
source "$SCRIPT_DIR/.env"
|
||||
fi
|
||||
|
||||
die() {
|
||||
echo "Error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
|
||||
}
|
||||
|
||||
[[ -n "$TYPE" ]] || die "Missing archive type (-t)"
|
||||
[[ -n "$INPUT" ]] || die "Missing input archive (-i)"
|
||||
[[ -n "$OUTPUT" ]] || die "Missing output directory (-o)"
|
||||
[[ -f "$INPUT" ]] || die "Archive file not found: $INPUT"
|
||||
|
||||
TYPE="${TYPE,,}"
|
||||
|
||||
mkdir -p "$OUTPUT"
|
||||
|
||||
case "$TYPE" in
|
||||
zip)
|
||||
if command -v unzip >/dev/null 2>&1; then
|
||||
unzip -o "$INPUT" -d "$OUTPUT"
|
||||
elif command -v 7z >/dev/null 2>&1; then
|
||||
7z x -y "-o$OUTPUT" "$INPUT"
|
||||
else
|
||||
die "ZIP extraction requires 'unzip' or '7z'"
|
||||
fi
|
||||
;;
|
||||
rar)
|
||||
if command -v unrar >/dev/null 2>&1; then
|
||||
unrar x -o+ "$INPUT" "$OUTPUT/"
|
||||
elif command -v 7z >/dev/null 2>&1; then
|
||||
7z x -y "-o$OUTPUT" "$INPUT"
|
||||
else
|
||||
die "RAR extraction requires 'unrar' or '7z'"
|
||||
fi
|
||||
;;
|
||||
xz)
|
||||
require_cmd tar
|
||||
tar -xJf "$INPUT" -C "$OUTPUT/"
|
||||
;;
|
||||
tar)
|
||||
require_cmd tar
|
||||
tar -xf "$INPUT" -C "$OUTPUT/"
|
||||
;;
|
||||
gz)
|
||||
require_cmd tar
|
||||
tar -xzf "$INPUT" -C "$OUTPUT/"
|
||||
;;
|
||||
*)
|
||||
die "Unsupported archive type: $TYPE"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Keep legacy behavior for archive layouts where the model is placed directly
|
||||
# in the extraction root and convert.sh is expected to be called from here.
|
||||
for filename in "$OUTPUT"*; do
|
||||
[[ -e "$filename" ]] || continue
|
||||
|
||||
fname=${filename##*/}
|
||||
name="${fname%.*}"
|
||||
ext=${filename##*.}
|
||||
ext="${ext,,}"
|
||||
|
||||
case "$ext" in
|
||||
abc|blend|dae|fbx|obj|ply|stl|wrl|x3d|ifc)
|
||||
"${SPATH}/scripts/convert.sh" -c 'true' -l '3' -b 'true' -i "${OUTPUT}${name}.${ext}" -o "${OUTPUT}" -f 'true' -a 'true'
|
||||
;;
|
||||
esac
|
||||
done
|
||||
47
scripts/worker.sh
Normal file
47
scripts/worker.sh
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
LOG="/opt/drupal/dfg3dworker.log"
|
||||
PID_FILE="/opt/drupal/dfg3dworker.pid"
|
||||
DRUPAL_SITE_URI="${DRUPAL_SITE_URI:-https://repository.covher.eu}"
|
||||
DRUSH_BIN="${DRUSH_BIN:-/opt/drupal/vendor/bin/drush}"
|
||||
|
||||
is_pid_running() {
|
||||
local pid="$1"
|
||||
if [[ -z "$pid" || ! "$pid" =~ ^[0-9]+$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -f "$PID_FILE"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
if [ -f "$LOG" ] && [ $(stat -c%s "$LOG") -gt 50000000 ]; then
|
||||
mv "$LOG" "${LOG}.old"
|
||||
fi
|
||||
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
EXISTING_PID="$(tr -d '[:space:]' < "$PID_FILE" || true)"
|
||||
if is_pid_running "$EXISTING_PID"; then
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') Worker already running with PID $EXISTING_PID, exiting" >> "$LOG"
|
||||
exit 0
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') Removed stale PID file" >> "$LOG"
|
||||
fi
|
||||
|
||||
echo "$$" > "$PID_FILE"
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') Worker started" >> "$LOG"
|
||||
|
||||
while true; do
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') Run started" >> "$LOG"
|
||||
"$DRUSH_BIN" --uri="$DRUPAL_SITE_URI" queue:run dfg_3dviewer_convert --time-limit=3600 2>&1 | awk '{ print strftime("%Y-%m-%d %H:%M:%S"), $0; fflush(); }' >> "$LOG"
|
||||
sleep 5
|
||||
done
|
||||
Loading…
Add table
Add a link
Reference in a new issue