LNXSDK/leenkx/blender/lnx/material/make_shader.py
2025-01-22 16:18:30 +01:00

226 lines
8.7 KiB
Python

import os
import subprocess
from typing import Dict, List, Tuple
import bpy
from bpy.types import Material
from bpy.types import Object
import lnx.api
import lnx.assets as assets
import lnx.exporter
import lnx.log as log
import lnx.material.cycles as cycles
import lnx.material.make_decal as make_decal
import lnx.material.make_depth as make_depth
import lnx.material.make_mesh as make_mesh
import lnx.material.make_overlay as make_overlay
import lnx.material.make_transluc as make_transluc
import lnx.material.make_refract as make_refract
import lnx.material.make_voxel as make_voxel
import lnx.material.mat_state as mat_state
import lnx.material.mat_utils as mat_utils
from lnx.material.shader import Shader, ShaderContext, ShaderData
import lnx.utils
if lnx.is_reload(__name__):
lnx.api = lnx.reload_module(lnx.api)
assets = lnx.reload_module(assets)
lnx.exporter = lnx.reload_module(lnx.exporter)
log = lnx.reload_module(log)
cycles = lnx.reload_module(cycles)
make_decal = lnx.reload_module(make_decal)
make_depth = lnx.reload_module(make_depth)
make_mesh = lnx.reload_module(make_mesh)
make_overlay = lnx.reload_module(make_overlay)
make_transluc = lnx.reload_module(make_transluc)
make_voxel = lnx.reload_module(make_voxel)
mat_state = lnx.reload_module(mat_state)
mat_utils = lnx.reload_module(mat_utils)
lnx.material.shader = lnx.reload_module(lnx.material.shader)
from lnx.material.shader import Shader, ShaderContext, ShaderData
lnx.utils = lnx.reload_module(lnx.utils)
else:
lnx.enable_reload(__name__)
rpass_hook = None
def build(material: Material, mat_users: Dict[Material, List[Object]], mat_lnxusers) -> Tuple:
mat_state.mat_users = mat_users
mat_state.mat_lnxusers = mat_lnxusers
mat_state.material = material
mat_state.nodes = material.node_tree.nodes
mat_state.data = ShaderData(material)
mat_state.output_node = cycles.node_by_type(mat_state.nodes, 'OUTPUT_MATERIAL')
if mat_state.output_node is None:
# Place empty material output to keep compiler happy..
mat_state.output_node = mat_state.nodes.new('ShaderNodeOutputMaterial')
wrd = bpy.data.worlds['Lnx']
rpdat = lnx.utils.get_rp()
rpasses = mat_utils.get_rpasses(material)
matname = lnx.utils.safesrc(lnx.utils.asset_name(material))
rel_path = lnx.utils.build_dir() + '/compiled/Shaders/'
full_path = lnx.utils.get_fp() + '/' + rel_path
if not os.path.exists(full_path):
os.makedirs(full_path)
make_instancing_and_skinning(material, mat_users)
bind_constants = dict()
bind_textures = dict()
for rp in rpasses:
car = []
bind_constants[rp] = car
mat_state.bind_constants = car
tar = []
bind_textures[rp] = tar
mat_state.bind_textures = tar
con = None
if rpdat.rp_driver != 'Leenkx' and lnx.api.drivers[rpdat.rp_driver]['make_rpass'] is not None:
con = lnx.api.drivers[rpdat.rp_driver]['make_rpass'](rp)
if con is not None:
pass
elif rp == 'mesh':
con = make_mesh.make(rp, rpasses)
elif rp == 'shadowmap':
con = make_depth.make(rp, rpasses, shadowmap=True)
elif rp == 'shadowmap_transparent':
con = make_depth.make(rp, rpasses, shadowmap=True, shadowmap_transparent=True)
elif rp == 'translucent':
con = make_transluc.make(rp)
elif rp == 'refraction':
con = make_refract.make(rp)
elif rp == 'overlay':
con = make_overlay.make(rp)
elif rp == 'decal':
con = make_decal.make(rp)
elif rp == 'depth':
con = make_depth.make(rp, rpasses)
elif rp == 'voxel':
con = make_voxel.make(rp)
elif rpass_hook is not None:
con = rpass_hook(rp)
write_shaders(rel_path, con, rp, matname)
shader_data_name = matname + '_data'
if wrd.lnx_single_data_file:
if 'shader_datas' not in lnx.exporter.current_output:
lnx.exporter.current_output['shader_datas'] = []
lnx.exporter.current_output['shader_datas'].append(mat_state.data.get()['shader_datas'][0])
else:
lnx.utils.write_lnx(full_path + '/' + matname + '_data.lnx', mat_state.data.get())
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
def write_shaders(rel_path: str, con: ShaderContext, rpass: str, matname: str) -> None:
keep_cache = mat_state.material.lnx_cached
write_shader(rel_path, con.vert, 'vert', rpass, matname, keep_cache=keep_cache)
write_shader(rel_path, con.frag, 'frag', rpass, matname, keep_cache=keep_cache)
write_shader(rel_path, con.geom, 'geom', rpass, matname, keep_cache=keep_cache)
write_shader(rel_path, con.tesc, 'tesc', rpass, matname, keep_cache=keep_cache)
write_shader(rel_path, con.tese, 'tese', rpass, matname, keep_cache=keep_cache)
def write_shader(rel_path: str, shader: Shader, ext: str, rpass: str, matname: str, keep_cache=True) -> None:
if shader is None or shader.is_linked:
return
# TODO: blend context
if rpass == 'mesh' and mat_state.material.lnx_blending:
rpass = 'blend'
file_ext = '.glsl'
if shader.noprocessing:
# Use hlsl directly
hlsl_dir = lnx.utils.build_dir() + '/compiled/Hlsl/'
if not os.path.exists(hlsl_dir):
os.makedirs(hlsl_dir)
file_ext = '.hlsl'
rel_path = rel_path.replace('/compiled/Shaders/', '/compiled/Hlsl/')
shader_file = matname + '_' + rpass + '.' + ext + file_ext
shader_path = lnx.utils.get_fp() + '/' + rel_path + '/' + shader_file
assets.add_shader(shader_path)
if not os.path.isfile(shader_path) or not keep_cache:
with open(shader_path, 'w') as f:
f.write(shader.get())
if shader.noprocessing:
cwd = os.getcwd()
os.chdir(lnx.utils.get_fp() + '/' + rel_path)
hlslbin_path = lnx.utils.get_sdk_path() + '/lib/leenkx_tools/hlslbin/hlslbin.exe'
prof = 'vs_5_0' if ext == 'vert' else 'ps_5_0' if ext == 'frag' else 'gs_5_0'
# noprocessing flag - gets renamed to .d3d11
args = [hlslbin_path.replace('/', '\\').replace('\\\\', '\\'), shader_file, shader_file[:-4] + 'glsl', prof]
if ext == 'vert':
args.append('-i')
args.append('pos')
proc = subprocess.call(args)
os.chdir(cwd)
def make_instancing_and_skinning(mat: Material, mat_users: Dict[Material, List[Object]]) -> None:
"""Build material with instancing or skinning if enabled.
If the material is a custom material, only validation checks for instancing are performed."""
global_elems = []
if mat_users is not None and mat in mat_users:
# Whether there are both an instanced object and a not instanced object with this material
instancing_usage = [False, False]
mat_state.uses_instancing = False
for bo in mat_users[mat]:
if mat.lnx_custom_material == '':
# Morph Targets
if lnx.utils.export_morph_targets(bo):
global_elems.append({'name': 'morph', 'data': 'short2norm'})
# GPU Skinning
if lnx.utils.export_bone_data(bo):
global_elems.append({'name': 'bone', 'data': 'short4norm'})
global_elems.append({'name': 'weight', 'data': 'short4norm'})
# Instancing
inst = bo.lnx_instanced
if inst != 'Off' or mat.lnx_particle_flag:
instancing_usage[0] = True
mat_state.uses_instancing = True
if mat.lnx_custom_material == '':
global_elems.append({'name': 'ipos', 'data': 'float3'})
if 'Rot' in inst:
global_elems.append({'name': 'irot', 'data': 'float3'})
if 'Scale' in inst:
global_elems.append({'name': 'iscl', 'data': 'float3'})
elif inst == 'Off':
# Ignore children of instanced objects, they are instanced even when set to 'Off'
instancing_usage[1] = bo.parent is None or bo.parent.lnx_instanced == 'Off'
if instancing_usage[0] and instancing_usage[1]:
# Display a warning for invalid instancing configurations
# See https://github.com/leenkx3d/leenkx/issues/2072
log.warn(f'Material "{mat.name}" has both instanced and not instanced objects, objects might flicker!')
if mat.lnx_custom_material == '':
mat_state.data.global_elems = global_elems