Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions blender/arm/material/cycles_nodes/nodes_shader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Add Parallax Occlusion Mapping node support
# Append to existing nodes_shader.py

def parse_parallaxocclusionnode(node, out_socket, state):
"""
Parallax Occlusion Mapping (POM) node with self-shadowing.

Inputs:
- Height: Height map texture (0-1 range)
- UV: UV coordinates (optional, defaults to texcoord)
- Scale: Depth scale factor
- Min Layers: Minimum raymarch steps
- Max Layers: Maximum raymarch steps

Outputs:
- UV: Displaced UV coordinates
- Shadow: Self-shadowing factor (0-1)
"""
import arm.material.cycles as cycles
import arm.material.make_state as state_mod

# Get inputs
height_map = cycles.parse_value_input(node.inputs['Height']) if node.inputs['Height'].is_linked else '0.5'
uv_input = cycles.parse_vector_input(node.inputs['UV']) if node.inputs['UV'].is_linked else 'texCoord'
scale = cycles.parse_value_input(node.inputs['Scale']) if node.inputs['Scale'].is_linked else str(node.inputs['Scale'].default_value)
min_layers = str(int(node.inputs['Min Layers'].default_value))
max_layers = str(int(node.inputs['Max Layers'].default_value))

# Add POM function to shader
state_mod.curdata['shader'].add_function('''
vec3 parallax_occlusion_mapping(
sampler2D heightMap,
vec2 texCoords,
vec3 viewDir,
float heightScale,
float minLayers,
float maxLayers
) {
// Calculate number of layers based on view angle
float numLayers = mix(maxLayers, minLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));
float layerDepth = 1.0 / numLayers;
float currentLayerDepth = 0.0;

vec2 p = viewDir.xy / viewDir.z * heightScale;
vec2 deltaTexCoords = p / numLayers;

vec2 currentTexCoords = texCoords;
float currentDepthMapValue = texture(heightMap, currentTexCoords).r;

// Raymarch until we find intersection
while (currentLayerDepth < currentDepthMapValue) {
currentTexCoords -= deltaTexCoords;
currentDepthMapValue = texture(heightMap, currentTexCoords).r;
currentLayerDepth += layerDepth;
}

// Binary search refinement
vec2 prevTexCoords = currentTexCoords + deltaTexCoords;
float afterDepth = currentDepthMapValue - currentLayerDepth;
float beforeDepth = texture(heightMap, prevTexCoords).r - currentLayerDepth + layerDepth;
float weight = afterDepth / (afterDepth - beforeDepth);
vec2 finalTexCoords = prevTexCoords * weight + currentTexCoords * (1.0 - weight);

// Return UV and depth for shadowing
float finalDepth = currentLayerDepth;
return vec3(finalTexCoords, finalDepth);
}

float pom_self_shadow(
sampler2D heightMap,
vec2 texCoords,
vec3 lightDir,
float initialHeight,
float heightScale
) {
float shadowMultiplier = 1.0;
const float numShadowLayers = 16.0;
float layerDepth = initialHeight / numShadowLayers;

vec2 deltaTexCoords = lightDir.xy / lightDir.z * heightScale / numShadowLayers;

vec2 currentTexCoords = texCoords;
float currentDepth = initialHeight - layerDepth;
float currentHeight = texture(heightMap, currentTexCoords).r;

while (currentDepth > 0.0) {
if (currentHeight > currentDepth) {
float newShadowMultiplier = (currentDepth - currentHeight) * (l.0 - currentDepth / initialHeight);
shadowMultiplier = max(shadowMultiplier, newShadowMultiplier);
}
currentDepth -= layerDepth;
currentTexCoords += deltaTexCoords;
currentHeight = texture(heightMap, currentTexCoords).r;
}

return 1.0 - clamp(shadowMultiplier * 2.0, 0.0, 1.0);
}
''')

# Generate unique variable names
var_name = cycles.node_name(node.name)

# Output the appropriate socket
if out_socket.name == 'UV':
return f'parallax_occlusion_mapping(heightTex, {uv_input}.xy, vViewPosition, {scale}, {min_layers}.0, {max_layers}.0).xy'
elif out_socket.name == 'Shadow':
return f'pom_self_shadow(heightTex, {uv_input}.xy, lightDir, 0.0, {scale})'
return 'vec2(0.0)'
100 changes: 100 additions & 0 deletions blender/arm/material/node_types/ParallaxOcclusionNode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""
Parallax Occlusion Mapping (POM) node for Armory3D.

This node provides POM with self-shadowing support for realistic
depth effects on surfaces using height maps.

Usage:
1. Connect a height/displacement map to the Height input
2. Connect the UV output to your texture nodes
3. Optionally use Shadow output to modulate lighting

Author: Armory3D Contributors
License: zlib
Issue: #2609
"""

import bpy
from bpy.types import Node
from bpy.props import FloatProperty, IntProperty, BoolProperty


class ArmParallaxOcclusionNode(Node):
"""Parallax Occlusion Mapping node with self-shadowing support."""

bl_idname = 'ArmParallaxOcclusionNodeType'
bl_label = 'Parallax Occlusion Mapping'
bl_icon = 'MATERIAL_DATA'

# Node properties
height_scale: FloatProperty(
name='Height Scale',
description='Depth scale of the parallax effect',
default=0.05,
min=0.0,
max=1.0,
soft_min=0.01,
soft_max=0.2
)

min_layers: IntProperty(
name='Min Layers',
description='Minimum number of raymarch steps (used at grazing angles)',
default=8,
min=1,
max=64
)

max_layers: IntProperty(
name='Max Layers',
description='Maximum number of raymarch steps (used at steep angles)',
default=32,
min=1,
max=128
)

enable_shadows: BoolProperty(
name='Self Shadowing',
description='Enable self-shadowing for more realistic depth',
default=True
)

def init(self, context):
"""Initialize node inputs and outputs."""
# Inputs
self.inputs.new('NodeSocketFloat', 'Height')
self.inputs.new('NodeSocketVector', 'UV')

scale_input = self.inputs.new('NodeSocketFloat', 'Scale')
scale_input.default_value = 0.05

min_input = self.inputs.new('NodeSocketFloat', 'Min Layers')
min_input.default_value = 8.0

max_input = self.inputs.new('NodeSocketFloat', 'Max Layers')
max_input.default_value = 32.0

# Outputs
self.outputs.new('NodeSocketVector', 'UV')
self.outputs.new('NodeSocketFloat', 'Shadow')

def draw_buttons(self, context, layout):
"""Draw node UI buttons."""
layout.prop(self, 'enable_shadows')

def draw_buttons_ext(self, context, layout):
"""Draw extended node UI in sidebar."""
layout.prop(self, 'height_scale')
layout.prop(self, 'min_layers')
layout.prop(self, 'max_layers')
layout.prop(self, 'enable_shadows')


def register():
"""Register the node class with Blender."""
bpy.utils.register_class(ArmParallaxOcclusionNode)


def unregister():
"""Unregister the node class from Blender."""
bpy.utils.unregister_class(ArmParallaxOcclusionNode)