rokojori_action_library/Runtime/Procedural/HeightMaps/Shaders/HeightMapShape.gdshader

226 lines
8.1 KiB
Plaintext
Raw Normal View History

// Content pass of a HeightMapGeneratorLayer's shape.
//
// Writes one shape's height contribution into the compositor's scratch buffer
// with blending disabled. The scratch buffer is then blended onto the height map
// by a plain BlitMaterial, which is where Add / Subtract / Replace actually
// happen - so this shader never reads the map and no ping-pong buffer is
// involved.
//
// Height lives in the R channel (G and B carry the same value so the map is
// readable as a greyscale preview), matching addTerrainOffset in
// Shading/Library/Terrain.gdshaderinc, which samples .r and maps 0..1 onto
// terrainMinHeight..terrainMaxHeight.
//
// Everything is evaluated in world XZ, not UV: HeightMapGenerator hands in the
// world-space rect this blit covers, so a shape that the map's edge clipped is
// still drawn at its true position and size. ( The TextureStack had a bug here
// once - clipping the rect while keeping rect-local UV silently rescaled the
// content. Deriving world space from the clipped rect cannot have that problem. )
//
// Every shape produces two values, because a falloff means two different things
// depending on the blend mode:
// amount - how much of the layer's height this texel gets, 0..1
// coverage - how much this texel is affected at all, 0..1
// Under Add/Subtract coverage scales the contribution; under Replace it is the
// alpha the Mix blend crossfades with.
shader_type texture_blit;
render_mode blend_disabled;
// World-space XZ rect this blit covers, from the (already clipped) pixel rect.
uniform vec2 shape_rect_world_min = vec2( 0.0, 0.0 );
uniform vec2 shape_rect_world_size = vec2( 1.0, 1.0 );
// HeightMapShapeType - 0 Circle, 1 Image.
uniform int shape_type = 0;
// HeightMapBlendMode - 0 Add, 1 Subtract, 2 Replace.
uniform int shape_blend_mode = 0;
// Normalized height, split into two uniforms so one formula covers both
// readings: the stored level is shape_value_base + shape_value_scale * amount.
//
// Add/Subtract leave base at 0 and put the layer's delta in scale. Replace puts
// the node's own world height in base, and scale is the amplitude layered on
// top of it - which is 0 for a shape with no per-texel amount of its own, like
// the circle, and the image layer's height for one that has.
uniform float shape_value_base = 0.0;
uniform float shape_value_scale = 0.0;
// Circle.
uniform vec2 shape_center = vec2( 0.0, 0.0 );
uniform float shape_inner_radius = 0.0;
uniform float shape_outer_radius = 1.0;
uniform bool shape_smooth_falloff = true;
// Image. The texture arrives as the blit's source 0; the layer's rect is
// described by its two world-space XZ axes and its size in meters, so a rotated
// layer needs no matrix and no UV rect.
//
// repeat_enable because a tiling layer relies on the sampler wrapping - which is
// also why nothing here calls fract(), that would put a filtering seam on every
// tile border. The non-tiling path emulates clamp-to-edge in UV instead.
uniform sampler2D shape_image : hint_blit_source0, filter_linear, repeat_enable;
uniform vec2 shape_axis_x = vec2( 1.0, 0.0 );
uniform vec2 shape_axis_z = vec2( 0.0, 1.0 );
uniform vec2 shape_size = vec2( 1.0, 1.0 );
uniform bool shape_image_tiling = false;
// HeightMapChannel - 0 R, 1 G, 2 B, 3 A, 4 RA, 5 GA, 6 BA.
uniform int shape_image_channel = 0;
// An extra multiplier on coverage, from source 1. An absent mask is bound to the
// shared white 1x1 stand-in, so "no mask" needs no flag - it just multiplies by
// 1 - and the mask is always clamped, so outside its own rect it extends its
// border value rather than disappearing.
uniform sampler2D shape_mask : hint_blit_source1, filter_linear, repeat_enable;
uniform int shape_mask_channel = 3;
uniform vec2 shape_mask_center = vec2( 0.0, 0.0 );
uniform vec2 shape_mask_axis_x = vec2( 1.0, 0.0 );
uniform vec2 shape_mask_axis_z = vec2( 0.0, 1.0 );
uniform vec2 shape_mask_size = vec2( 1.0, 1.0 );
/** 1 inside the inner radius, falling to 0 at the outer radius. */
void heightMapShape_circle( vec2 worldXZ, out float amount, out float coverage )
{
float distance = length( worldXZ - shape_center );
float inner = min( shape_inner_radius, shape_outer_radius );
float outer = max( shape_inner_radius, shape_outer_radius );
float weight;
// A zero-width band is a hard edge, not a division by zero.
if ( outer <= inner )
{
weight = distance <= outer ? 1.0 : 0.0;
}
else
{
float t = clamp( ( distance - inner ) / ( outer - inner ), 0.0, 1.0 );
weight = 1.0 - t;
if ( shape_smooth_falloff )
{
weight = weight * weight * ( 3.0 - 2.0 * weight );
}
}
amount = weight;
// The circle has no per-texel height of its own: under Replace it writes one
// flat level and the falloff is the crossfade, so it belongs in coverage;
// under Add/Subtract the falloff already scales the height through amount and
// coverage has to stay out of the way, or it would be applied twice.
coverage = shape_blend_mode == 2 ? weight : 1.0;
}
float heightMapShape_channel( vec4 color, int channel )
{
if ( channel == 0 || channel == 4 ) { return color.r; }
if ( channel == 1 || channel == 5 ) { return color.g; }
if ( channel == 2 || channel == 6 ) { return color.b; }
return color.a;
}
/** 0..1 across a rotated world-space XZ rect.
Image row 0 lands on the rect's -Z edge, so the texture reads upright when
the map is looked at from above.
*/
vec2 heightMapShape_rectUV( vec2 worldXZ, vec2 center, vec2 axisX, vec2 axisZ, vec2 size )
{
vec2 delta = worldXZ - center;
vec2 safeSize = max( size, vec2( 0.0001, 0.0001 ) );
return vec2( dot( delta, axisX ), dot( delta, axisZ ) ) / safeSize + 0.5;
}
vec2 heightMapShape_clampToEdge( sampler2D source, vec2 uv )
{
vec2 halfTexel = 0.5 / vec2( textureSize( source, 0 ) );
return clamp( uv, halfTexel, 1.0 - halfTexel );
}
/** The layer's texture, sampled in the rotated rect's own space. */
void heightMapShape_image( vec2 worldXZ, out float amount, out float coverage )
{
amount = 0.0;
coverage = 0.0;
vec2 uv = heightMapShape_rectUV(
worldXZ, shape_center, shape_axis_x, shape_axis_z, shape_size
);
if ( ! shape_image_tiling )
{
if ( uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 )
{
return;
}
// The sampler repeats for the tiling case, so clamp-to-edge has to be
// emulated here or a linear tap on the border would bleed the opposite edge
// of the texture in.
uv = heightMapShape_clampToEdge( shape_image, uv );
}
vec4 color = texture( shape_image, uv );
amount = heightMapShape_channel( color, shape_image_channel );
coverage = shape_image_channel >= 4 ? color.a : 1.0;
}
/** The mask's contribution to coverage. 1 everywhere when no mask is assigned,
because the stand-in bound in its place is white.
*/
float heightMapShape_mask( vec2 worldXZ )
{
vec2 uv = heightMapShape_rectUV(
worldXZ, shape_mask_center, shape_mask_axis_x, shape_mask_axis_z, shape_mask_size
);
vec4 color = texture( shape_mask, heightMapShape_clampToEdge( shape_mask, uv ) );
float value = heightMapShape_channel( color, shape_mask_channel );
return shape_mask_channel >= 4 ? value * color.a : value;
}
void blit()
{
vec2 worldXZ = shape_rect_world_min + UV * shape_rect_world_size;
float amount = 0.0;
float coverage = 0.0;
switch ( shape_type )
{
case 0: heightMapShape_circle( worldXZ, amount, coverage ); break;
case 1: heightMapShape_image( worldXZ, amount, coverage ); break;
}
// Applied to coverage rather than to the height, so a masked-out region falls
// back to what is underneath under Replace instead of being flattened to the
// layer's base level.
coverage *= heightMapShape_mask( worldXZ );
if ( shape_blend_mode == 2 )
{
// Replace: the height goes in RGB and the coverage rides on alpha, so the
// Mix blend crossfades from the layer's height back to what is underneath.
COLOR0 = vec4( vec3( shape_value_base + shape_value_scale * amount ), coverage );
}
else
{
// Add / Subtract: alpha has to be 1, because the hardware blend multiplies
// the source by its own alpha before summing. Coverage is applied here
// instead.
COLOR0 = vec4( vec3( shape_value_scale * amount * coverage ), 1.0 );
}
}