This commit is contained in:
2026-02-24 17:35:26 -08:00
parent 1c3c30e6ce
commit d45c632dcd
28 changed files with 1982 additions and 97 deletions

View File

@ -16,11 +16,13 @@
#
import os
import shutil
import subprocess
from typing import Any, Dict, Optional, Tuple
import bpy
import os
import lnx.assets
import lnx.assets as assets
import lnx.log as log
import lnx.make_state
import lnx.material.cycles_functions as c_functions
@ -1003,7 +1005,12 @@ def make_texture(
wrd = bpy.data.worlds['Lnx']
max_size = int(wrd.lnx_max_texture_size)
if max_size > 0 and image is not None:
original_filepath = filepath
filepath = resize_texture_if_needed(image, filepath, max_size)
if filepath != original_filepath:
resized_filename = lnx.utils.extract_filename(filepath)
tex['file'] = lnx.utils.safestr(resized_filename)
# Link image path to assets
# TODO: Khamake converts .PNG to .jpg? Convert ext to lowercase on windows

View File

@ -1,4 +1,3 @@
from __future__ import annotations
import bpy
from bpy.types import NodeSocket
@ -36,12 +35,17 @@ def parse_mixshader(node: bpy.types.ShaderNodeMixShader, out_socket: NodeSocket,
state.curshader.write('{0}float {1} = 1.0 - {2};'.format(prefix, fac_inv_var, fac_var))
mat_state.emission_type = mat_state.EmissionType.NO_EMISSION
sss_before_1 = mat_state.needs_sss
bc1, rough1, met1, occ1, spec1, opac1, ior1, emi1 = c.parse_shader_input(node.inputs[1])
sss_1 = mat_state.needs_sss
ek1 = mat_state.emission_type
mat_state.emission_type = mat_state.EmissionType.NO_EMISSION
mat_state.needs_sss = sss_before_1 # Reset to state before parsing input 1
bc2, rough2, met2, occ2, spec2, opac2, ior2, emi2 = c.parse_shader_input(node.inputs[2])
sss_2 = mat_state.needs_sss
ek2 = mat_state.emission_type
mat_state.needs_sss = sss_1 or sss_2
if state.parse_surface:
state.out_basecol = '({0} * {3} + {1} * {2})'.format(bc1, bc2, fac_var, fac_inv_var)
@ -57,12 +61,17 @@ def parse_mixshader(node: bpy.types.ShaderNodeMixShader, out_socket: NodeSocket,
def parse_addshader(node: bpy.types.ShaderNodeAddShader, out_socket: NodeSocket, state: ParserState) -> None:
mat_state.emission_type = mat_state.EmissionType.NO_EMISSION
sss_before_1 = mat_state.needs_sss
bc1, rough1, met1, occ1, spec1, opac1, ior1, emi1 = c.parse_shader_input(node.inputs[0])
sss_1 = mat_state.needs_sss
ek1 = mat_state.emission_type
mat_state.emission_type = mat_state.EmissionType.NO_EMISSION
mat_state.needs_sss = sss_before_1 # Reset to state before parsing input 0
bc2, rough2, met2, occ2, spec2, opac2, ior2, emi2 = c.parse_shader_input(node.inputs[1])
sss_2 = mat_state.needs_sss
ek2 = mat_state.emission_type
mat_state.needs_sss = sss_1 or sss_2
if state.parse_surface:
state.out_basecol = '({0} + {1})'.format(bc1, bc2)
@ -82,6 +91,8 @@ if bpy.app.version < (2, 92, 0):
if state.parse_surface:
c.write_normal(node.inputs[20])
state.out_basecol = c.parse_vector_input(node.inputs[0])
if node.inputs[1].is_linked or node.inputs[1].default_value > 0.0:
mat_state.needs_sss = True
state.out_metallic = c.parse_value_input(node.inputs[4])
state.out_specular = c.parse_value_input(node.inputs[5])
state.out_roughness = c.parse_value_input(node.inputs[7])
@ -103,7 +114,8 @@ if bpy.app.version >= (2, 92, 0) and bpy.app.version <= (4, 1, 0):
if state.parse_surface:
c.write_normal(node.inputs[22])
state.out_basecol = c.parse_vector_input(node.inputs[0])
# subsurface = c.parse_vector_input(node.inputs[1])
if node.inputs[1].is_linked or node.inputs[1].default_value > 0.0:
mat_state.needs_sss = True
# subsurface_radius = c.parse_vector_input(node.inputs[2])
# subsurface_color = c.parse_vector_input(node.inputs[3])
state.out_metallic = c.parse_value_input(node.inputs[6])
@ -138,14 +150,27 @@ if bpy.app.version > (4, 1, 0):
if state.parse_surface:
c.write_normal(node.inputs[5])
state.out_basecol = c.parse_vector_input(node.inputs[0])
sss_input = node.inputs.get('Subsurface Weight') or node.inputs.get('Subsurface')
if sss_input is not None:
if sss_input.is_linked or sss_input.default_value > 0.0:
mat_state.needs_sss = True
subsurface = c.parse_value_input(node.inputs[7])
subsurface_radius = c.parse_vector_input(node.inputs[9])
subsurface_color = c.parse_vector_input(node.inputs[8])
state.out_metallic = c.parse_value_input(node.inputs[1])
if bpy.app.version > (4, 2, 4):
state.out_specular = c.parse_value_input(node.inputs[13])
specular_socket = node.inputs.get('Specular IOR Level')
if specular_socket is not None:
state.out_specular = f'({c.parse_value_input(specular_socket)} * 2.0)'
else:
state.out_specular = c.parse_value_input(node.inputs[12])
specular_socket = node.inputs.get('Specular')
if specular_socket is not None:
state.out_specular = c.parse_value_input(specular_socket)
else:
state.out_specular = '1.0'
state.out_roughness = c.parse_value_input(node.inputs[2])
# Prevent black material when metal = 1.0 and roughness = 0.0
try:
@ -278,6 +303,8 @@ def parse_bsdfrefraction(node: bpy.types.ShaderNodeBsdfRefraction, out_socket: N
def parse_subsurfacescattering(node: bpy.types.ShaderNodeSubsurfaceScattering, out_socket: NodeSocket, state: ParserState) -> None:
if state.parse_surface:
# Mark that this material needs SSS
mat_state.needs_sss = True
if bpy.app.version < (4, 1, 0):
c.write_normal(node.inputs[4])
else:

View File

@ -8,6 +8,7 @@ import lnx.log as log
import lnx.material.cycles as cycles
import lnx.material.make_shader as make_shader
import lnx.material.mat_batch as mat_batch
import lnx.material.mat_state as mat_state
import lnx.material.mat_utils as mat_utils
import lnx.node_utils
import lnx.utils
@ -17,6 +18,7 @@ if lnx.is_reload(__name__):
cycles = lnx.reload_module(cycles)
make_shader = lnx.reload_module(make_shader)
mat_batch = lnx.reload_module(mat_batch)
mat_state = lnx.reload_module(mat_state)
mat_utils = lnx.reload_module(mat_utils)
lnx.node_utils = lnx.reload_module(lnx.node_utils)
lnx.utils = lnx.reload_module(lnx.utils)
@ -60,6 +62,7 @@ def parse(material: Material, mat_data, mat_users: Dict[Material, List[Object]],
shader_data_name = material.lnx_custom_material
bind_constants = {'mesh': []}
bind_textures = {'mesh': []}
mat_uses_sss = False
make_shader.make_instancing_and_skinning(material, mat_users)
@ -77,10 +80,11 @@ def parse(material: Material, mat_data, mat_users: Dict[Material, List[Object]],
log.warn(f'Material "{material.name}": skipping export of bind texture at slot {idx + 1} ("{item.uniform_name}") with no image selected')
elif not wrd.lnx_batch_materials or material.name.startswith('lnxdefault'):
rpasses, shader_data, shader_data_name, bind_constants, bind_textures = make_shader.build(material, mat_users, mat_lnxusers)
rpasses, shader_data, shader_data_name, bind_constants, bind_textures, mat_uses_sss = make_shader.build(material, mat_users, mat_lnxusers)
sd = shader_data.sd
else:
rpasses, shader_data, shader_data_name, bind_constants, bind_textures = mat_batch.get(material)
result = mat_batch.get(material)
rpasses, shader_data, shader_data_name, bind_constants, bind_textures, mat_uses_sss = result
sd = shader_data.sd
sss_used = False
@ -106,9 +110,12 @@ def parse(material: Material, mat_data, mat_users: Dict[Material, List[Object]],
elif rpdat.rp_sss_state != 'Off':
const = {'name': 'materialID'}
if needs_sss:
# Use per-material SSS flag from shader build
if mat_uses_sss:
const['intValue'] = 2
sss_used = True
if '_SSS' not in wrd.world_defs:
wrd.world_defs += '_SSS'
else:
const['intValue'] = 0
c['bind_constants'].append(const)
@ -167,4 +174,10 @@ def material_needs_sss(material: Material) -> bool:
if sss_node is not None and sss_node.outputs[0].is_linked and (sss_node.inputs[8].is_linked or sss_node.inputs[8].default_value != 0.0):
return True
for principled_node in lnx.node_utils.iter_nodes_by_type(material.node_tree, 'BSDF_PRINCIPLED'):
if principled_node is not None and principled_node.outputs[0].is_linked:
sss_input = principled_node.inputs.get('Subsurface Weight') or principled_node.inputs.get('Subsurface')
if sss_input is not None and (sss_input.is_linked or sss_input.default_value > 0.0):
return True
return False

View File

@ -107,7 +107,7 @@ def write(vert: shader.Shader, frag: shader.Shader):
if '_MicroShadowing' in wrd.world_defs and not is_mobile:
frag.write('\t, occlusion')
if '_SSRS' in wrd.world_defs:
frag.add_uniform('sampler2D gbufferD')
frag.add_uniform('sampler2D gbufferD', top=True)
frag.add_uniform('mat4 invVP', '_inverseViewProjectionMatrix')
frag.add_uniform('vec3 eye', '_cameraPosition')
frag.write(', gbufferD, invVP, eye')

View File

@ -198,7 +198,7 @@ def make_deferred(con_mesh, rpasses):
rpdat = lnx.utils.get_rp()
lnx_discard = mat_state.material.lnx_discard
parse_opacity = lnx_discard or 'translucent' or 'refraction' in rpasses
parse_opacity = lnx_discard or 'translucent' in rpasses or 'refraction' in rpasses
make_base(con_mesh, parse_opacity=parse_opacity)
@ -213,6 +213,7 @@ def make_deferred(con_mesh, rpasses):
opac = '0.9999' # 1.0 - eps
frag.write('if (opacity < {0}) discard;'.format(opac))
frag.add_out(f'vec4 fragColor[GBUF_SIZE]')
if '_gbuffer2' in wrd.world_defs:
@ -281,7 +282,7 @@ def make_deferred(con_mesh, rpasses):
frag.write('#endif')
if '_SSRefraction' in wrd.world_defs or '_VoxelRefract' in wrd.world_defs:
frag.write('fragColor[GBUF_IDX_REFRACTION] = vec4(1.0, 1.0, 0.0, 1.0);')
frag.write('fragColor[GBUF_IDX_REFRACTION] = vec4(1.0, 0.0, 0.0, 1.0);')
return con_mesh
@ -559,7 +560,7 @@ def make_forward(con_mesh):
frag.write('fragColor[0] = vec4(direct + indirect, packFloat2(occlusion, specular));')
frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);')
if rpdat.rp_ss_refraction or rpdat.lnx_voxelgi_refract:
frag.write(f'fragColor[2] = vec4(1.0, 1.0, 0.0, 0.0);')
frag.write(f'fragColor[2] = vec4(1.0, 0.0, 0.0, 1.0);')
else:
frag.add_out('vec4 fragColor[1]')
@ -716,8 +717,12 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False):
else:
frag.write('vec3 indirect = envl;')
if '_VoxelShadow' in wrd.world_defs or '_VoxelGI' in wrd.world_defs:
velocity_already_defined = '_gbuffer2' in wrd.world_defs and '_Veloc' in wrd.world_defs
if not velocity_already_defined:
frag.write('vec2 velocity = gl_FragCoord.xy;')
if '_VoxelGI' in wrd.world_defs:
frag.write('vec2 velocity = gl_FragCoord.xy;')
frag.write('vec4 diffuse_indirect = traceDiffuse(wposition, n, voxels, clipmaps);')
frag.write('indirect = (diffuse_indirect.rgb * albedo * (1.0 - F) + envl * (1.0 - diffuse_indirect.a)) * voxelgiDiff;')
frag.write('if (roughness < 1.0 && specular > 0.0) {')
@ -810,12 +815,11 @@ def make_forward_base(con_mesh, parse_opacity=False, transluc_pass=False):
frag.write(', true, spotData.x, spotData.y, spotDir, spotData.zw, spotRight')
if '_VoxelShadow' in wrd.world_defs:
frag.write(', voxels, voxelsSDF, clipmaps')
if '_Veloc' in wrd.world_defs or '_VoxelShadow' in wrd.world_defs:
frag.write(', velocity')
if '_MicroShadowing' in wrd.world_defs:
frag.write(', occlusion')
if '_SSRS' in wrd.world_defs:
frag.add_uniform('sampler2D gbufferD')
frag.add_uniform('sampler2D gbufferD', top=True)
frag.add_uniform('mat4 invVP', '_inverseViewProjectionMatrix')
frag.add_uniform('vec3 eye', '_cameraPosition')
frag.write(', gbufferD, invVP, eye')

View File

@ -6,6 +6,8 @@ else:
lnx.enable_reload(__name__)
def morph_pos(vert):
if vert.has_attrib('vec2 texCoordMorph = morph * texUnpack;'):
return
rpdat = lnx.utils.get_rp()
vert.add_include('compiled.inc')
vert.add_include('std/morph_target.glsl')
@ -22,6 +24,8 @@ def morph_pos(vert):
vert.write_attrib('spos.xyz /= posUnpack;')
def morph_nor(vert, is_bone, prep):
if vert.has_attrib('vec3 morphNor = vec3(0, 0, 0);'):
return
vert.write_attrib('vec3 morphNor = vec3(0, 0, 0);')
vert.write_attrib('getMorphedNormal(texCoordMorph, vec3(nor.xy, pos.w), morphNor);')
if not is_bone:

View File

@ -18,7 +18,15 @@ else:
def make(context_id):
con_refract = mat_state.data.add_context({ 'name': context_id, 'depth_write': True, 'compare_mode': 'less', 'cull_mode': 'clockwise' })
con_refract = mat_state.data.add_context({
'name': context_id,
'depth_write': False,
'compare_mode': 'less',
'cull_mode': 'clockwise',
'blend_source': 'blend_one',
'blend_destination': 'inverse_source_alpha',
'blend_operation': 'add'
})
make_mesh.make_forward_base(con_refract, parse_opacity=True, transluc_pass=True)
vert = con_refract.vert
@ -51,14 +59,16 @@ def make(context_id):
frag.write('const uint matid = 0;')
if rpdat.rp_renderer == 'Deferred':
frag.write('fragColor[0] = vec4(n.xy, roughness, packFloatInt16(metallic, matid));')
frag.write('fragColor[1] = vec4(direct + indirect, packFloat2(occlusion, specular));')
frag.write('fragColor[0] = vec4(n.xy, roughness, 1.0);')
frag.write('vec3 finalColor = direct + indirect;')
frag.write('fragColor[1] = vec4(finalColor * opacity, opacity);')
else:
frag.write('fragColor[0] = vec4(direct + indirect, packFloat2(occlusion, specular));')
frag.write('fragColor[1] = vec4(n.xy, roughness, metallic);')
frag.write('vec3 finalColor = direct + indirect;')
frag.write('fragColor[0] = vec4(finalColor * opacity, opacity);')
frag.write('fragColor[1] = vec4(n.xy, roughness, 1.0);')
frag.write('fragColor[2] = vec4(ior, opacity, 0.0, 1.0);')
# frag.write('fragColor[2] = vec4(ior, opacity, packFloat2(basecol.r, basecol.g), basecol.b);')
frag.write('fragColor[2] = vec4(ior, 1.0 - opacity, gl_FragCoord.z, 1.0);')
# frag.write('fragColor[2] = vec4(ior, 1.0 - opacity, packFloat2(basecol.r, basecol.g), basecol.b);')
make_finalize.make(con_refract)

View File

@ -56,6 +56,9 @@ def build(material: Material, mat_users: Dict[Material, List[Object]], mat_lnxus
if mat_state.output_node is None:
# Place empty material output to keep compiler happy..
mat_state.output_node = mat_state.nodes.new('ShaderNodeOutputMaterial')
# reset for each material
mat_state.needs_sss = False
wrd = bpy.data.worlds['Lnx']
rpdat = lnx.utils.get_rp()
@ -130,7 +133,8 @@ def build(material: Material, mat_users: Dict[Material, List[Object]], mat_lnxus
shader_data_path = lnx.utils.get_fp_build() + '/compiled/Shaders/' + shader_data_name + '.lnx'
assets.add_shader_data(shader_data_path)
return rpasses, mat_state.data, shader_data_name, bind_constants, bind_textures
needs_sss_result = mat_state.needs_sss
return rpasses, mat_state.data, shader_data_name, bind_constants, bind_textures, needs_sss_result
def write_shaders(rel_path: str, con: ShaderContext, rpass: str, matname: str) -> None:
@ -146,6 +150,11 @@ def write_shader(rel_path: str, shader: Shader, ext: str, rpass: str, matname: s
if shader is None or shader.is_linked:
return
validation_issues = shader.validate()
if validation_issues:
for issue in validation_issues:
log.warn(f"Shader validation issue in {matname}_{rpass}.{ext}: {issue}. ")
# TODO: blend context
if rpass == 'mesh' and mat_state.material.lnx_blending:
rpass = 'blend'

View File

@ -7,6 +7,9 @@ else:
def skin_pos(vert):
if vert.has_attrib('vec4 skinA;'):
return
vert.add_include('compiled.inc')
rpdat = lnx.utils.get_rp()
@ -25,6 +28,11 @@ def skin_pos(vert):
def skin_nor(vert, is_morph, prep):
morph_normal_code = 'wnormal = normalize(N * (morphNor + 2.0 * cross(skinA.xyz'
static_normal_code = 'wnormal = normalize(N * (vec3(nor.xy, pos.w) + 2.0 * cross(skinA.xyz'
if vert.has_attrib(morph_normal_code) or vert.has_attrib(static_normal_code):
return
rpdat = lnx.utils.get_rp()
if(is_morph):
vert.write_attrib(prep + 'wnormal = normalize(N * (morphNor + 2.0 * cross(skinA.xyz, cross(skinA.xyz, morphNor) + skinA.w * morphNor)));')

View File

@ -41,7 +41,7 @@ def make(context_id):
frag.write('n /= (abs(n.x) + abs(n.y) + abs(n.z));')
frag.write('n.xy = n.z >= 0.0 ? n.xy : octahedronWrap(n.xy);')
frag.write('vec4 premultipliedReflect = vec4(vec3(direct + indirect * 0.5) * opacity, opacity);');
frag.write('vec4 premultipliedReflect = vec4(vec3(direct + indirect) * opacity, opacity);');
frag.write('float w = clamp(pow(min(1.0, premultipliedReflect.a * 10.0) + 0.01, 3.0) * 1e8 * pow(1.0 - (gl_FragCoord.z) * 0.9, 3.0), 1e-2, 3e3);')
frag.write('fragColor[0] = vec4(premultipliedReflect.rgb * w, premultipliedReflect.a);')
frag.write('fragColor[1] = vec4(premultipliedReflect.a * w, 0.0, 0.0, 1.0);')

View File

@ -38,3 +38,4 @@ texture_grad = False # Sample textures using textureGrad()
con_mesh = None # Mesh context
uses_instancing = False # Whether the current material has at least one user with instancing enabled
emission_type = EmissionType.NO_EMISSION
needs_sss = False

View File

@ -361,9 +361,14 @@ class Shader:
def write_header(self, s):
self.header += s + '\n'
def write_attrib(self, s):
def write_attrib(self, s, unique=False):
if unique and s in self.main_attribs:
return
self.main_attribs += '\t' + s + '\n'
def has_attrib(self, s):
return s in self.main_attribs
def is_equal(self, sh):
self.vstruct_to_vsin()
return self.ins == sh.ins and \
@ -394,6 +399,25 @@ class Shader:
for e in vs:
self.add_in('vec' + self.data_size(e['data']) + ' ' + e['name'])
def validate(self):
import re
issues = []
# Check for duplicate variable declarations in main_attribs
var_pattern = re.compile(r'\b(vec[234]|float|int|mat[234])\s+(\w+)\s*[;=]')
declared_vars = {}
for line in self.main_attribs.split('\n'):
match = var_pattern.search(line)
if match:
var_type, var_name = match.groups()
if var_name in declared_vars:
issues.append(f"Duplicate variable declaration: '{var_name}' (type: {var_type})")
else:
declared_vars[var_name] = var_type
return issues
def get(self):
if self.noprocessing:
return self.main