2025-01-22 16:18:30 +01:00
from contextlib import contextmanager
import math
import multiprocessing
import os
import re
import subprocess
import time
import bpy
import lnx . assets as assets
import lnx . log as log
import lnx . utils
if lnx . is_reload ( __name__ ) :
import lnx
assets = lnx . reload_module ( assets )
log = lnx . reload_module ( log )
lnx . utils = lnx . reload_module ( lnx . utils )
else :
lnx . enable_reload ( __name__ )
# The format used for rendering the environment. Choose HDR or JPEG.
ENVMAP_FORMAT = ' JPEG '
ENVMAP_EXT = ' hdr ' if ENVMAP_FORMAT == ' HDR ' else ' jpg '
__cmft_start_time_seconds = 0.0
__cmft_end_time_seconds = 0.0
def add_irr_assets ( output_file_irr ) :
assets . add ( output_file_irr + ' .lnx ' )
def add_rad_assets ( output_file_rad , rad_format , num_mips ) :
assets . add ( output_file_rad + ' . ' + rad_format )
for i in range ( 0 , num_mips ) :
assets . add ( output_file_rad + ' _ ' + str ( i ) + ' . ' + rad_format )
@contextmanager
def setup_envmap_render ( ) :
""" Creates a background scene for rendering environment textures.
Use it as a context manager to automatically clean up on errors .
"""
rpdat = lnx . utils . get_rp ( )
radiance_size = int ( rpdat . lnx_radiance_size )
# Render worlds in a different scene so that there are no other
# objects. The actual scene might be called differently if the name
# is already taken
scene = bpy . data . scenes . new ( " _lnx_envmap_render " )
scene . render . engine = " CYCLES "
scene . render . image_settings . file_format = ENVMAP_FORMAT
if ENVMAP_FORMAT == ' HDR ' :
scene . render . image_settings . color_depth = ' 32 '
# Export in linear space and with default color management settings
if bpy . app . version < ( 4 , 1 , 0 ) :
scene . display_settings . display_device = " None "
else :
scene . display_settings . display_device = " sRGB "
scene . view_settings . view_transform = " Standard "
scene . view_settings . look = " None "
scene . view_settings . exposure = 0
scene . view_settings . gamma = 1
scene . render . image_settings . quality = 100
scene . render . resolution_x = radiance_size
scene . render . resolution_y = radiance_size / / 2
# Set GPU as rendering device if the user enabled it
if bpy . context . preferences . addons [ " cycles " ] . preferences . compute_device_type != " NONE " :
scene . cycles . device = " GPU "
else :
log . info ( ' Using CPU for environment render (might be slow). If possible, configure GPU rendering in Blender preferences (System > Cycles Render Devices). ' )
# Those settings are sufficient for rendering only the world background
scene . cycles . samples = 1
scene . cycles . max_bounces = 1
scene . cycles . diffuse_bounces = 1
scene . cycles . glossy_bounces = 1
scene . cycles . transmission_bounces = 1
scene . cycles . volume_bounces = 1
scene . cycles . transparent_max_bounces = 1
scene . cycles . caustics_reflective = False
scene . cycles . caustics_refractive = False
scene . cycles . use_denoising = False
# Setup scene
cam = bpy . data . cameras . new ( " _lnx_cam_envmap_render " )
cam_obj = bpy . data . objects . new ( " _lnx_cam_envmap_render " , cam )
scene . collection . objects . link ( cam_obj )
scene . camera = cam_obj
cam_obj . location = [ 0.0 , 0.0 , 0.0 ]
cam . type = " PANO "
if bpy . app . version < ( 4 , 1 , 0 ) :
cam . cycles . panorama_type = " EQUIRECTANGULAR "
else :
cam . panorama_type = " EQUIRECTANGULAR "
cam_obj . rotation_euler = [ math . radians ( 90 ) , 0 , math . radians ( - 90 ) ]
try :
yield
finally :
bpy . data . objects . remove ( cam_obj )
bpy . data . cameras . remove ( cam )
bpy . data . scenes . remove ( scene )
def render_envmap ( target_dir : str , world : bpy . types . World ) - > str :
""" Render an environment texture for the given world into the
target_dir and return the filename of the rendered image . Use in
combination with setup_envmap_render ( ) .
"""
scene = bpy . data . scenes [ ' _lnx_envmap_render ' ]
scene . world = world
2025-09-19 18:54:44 +00:00
world_name = lnx . utils . asset_name ( world ) if world . library else world . name
image_name = f ' env_ { lnx . utils . safesrc ( world_name ) } . { ENVMAP_EXT } '
2025-01-22 16:18:30 +01:00
render_path = os . path . join ( target_dir , image_name )
scene . render . filepath = render_path
bpy . ops . render . render ( write_still = True , scene = scene . name )
return image_name
def write_probes ( image_filepath : str , disable_hdr : bool , from_srgb : bool , cached_num_mips : int , lnx_radiance = True ) - > int :
""" Generate probes from environment map and return the mipmap count. """
envpath = lnx . utils . get_fp_build ( ) + ' /compiled/Assets/envmaps '
if not os . path . exists ( envpath ) :
os . makedirs ( envpath )
base_name = lnx . utils . extract_filename ( image_filepath ) . rsplit ( ' . ' , 1 ) [ 0 ]
# Assets to be generated
output_file_irr = envpath + ' / ' + base_name + ' _irradiance '
output_file_rad = envpath + ' / ' + base_name + ' _radiance '
rad_format = ' jpg ' if disable_hdr else ' hdr '
# Radiance & irradiance exists, keep cache
basep = envpath + ' / ' + base_name
if os . path . exists ( basep + ' _irradiance.lnx ' ) :
if not lnx_radiance or os . path . exists ( basep + ' _radiance_0. ' + rad_format ) :
add_irr_assets ( output_file_irr )
if lnx_radiance :
add_rad_assets ( output_file_rad , rad_format , cached_num_mips )
return cached_num_mips
# Get paths
sdk_path = lnx . utils . get_sdk_path ( )
kha_path = lnx . utils . get_kha_path ( )
if lnx . utils . get_os ( ) == ' win ' :
cmft_path = sdk_path + ' /lib/leenkx_tools/cmft/cmft.exe '
kraffiti_path = kha_path + ' /Kinc/Tools/windows_x64/kraffiti.exe '
elif lnx . utils . get_os ( ) == ' mac ' :
cmft_path = ' " ' + sdk_path + ' /lib/leenkx_tools/cmft/cmft-osx " '
kraffiti_path = ' " ' + kha_path + ' /Kinc/Tools/macos/kraffiti " '
else :
cmft_path = ' " ' + sdk_path + ' /lib/leenkx_tools/cmft/cmft-linux64 " '
kraffiti_path = ' " ' + kha_path + ' /Kinc/Tools/linux_x64/kraffiti " '
input_file = lnx . utils . asset_path ( image_filepath )
# Scale map, ensure 2:1 ratio (required by cmft)
rpdat = lnx . utils . get_rp ( )
target_w = int ( rpdat . lnx_radiance_size )
target_h = int ( target_w / 2 )
scaled_file = output_file_rad + ' . ' + rad_format
if lnx . utils . get_os ( ) == ' win ' :
subprocess . check_output ( [
kraffiti_path ,
' from= ' + input_file ,
' to= ' + scaled_file ,
' format= ' + rad_format ,
' width= ' + str ( target_w ) ,
' height= ' + str ( target_h ) ] )
else :
subprocess . check_output ( [
kraffiti_path
+ ' from= " ' + input_file + ' " '
+ ' to= " ' + scaled_file + ' " '
+ ' format= ' + rad_format
+ ' width= ' + str ( target_w )
+ ' height= ' + str ( target_h ) ] , shell = True )
# Convert sRGB colors into linear color space first (approximately)
input_gamma_numerator = ' 2.2 ' if from_srgb else ' 1.0 '
# Irradiance spherical harmonics
if lnx . utils . get_os ( ) == ' win ' :
subprocess . call ( [
cmft_path ,
' --input ' , scaled_file ,
' --filter ' , ' shcoeffs ' ,
' --outputNum ' , ' 1 ' ,
' --output0 ' , output_file_irr ,
' --inputGammaNumerator ' , input_gamma_numerator ,
' --inputGammaDenominator ' , ' 1.0 ' ,
' --outputGammaNumerator ' , ' 1.0 ' ,
' --outputGammaDenominator ' , ' 1.0 '
] )
else :
subprocess . call ( [
cmft_path
+ ' --input ' + ' " ' + scaled_file + ' " '
+ ' --filter shcoeffs '
+ ' --outputNum 1 '
+ ' --output0 ' + ' " ' + output_file_irr + ' " '
+ ' --inputGammaNumerator ' + input_gamma_numerator
+ ' --inputGammaDenominator ' + ' 1.0 '
+ ' --outputGammaNumerator ' + ' 1.0 '
+ ' --outputGammaDenominator ' + ' 1.0 '
] , shell = True )
sh_to_json ( output_file_irr )
add_irr_assets ( output_file_irr )
# Mip-mapped radiance
if not lnx_radiance :
return cached_num_mips
# 4096 = 256 face
# 2048 = 128 face
# 1024 = 64 face
face_size = target_w / 8
if target_w == 2048 :
mip_count = 9
elif target_w == 1024 :
mip_count = 8
else :
mip_count = 7
wrd = bpy . data . worlds [ ' Lnx ' ]
use_opencl = ' true ' if lnx . utils . get_pref_or_default ( " cmft_use_opencl " , True ) else ' false '
# cmft doesn't work correctly when passing the number of logical
# CPUs if there are more logical than physical CPUs on a machine
cpu_count = lnx . utils . cpu_count ( physical_only = True )
# CMFT might hang with OpenCl enabled, output warning in that case.
# See https://github.com/leenkx3d/leenkx/issues/2760 for details.
global __cmft_start_time_seconds , __cmft_end_time_seconds
__cmft_start_time_seconds = time . time ( )
try :
if lnx . utils . get_os ( ) == ' win ' :
cmd = [
cmft_path ,
' --input ' , scaled_file ,
' --filter ' , ' radiance ' ,
' --dstFaceSize ' , str ( face_size ) ,
' --srcFaceSize ' , str ( face_size ) ,
' --excludeBase ' , ' false ' ,
# '--mipCount', str(mip_count),
' --glossScale ' , ' 8 ' ,
' --glossBias ' , ' 3 ' ,
' --lightingModel ' , ' blinnbrdf ' ,
' --edgeFixup ' , ' none ' ,
' --numCpuProcessingThreads ' , str ( cpu_count ) ,
' --useOpenCL ' , use_opencl ,
' --clVendor ' , ' anyGpuVendor ' ,
' --deviceType ' , ' gpu ' ,
' --deviceIndex ' , ' 0 ' ,
' --generateMipChain ' , ' true ' ,
' --inputGammaNumerator ' , input_gamma_numerator ,
' --inputGammaDenominator ' , ' 1.0 ' ,
' --outputGammaNumerator ' , ' 1.0 ' ,
' --outputGammaDenominator ' , ' 1.0 ' ,
' --outputNum ' , ' 1 ' ,
' --output0 ' , output_file_rad ,
' --output0params ' , ' hdr,rgbe,latlong '
]
if not wrd . lnx_verbose_output :
cmd . append ( ' --silent ' )
print ( cmd )
subprocess . call ( cmd )
else :
cmd = cmft_path + \
' --input " ' + scaled_file + ' " ' + \
' --filter radiance ' + \
' --dstFaceSize ' + str ( face_size ) + \
' --srcFaceSize ' + str ( face_size ) + \
' --excludeBase false ' + \
' --glossScale 8 ' + \
' --glossBias 3 ' + \
' --lightingModel blinnbrdf ' + \
' --edgeFixup none ' + \
' --numCpuProcessingThreads ' + str ( cpu_count ) + \
' --useOpenCL ' + use_opencl + \
' --clVendor anyGpuVendor ' + \
' --deviceType gpu ' + \
' --deviceIndex 0 ' + \
' --generateMipChain true ' + \
' --inputGammaNumerator ' + input_gamma_numerator + \
' --inputGammaDenominator 1.0 ' + \
' --outputGammaNumerator 1.0 ' + \
' --outputGammaDenominator 1.0 ' + \
' --outputNum 1 ' + \
' --output0 " ' + output_file_rad + ' " ' + \
' --output0params hdr,rgbe,latlong '
if not wrd . lnx_verbose_output :
cmd + = ' --silent '
print ( cmd )
subprocess . call ( [ cmd ] , shell = True )
except KeyboardInterrupt as e :
__cmft_end_time_seconds = time . time ( )
check_last_cmft_time ( )
raise e
__cmft_end_time_seconds = time . time ( )
# Remove size extensions in file name
mip_w = int ( face_size * 4 )
mip_base = output_file_rad + ' _ '
mip_num = 0
while mip_w > = 4 :
mip_name = mip_base + str ( mip_num )
os . rename (
mip_name + ' _ ' + str ( mip_w ) + ' x ' + str ( int ( mip_w / 2 ) ) + ' .hdr ' ,
mip_name + ' .hdr ' )
mip_w = int ( mip_w / 2 )
mip_num + = 1
# Append mips
generated_files = [ ]
for i in range ( 0 , mip_count ) :
generated_files . append ( output_file_rad + ' _ ' + str ( i ) )
# Convert to jpgs
if disable_hdr is True :
for f in generated_files :
if lnx . utils . get_os ( ) == ' win ' :
subprocess . call ( [
kraffiti_path ,
' from= ' + f + ' .hdr ' ,
' to= ' + f + ' .jpg ' ,
' format=jpg ' ] )
else :
subprocess . call ( [
kraffiti_path
+ ' from= " ' + f + ' .hdr " '
+ ' to= " ' + f + ' .jpg " '
+ ' format=jpg ' ] , shell = True )
os . remove ( f + ' .hdr ' )
# Scale from (4x2 to 1x1>
for i in range ( 0 , 2 ) :
last = generated_files [ - 1 ]
out = output_file_rad + ' _ ' + str ( mip_count + i )
if lnx . utils . get_os ( ) == ' win ' :
subprocess . call ( [
kraffiti_path ,
' from= ' + last + ' . ' + rad_format ,
' to= ' + out + ' . ' + rad_format ,
' scale=0.5 ' ,
' format= ' + rad_format ] , shell = True )
else :
subprocess . call ( [
kraffiti_path
+ ' from= ' + ' " ' + last + ' . ' + rad_format + ' " '
+ ' to= ' + ' " ' + out + ' . ' + rad_format + ' " '
+ ' scale=0.5 '
+ ' format= ' + rad_format ] , shell = True )
generated_files . append ( out )
mip_count + = 2
add_rad_assets ( output_file_rad , rad_format , mip_count )
return mip_count
def sh_to_json ( sh_file ) :
""" Parse sh coefs produced by cmft into json array """
with open ( sh_file + ' .c ' ) as f :
sh_lines = f . read ( ) . splitlines ( )
band0_line = sh_lines [ 5 ]
band1_line = sh_lines [ 6 ]
band2_line = sh_lines [ 7 ]
irradiance_floats = [ ]
parse_band_floats ( irradiance_floats , band0_line )
parse_band_floats ( irradiance_floats , band1_line )
parse_band_floats ( irradiance_floats , band2_line )
# Lower exposure to adjust to Eevee and Cycles
for i in range ( 0 , len ( irradiance_floats ) ) :
irradiance_floats [ i ] / = 2
sh_json = { ' irradiance ' : irradiance_floats }
ext = ' .lnx ' if bpy . data . worlds [ ' Lnx ' ] . lnx_minimize else ' '
lnx . utils . write_lnx ( sh_file + ext , sh_json )
# Clean up .c
os . remove ( sh_file + ' .c ' )
def parse_band_floats ( irradiance_floats , band_line ) :
string_floats = re . findall ( r ' [-+]? \ d* \ . \ d+| \ d+ ' , band_line )
string_floats = string_floats [ 1 : ] # Remove 'Band 0/1/2' number
for s in string_floats :
irradiance_floats . append ( float ( s ) )
def write_sky_irradiance ( base_name ) :
# Hosek spherical harmonics
irradiance_floats = [
1.5519331988822218 , 2.3352207154503266 , 2.997277451988076 ,
0.2673894962434794 , 0.4305630474135794 , 0.11331825259716752 ,
- 0.04453633521758638 , - 0.038753175134160295 , - 0.021302768541875794 ,
0.00055858020486499 , 0.000371654770334503 , 0.000126606145406403 ,
- 0.000135708721978705 , - 0.000787399554583089 , - 0.001550090690860059 ,
0.021947399048903773 , 0.05453650591711572 , 0.08783641266630278 ,
0.17053593578630663 , 0.14734127083304463 , 0.07775404698816404 ,
- 2.6924363189795e-05 , - 7.9350169701934e-05 , - 7.559914435231e-05 ,
0.27035455385870993 , 0.23122918445556914 , 0.12158817295211832 ]
for i in range ( 0 , len ( irradiance_floats ) ) :
irradiance_floats [ i ] / = 2
envpath = os . path . join ( lnx . utils . get_fp_build ( ) , ' compiled ' , ' Assets ' , ' envmaps ' )
if not os . path . exists ( envpath ) :
os . makedirs ( envpath )
output_file = os . path . join ( envpath , base_name + ' _irradiance ' )
sh_json = { ' irradiance ' : irradiance_floats }
lnx . utils . write_lnx ( output_file + ' .lnx ' , sh_json )
assets . add ( output_file + ' .lnx ' )
def write_color_irradiance ( base_name , col ) :
""" Constant color irradiance """
# Adjust to Cycles
irradiance_floats = [ col [ 0 ] * 1.13 , col [ 1 ] * 1.13 , col [ 2 ] * 1.13 ]
for i in range ( 0 , 24 ) :
irradiance_floats . append ( 0.0 )
envpath = os . path . join ( lnx . utils . get_fp_build ( ) , ' compiled ' , ' Assets ' , ' envmaps ' )
if not os . path . exists ( envpath ) :
os . makedirs ( envpath )
output_file = os . path . join ( envpath , base_name + ' _irradiance ' )
sh_json = { ' irradiance ' : irradiance_floats }
lnx . utils . write_lnx ( output_file + ' .lnx ' , sh_json )
assets . add ( output_file + ' .lnx ' )
def check_last_cmft_time ( ) :
global __cmft_start_time_seconds , __cmft_end_time_seconds
if __cmft_start_time_seconds < = 0.0 :
# CMFT was not called
return
if __cmft_end_time_seconds < = 0.0 :
# Build was aborted and CMFT didn't finish
__cmft_end_time_seconds = time . time ( )
cmft_duration_seconds = __cmft_end_time_seconds - __cmft_start_time_seconds
# We could also check here if the user already disabled OpenCL and
# then don't show the warning, but this might trick users into
# thinking they have fixed their issue even in the case that the
# slow runtime isn't caused by using OpenCL
if cmft_duration_seconds > 20 :
log . warn (
" Generating the radiance map with CMFT took an unusual amount "
f " of time ( { cmft_duration_seconds : .2f } s). If the issue persists, "
" try disabling \" CMFT: Use OpenCL \" in the Leenkx add-on preferences. "
" For more information see https://github.com/leenkx3d/leenkx/issues/2760. "
)
__cmft_start_time_seconds = 0.0
__cmft_end_time_seconds = 0.0