226 lines
8.1 KiB
Plaintext
226 lines
8.1 KiB
Plaintext
// 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 );
|
|
}
|
|
}
|