730 lines
32 KiB
Python
730 lines
32 KiB
Python
"""
|
|
materials.py
|
|
------------
|
|
Shader catalogue and material management for knot_animation.
|
|
|
|
Shader builders
|
|
---------------
|
|
Each `_inner_<SHADER_ID>` function receives an already-cleared node tree
|
|
(nodes, links, y_offset) and returns the final output shader socket.
|
|
They do NOT add an Output Material node — that is the caller's responsibility.
|
|
|
|
Adding a new shader
|
|
-------------------
|
|
1. Define `_inner_MYSHADER(nodes, links, y=0, color=..., roughness=...,
|
|
metallic=..., emission_strength=..., **kwargs)`.
|
|
2. Add its name to `_TRANSPARENT_SHADERS` below ONLY if it needs alpha blending.
|
|
|
|
No other changes are required — INNER_FACTORIES and SHADER_IDS are built
|
|
automatically from the naming convention.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import bpy
|
|
|
|
from .constants import KNOT_OBJ_NAME, KNOT_MAT_NAME, SHADER_BLEND_MAT_NAME
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Inner shader builders
|
|
# Convention: _inner_<SHADER_ID>(nodes, links, y, color, roughness, metallic,
|
|
# emission_strength, **kwargs) -> output_socket
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _inner_GLOSS_BLUE(nodes, links, y=0, color=(0.2, 0.6, 1.0, 1.0),
|
|
roughness=0.1, metallic=0.0, emission_strength=1.0, **kwargs):
|
|
mix = nodes.new("ShaderNodeMixShader"); mix.location = (300, y)
|
|
glos = nodes.new("ShaderNodeBsdfGlossy"); glos.location = (0, y + 80)
|
|
emit = nodes.new("ShaderNodeEmission"); emit.location = (0, y - 80)
|
|
glos.inputs["Color"].default_value = color
|
|
glos.inputs["Roughness"].default_value = roughness
|
|
emit.inputs["Color"].default_value = (0.4, 0.8, 1.0, 1.0)
|
|
emit.inputs["Strength"].default_value = emission_strength * 0.5
|
|
mix.inputs["Fac"].default_value = 0.3
|
|
links.new(glos.outputs["BSDF"], mix.inputs[1])
|
|
links.new(emit.outputs["Emission"], mix.inputs[2])
|
|
return mix.outputs["Shader"]
|
|
|
|
|
|
def _inner_NEON_GLOW(nodes, links, y=0, color=(0.0, 1.0, 0.9, 1.0),
|
|
roughness=0.1, metallic=0.0, emission_strength=1.0, **kwargs):
|
|
mix = nodes.new("ShaderNodeMixShader"); mix.location = (300, y)
|
|
em1 = nodes.new("ShaderNodeEmission"); em1.location = (0, y + 80)
|
|
em2 = nodes.new("ShaderNodeEmission"); em2.location = (0, y - 80)
|
|
em1.inputs["Color"].default_value = color
|
|
em1.inputs["Strength"].default_value = emission_strength * 3.0
|
|
em2.inputs["Color"].default_value = (1.0, 0.05, 0.8, 1.0)
|
|
em2.inputs["Strength"].default_value = emission_strength * 3.0
|
|
mix.inputs["Fac"].default_value = 0.5
|
|
links.new(em1.outputs["Emission"], mix.inputs[1])
|
|
links.new(em2.outputs["Emission"], mix.inputs[2])
|
|
return mix.outputs["Shader"]
|
|
|
|
|
|
def _inner_METALLIC(nodes, links, y=0, color=(1.0, 0.78, 0.28, 1.0),
|
|
roughness=0.05, metallic=1.0, emission_strength=1.0, **kwargs):
|
|
prin = nodes.new("ShaderNodeBsdfPrincipled"); prin.location = (0, y)
|
|
prin.inputs["Base Color"].default_value = color
|
|
prin.inputs["Metallic"].default_value = metallic
|
|
prin.inputs["Roughness"].default_value = roughness
|
|
try:
|
|
prin.inputs["Specular IOR Level"].default_value = 1.0
|
|
except KeyError:
|
|
pass # name differs in older Blender versions
|
|
return prin.outputs["BSDF"]
|
|
|
|
|
|
def _inner_GLASS(nodes, links, y=0, color=(0.85, 0.95, 1.0, 1.0),
|
|
roughness=0.0, metallic=0.0, emission_strength=1.0, **kwargs):
|
|
prin = nodes.new("ShaderNodeBsdfPrincipled"); prin.location = (0, y)
|
|
prin.inputs["Base Color"].default_value = color
|
|
prin.inputs["Metallic"].default_value = metallic
|
|
prin.inputs["Roughness"].default_value = roughness
|
|
prin.inputs["IOR"].default_value = 1.45
|
|
for key in ("Transmission Weight", "Transmission"):
|
|
if key in prin.inputs:
|
|
prin.inputs[key].default_value = 1.0
|
|
break
|
|
return prin.outputs["BSDF"]
|
|
|
|
|
|
def _inner_HOLOGRAM(nodes, links, y=0, color=(0.2, 1.0, 0.5, 1.0),
|
|
roughness=0.1, metallic=0.0, emission_strength=1.0, **kwargs):
|
|
mix = nodes.new("ShaderNodeMixShader"); mix.location = (400, y)
|
|
emit = nodes.new("ShaderNodeEmission"); emit.location = (0, y + 100)
|
|
trans = nodes.new("ShaderNodeBsdfTransparent"); trans.location = (0, y - 100)
|
|
fres = nodes.new("ShaderNodeFresnel"); fres.location = (0, y + 220)
|
|
emit.inputs["Color"].default_value = color
|
|
emit.inputs["Strength"].default_value = emission_strength * 2.0
|
|
fres.inputs["IOR"].default_value = 1.3
|
|
links.new(fres.outputs["Fac"], mix.inputs["Fac"])
|
|
links.new(emit.outputs["Emission"], mix.inputs[1])
|
|
links.new(trans.outputs["BSDF"], mix.inputs[2])
|
|
return mix.outputs["Shader"]
|
|
|
|
|
|
def _inner_LAVA(nodes, links, y=0, color=(1.0, 0.1, 0.0, 1.0),
|
|
roughness=1.0, metallic=0.0, emission_strength=1.0, **kwargs):
|
|
add = nodes.new("ShaderNodeAddShader"); add.location = (500, y)
|
|
diff = nodes.new("ShaderNodeBsdfDiffuse"); diff.location = (0, y + 100)
|
|
emit = nodes.new("ShaderNodeEmission"); emit.location = (0, y - 100)
|
|
ramp = nodes.new("ShaderNodeValToRGB"); ramp.location = (-250, y - 100)
|
|
noise = nodes.new("ShaderNodeTexNoise"); noise.location = (-500, y - 100)
|
|
coord = nodes.new("ShaderNodeTexCoord"); coord.location = (-700, y - 100)
|
|
diff.inputs["Color"].default_value = (color[0] * 0.08, color[1] * 0.08, color[2] * 0.08, 1.0)
|
|
diff.inputs["Roughness"].default_value = roughness
|
|
emit.inputs["Strength"].default_value = emission_strength * 4.0
|
|
noise.inputs["Scale"].default_value = 4.0
|
|
noise.inputs["Detail"].default_value = 8.0
|
|
cr = ramp.color_ramp
|
|
cr.elements[0].position = 0.0; cr.elements[0].color = (0.0, 0.0, 0.0, 1.0)
|
|
cr.elements[1].position = 1.0; cr.elements[1].color = color
|
|
cr.elements.new(0.55).color = (color[0], color[1] * 0.1, color[2] * 0.1, 1.0)
|
|
cr.elements.new(0.85).color = (color[0], color[1] * 0.9, color[2] * 0.9, 1.0)
|
|
links.new(coord.outputs["Generated"], noise.inputs["Vector"])
|
|
links.new(noise.outputs["Fac"], ramp.inputs["Fac"])
|
|
links.new(ramp.outputs["Color"], emit.inputs["Color"])
|
|
links.new(diff.outputs["BSDF"], add.inputs[0])
|
|
links.new(emit.outputs["Emission"], add.inputs[1])
|
|
return add.outputs["Shader"]
|
|
|
|
|
|
def _inner_IRIDESCENT(nodes, links, y=0, color=(1.0, 1.0, 1.0, 1.0),
|
|
roughness=0.05, metallic=0.0, emission_strength=1.0, **kwargs):
|
|
add = nodes.new("ShaderNodeAddShader"); add.location = (500, y)
|
|
glos = nodes.new("ShaderNodeBsdfGlossy"); glos.location = (0, y + 100)
|
|
emit = nodes.new("ShaderNodeEmission"); emit.location = (0, y - 100)
|
|
ramp = nodes.new("ShaderNodeValToRGB"); ramp.location = (-250, y - 100)
|
|
lw = nodes.new("ShaderNodeLayerWeight"); lw.location = (-450, y - 100)
|
|
glos.inputs["Roughness"].default_value = roughness
|
|
emit.inputs["Strength"].default_value = emission_strength * 1.2
|
|
lw.inputs["Blend"].default_value = 0.5
|
|
cr = ramp.color_ramp
|
|
cr.interpolation = 'LINEAR'
|
|
rainbow = [
|
|
(0.0, (1.0, 0.0, 0.0, 1.0)),
|
|
(0.17, (1.0, 0.5, 0.0, 1.0)),
|
|
(0.33, (1.0, 1.0, 0.0, 1.0)),
|
|
(0.5, (0.0, 1.0, 0.0, 1.0)),
|
|
(0.67, (0.0, 0.5, 1.0, 1.0)),
|
|
(0.83, (0.5, 0.0, 1.0, 1.0)),
|
|
(1.0, (1.0, 0.0, 0.5, 1.0)),
|
|
]
|
|
for i, (pos, col) in enumerate(rainbow):
|
|
if i < 2:
|
|
cr.elements[i].position = pos
|
|
cr.elements[i].color = col
|
|
else:
|
|
cr.elements.new(pos).color = col
|
|
links.new(lw.outputs["Facing"], ramp.inputs["Fac"])
|
|
links.new(ramp.outputs["Color"], emit.inputs["Color"])
|
|
links.new(ramp.outputs["Color"], glos.inputs["Color"])
|
|
links.new(glos.outputs["BSDF"], add.inputs[0])
|
|
links.new(emit.outputs["Emission"], add.inputs[1])
|
|
return add.outputs["Shader"]
|
|
|
|
|
|
def _inner_MATTE_CLAY(nodes, links, y=0, color=(0.6, 0.4, 0.3, 1.0),
|
|
roughness=0.95, metallic=0.0, emission_strength=1.0, **kwargs):
|
|
prin = nodes.new("ShaderNodeBsdfPrincipled"); prin.location = (0, y)
|
|
prin.inputs["Base Color"].default_value = color
|
|
prin.inputs["Roughness"].default_value = roughness
|
|
if "Specular" in prin.inputs:
|
|
prin.inputs["Specular"].default_value = 0.1
|
|
return prin.outputs["BSDF"]
|
|
|
|
|
|
def _inner_GHOST(nodes, links, y=0, color=(0.7, 0.8, 1.0, 1.0),
|
|
roughness=0.1, metallic=0.0, emission_strength=1.0, **kwargs):
|
|
mix = nodes.new("ShaderNodeMixShader"); mix.location = (400, y)
|
|
trans = nodes.new("ShaderNodeBsdfTransparent"); trans.location = (0, y)
|
|
emit = nodes.new("ShaderNodeEmission"); emit.location = (0, y + 150)
|
|
fres = nodes.new("ShaderNodeFresnel"); fres.location = (0, y + 300)
|
|
emit.inputs["Color"].default_value = color
|
|
emit.inputs["Strength"].default_value = emission_strength * 2.0
|
|
fres.inputs["IOR"].default_value = 1.05
|
|
links.new(fres.outputs["Fac"], mix.inputs["Fac"])
|
|
links.new(trans.outputs["BSDF"], mix.inputs[1])
|
|
links.new(emit.outputs["Emission"], mix.inputs[2])
|
|
return mix.outputs["Shader"]
|
|
|
|
|
|
def _inner_CAR_PAINT(nodes, links, y=0, color=(0.8, 0.05, 0.1, 1.0),
|
|
roughness=0.3, metallic=0.8, emission_strength=1.0, **kwargs):
|
|
prin = nodes.new("ShaderNodeBsdfPrincipled"); prin.location = (0, y)
|
|
prin.inputs["Base Color"].default_value = color
|
|
prin.inputs["Metallic"].default_value = metallic
|
|
prin.inputs["Roughness"].default_value = roughness
|
|
if "Clearcoat" in prin.inputs:
|
|
prin.inputs["Clearcoat"].default_value = 1.0
|
|
prin.inputs["Clearcoat Roughness"].default_value = 0.03
|
|
elif "Coat Weight" in prin.inputs:
|
|
prin.inputs["Coat Weight"].default_value = 1.0
|
|
prin.inputs["Coat Roughness"].default_value = 0.03
|
|
return prin.outputs["BSDF"]
|
|
|
|
|
|
def _inner_CHROME(nodes, links, y=0, color=(0.95, 0.95, 0.95, 1.0),
|
|
roughness=0.01, metallic=1.0, emission_strength=1.0, **kwargs):
|
|
prin = nodes.new("ShaderNodeBsdfPrincipled"); prin.location = (0, y)
|
|
prin.inputs["Base Color"].default_value = color
|
|
prin.inputs["Metallic"].default_value = metallic
|
|
prin.inputs["Roughness"].default_value = roughness
|
|
return prin.outputs["BSDF"]
|
|
|
|
|
|
def _inner_RUBY_GLASS(nodes, links, y=0, color=(1.0, 0.05, 0.05, 1.0),
|
|
roughness=0.02, metallic=0.0, emission_strength=1.0, **kwargs):
|
|
prin = nodes.new("ShaderNodeBsdfPrincipled"); prin.location = (0, y)
|
|
prin.inputs["Base Color"].default_value = color
|
|
prin.inputs["Metallic"].default_value = metallic
|
|
prin.inputs["Roughness"].default_value = roughness
|
|
prin.inputs["IOR"].default_value = 1.6
|
|
for key in ("Transmission Weight", "Transmission"):
|
|
if key in prin.inputs:
|
|
prin.inputs[key].default_value = 1.0
|
|
break
|
|
return prin.outputs["BSDF"]
|
|
|
|
|
|
def _inner_FROSTED_ICE(nodes, links, y=0, color=(0.9, 0.95, 1.0, 1.0),
|
|
roughness=0.4, metallic=0.0, emission_strength=1.0, **kwargs):
|
|
prin = nodes.new("ShaderNodeBsdfPrincipled"); prin.location = (0, y)
|
|
prin.inputs["Base Color"].default_value = color
|
|
prin.inputs["Metallic"].default_value = metallic
|
|
prin.inputs["Roughness"].default_value = roughness
|
|
prin.inputs["IOR"].default_value = 1.31
|
|
for key in ("Transmission Weight", "Transmission"):
|
|
if key in prin.inputs:
|
|
prin.inputs[key].default_value = 1.0
|
|
break
|
|
return prin.outputs["BSDF"]
|
|
|
|
|
|
def _inner_LICHEN_ROUGH(nodes, links, y=0, color=(0.3, 0.6, 0.1, 1.0),
|
|
roughness=0.9, metallic=0.0, emission_strength=1.0, **kwargs):
|
|
mix = nodes.new("ShaderNodeMixShader"); mix.location = (300, y)
|
|
diff1 = nodes.new("ShaderNodeBsdfDiffuse"); diff1.location = (0, y + 100)
|
|
diff2 = nodes.new("ShaderNodeBsdfDiffuse"); diff2.location = (0, y - 100)
|
|
noise = nodes.new("ShaderNodeTexNoise"); noise.location = (-400, y)
|
|
ramp = nodes.new("ShaderNodeValToRGB"); ramp.location = (-200, y)
|
|
coord = nodes.new("ShaderNodeTexCoord"); coord.location = (-600, y)
|
|
diff1.inputs["Color"].default_value = (0.15, 0.15, 0.15, 1.0)
|
|
diff1.inputs["Roughness"].default_value = 1.0
|
|
diff2.inputs["Color"].default_value = color
|
|
diff2.inputs["Roughness"].default_value = roughness
|
|
noise.inputs["Scale"].default_value = 8.0
|
|
noise.inputs["Detail"].default_value = 6.0
|
|
cr = ramp.color_ramp
|
|
cr.elements[0].position = 0.45; cr.elements[0].color = (0.0, 0.0, 0.0, 1.0)
|
|
cr.elements[1].position = 0.55; cr.elements[1].color = (1.0, 1.0, 1.0, 1.0)
|
|
links.new(coord.outputs["Generated"], noise.inputs["Vector"])
|
|
links.new(noise.outputs["Fac"], ramp.inputs["Fac"])
|
|
links.new(ramp.outputs["Color"], mix.inputs["Fac"])
|
|
links.new(diff1.outputs["BSDF"], mix.inputs[1])
|
|
links.new(diff2.outputs["BSDF"], mix.inputs[2])
|
|
return mix.outputs["Shader"]
|
|
|
|
|
|
def _inner_COPPER_PATINA(nodes, links, y=0, color=(0.2, 0.6, 0.45, 1.0),
|
|
roughness=0.15, metallic=1.0, emission_strength=1.0, **kwargs):
|
|
mix = nodes.new("ShaderNodeMixShader"); mix.location = (300, y)
|
|
prin1 = nodes.new("ShaderNodeBsdfPrincipled"); prin1.location = (0, y + 120)
|
|
prin2 = nodes.new("ShaderNodeBsdfPrincipled"); prin2.location = (0, y - 120)
|
|
noise = nodes.new("ShaderNodeTexNoise"); noise.location = (-400, y)
|
|
ramp = nodes.new("ShaderNodeValToRGB"); ramp.location = (-200, y)
|
|
coord = nodes.new("ShaderNodeTexCoord"); coord.location = (-600, y)
|
|
prin1.inputs["Base Color"].default_value = (0.95, 0.45, 0.3, 1.0)
|
|
prin1.inputs["Metallic"].default_value = metallic
|
|
prin1.inputs["Roughness"].default_value = roughness
|
|
prin2.inputs["Base Color"].default_value = color
|
|
prin2.inputs["Metallic"].default_value = 0.0
|
|
prin2.inputs["Roughness"].default_value = 0.85
|
|
noise.inputs["Scale"].default_value = 6.0
|
|
noise.inputs["Detail"].default_value = 4.0
|
|
cr = ramp.color_ramp
|
|
cr.elements[0].position = 0.4; cr.elements[0].color = (0.0, 0.0, 0.0, 1.0)
|
|
cr.elements[1].position = 0.6; cr.elements[1].color = (1.0, 1.0, 1.0, 1.0)
|
|
links.new(coord.outputs["Generated"], noise.inputs["Vector"])
|
|
links.new(noise.outputs["Fac"], ramp.inputs["Fac"])
|
|
links.new(ramp.outputs["Color"], mix.inputs["Fac"])
|
|
links.new(prin1.outputs["BSDF"], mix.inputs[1])
|
|
links.new(prin2.outputs["BSDF"], mix.inputs[2])
|
|
return mix.outputs["Shader"]
|
|
|
|
|
|
def _inner_ZEBRA_STRIPES(nodes, links, y=0, color=(0.98, 0.98, 0.98, 1.0),
|
|
roughness=0.1, metallic=0.0, emission_strength=1.0, **kwargs):
|
|
mix = nodes.new("ShaderNodeMixShader"); mix.location = (300, y)
|
|
prin1 = nodes.new("ShaderNodeBsdfPrincipled"); prin1.location = (0, y + 100)
|
|
prin2 = nodes.new("ShaderNodeBsdfPrincipled"); prin2.location = (0, y - 100)
|
|
wave = nodes.new("ShaderNodeTexWave"); wave.location = (-400, y)
|
|
ramp = nodes.new("ShaderNodeValToRGB"); ramp.location = (-200, y)
|
|
coord = nodes.new("ShaderNodeTexCoord"); coord.location = (-600, y)
|
|
prin1.inputs["Base Color"].default_value = (0.02, 0.02, 0.02, 1.0)
|
|
prin1.inputs["Metallic"].default_value = metallic
|
|
prin1.inputs["Roughness"].default_value = roughness
|
|
prin2.inputs["Base Color"].default_value = color
|
|
prin2.inputs["Metallic"].default_value = metallic
|
|
prin2.inputs["Roughness"].default_value = roughness
|
|
wave.inputs["Scale"].default_value = 5.0
|
|
cr = ramp.color_ramp
|
|
cr.interpolation = 'CONSTANT'
|
|
cr.elements[0].position = 0.0; cr.elements[0].color = (0.0, 0.0, 0.0, 1.0)
|
|
cr.elements[1].position = 0.5; cr.elements[1].color = (1.0, 1.0, 1.0, 1.0)
|
|
links.new(coord.outputs["Generated"], wave.inputs["Vector"])
|
|
links.new(wave.outputs["Color"], ramp.inputs["Fac"])
|
|
links.new(ramp.outputs["Color"], mix.inputs["Fac"])
|
|
links.new(prin1.outputs["BSDF"], mix.inputs[1])
|
|
links.new(prin2.outputs["BSDF"], mix.inputs[2])
|
|
return mix.outputs["Shader"]
|
|
|
|
|
|
def _inner_WOOD_VENEER(nodes, links, y=0, color=(0.4, 0.2, 0.08, 1.0),
|
|
roughness=0.2, metallic=0.0, emission_strength=1.0, **kwargs):
|
|
prin = nodes.new("ShaderNodeBsdfPrincipled"); prin.location = (300, y)
|
|
noise = nodes.new("ShaderNodeTexNoise"); noise.location = (-100, y)
|
|
ramp = nodes.new("ShaderNodeValToRGB"); ramp.location = (100, y)
|
|
mapping = nodes.new("ShaderNodeMapping"); mapping.location = (-300, y)
|
|
coord = nodes.new("ShaderNodeTexCoord"); coord.location = (-500, y)
|
|
mapping.inputs["Scale"].default_value = (1.0, 10.0, 1.0)
|
|
noise.inputs["Scale"].default_value = 4.0
|
|
noise.inputs["Detail"].default_value = 4.0
|
|
cr = ramp.color_ramp
|
|
cr.interpolation = 'LINEAR'
|
|
cr.elements[0].position = 0.0; cr.elements[0].color = (color[0] * 0.4, color[1] * 0.4, color[2] * 0.4, 1.0)
|
|
cr.elements[1].position = 1.0; cr.elements[1].color = color
|
|
cr.elements.new(0.5).color = (color[0] * 0.7, color[1] * 0.7, color[2] * 0.7, 1.0)
|
|
prin.inputs["Metallic"].default_value = metallic
|
|
prin.inputs["Roughness"].default_value = roughness
|
|
links.new(coord.outputs["Generated"], mapping.inputs["Vector"])
|
|
links.new(mapping.outputs["Vector"], noise.inputs["Vector"])
|
|
links.new(noise.outputs["Fac"], ramp.inputs["Fac"])
|
|
links.new(ramp.outputs["Color"], prin.inputs["Base Color"])
|
|
return prin.outputs["BSDF"]
|
|
|
|
|
|
def _inner_CARBON_FIBER(nodes, links, y=0, color=(0.1, 0.1, 0.1, 1.0),
|
|
roughness=0.15, metallic=0.0, emission_strength=1.0, **kwargs):
|
|
mix = nodes.new("ShaderNodeMixShader"); mix.location = (300, y)
|
|
prin1 = nodes.new("ShaderNodeBsdfPrincipled"); prin1.location = (0, y + 100)
|
|
prin2 = nodes.new("ShaderNodeBsdfPrincipled"); prin2.location = (0, y - 100)
|
|
check = nodes.new("ShaderNodeTexChecker"); check.location = (-200, y)
|
|
coord = nodes.new("ShaderNodeTexCoord"); coord.location = (-400, y)
|
|
prin1.inputs["Base Color"].default_value = (0.05, 0.05, 0.05, 1.0)
|
|
prin1.inputs["Metallic"].default_value = 0.5
|
|
prin1.inputs["Roughness"].default_value = roughness
|
|
prin2.inputs["Base Color"].default_value = color
|
|
prin2.inputs["Metallic"].default_value = 0.5
|
|
prin2.inputs["Roughness"].default_value = roughness
|
|
check.inputs["Scale"].default_value = 25.0
|
|
links.new(coord.outputs["Generated"], check.inputs["Vector"])
|
|
links.new(check.outputs["Color"], mix.inputs["Fac"])
|
|
links.new(prin1.outputs["BSDF"], mix.inputs[1])
|
|
links.new(prin2.outputs["BSDF"], mix.inputs[2])
|
|
return mix.outputs["Shader"]
|
|
|
|
|
|
def _inner_PEARLESCENT(nodes, links, y=0, color=(1.0, 0.4, 0.7, 1.0),
|
|
roughness=0.05, metallic=0.1, emission_strength=1.0, **kwargs):
|
|
prin = nodes.new("ShaderNodeBsdfPrincipled"); prin.location = (300, y)
|
|
ramp = nodes.new("ShaderNodeValToRGB"); ramp.location = (100, y)
|
|
lw = nodes.new("ShaderNodeLayerWeight"); lw.location = (-100, y)
|
|
lw.inputs["Blend"].default_value = 0.35
|
|
cr = ramp.color_ramp
|
|
cr.interpolation = 'LINEAR'
|
|
cr.elements[0].position = 0.0; cr.elements[0].color = (0.1, 0.8, 1.0, 1.0)
|
|
cr.elements[1].position = 1.0; cr.elements[1].color = color
|
|
prin.inputs["Metallic"].default_value = metallic
|
|
prin.inputs["Roughness"].default_value = roughness
|
|
if "Clearcoat" in prin.inputs:
|
|
prin.inputs["Clearcoat"].default_value = 1.0
|
|
prin.inputs["Clearcoat Roughness"].default_value = 0.02
|
|
links.new(lw.outputs["Facing"], ramp.inputs["Fac"])
|
|
links.new(ramp.outputs["Color"], prin.inputs["Base Color"])
|
|
return prin.outputs["BSDF"]
|
|
|
|
|
|
def _inner_PLASMA_GLOW(nodes, links, y=0, color=(1.0, 0.2, 0.8, 1.0),
|
|
roughness=0.1, metallic=0.0, emission_strength=1.0, **kwargs):
|
|
emit = nodes.new("ShaderNodeEmission"); emit.location = (300, y)
|
|
ramp = nodes.new("ShaderNodeValToRGB"); ramp.location = (100, y)
|
|
voro = nodes.new("ShaderNodeTexVoronoi"); voro.location = (-100, y)
|
|
coord = nodes.new("ShaderNodeTexCoord"); coord.location = (-300, y)
|
|
voro.inputs["Scale"].default_value = 6.0
|
|
cr = ramp.color_ramp
|
|
cr.interpolation = 'LINEAR'
|
|
cr.elements[0].position = 0.0; cr.elements[0].color = (0.1, 0.0, 0.3, 1.0)
|
|
cr.elements[1].position = 1.0; cr.elements[1].color = (1.0, 0.9, 0.5, 1.0)
|
|
cr.elements.new(0.5).color = color
|
|
emit.inputs["Strength"].default_value = emission_strength * 8.0
|
|
links.new(coord.outputs["Generated"], voro.inputs["Vector"])
|
|
links.new(voro.outputs["Distance"], ramp.inputs["Fac"])
|
|
links.new(ramp.outputs["Color"], emit.inputs["Color"])
|
|
return emit.outputs["Emission"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Auto-discovery — P5 fix
|
|
# INNER_FACTORIES and SHADER_IDS are built from the _inner_* naming convention.
|
|
# Python 3.7+ dicts preserve insertion order, so SHADER_IDS matches the
|
|
# function definition order above.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
INNER_FACTORIES: dict = {
|
|
name[len("_inner_"):]: fn
|
|
for name, fn in globals().items()
|
|
if name.startswith("_inner_") and callable(fn)
|
|
}
|
|
|
|
# Stable ordered list used by EnumProperty items and the unregister purge.
|
|
SHADER_IDS: list = list(INNER_FACTORIES)
|
|
|
|
# Shaders that require alpha blending on the material.
|
|
_TRANSPARENT_SHADERS: frozenset = frozenset({
|
|
"GLASS", "HOLOGRAM", "GHOST", "RUBY_GLASS", "FROSTED_ICE",
|
|
})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Material name helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _shader_mat_name(shader_id: str) -> str:
|
|
return f"KnotShader_{shader_id}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shader material cache
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _get_shader_material(shader_id: str):
|
|
"""Look up the named shader material. Returns None if not yet built.
|
|
|
|
This is the HANDLER-SAFE path — it never allocates.
|
|
"""
|
|
return bpy.data.materials.get(_shader_mat_name(shader_id))
|
|
|
|
|
|
def _ensure_shader_material(shader_id: str):
|
|
"""Get-or-create the named shader material with default properties.
|
|
|
|
MAIN THREAD ONLY.
|
|
"""
|
|
mat_name = _shader_mat_name(shader_id)
|
|
mat = bpy.data.materials.get(mat_name)
|
|
if mat is None:
|
|
mat = bpy.data.materials.new(name=mat_name)
|
|
if hasattr(mat, "use_nodes"):
|
|
try:
|
|
mat.use_nodes = True
|
|
except AttributeError:
|
|
pass
|
|
nodes = mat.node_tree.nodes
|
|
links = mat.node_tree.links
|
|
nodes.clear()
|
|
out = nodes.new("ShaderNodeOutputMaterial")
|
|
out.location = (600, 0)
|
|
sock = INNER_FACTORIES[shader_id](nodes, links, y=0)
|
|
links.new(sock, out.inputs["Surface"])
|
|
if shader_id in _TRANSPARENT_SHADERS:
|
|
mat.blend_method = 'BLEND'
|
|
return mat
|
|
|
|
|
|
def _ensure_item_preset_material(item):
|
|
"""Ensure the per-KnotItem preset material exists and is up to date.
|
|
|
|
MAIN THREAD ONLY. Call before playback or from property update callbacks.
|
|
"""
|
|
import random
|
|
if not item.uid:
|
|
item.uid = f"knot_{random.randint(100000, 999999)}"
|
|
|
|
mat_name = f"KnotItem_Preset_{item.uid}"
|
|
mat = bpy.data.materials.get(mat_name)
|
|
if mat is None:
|
|
mat = bpy.data.materials.new(name=mat_name)
|
|
|
|
if hasattr(mat, "use_nodes"):
|
|
try:
|
|
mat.use_nodes = True
|
|
except AttributeError:
|
|
pass
|
|
|
|
nodes = mat.node_tree.nodes
|
|
links = mat.node_tree.links
|
|
nodes.clear()
|
|
|
|
out = nodes.new("ShaderNodeOutputMaterial")
|
|
out.location = (600, 0)
|
|
|
|
color = (item.preset_color[0], item.preset_color[1], item.preset_color[2], 1.0)
|
|
roughness = item.preset_roughness
|
|
metallic = item.preset_metallic
|
|
emission_strength = item.preset_emission_strength
|
|
|
|
sock = INNER_FACTORIES[item.shader_id](
|
|
nodes, links, y=0,
|
|
color=color,
|
|
roughness=roughness,
|
|
metallic=metallic,
|
|
emission_strength=emission_strength,
|
|
)
|
|
links.new(sock, out.inputs["Surface"])
|
|
|
|
if item.shader_id in _TRANSPARENT_SHADERS:
|
|
mat.blend_method = 'BLEND'
|
|
else:
|
|
mat.blend_method = 'OPAQUE'
|
|
return mat
|
|
|
|
|
|
def get_effective_material(item):
|
|
"""Return the bpy.types.Material currently active for this KnotItem."""
|
|
if item.material_mode == 'PROJECT':
|
|
return item.project_material
|
|
return _ensure_item_preset_material(item)
|
|
|
|
|
|
def _update_viewport_knot_material(scene) -> None:
|
|
"""Instantly update the viewport AnimKnot object's material slot."""
|
|
obj = bpy.data.objects.get(KNOT_OBJ_NAME)
|
|
if not obj:
|
|
return
|
|
idx = scene.knot_list_index
|
|
if 0 <= idx < len(scene.knot_list):
|
|
item = scene.knot_list[idx]
|
|
mat = get_effective_material(item)
|
|
if mat:
|
|
if len(obj.data.materials) == 0:
|
|
obj.data.materials.append(mat)
|
|
else:
|
|
obj.data.materials[0] = mat
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Node-copy helper for cross-material blend materials
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _copy_nodes_to_blend(src_mat, dst_mat, offset_y: int, prefix: str):
|
|
"""Robustly duplicate a source material's node setup into a blend material.
|
|
|
|
Returns the final output shader socket so the caller can wire it into a
|
|
MixShader. Falls back to a plain Principled BSDF if anything goes wrong.
|
|
"""
|
|
if not src_mat or getattr(src_mat, "node_tree", None) is None:
|
|
nodes = dst_mat.node_tree.nodes
|
|
fallback = nodes.new("ShaderNodeBsdfPrincipled")
|
|
fallback.name = f"{prefix}_fallback"
|
|
fallback.location = (-300, offset_y)
|
|
if src_mat:
|
|
fallback.inputs["Base Color"].default_value = src_mat.diffuse_color
|
|
return fallback.outputs["BSDF"]
|
|
|
|
nodes = dst_mat.node_tree.nodes
|
|
links = dst_mat.node_tree.links
|
|
|
|
node_map = {}
|
|
for src_node in src_mat.node_tree.nodes:
|
|
if src_node.type == 'OUTPUT_MATERIAL':
|
|
continue
|
|
new_node = nodes.new(src_node.bl_idname)
|
|
new_node.name = f"{prefix}_{src_node.name}"
|
|
new_node.label = src_node.label
|
|
new_node.location = (src_node.location.x - 300, src_node.location.y + offset_y)
|
|
|
|
# Copy only non-readonly primitive values
|
|
for prop in src_node.bl_rna.properties:
|
|
if prop.identifier in {'rna_type', 'type', 'inputs', 'outputs',
|
|
'internal_links', 'bl_idname'}:
|
|
continue
|
|
if not prop.is_readonly and prop.type in {'BOOLEAN', 'INT', 'FLOAT', 'STRING', 'ENUM'}:
|
|
try:
|
|
setattr(new_node, prop.identifier, getattr(src_node, prop.identifier))
|
|
except Exception:
|
|
pass
|
|
|
|
# Special-case: ColorRamp needs manual element duplication
|
|
if src_node.type == 'VALTO_RGB':
|
|
try:
|
|
new_ramp = new_node.color_ramp
|
|
src_ramp = src_node.color_ramp
|
|
new_ramp.interpolation = src_ramp.interpolation
|
|
while len(new_ramp.elements) < len(src_ramp.elements):
|
|
new_ramp.elements.new(0.5)
|
|
while len(new_ramp.elements) > len(src_ramp.elements):
|
|
new_ramp.elements.remove(new_ramp.elements[-1])
|
|
for idx, el in enumerate(src_ramp.elements):
|
|
new_ramp.elements[idx].position = el.position
|
|
new_ramp.elements[idx].color = el.color
|
|
except Exception:
|
|
pass
|
|
|
|
for i, inp in enumerate(src_node.inputs):
|
|
try:
|
|
new_node.inputs[i].default_value = inp.default_value
|
|
except Exception:
|
|
pass
|
|
|
|
node_map[src_node] = new_node
|
|
|
|
# Reconnect internal links
|
|
for link in src_mat.node_tree.links:
|
|
if link.to_node.type == 'OUTPUT_MATERIAL':
|
|
continue
|
|
try:
|
|
from_node = node_map.get(link.from_node)
|
|
to_node = node_map.get(link.to_node)
|
|
if not from_node or not to_node:
|
|
continue
|
|
try:
|
|
out_sock = from_node.outputs[link.from_socket.identifier]
|
|
except (KeyError, IndexError):
|
|
f_idx = list(link.from_node.outputs).index(link.from_socket)
|
|
out_sock = from_node.outputs[f_idx] if f_idx < len(from_node.outputs) else None
|
|
try:
|
|
in_sock = to_node.inputs[link.to_socket.identifier]
|
|
except (KeyError, IndexError):
|
|
t_idx = list(link.to_node.inputs).index(link.to_socket)
|
|
in_sock = to_node.inputs[t_idx] if t_idx < len(to_node.inputs) else None
|
|
if out_sock and in_sock:
|
|
links.new(out_sock, in_sock)
|
|
except Exception:
|
|
pass
|
|
|
|
# Find the source surface output socket via the Material Output node
|
|
src_out = next(
|
|
(n for n in src_mat.node_tree.nodes if n.type == 'OUTPUT_MATERIAL' and n.is_active_output),
|
|
None,
|
|
) or next(
|
|
(n for n in src_mat.node_tree.nodes if n.type == 'OUTPUT_MATERIAL'),
|
|
None,
|
|
)
|
|
|
|
if src_out and len(src_out.inputs["Surface"].links) > 0:
|
|
src_link = src_out.inputs["Surface"].links[0]
|
|
mapped_node = node_map.get(src_link.from_node)
|
|
if mapped_node and len(mapped_node.outputs) > 0:
|
|
try:
|
|
return mapped_node.outputs[src_link.from_socket.identifier]
|
|
except (KeyError, IndexError):
|
|
pass
|
|
try:
|
|
f_idx = list(src_link.from_node.outputs).index(src_link.from_socket)
|
|
if f_idx < len(mapped_node.outputs):
|
|
return mapped_node.outputs[f_idx]
|
|
except (ValueError, IndexError):
|
|
pass
|
|
return mapped_node.outputs[0]
|
|
|
|
# Final fallback
|
|
fallback = nodes.new("ShaderNodeBsdfPrincipled")
|
|
fallback.name = f"{prefix}_fallback"
|
|
fallback.location = (-300, offset_y)
|
|
return fallback.outputs["BSDF"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Playlist blend material system
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def prebuild_playlist_blend_materials(scene) -> None:
|
|
"""Pre-build cross-fade blend materials for every adjacent playlist pair.
|
|
|
|
Called from the main thread (operators, property callbacks) so the frame
|
|
handler never needs to allocate node trees itself.
|
|
"""
|
|
if not hasattr(scene, "knot_list") or len(scene.knot_list) == 0:
|
|
return
|
|
|
|
for i, item in enumerate(scene.knot_list):
|
|
if item.transition_frames <= 0:
|
|
continue
|
|
next_item = scene.knot_list[(i + 1) % len(scene.knot_list)]
|
|
mat_a = get_effective_material(item)
|
|
mat_b = get_effective_material(next_item)
|
|
|
|
blend_name = f"KnotBlend_Item_{i}"
|
|
blend_mat = bpy.data.materials.get(blend_name)
|
|
if not blend_mat:
|
|
blend_mat = bpy.data.materials.new(name=blend_name)
|
|
|
|
if hasattr(blend_mat, "use_nodes"):
|
|
try:
|
|
blend_mat.use_nodes = True
|
|
except AttributeError:
|
|
pass
|
|
|
|
nodes = blend_mat.node_tree.nodes
|
|
links = blend_mat.node_tree.links
|
|
nodes.clear()
|
|
|
|
out = nodes.new("ShaderNodeOutputMaterial")
|
|
out.location = (600, 0)
|
|
mix = nodes.new("ShaderNodeMixShader")
|
|
mix.name = "_KnotBlendMix"
|
|
mix.location = (300, 0)
|
|
|
|
sock_a = _copy_nodes_to_blend(mat_a, blend_mat, 250, "A")
|
|
sock_b = _copy_nodes_to_blend(mat_b, blend_mat, -250, "B")
|
|
|
|
links.new(sock_a, mix.inputs[1])
|
|
links.new(sock_b, mix.inputs[2])
|
|
links.new(mix.outputs["Shader"], out.inputs["Surface"])
|
|
|
|
if (mat_a and mat_a.blend_method == 'BLEND') or (mat_b and mat_b.blend_method == 'BLEND'):
|
|
blend_mat.blend_method = 'BLEND'
|
|
else:
|
|
blend_mat.blend_method = 'OPAQUE'
|
|
|
|
|
|
def prewarm_materials_and_blends(scene) -> None:
|
|
"""Ensure all playlist preset materials are generated and transitions pre-built."""
|
|
if not hasattr(scene, "knot_list"):
|
|
return
|
|
for item in scene.knot_list:
|
|
if item.material_mode == 'PRESET':
|
|
_ensure_item_preset_material(item)
|
|
prebuild_playlist_blend_materials(scene)
|