// 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 ); } }