Source code for procfunc.nodes.texture

"""
Texture node bindings for Blender (ShaderNodeTex*).

These pattern-generator nodes (noise, voronoi, etc.) work in BOTH shader and
geometry node trees. They live in their own module to avoid the misleading
implication that they are shader-only.
"""

import logging
from typing import Any, Literal, NamedTuple

from procfunc import types as pt
from procfunc.nodes import types as nt
from procfunc.nodes.util.bindings_util import raise_explicit_noise_vector_error

logger = logging.getLogger(__name__)


TNoiseType = Literal[
    "MULTIFRACTAL",
    "FBM",
    "RIDGED_MULTIFRACTAL",
    "HYBRID_MULTIFRACTAL",
    "HETERO_TERRAIN",
]
TNoiseDimensions = Literal["1D", "2D", "3D", "4D"]
TDistanceMetric = Literal["EUCLIDEAN", "MANHATTAN", "CHEBYCHEV", "MINKOWSKI"]
TTextureInterpolationType = Literal["Linear", "Closest", "Cubic", "Smart"]  # TODO


[docs] class TextureResult(NamedTuple): fac: nt.ProcNode[float] color: nt.ProcNode[pt.Color]
[docs] class VoronoiResult(NamedTuple): color: nt.ProcNode[pt.Color] distance: nt.ProcNode[float] position: nt.ProcNode[pt.Vector] w: nt.ProcNode[float] | None
[docs] class PointDensityResult(NamedTuple): color: nt.ProcNode[pt.Color] density: nt.ProcNode[float]
[docs] def brick( vector: nt.SocketOrVal[pt.Vector], color1: nt.SocketOrVal[pt.Color] = (0.8, 0.8, 0.8, 1), color2: nt.SocketOrVal[pt.Color] = (0.2, 0.2, 0.2, 1), mortar: nt.SocketOrVal[pt.Color] = (0, 0, 0, 1), scale: nt.SocketOrVal[float] = 5.0, mortar_size: nt.SocketOrVal[float] = 0.02, mortar_smooth: nt.SocketOrVal[float] = 0.1, bias: nt.SocketOrVal[float] = 0.0, brick_width: nt.SocketOrVal[float] = 0.5, row_height: nt.SocketOrVal[float] = 0.25, offset: float = 0.5, offset_frequency: int = 2, squash: float = 1.0, squash_frequency: int = 2, ) -> TextureResult: """ Uses a TexBrick Shader Node. procfunc requires an explicit `vector` input - the node will not default to using texture coordinates or world coordinates the way Blender does See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/textures/brick.html """ if vector is None: raise_explicit_noise_vector_error("brick", logger=logger) res = nt.ProcNode.from_nodetype( node_type="ShaderNodeTexBrick", inputs={ "Vector": vector, "Color1": color1, "Color2": color2, "Mortar": mortar, "Scale": scale, "Mortar Size": mortar_size, "Mortar Smooth": mortar_smooth, "Bias": bias, "Brick Width": brick_width, "Row Height": row_height, }, attrs={ "offset": offset, "offset_frequency": offset_frequency, "squash": squash, "squash_frequency": squash_frequency, }, ) return TextureResult( fac=res._output_socket("fac"), color=res._output_socket("color"), )
[docs] def checker( vector: nt.SocketOrVal[pt.Vector], color1: nt.SocketOrVal[pt.Color] = (0.8, 0.8, 0.8, 1), color2: nt.SocketOrVal[pt.Color] = (0.2, 0.2, 0.2, 1), scale: nt.SocketOrVal[float] = 5.0, ) -> TextureResult: """ Uses a TexChecker Shader Node. procfunc requires an explicit `vector` input - the node will not default to using texture coordinates or world coordinates the way Blender does See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/textures/checker.html """ if vector is None: raise_explicit_noise_vector_error("checker", logger=logger) res = nt.ProcNode.from_nodetype( node_type="ShaderNodeTexChecker", inputs={"Vector": vector, "Color1": color1, "Color2": color2, "Scale": scale}, attrs={}, ) return TextureResult( fac=res._output_socket("fac"), color=res._output_socket("color"), )
[docs] def environment( vector: nt.SocketOrVal[pt.Vector], image: Any = None, interpolation: TTextureInterpolationType = "Linear", projection: Literal["EQUIRECTANGULAR", "MIRROR_BALL"] = "EQUIRECTANGULAR", ) -> nt.ProcNode[pt.Color]: """ Uses a TexEnvironment Shader Node. procfunc requires an explicit `vector` input - the node will not default to using texture coordinates or world coordinates the way Blender does See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/textures/environment.html """ if vector is None: raise_explicit_noise_vector_error("environment", logger=logger) return nt.ProcNode.from_nodetype( node_type="ShaderNodeTexEnvironment", inputs={"Vector": vector}, attrs={ "image": image, "interpolation": interpolation, "projection": projection, }, )
[docs] def gradient( vector: nt.SocketOrVal[pt.Vector], gradient_type: Literal[ "LINEAR", "QUADRATIC", "EASING", "DIAGONAL", "SPHERICAL", "QUADRATIC_SPHERE", "RADIAL", ] = "LINEAR", ) -> TextureResult: """ Uses a TexGradient Shader Node. procfunc requires an explicit `vector` input - the node will not default to using texture coordinates or world coordinates the way Blender does See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/textures/gradient.html """ if vector is None: raise_explicit_noise_vector_error("gradient", logger=logger) res = nt.ProcNode.from_nodetype( node_type="ShaderNodeTexGradient", inputs={"Vector": vector}, attrs={"gradient_type": gradient_type}, ) return TextureResult( fac=res._output_socket("fac"), color=res._output_socket("color"), )
[docs] def ies( vector: nt.SocketOrVal[pt.Vector], strength: nt.SocketOrVal[float] = 1.0, filepath: str = "", ies: Any = None, mode: Literal["INTERNAL", "EXTERNAL"] = "INTERNAL", ) -> nt.ProcNode[float]: """ Uses a TexIES Shader Node. procfunc requires an explicit `vector` input - the node will not default to using texture coordinates or world coordinates the way Blender does See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/textures/ies.html """ if vector is None: raise_explicit_noise_vector_error("ies", logger=logger) return nt.ProcNode.from_nodetype( node_type="ShaderNodeTexIES", inputs={"Vector": vector, "Strength": strength}, attrs={"filepath": filepath, "ies": ies, "mode": mode}, )
[docs] def image( vector: nt.SocketOrVal[pt.Vector], # None mirrors a bare ShaderNodeTexImage; attr not socket, strict-None doesnt apply image: pt.Image | None = None, extension: Literal["REPEAT", "EXTEND", "CLIP", "MIRROR"] = "REPEAT", interpolation: TTextureInterpolationType = "Linear", projection: Literal["FLAT", "BOX", "SPHERE", "TUBE"] = "FLAT", projection_blend: float = 0.0, ) -> TextureResult: """ Uses a TexImage Shader Node. procfunc requires an explicit `vector` input - the node will not default to using texture coordinates or world coordinates the way Blender does See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/textures/image.html """ if vector is None: raise_explicit_noise_vector_error("image", logger=logger) res = nt.ProcNode.from_nodetype( node_type="ShaderNodeTexImage", inputs={"Vector": vector}, attrs={ "extension": extension, "image": image, "interpolation": interpolation, "projection": projection, "projection_blend": projection_blend, }, ) return TextureResult( color=res._output_socket("color"), fac=res._output_socket("alpha"), )
[docs] def magic( vector: nt.SocketOrVal[pt.Vector], scale: nt.SocketOrVal[float] = 5.0, distortion: nt.SocketOrVal[float] = 1.0, turbulence_depth: int = 2, ) -> TextureResult: """ Uses a TexMagic Shader Node. procfunc requires an explicit `vector` input - the node will not default to using texture coordinates or world coordinates the way Blender does See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/textures/magic.html """ if vector is None: raise_explicit_noise_vector_error("magic", logger=logger) res = nt.ProcNode.from_nodetype( node_type="ShaderNodeTexMagic", inputs={"Vector": vector, "Scale": scale, "Distortion": distortion}, attrs={"turbulence_depth": turbulence_depth}, ) return TextureResult( fac=res._output_socket("fac"), color=res._output_socket("color"), )
[docs] def noise( vector: nt.SocketOrVal[pt.Vector] | None, scale: nt.SocketOrVal[float] = 5.0, detail: nt.SocketOrVal[float] = 2.0, roughness: nt.SocketOrVal[float] = 0.5, lacunarity: nt.SocketOrVal[float] = 2.0, offset: nt.SocketOrVal[float] = 0.0, gain: nt.SocketOrVal[float] = 1.0, distortion: nt.SocketOrVal[float] = 0.0, noise_dimensions: TNoiseDimensions = "3D", noise_type: TNoiseType = "FBM", normalize: bool = True, w: nt.SocketOrVal[float] | None = None, ) -> TextureResult: """ Uses a TexNoise Shader Node. Args: - offset: Only supported for RIDGED_MULTIFRACTAL, HYBRID_MULTIFRACTAL, HETERO_TERRAIN noise types - gain: Only supported for RIDGED_MULTIFRACTAL and HYBRID_MULTIFRACTAL noise types - distortion: Only supported for RIDGED_MULTIFRACTAL, HYBRID_MULTIFRACTAL, HETERO_TERRAIN noise types - w: Only supported for 1D and 4D noise dimensions - vector: The coordinate to evaluate the noise at. Not available in 1D mode (pass vector=None and use w instead). Passing None explicitly falls back to blender's implicit coordinates, gated by pf.context.globals.warn_mode_avoid_implicit_vector. See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/textures/noise.html """ inputs = { "Scale": scale, "Detail": detail, "Roughness": roughness, "Lacunarity": lacunarity, "Distortion": distortion, } if w is not None: assert noise_dimensions in ["4D", "1D"] inputs["W"] = w elif noise_dimensions == "1D": raise_explicit_noise_vector_error("noise", logger=logger) if noise_dimensions == "1D": if vector is not None: raise ValueError( "noise with noise_dimensions='1D' has no Vector input; use w instead" ) elif vector is None: raise_explicit_noise_vector_error("noise", logger=logger) else: inputs["Vector"] = vector _extra_args_modes = ["RIDGED_MULTIFRACTAL", "HYBRID_MULTIFRACTAL"] # noqa: F841 if offset != 0.0: inputs["Offset"] = offset if gain != 1.0: inputs["Gain"] = gain res = nt.ProcNode.from_nodetype( node_type="ShaderNodeTexNoise", inputs=inputs, attrs={ "noise_dimensions": noise_dimensions, "noise_type": noise_type, "normalize": normalize, }, ) return TextureResult( fac=res._output_socket("fac"), color=res._output_socket("color"), )
[docs] def point_density( vector: nt.SocketOrVal[pt.Vector], interpolation: Literal["Closest", "Linear", "Cubic"] = "Linear", object: Any = None, particle_color_source: Literal[ "PARTICLE_AGE", "PARTICLE_SPEED", "PARTICLE_VELOCITY" ] = "PARTICLE_AGE", particle_system: Any = None, point_source: Literal["OBJECT", "PARTICLE_SYSTEM"] = "PARTICLE_SYSTEM", radius: float = 0.3, resolution: int = 100, space: Literal["OBJECT", "WORLD"] = "OBJECT", vertex_attribute_name: str = "", vertex_color_source: Literal[ "VERTEX_COLOR", "VERTEX_NORMAL", "VERTEX_WEIGHT" ] = "VERTEX_COLOR", ) -> PointDensityResult: """ Uses a TexPointDensity Shader Node. procfunc requires an explicit `vector` input - the node will not default to using texture coordinates or world coordinates the way Blender does TODO: vertex_attribute_name and vertex_color_source are only available for point_source OBJECT See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/textures/point_density.html """ if vector is None: raise_explicit_noise_vector_error("point_density", logger=logger) res = nt.ProcNode.from_nodetype( node_type="ShaderNodeTexPointDensity", inputs={"Vector": vector}, attrs={ "interpolation": interpolation, "object": object, "particle_color_source": particle_color_source, "particle_system": particle_system, "point_source": point_source, "radius": radius, "resolution": resolution, "space": space, "vertex_attribute_name": vertex_attribute_name, "vertex_color_source": vertex_color_source, }, ) return PointDensityResult( color=res._output_socket("color"), density=res._output_socket("density"), )
[docs] def sky( air_density: float = 1.0, altitude: float = 0.0, dust_density: float = 1.0, ground_albedo: float = 0.3, ozone_density: float = 1.0, sky_type: Literal["NISHITA", "HOSEK_WILKIE", "PREETHAM"] = "NISHITA", sun_direction: tuple = (0.0, 0.0, 1.0), sun_disc: bool = True, sun_elevation: float = 0.261799, sun_intensity: float = 1.0, sun_rotation: float = 0.0, sun_size: float = 0.009512, turbidity: float = 2.2, ) -> nt.ProcNode[pt.Color]: """ Uses a TexSky Shader Node. See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/textures/sky.html """ return nt.ProcNode.from_nodetype( node_type="ShaderNodeTexSky", inputs={}, attrs={ "air_density": air_density, "altitude": altitude, "dust_density": dust_density, "ground_albedo": ground_albedo, "ozone_density": ozone_density, "sky_type": sky_type, "sun_direction": sun_direction, "sun_disc": sun_disc, "sun_elevation": sun_elevation, "sun_intensity": sun_intensity, "sun_rotation": sun_rotation, "sun_size": sun_size, "turbidity": turbidity, }, )
[docs] def voronoi( vector: nt.SocketOrVal[pt.Vector] | None, scale: nt.SocketOrVal[float] = 5.0, detail: nt.SocketOrVal[float] = 0.0, roughness: nt.SocketOrVal[float] = 0.5, lacunarity: nt.SocketOrVal[float] = 2.0, randomness: nt.SocketOrVal[float] = 1.0, exponent: nt.SocketOrVal[float] = 0.0, distance: TDistanceMetric = "EUCLIDEAN", feature: Literal["F1", "F2"] = "F1", normalize: bool = False, voronoi_dimensions: TNoiseDimensions = "3D", w: nt.SocketOrVal[float] | None = None, ) -> VoronoiResult: """ Uses a TexVoronoi Shader Node. Args: exponent: Only supported for Minkowski distance. vector: The coordinate to evaluate at. Not available in 1D mode (pass vector=None and use w instead). Passing None explicitly falls back to blender's implicit coordinates, gated by pf.context.globals.warn_mode_avoid_implicit_vector. w: Only supported for 1D and 4D dimensions. See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/textures/voronoi.html """ inputs = { "Scale": scale, "Detail": detail, "Roughness": roughness, "Lacunarity": lacunarity, "Randomness": randomness, } if voronoi_dimensions == "1D": if vector is not None: raise ValueError( "voronoi with voronoi_dimensions='1D' has no Vector input; use w instead" ) elif vector is None: raise_explicit_noise_vector_error("voronoi", logger=logger) else: inputs["Vector"] = vector if exponent != 0.0: assert distance == "MINKOWSKI", ( f"exponent is only supported for Minkowski distance, got {distance=}" ) inputs["Exponent"] = exponent if w is not None: assert voronoi_dimensions in ["4D", "1D"] inputs["W"] = w elif voronoi_dimensions == "1D": raise_explicit_noise_vector_error("voronoi", logger=logger) res = nt.ProcNode.from_nodetype( node_type="ShaderNodeTexVoronoi", inputs=inputs, attrs={ "distance": distance, "feature": feature, "normalize": normalize, "voronoi_dimensions": voronoi_dimensions, }, ) w = res._output_socket("w") if voronoi_dimensions == "4D" else None return VoronoiResult( distance=res._output_socket("distance"), color=res._output_socket("color"), position=res._output_socket("position"), w=w, )
[docs] def voronoi_distance( vector: nt.SocketOrVal[pt.Vector] | None, scale: nt.SocketOrVal[float] = 5.0, detail: nt.SocketOrVal[float] = 0.0, roughness: nt.SocketOrVal[float] = 0.5, lacunarity: nt.SocketOrVal[float] = 2.0, randomness: nt.SocketOrVal[float] = 1.0, normalize: bool = False, voronoi_dimensions: TNoiseDimensions = "3D", w: nt.SocketOrVal[float] | None = None, ) -> nt.ProcNode[float]: inputs = { "Scale": scale, "Detail": detail, "Roughness": roughness, "Lacunarity": lacunarity, "Randomness": randomness, } if voronoi_dimensions == "1D": if vector is not None: raise ValueError( "voronoi_distance with voronoi_dimensions='1D' has no Vector " "input; use w instead" ) elif vector is None: raise_explicit_noise_vector_error("voronoi_distance", logger=logger) else: inputs["Vector"] = vector if w is not None: assert voronoi_dimensions in ["4D", "1D"] inputs["W"] = w elif voronoi_dimensions == "1D": raise_explicit_noise_vector_error("voronoi_distance", logger=logger) return nt.ProcNode.from_nodetype( node_type="ShaderNodeTexVoronoi", inputs=inputs, attrs={ "feature": "DISTANCE_TO_EDGE", "normalize": normalize, "voronoi_dimensions": voronoi_dimensions, }, )
[docs] def voronoi_smooth_f1( vector: nt.SocketOrVal[pt.Vector] | None, scale: nt.SocketOrVal[float] = 5.0, detail: nt.SocketOrVal[float] = 0.0, roughness: nt.SocketOrVal[float] = 0.5, lacunarity: nt.SocketOrVal[float] = 2.0, smoothness: nt.SocketOrVal[float] = 0.5, randomness: nt.SocketOrVal[float] = 1.0, distance: TDistanceMetric = "EUCLIDEAN", normalize: bool = False, voronoi_dimensions: TNoiseDimensions = "3D", w: nt.SocketOrVal[float] | None = None, ) -> VoronoiResult: inputs = { "Scale": scale, "Detail": detail, "Roughness": roughness, "Lacunarity": lacunarity, "Randomness": randomness, "Smoothness": smoothness, } if voronoi_dimensions == "1D": if vector is not None: raise ValueError( "voronoi_smooth_f1 with voronoi_dimensions='1D' has no Vector " "input; use w instead" ) elif vector is None: raise_explicit_noise_vector_error("voronoi_smooth_f1", logger=logger) else: inputs["Vector"] = vector if w is not None: assert voronoi_dimensions in ["4D", "1D"] inputs["W"] = w elif voronoi_dimensions == "1D": raise_explicit_noise_vector_error("voronoi_smooth_f1", logger=logger) res = nt.ProcNode.from_nodetype( node_type="ShaderNodeTexVoronoi", inputs=inputs, attrs={ "distance": distance, "feature": "SMOOTH_F1", "normalize": normalize, "voronoi_dimensions": voronoi_dimensions, }, ) w = res._output_socket("w") if voronoi_dimensions == "4D" else None return VoronoiResult( distance=res._output_socket("distance"), color=res._output_socket("color"), position=res._output_socket("position"), w=w, )
[docs] def voronoi_n_spheres_distance( vector: nt.SocketOrVal[pt.Vector], scale: nt.SocketOrVal[float] = 5.0, randomness: nt.SocketOrVal[float] = 1.0, normalize: bool = False, ) -> nt.ProcNode[float]: if vector is None: raise_explicit_noise_vector_error("voronoi_spheres_distance", logger=logger) return nt.ProcNode.from_nodetype( node_type="ShaderNodeTexVoronoi", inputs={ "Vector": vector, "Scale": scale, "Randomness": randomness, }, attrs={ "feature": "N_SPHERE_RADIUS", "normalize": normalize, }, )
[docs] def wave( vector: nt.SocketOrVal[pt.Vector], scale: nt.SocketOrVal[float] = 5.0, distortion: nt.SocketOrVal[float] = 0.0, detail: nt.SocketOrVal[float] = 2.0, detail_scale: nt.SocketOrVal[float] = 1.0, detail_roughness: nt.SocketOrVal[float] = 0.5, phase_offset: nt.SocketOrVal[float] = 0.0, bands_direction: Literal["X", "Y", "Z", "SPHERICAL"] = "X", rings_direction: Literal["X", "Y", "Z", "SPHERICAL"] = "X", wave_profile: Literal["SIN", "SAW", "TRI"] = "SIN", wave_type: Literal["BANDS", "RINGS"] = "BANDS", ) -> TextureResult: """ Uses a TexWave Shader Node. TODO: bands_direction and rings_direction are only available for wave_type BANDS or RINGS respectively See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/textures/wave.html """ if vector is None: raise_explicit_noise_vector_error("wave", logger=logger) res = nt.ProcNode.from_nodetype( node_type="ShaderNodeTexWave", inputs={ "Vector": vector, "Scale": scale, "Distortion": distortion, "Detail": detail, "Detail Scale": detail_scale, "Detail Roughness": detail_roughness, "Phase Offset": phase_offset, }, attrs={ "bands_direction": bands_direction, "rings_direction": rings_direction, "wave_profile": wave_profile, "wave_type": wave_type, }, ) return TextureResult( fac=res._output_socket("fac"), color=res._output_socket("color"), )
[docs] def white_noise( vector: nt.SocketOrVal[pt.Vector] | None = None, noise_dimensions: TNoiseDimensions = "3D", w: nt.SocketOrVal[float] = None, ) -> TextureResult: """ Uses a TexWhiteNoise Shader Node. See: https://docs.blender.org/manual/en/4.2/render/shader_nodes/textures/white_noise.html """ if noise_dimensions in ["1D", "4D"] and w is None: raise ValueError(f"w is required for {noise_dimensions} white noise") if noise_dimensions in ["2D", "3D", "4D"] and vector is None: raise_explicit_noise_vector_error("white_noise", logger=logger) if noise_dimensions in ["2D", "3D"] and w is not None: raise ValueError("w is not supported for 2D or 3D white noise") # only mention the sockets this mode enables (1D has no Vector; 2D/3D no W) inputs = {k: v for k, v in {"Vector": vector, "W": w}.items() if v is not None} res = nt.ProcNode.from_nodetype( node_type="ShaderNodeTexWhiteNoise", inputs=inputs, attrs={"noise_dimensions": noise_dimensions}, ) return TextureResult( fac=res._output_socket("value"), color=res._output_socket("color"), )