Initial commit
This commit is contained in:
commit
a437c068c8
64 changed files with 561683 additions and 0 deletions
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)")
|
||||
# --------------------------------------------------
|
||||
Loading…
Add table
Add a link
Reference in a new issue