Files
LNXSDK/leenkx/blender/lnx/material/cycles_nodes/nodes_texture.py

829 lines
35 KiB
Python

from __future__ import annotations
import math
import os
from typing import Union
import bpy
import lnx.assets as assets
import lnx.log as log
import lnx.material.cycles as c
import lnx.material.cycles_functions as c_functions
from lnx.material.parser_state import ParserState, ParserContext, ParserPass
from lnx.material.shader import floatstr, vec3str
import lnx.utils
import lnx.write_probes as write_probes
if lnx.is_reload(__name__):
assets = lnx.reload_module(assets)
log = lnx.reload_module(log)
c = lnx.reload_module(c)
c_functions = lnx.reload_module(c_functions)
lnx.material.parser_state = lnx.reload_module(lnx.material.parser_state)
from lnx.material.parser_state import ParserState, ParserContext, ParserPass
lnx.material.shader = lnx.reload_module(lnx.material.shader)
from lnx.material.shader import floatstr, vec3str
lnx.utils = lnx.reload_module(lnx.utils)
write_probes = lnx.reload_module(write_probes)
else:
lnx.enable_reload(__name__)
def parse_tex_brick(node: bpy.types.ShaderNodeTexBrick, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
state.curshader.add_function(c_functions.str_tex_brick_blender)
if node.inputs['Vector'].is_linked:
co = c.get_vector_input(node, ['Vector'])
else:
co = 'bposition'
offset_amount = node.offset
offset_frequency = node.offset_frequency
squash_amount = node.squash
squash_frequency = node.squash_frequency
col1 = c.get_vector_input(node, ['Color1'])
col2 = c.get_vector_input(node, ['Color2'])
mortar = c.get_vector_input(node, ['Mortar'])
scale = c.get_value_input(node, ['Scale'])
mortar_size = c.get_value_input(node, ['Mortar Size'])
mortar_smooth = c.get_value_input(node, ['Mortar Smooth'])
bias = c.get_value_input(node, ['Bias'])
brick_width = c.get_value_input(node, ['Brick Width'])
row_height = c.get_value_input(node, ['Row Height'])
#res = f'tex_brick({co} * {scale}, {col1}, {col2}, {mortar})'
# Color
if out_socket == node.outputs['Color']:
res = f'tex_brick_blender({co}, {col1}, {col2}, {mortar}, {scale}, {mortar_size}, {mortar_smooth}, {bias}, {brick_width}, {row_height}, {offset_amount}, {offset_frequency}, {squash_amount}, {squash_frequency})'
# Fac
else:
res = f'tex_brick_blender_f({co}, {col1}, {col2}, {mortar}, {scale}, {mortar_size}, {mortar_smooth}, {bias}, {brick_width}, {row_height}, {offset_amount}, {offset_frequency}, {squash_amount}, {squash_frequency})'
return res
def parse_tex_checker(node: bpy.types.ShaderNodeTexChecker, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
state.curshader.add_function(c_functions.str_tex_checker)
if node.inputs['Vector'].is_linked:
co = c.get_vector_input(node, ['Vector'])
else:
co = 'bposition'
scale = c.get_value_input(node, ['Scale'])
# Color
if out_socket == node.outputs['Color']:
col1 = c.get_vector_input(node, ['Color1'])
col2 = c.get_vector_input(node, ['Color2'])
res = f'tex_checker({co}, {col1}, {col2}, {scale})'
# Fac
else:
res = 'tex_checker_f({0}, {1})'.format(co, scale)
return res
def parse_tex_gradient(node: bpy.types.ShaderNodeTexGradient, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
if node.inputs['Vector'].is_linked:
co = c.get_vector_input(node, ['Vector'])
else:
co = 'bposition'
grad = node.gradient_type
if grad == 'LINEAR':
f = f'{co}.x'
elif grad == 'QUADRATIC':
f = f'max({co}.x, 0.0)'
f = f'({f} * {f})'
elif grad == 'EASING':
f = f'clamp({co}.x, 0.0, 1.0)'
f = f'({f} * {f} * (3.0 - 2.0 * {f}))'
elif grad == 'DIAGONAL':
f = f'({co}.x + {co}.y) * 0.5'
elif grad == 'RADIAL':
f = f'atan({co}.y, {co}.x) / PI2 + 0.5'
elif grad == 'QUADRATIC_SPHERE':
f = f'max(1.0 - sqrt({co}.x * {co}.x + {co}.y * {co}.y + {co}.z * {co}.z), 0.0)'
f = f'({f} * {f})'
else: # SPHERICAL
f = f'max(1.0 - sqrt({co}.x * {co}.x + {co}.y * {co}.y + {co}.z * {co}.z), 0.0)'
# Color
if out_socket == node.outputs['Color']:
res = f'vec3(clamp({f}, 0.0, 1.0))'
# Fac
else:
res = f'(clamp({f}, 0.0, 1.0))'
return res
def parse_tex_image(node: bpy.types.ShaderNodeTexImage, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
# Color or Alpha output
use_color_out = out_socket == node.outputs['Color']
if state.context == ParserContext.OBJECT:
tex_store = c.store_var_name(node)
if c.node_need_reevaluation_for_screenspace_derivative(node):
tex_store += state.get_parser_pass_suffix()
# Already fetched
if c.is_parsed(tex_store):
if use_color_out:
return f'{tex_store}.rgb'
else:
return f'{tex_store}.a'
tex_name = c.node_name(node.name)
tex = c.make_texture_from_image_node(node, tex_name)
tex_link = None
tex_default_file = None
is_lnx_mat_param = None
if node.lnx_material_param:
tex_link = node.name
is_lnx_mat_param = True
if tex is not None:
state.curshader.write_textures += 1
if node.lnx_material_param and tex['file'] is not None:
tex_default_file = tex['file']
unpremultiply = node.image is not None and node.image.alpha_mode != 'CHANNEL_PACKED'
if use_color_out:
to_linear = node.image is not None and node.image.colorspace_settings.name == 'sRGB'
res = f'{c.texture_store(node, tex, tex_name, to_linear, unpremultiply, tex_link=tex_link, default_value=tex_default_file, is_lnx_mat_param=is_lnx_mat_param)}.rgb'
else:
res = f'{c.texture_store(node, tex, tex_name, unpremultiply, tex_link=tex_link, default_value=tex_default_file, is_lnx_mat_param=is_lnx_mat_param)}.a'
state.curshader.write_textures -= 1
return res
# Empty texture
elif node.image is None:
tex = {
'name': tex_name,
'file': ''
}
if use_color_out:
return '{0}.rgb'.format(c.texture_store(node, tex, tex_name, to_linear=False, unpremultiply=False, tex_link=tex_link, is_lnx_mat_param=is_lnx_mat_param))
return '{0}.a'.format(c.texture_store(node, tex, tex_name, to_linear=True, unpremultiply=False, tex_link=tex_link, is_lnx_mat_param=is_lnx_mat_param))
# Pink color for missing texture
else:
if use_color_out:
state.parsed.add(tex_store)
state.curshader.write_textures += 1
state.curshader.write(f'vec4 {tex_store} = vec4(1.0, 0.0, 1.0, 1.0);')
state.curshader.write_textures -= 1
return f'{tex_store}.rgb'
else:
state.curshader.write(f'vec4 {tex_store} = vec4(1.0, 0.0, 1.0, 1.0);')
return f'{tex_store}.a'
# World context
# TODO: Merge with above implementation to also allow mappings other than using view coordinates
else:
world = state.world
world.world_defs += '_EnvImg'
# Background texture
state.curshader.add_uniform('sampler2D envmap', link='_envmap')
state.curshader.add_uniform('vec2 screenSize', link='_screenSize')
image = node.image
if image is None:
log.warn(f'World "{world.name}": image texture node "{node.name}" is empty')
return 'vec3(0.0, 0.0, 0.0)' if use_color_out else '0.0'
filepath = image.filepath
if image.packed_file is not None:
# Extract packed data
filepath = lnx.utils.build_dir() + '/compiled/Assets/unpacked'
unpack_path = lnx.utils.get_fp() + filepath
if not os.path.exists(unpack_path):
os.makedirs(unpack_path)
unpack_filepath = unpack_path + '/' + image.name
if not os.path.isfile(unpack_filepath) or os.path.getsize(unpack_filepath) != image.packed_file.size:
with open(unpack_filepath, 'wb') as f:
f.write(image.packed_file.data)
assets.add(unpack_filepath)
else:
# Link image path to assets
assets.add(lnx.utils.asset_path(image.filepath))
# Reference image name
tex_file = lnx.utils.extract_filename(image.filepath)
base = tex_file.rsplit('.', 1)
ext = base[1].lower()
if ext == 'hdr':
target_format = 'HDR'
else:
target_format = 'JPEG'
# Generate prefiltered envmaps
world.lnx_envtex_name = tex_file
world.lnx_envtex_irr_name = tex_file.rsplit('.', 1)[0]
disable_hdr = target_format == 'JPEG'
from_srgb = image.colorspace_settings.name == "sRGB"
rpdat = lnx.utils.get_rp()
mip_count = world.lnx_envtex_num_mips
mip_count = write_probes.write_probes(filepath, disable_hdr, from_srgb, mip_count, lnx_radiance=rpdat.lnx_radiance)
world.lnx_envtex_num_mips = mip_count
# Will have to get rid of gl_FragCoord, pass texture coords from vertex shader
state.curshader.write_init('vec2 texco = gl_FragCoord.xy / screenSize;')
return 'texture(envmap, vec2(texco.x, 1.0 - texco.y)).rgb * envmapStrength'
def parse_tex_magic(node: bpy.types.ShaderNodeTexMagic, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
state.curshader.add_function(c_functions.str_tex_magic)
if node.inputs['Vector'].is_linked:
co = c.get_vector_input(node, ['Vector'])
else:
co = 'bposition'
scale = c.get_value_input(node, ['Scale'])
distortion = c.get_value_input(node, ['Distortion'])
depth = node.turbulence_depth
if out_socket == node.outputs['Color']:
res = f'tex_magic({co} * {scale}, {distortion}, {depth})'
else:
res = f'tex_magic_f({co} * {scale}, {distortion}, {depth})'
return res
if bpy.app.version < (4, 1, 0):
def parse_tex_musgrave(node: bpy.types.ShaderNodeTexMusgrave, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
state.curshader.add_function(c_functions.str_tex_musgrave)
if node.inputs['Vector'].is_linked:
co = c.get_vector_input(node, ['Vector'])
else:
co = 'bposition'
scale = c.get_value_input(node, ['Scale'])
detail = c.get_value_input(node, ['Detail'])
dimension = c.get_value_input(node, ['Dimension'])
res = f'tex_musgrave_f({co} * {scale} * 0.5, {detail}, {dimension})' # FIXME: a `distortion` is applied instead of a `dimension`
return res
def parse_tex_noise(node: bpy.types.ShaderNodeTexNoise, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
c.write_procedurals()
state.curshader.add_function(c_functions.str_tex_noise)
if 'Vector' in node.inputs and node.inputs['Vector'].is_linked:
co = c.parse_vector_input(node.inputs['Vector'])
elif node.inputs[0].is_linked:
co = c.parse_vector_input(node.inputs[0])
else:
co = 'bposition'
w = c.parse_value_input(node.inputs['W']) if 'W' in node.inputs else '0.0'
scale = c.parse_value_input(node.inputs['Scale']) if 'Scale' in node.inputs else '1.0'
detail = c.parse_value_input(node.inputs['Detail']) if 'Detail' in node.inputs else '2.0'
roughness = c.parse_value_input(node.inputs['Roughness']) if 'Roughness' in node.inputs else '0.5'
lacunarity = c.parse_value_input(node.inputs['Lacunarity']) if 'Lacunarity' in node.inputs else '2.0'
offset = c.parse_value_input(node.inputs['Offset']) if 'Offset' in node.inputs else '0.0'
gain = c.parse_value_input(node.inputs['Gain']) if 'Gain' in node.inputs else '1.0'
distortion = c.parse_value_input(node.inputs['Distortion']) if 'Distortion' in node.inputs else '0.0'
dimensions = getattr(node, 'noise_dimensions', '3D')
noise_type = getattr(node, 'noise_type', 'FBM')
normalize = 'true' if getattr(node, 'normalize', True) else 'false'
type_map = {
'FBM': 'noise_fbm',
'MULTIFRACTAL': 'noise_multi_fractal',
'RIDGED_MULTIFRACTAL': 'noise_ridged_multi_fractal',
'HYBRID_MULTIFRACTAL': 'noise_hybrid_multi_fractal',
'HETERO_TERRAIN': 'noise_hetero_terrain'
}
func_name = type_map.get(noise_type, 'noise_fbm')
is_color = (out_socket == node.outputs[1]) or (getattr(out_socket, 'name', '') == 'Color')
if dimensions == '1D':
p_expr = f"({w}) * ({scale})"
dist_expr = f"({p_expr}) + snoise(({p_expr}) + random_float_offset(0.0)) * ({distortion})" if distortion != '0.0' else p_expr
if is_color:
res = f"vec3({func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_float_offset(1.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_float_offset(2.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}))"
else:
res = f"{func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize})"
elif dimensions == '2D':
p_expr = f"({co}).xy * ({scale})"
dist_expr = f"({p_expr}) + vec2(snoise(({p_expr}) + random_vec2_offset(0.0)) * ({distortion}), snoise(({p_expr}) + random_vec2_offset(1.0)) * ({distortion}))" if distortion != '0.0' else p_expr
if is_color:
res = f"vec3({func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_vec2_offset(2.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_vec2_offset(3.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}))"
else:
res = f"{func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize})"
elif dimensions == '4D':
p_expr = f"vec4({co}, {w}) * ({scale})"
dist_expr = f"({p_expr}) + vec4(snoise(({p_expr}) + random_vec4_offset(0.0)) * ({distortion}), snoise(({p_expr}) + random_vec4_offset(1.0)) * ({distortion}), snoise(({p_expr}) + random_vec4_offset(2.0)) * ({distortion}), snoise(({p_expr}) + random_vec4_offset(3.0)) * ({distortion}))" if distortion != '0.0' else p_expr
if is_color:
res = f"vec3({func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_vec4_offset(4.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_vec4_offset(5.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}))"
else:
res = f"{func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize})"
else:
p_expr = f"({co}) * ({scale})"
dist_expr = f"({p_expr}) + vec3(snoise(({p_expr}) + random_vec3_offset(0.0)) * ({distortion}), snoise(({p_expr}) + random_vec3_offset(1.0)) * ({distortion}), snoise(({p_expr}) + random_vec3_offset(2.0)) * ({distortion}))" if distortion != '0.0' else p_expr
if is_color:
res = f"vec3({func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_vec3_offset(3.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}), {func_name}(({dist_expr}) + random_vec3_offset(4.0), clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize}))"
else:
res = f"{func_name}({dist_expr}, clamp({detail}, 0.0, 15.0), max({roughness}, 0.0), {lacunarity}, {offset}, {gain}, {normalize})"
return res
if bpy.app.version < (5, 0, 0):
def parse_tex_pointdensity(node: bpy.types.ShaderNodeTexPointDensity, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
# Pass through
# Color
if out_socket == node.outputs['Color']:
return c.to_vec3([0.0, 0.0, 0.0])
# Density
else:
return '0.0'
def parse_tex_sky(node: bpy.types.ShaderNodeTexSky, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str:
if state.context == ParserContext.OBJECT:
# Pass through
return c.to_vec3([0.0, 0.0, 0.0])
state.world.world_defs += '_EnvSky'
if node.sky_type == 'PREETHAM' or node.sky_type == 'HOSEK_WILKIE':
return parse_sky_hosekwilkie(node, state)
elif node.sky_type == 'NISHITA' or node.sky_type == 'SINGLE_SCATTERING':
return parse_sky_single_scattering(node, state)
elif node.sky_type == 'MULTIPLE_SCATTERING':
return parse_sky_multiple_scattering(node, state)
else:
log.error(f'Unsupported sky model: {node.sky_type}!')
return c.to_vec3([0.0, 0.0, 0.0])
def parse_sky_hosekwilkie(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> vec3str:
world = state.world
curshader = state.curshader
assets.add_khafile_def('lnx_hosek')
curshader.add_uniform('vec3 A', link="_hosekA")
curshader.add_uniform('vec3 B', link="_hosekB")
curshader.add_uniform('vec3 C', link="_hosekC")
curshader.add_uniform('vec3 D', link="_hosekD")
curshader.add_uniform('vec3 E', link="_hosekE")
curshader.add_uniform('vec3 F', link="_hosekF")
curshader.add_uniform('vec3 G', link="_hosekG")
curshader.add_uniform('vec3 H', link="_hosekH")
curshader.add_uniform('vec3 I', link="_hosekI")
curshader.add_uniform('vec3 Z', link="_hosekZ")
curshader.add_uniform('vec3 hosekSunDirection', link="_hosekSunDirection")
curshader.add_function("""vec3 hosekWilkie(float cos_theta, float gamma, float cos_gamma) {
\tvec3 chi = (1 + cos_gamma * cos_gamma) / pow(1 + H * H - 2 * cos_gamma * H, vec3(1.5));
\treturn (1 + A * exp(B / (cos_theta + 0.01))) * (C + D * exp(E * gamma) + F * (cos_gamma * cos_gamma) + G * chi + I * sqrt(cos_theta));
}""")
world.lnx_envtex_sun_direction = [node.sun_direction[0], node.sun_direction[1], node.sun_direction[2]]
world.lnx_envtex_turbidity = node.turbidity
world.lnx_envtex_ground_albedo = node.ground_albedo
wrd = bpy.data.worlds['Lnx']
rpdat = lnx.utils.get_rp()
mobile_mat = rpdat.lnx_material_model == 'Mobile' or rpdat.lnx_material_model == 'Solid'
if not state.radiance_written:
# Irradiance json file name
wname = lnx.utils.safestr(world.name)
world.lnx_envtex_irr_name = wname
write_probes.write_sky_irradiance(wname)
# Radiance
if rpdat.lnx_radiance and rpdat.lnx_irradiance and not mobile_mat:
if '_Rad' not in wrd.world_defs:
wrd.world_defs += '_Rad'
assets.add_khafile_def("lnx_radiance")
hosek_path = 'leenkx/Assets/hosek/'
sdk_path = lnx.utils.get_sdk_path()
# Use fake maps for now
assets.add(sdk_path + '/' + hosek_path + 'hosek_radiance.hdr')
for i in range(0, 8):
assets.add(sdk_path + '/' + hosek_path + 'hosek_radiance_' + str(i) + '.hdr')
world.lnx_envtex_name = 'hosek'
world.lnx_envtex_num_mips = 8
state.radiance_written = True
curshader.write('float cos_theta = clamp(pos.z, 0.0, 1.0);')
curshader.write('float cos_gamma = dot(pos, hosekSunDirection);')
curshader.write('float gamma_val = acos(cos_gamma);')
return 'Z * hosekWilkie(cos_theta, gamma_val, cos_gamma) * envmapStrength;'
def parse_sky_single_scattering(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> vec3str:
curshader = state.curshader
curshader.add_include('std/sky.glsl')
curshader.add_uniform('vec3 sunDir', link='_sunDirection')
curshader.add_uniform('sampler2D singleScatterLUT', link='_singleScatterLUT', included=True,
tex_addr_u='clamp', tex_addr_v='clamp')
curshader.add_uniform('vec2 skyDensity', link='_skyDensity', included=True)
planet_radius = 6360e3 # Earth radius used in Blender
ray_origin_z = planet_radius + node.altitude
dust_density = node.aerosol_density if bpy.app.version >= (5, 0, 0) else node.dust_density
state.world.lnx_sky_density = [node.air_density, dust_density, node.ozone_density]
state.world.lnx_envtex_sun_direction = [node.sun_direction[0], node.sun_direction[1], node.sun_direction[2]]
sun = ''
if node.sun_disc:
# The sun size is calculated relative in terms of the distance
# between the sun position and the sky dome normal at every
# pixel (see sun_disk() in sky.glsl).
#
# An isosceles triangle is created with the camera at the
# opposite side of the base with node.sun_size being the vertex
# angle from which the base angle theta is calculated. Iron's
# skydome geometry roughly resembles a unit sphere, so the leg
# size is set to 1. The base size is the doubled normal-relative
# target size.
# sun_size is already in radians despite being degrees in the UI
theta = 0.5 * (math.pi - node.sun_size)
size = math.cos(theta)
sun = f'* sun_disk(pos, sunDir, {size}, {node.sun_intensity})'
return f'single_scatter_atmosphere(pos, vec3(0, 0, {ray_origin_z}), sunDir, {planet_radius}){sun}'
def parse_sky_multiple_scattering(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> vec3str:
curshader = state.curshader
curshader.add_include('std/sky.glsl')
curshader.add_uniform('vec3 sunDir', link='_sunDirection')
curshader.add_uniform('sampler2D multiScatterLUT', link='_multiScatterLUT', included=True, tex_addr_u='repeat', tex_addr_v='clamp')
curshader.add_uniform('vec4 multiScatterParams', link='_multiScatterParams', included=True)
curshader.add_uniform('vec4 multiScatterSunBottom', link='_multiScatterSunBottom', included=True)
curshader.add_uniform('vec3 multiScatterSunTop', link='_multiScatterSunTop', included=True)
dust_density = node.aerosol_density if bpy.app.version >= (5, 0, 0) else node.dust_density
state.world.lnx_sky_density = [node.air_density, dust_density, node.ozone_density]
state.world.lnx_sky_sun_elevation = node.sun_elevation
state.world.lnx_sky_sun_rotation = node.sun_rotation
state.world.lnx_sky_sun_size = node.sun_size
state.world.lnx_sky_sun_intensity = node.sun_intensity if node.sun_disc else 0.0
state.world.lnx_sky_altitude = node.altitude
state.world.lnx_sky_sun_disc = 1 if node.sun_disc else 0
state.world.lnx_envtex_sun_direction = [node.sun_direction[0], node.sun_direction[1], node.sun_direction[2]]
return f'multi_scatter_atmosphere(pos)'
def parse_tex_environment(node: bpy.types.ShaderNodeTexEnvironment, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str:
if node.image is None:
return c.to_vec3([1.0, 0.0, 1.0])
image = node.image
# Object context: sample environment texture directly in material shader.
if state.context == ParserContext.OBJECT:
tex_store = c.store_var_name(node)
if c.node_need_reevaluation_for_screenspace_derivative(node):
tex_store += state.get_parser_pass_suffix()
if c.is_parsed(tex_store):
return f'{tex_store}.rgb'
state.parsed.add(tex_store)
tex_name = c.node_name(node.name)
tex_link = None
tex_default_file = None
is_lnx_mat_param = None
if node.lnx_material_param:
tex_link = node.name
is_lnx_mat_param = True
tex = c.make_texture(
image,
tex_name,
c.mat_get_material(),
getattr(node, 'interpolation', 'Smart'),
getattr(node, 'extension', 'REPEAT')
)
if tex is None:
log.warn(f'Object "{state.tree_name}": missing environment texture image "{node.name}"')
return c.to_vec3([1.0, 0.0, 1.0])
if is_lnx_mat_param is None:
c.mat_bind_texture(tex)
state.con.add_elem('tex', 'short2norm')
state.curshader.add_uniform(f'sampler2D {tex_name}', link=tex_link, default_value=tex_default_file, is_lnx_mat_param=is_lnx_mat_param)
state.curshader.add_include('std/math.glsl')
if node.inputs[0].is_linked:
co = c.parse_vector_input(node.inputs[0])
else:
state.curshader.add_uniform('vec3 cameraPos', link='_cameraPosition')
co = 'reflect(normalize(wposition - cameraPos), n)'
if node.projection == 'EQUIRECTANGULAR':
state.curshader.write(f'vec2 uv = envMapEquirect(normalize({co}));')
else:
state.curshader.write(f'vec2 uv = envMapMirror(normalize({co}));')
state.curshader.write(f'vec4 {tex_store} = textureLod({tex_name}, uv, 0.0);')
if image.colorspace_settings.name == 'sRGB':
state.curshader.write(f'{tex_store}.rgb = pow({tex_store}.rgb, vec3(2.2));')
return f'{tex_store}.rgb'
world = state.world
world.world_defs += '_EnvTex'
curshader = state.curshader
curshader.add_include('std/math.glsl')
curshader.add_uniform('sampler2D envmap', link='_envmap')
filepath = image.filepath
if image.packed_file is None and not os.path.isfile(lnx.utils.asset_path(filepath)):
log.warn(world.name + ' - unable to open ' + image.filepath)
return c.to_vec3([1.0, 0.0, 1.0])
# Reference image name
tex_file = lnx.utils.extract_filename(image.filepath)
base = tex_file.rsplit('.', 1)
ext = base[1].lower()
if ext == 'hdr':
target_format = 'HDR'
else:
target_format = 'JPEG'
do_convert = ext != 'hdr' and ext != 'jpg'
if do_convert:
if ext == 'exr':
tex_file = base[0] + '.hdr'
target_format = 'HDR'
else:
tex_file = base[0] + '.jpg'
target_format = 'JPEG'
if image.packed_file is not None:
# Extract packed data
unpack_path = lnx.utils.get_fp_build() + '/compiled/Assets/unpacked'
if not os.path.exists(unpack_path):
os.makedirs(unpack_path)
unpack_filepath = unpack_path + '/' + tex_file
filepath = unpack_filepath
if do_convert:
if not os.path.isfile(unpack_filepath):
lnx.utils.convert_image(image, unpack_filepath, target_format)
elif not os.path.isfile(unpack_filepath) or os.path.getsize(unpack_filepath) != image.packed_file.size:
with open(unpack_filepath, 'wb') as f:
f.write(image.packed_file.data)
assets.add(unpack_filepath)
else:
if do_convert:
unpack_path = lnx.utils.get_fp_build() + '/compiled/Assets/unpacked'
if not os.path.exists(unpack_path):
os.makedirs(unpack_path)
converted_path = unpack_path + '/' + tex_file
filepath = converted_path
# TODO: delete cache when file changes
if not os.path.isfile(converted_path):
lnx.utils.convert_image(image, converted_path, file_format=target_format)
assets.add(converted_path)
else:
# Link image path to assets
assets.add(lnx.utils.asset_path(image.filepath))
rpdat = lnx.utils.get_rp()
if not state.radiance_written:
# Generate prefiltered envmaps
world.lnx_envtex_name = tex_file
world.lnx_envtex_irr_name = tex_file.rsplit('.', 1)[0]
disable_hdr = target_format == 'JPEG'
from_srgb = image.colorspace_settings.name == "sRGB"
mip_count = world.lnx_envtex_num_mips
mip_count = write_probes.write_probes(filepath, disable_hdr, from_srgb, mip_count, lnx_radiance=rpdat.lnx_radiance)
world.lnx_envtex_num_mips = mip_count
state.radiance_written = True
# Append LDR define
if disable_hdr:
world.world_defs += '_EnvLDR'
assets.add_khafile_def("lnx_envldr")
wrd = bpy.data.worlds['Lnx']
mobile_mat = rpdat.lnx_material_model == 'Mobile' or rpdat.lnx_material_model == 'Solid'
# Append radiance define
if rpdat.lnx_irradiance and rpdat.lnx_radiance and not mobile_mat:
if '_Rad' not in wrd.world_defs:
wrd.world_defs += '_Rad'
assets.add_khafile_def("lnx_radiance")
if node.inputs[0].is_linked:
co = c.parse_vector_input(node.inputs[0])
else:
co = 'pos'
if node.projection == 'EQUIRECTANGULAR':
return f'texture(envmap, envMapEquirect({co})).rgb * envmapStrength'
else:
return f'texture(envmap, envMapMirror({co})).rgb * envmapStrength'
def parse_tex_voronoi(node: bpy.types.ShaderNodeTexVoronoi, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
outp = 0
if out_socket.type == 'RGBA':
outp = 1
elif out_socket.type == 'VECTOR':
outp = 2
elif out_socket.name == 'W':
outp = 3
m = 0
if node.distance == 'MANHATTAN':
m = 1
elif node.distance == 'CHEBYCHEV':
m = 2
elif node.distance == 'MINKOWSKI':
m = 3
exp = c.get_value_input(node, ['Exponent'])
f = 0
if node.feature == 'F2':
f = 1
elif node.feature == 'SMOOTH_F1':
f = 2
elif node.feature == 'DISTANCE_TO_EDGE':
f = 3
elif node.feature == 'N_SPHERE_RADIUS':
f = 4
dim = node.voronoi_dimensions
normalize = 1 if node.normalize else 0
c.write_procedurals()
state.curshader.add_function(getattr(c_functions, f'str_tex_voronoi_{bpy.app.version[0]}'))
if node.inputs['Vector'].is_linked:
co = c.get_vector_input(node, ['Vector'])
else:
co = 'bposition'
w = c.get_value_input(node, ['W']) if 'W' in node.inputs else '0.0'
scale = c.get_value_input(node, ['Scale']) if 'Scale' in node.inputs else '5.0'
detail = c.get_value_input(node, ['Detail']) if 'Detail' in node.inputs else '0.0'
roughness = c.get_value_input(node, ['Roughness']) if 'Roughness' in node.inputs else '0.5'
lacunarity = c.get_value_input(node, ['Lacunarity']) if 'Lacunarity' in node.inputs else '2.0'
smoothness = c.get_value_input(node, ['Smoothness']) if 'Smoothness' in node.inputs else '1.0'
randomness = c.get_value_input(node, ['Randomness']) if 'Randomness' in node.inputs else '1.0'
if out_socket == node.outputs['Color'] or out_socket == node.outputs['Position']:
res = 'tex_voronoi_{0}({1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13})'.format(dim.lower(), co, randomness, m, outp, scale, exp, w, detail, roughness, lacunarity, smoothness, f, normalize)
else:
res = 'tex_voronoi_{0}({1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}).x'.format(dim.lower(), co, randomness, m, outp, scale, exp, w, detail, roughness, lacunarity, smoothness, f, normalize)
return res
def parse_tex_wave(node: bpy.types.ShaderNodeTexWave, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
c.write_procedurals()
state.curshader.add_function(c_functions.str_tex_noise)
state.curshader.add_function(c_functions.str_tex_wave)
if node.inputs['Vector'].is_linked:
co = c.get_vector_input(node, ['Vector'])
else:
co = 'bposition'
scale = c.get_value_input(node, ['Scale'])
distortion = c.get_value_input(node, ['Distortion'])
detail = c.get_value_input(node, ['Detail'])
detail_scale = c.get_value_input(node, ['Detail Scale'])
detail_roughness = c.get_value_input(node, ['Detail Roughness'])
phase_offset = c.get_value_input(node, ['Phase Offset'])
wave_type = 0 if node.wave_type == 'BANDS' else 1
dir_map = {'X': 0, 'Y': 1, 'Z': 2, 'DIAGONAL': 3}
if hasattr(node, 'wave_direction'):
wave_dir = dir_map.get(node.wave_direction, 0)
elif wave_type == 0:
wave_dir = dir_map.get(node.bands_direction, 0)
else:
wave_dir = dir_map.get(node.rings_direction, 0)
if node.wave_profile == 'SIN':
wave_profile = 0
elif node.wave_profile == 'SAW':
wave_profile = 1
else:
wave_profile = 2
args = '{0} * {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}'.format(
co, scale, wave_type, wave_dir, wave_profile, distortion, detail, detail_scale, phase_offset, detail_roughness
)
if out_socket == node.outputs['Color']:
res = 'vec3(tex_wave_f({0}))'.format(args)
else:
res = 'tex_wave_f({0})'.format(args)
return res
def parse_tex_gabor(node: bpy.types.ShaderNodeTexGabor, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
c.write_procedurals()
state.curshader.add_function(c_functions.str_tex_gabor)
if node.inputs['Vector'].is_linked:
co = c.get_vector_input(node, ['Vector'])
else:
co = 'bposition'
scale = c.get_value_input(node, ['Scale'])
freq = c.get_value_input(node, ['Frequency'])
anisotropy = c.get_value_input(node, ['Anisotropy'])
gabor_type = '0.0' if node.gabor_type == '2D' else '1.0'
if node.gabor_type == '2D':
orientation_2d = c.get_value_input(node, ['Orientation'])
orientation_3d = 'vec3(0.0)'
else:
orientation_2d = '0.0'
orientation_3d = c.get_vector_input(node, ['Orientation'])
args = '{0}, {1}, {2}, {3}, {4}, {5}, {6}'.format(
co, scale, freq, anisotropy, orientation_2d, orientation_3d, gabor_type
)
if out_socket == node.outputs['Phase']:
return 'tex_gabor_phase({0})'.format(args)
elif out_socket == node.outputs['Intensity']:
return 'tex_gabor_intensity({0})'.format(args)
return 'tex_gabor_value({0})'.format(args)
def parse_tex_white_noise(node: bpy.types.ShaderNodeTexWhiteNoise, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
c.write_procedurals()
state.curshader.add_function(c_functions.str_tex_noise)
if node.inputs[0].is_linked:
co = c.parse_vector_input(node.inputs[0])
else:
co = 'bposition'
w = c.parse_value_input(node.inputs['W']) if 'W' in node.inputs else '0.0'
dimensions = getattr(node, 'noise_dimensions', '3D')
is_color = (out_socket == node.outputs[1]) or (getattr(out_socket, 'name', '') == 'Color')
if dimensions == '1D':
if is_color:
return f'hash_float_to_vec3({w})'
return f'hash_float_to_float({w})'
elif dimensions == '2D':
if is_color:
return f'hash_vec2_to_vec3(({co}).xy)'
return f'hash_vec2_to_float(({co}).xy)'
elif dimensions == '4D':
if is_color:
return f'hash_vec4_to_vec3(vec4({co}, {w}))'
return f'hash_vec4_to_float(vec4({co}, {w}))'
else:
if is_color:
return f'hash_vec3_to_vec3({co})'
return f'hash_vec3_to_float({co})'