632 lines
21 KiB
Plaintext
632 lines
21 KiB
Plaintext
|
|
// #include "res://addons/rokojori_action_library/Runtime/Shading/Library/ControlMap.gdshaderinc"
|
||
|
|
//
|
||
|
|
// Terrain texture painting from a packed control map, in the shape Terrain3D
|
||
|
|
// uses: one 32-bit integer per texel, carrying a base texture index, an overlay
|
||
|
|
// index, the blend between them, a uv scale and rotation, and the hole and
|
||
|
|
// navigable flags.
|
||
|
|
//
|
||
|
|
// WHY A PACKED INTEGER
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// A weight map per texture does not scale - eight textures is eight channels,
|
||
|
|
// and every fragment pays for all of them whether they are present or not. Two
|
||
|
|
// indices plus one blend is a fixed cost for any number of textures, and the
|
||
|
|
// indices are exact: texture 7 next to texture 3 has no third texture bleeding
|
||
|
|
// into the seam.
|
||
|
|
//
|
||
|
|
// The price is that the data is <marked>not interpolable</marked>. Bit 14 of a
|
||
|
|
// texture index means nothing in common with bit 14 of its neighbour, so the
|
||
|
|
// control map can only ever be read with texelFetch - never with a filtered
|
||
|
|
// texture() call, and never through a hardware blend. Everything below follows
|
||
|
|
// from that one constraint:
|
||
|
|
//
|
||
|
|
// - reads are texelFetch, so the map must be a single-channel float format
|
||
|
|
// ( R32F ) and the bits come back through floatBitsToUint
|
||
|
|
// - the softness a bilinear filter would have given has to be rebuilt by
|
||
|
|
// hand: 4 taps, decoded separately, and their shading results blended by
|
||
|
|
// the bilinear weights ( ControlMap_getTaps )
|
||
|
|
// - a per-texel uv scale and rotation means the uv is discontinuous between
|
||
|
|
// texels, which breaks the implicit derivative a texture() call uses to
|
||
|
|
// pick its mip. So every sample is textureGrad with the gradient computed
|
||
|
|
// analytically from the world-space derivative.
|
||
|
|
//
|
||
|
|
// THE LAYOUT IS DATA, NOT CODE
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Terrain3D hard-codes its bit layout in macros ( DECODE_BASE is >>27 & 0x1F ).
|
||
|
|
// Here every field is an ivec2 of ( bit offset, bit count ) in ControlMapSetup,
|
||
|
|
// so the layout is decided by ControlMapFormat on the C# side and pushed in as
|
||
|
|
// uniforms. That is what keeps the CPU packer and the GPU decoder from being
|
||
|
|
// able to disagree - there is only one description of the layout, and both read
|
||
|
|
// it. Narrowing a field to buy bits for another one is then an inspector edit
|
||
|
|
// rather than a shader edit.
|
||
|
|
//
|
||
|
|
// A field with a bit count of 0 is absent and decodes to 0 / false, so a setup
|
||
|
|
// that does not want holes simply does not allocate the bit.
|
||
|
|
//
|
||
|
|
// INCLUDES NOTHING, ON PURPOSE
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Godot's shader preprocessor does not guard against including a file twice,
|
||
|
|
// and this is meant to be used alongside Terrain.gdshaderinc, which already
|
||
|
|
// pulls in Math, Transform and Quaternion. So the two small helpers it needs
|
||
|
|
// ( a 2D rotation and a hash ) are defined here under the ControlMap_ prefix
|
||
|
|
// rather than taken from Transform/Noise.
|
||
|
|
|
||
|
|
const float ControlMap_TAU = 6.283185307179586;
|
||
|
|
|
||
|
|
|
||
|
|
/* THE SETUP
|
||
|
|
-------------------------------------------------------------------------
|
||
|
|
Everything the decoder needs that is not a sampler. Samplers are passed
|
||
|
|
separately because Godot's shading language will not accept them as struct
|
||
|
|
members.
|
||
|
|
|
||
|
|
A consumer fills this once per fragment from its own uniforms - see
|
||
|
|
_research/control-map/ControlMapPreview.gdshader - which keeps this file free
|
||
|
|
of a uniform block of its own and therefore mergeable into a terrain material
|
||
|
|
that already has one.
|
||
|
|
*/
|
||
|
|
struct ControlMapSetup
|
||
|
|
{
|
||
|
|
// The map's placement in the world, same convention as HeightMapGenerator:
|
||
|
|
// centerXZ is the middle of the map, sizeXZ is how many meters it covers.
|
||
|
|
vec2 centerXZ;
|
||
|
|
vec2 sizeXZ;
|
||
|
|
|
||
|
|
// Bit layout. x = offset of the field's lowest bit, y = how many bits.
|
||
|
|
ivec2 baseField;
|
||
|
|
ivec2 overlayField;
|
||
|
|
ivec2 blendField;
|
||
|
|
ivec2 angleField;
|
||
|
|
ivec2 scaleField;
|
||
|
|
ivec2 holeField;
|
||
|
|
ivec2 navigableField;
|
||
|
|
|
||
|
|
// Value mapping for the scale field: multiplier = scaleMin + raw * scaleStep.
|
||
|
|
// Explicit rather than Terrain3D's modulo formula, because with a variable
|
||
|
|
// bit count there is no fixed table to index.
|
||
|
|
float scaleMin;
|
||
|
|
float scaleStep;
|
||
|
|
|
||
|
|
// How sharply the 4 taps and the base/overlay pair resolve against each
|
||
|
|
// other, using the albedo's alpha as a height map. 1 is a plain weighted
|
||
|
|
// average; higher lets the taller texture's detail win, so gravel pokes
|
||
|
|
// through sand instead of crossfading into it.
|
||
|
|
float blendSharpness;
|
||
|
|
|
||
|
|
// Off reads one texel per fragment instead of four. Four times cheaper and
|
||
|
|
// shows the control map's texel grid as hard edges - useful for seeing what
|
||
|
|
// is actually painted.
|
||
|
|
bool bilinear;
|
||
|
|
|
||
|
|
// Whether the per-texture parameter strip is assigned. A 1x1 strip is a
|
||
|
|
// legitimate one-texture set, so its size cannot be the test.
|
||
|
|
bool hasTextureParams;
|
||
|
|
|
||
|
|
// Used for every texture when hasTextureParams is false:
|
||
|
|
// ( meters per tile, detile amount, normal strength, roughness scale ).
|
||
|
|
vec4 defaultParams;
|
||
|
|
};
|
||
|
|
|
||
|
|
/** The four control texels around a fragment, decoded by the functions below.
|
||
|
|
|
||
|
|
baseTexel is the lower-left of the 2x2 neighbourhood; the four taps are at
|
||
|
|
( 0, 0 ), ( 1, 0 ), ( 0, 1 ) and ( 1, 1 ) from it, which is the order the
|
||
|
|
weights are in. In nearest mode baseTexel is the one texel the fragment sits
|
||
|
|
in and only tap 0 has any weight.
|
||
|
|
*/
|
||
|
|
struct ControlMapTaps
|
||
|
|
{
|
||
|
|
uvec4 controls;
|
||
|
|
vec4 weights;
|
||
|
|
|
||
|
|
ivec2 baseTexel;
|
||
|
|
ivec2 resolution;
|
||
|
|
};
|
||
|
|
|
||
|
|
|
||
|
|
/* PRIVATE HELPERS */
|
||
|
|
|
||
|
|
vec2 ControlMap_rotate( vec2 v, float angle )
|
||
|
|
{
|
||
|
|
float s = sin( angle );
|
||
|
|
float c = cos( angle );
|
||
|
|
|
||
|
|
return vec2( c * v.x - s * v.y, s * v.x + c * v.y );
|
||
|
|
}
|
||
|
|
|
||
|
|
float ControlMap_random( vec2 p )
|
||
|
|
{
|
||
|
|
return fract( sin( dot( p, vec2( 12.9898, 78.233 ) ) ) * 43758.5453 );
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/* COORDINATES
|
||
|
|
-------------------------------------------------------------------------
|
||
|
|
Same mapping as addTerrainOffset in Terrain.gdshaderinc, so a control map and
|
||
|
|
a height map authored over the same rect line up texel for texel.
|
||
|
|
*/
|
||
|
|
|
||
|
|
/** 0..1 across the map. */
|
||
|
|
vec2 ControlMap_uv( ControlMapSetup setup, vec3 worldPosition )
|
||
|
|
{
|
||
|
|
vec2 mapOffset = setup.centerXZ - setup.sizeXZ / 2.0;
|
||
|
|
|
||
|
|
return ( worldPosition.xz - mapOffset ) / setup.sizeXZ;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** World XZ of a texel's center - the pivot a texel's uv rotation turns
|
||
|
|
around.
|
||
|
|
*/
|
||
|
|
vec2 ControlMap_texelCenterXZ( ControlMapSetup setup, ivec2 resolution, ivec2 texel )
|
||
|
|
{
|
||
|
|
vec2 mapOffset = setup.centerXZ - setup.sizeXZ / 2.0;
|
||
|
|
vec2 texelSize = setup.sizeXZ / vec2( max( resolution, ivec2( 1 ) ) );
|
||
|
|
|
||
|
|
return mapOffset + ( vec2( texel ) + 0.5 ) * texelSize;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/* DECODING
|
||
|
|
-------------------------------------------------------------------------
|
||
|
|
One extraction function driven by the field descriptor, and a decoder per
|
||
|
|
field on top of it. Nothing here knows an offset or a width.
|
||
|
|
*/
|
||
|
|
|
||
|
|
/** The raw integer in a field. 0 for an absent field. */
|
||
|
|
int ControlMap_extract( uint control, ivec2 field )
|
||
|
|
{
|
||
|
|
if ( field.y <= 0 )
|
||
|
|
{
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 1u << 32u is undefined, and a 32-bit field is the whole word anyway.
|
||
|
|
if ( field.y >= 32 )
|
||
|
|
{
|
||
|
|
return int( control );
|
||
|
|
}
|
||
|
|
|
||
|
|
uint mask = ( 1u << uint( field.y ) ) - 1u;
|
||
|
|
|
||
|
|
return int( ( control >> uint( field.x ) ) & mask );
|
||
|
|
}
|
||
|
|
|
||
|
|
/** A field as 0..1, with the field's maximum mapping to exactly 1. For a
|
||
|
|
quantity whose endpoints both matter, like the base/overlay blend.
|
||
|
|
*/
|
||
|
|
float ControlMap_extractUnit( uint control, ivec2 field )
|
||
|
|
{
|
||
|
|
if ( field.y <= 0 )
|
||
|
|
{
|
||
|
|
return 0.0;
|
||
|
|
}
|
||
|
|
|
||
|
|
float maxValue = float( ( 1u << uint( min( field.y, 31 ) ) ) - 1u );
|
||
|
|
|
||
|
|
return float( ControlMap_extract( control, field ) ) / maxValue;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** A field as 0..1 with the maximum mapping just short of 1, for a quantity
|
||
|
|
that wraps. An angle divided by its maximum would make the last step equal
|
||
|
|
to the first and waste it.
|
||
|
|
*/
|
||
|
|
float ControlMap_extractCyclic( uint control, ivec2 field )
|
||
|
|
{
|
||
|
|
if ( field.y <= 0 )
|
||
|
|
{
|
||
|
|
return 0.0;
|
||
|
|
}
|
||
|
|
|
||
|
|
float steps = float( 1u << uint( min( field.y, 31 ) ) );
|
||
|
|
|
||
|
|
return float( ControlMap_extract( control, field ) ) / steps;
|
||
|
|
}
|
||
|
|
|
||
|
|
int ControlMap_baseIndex( uint control, ControlMapSetup setup )
|
||
|
|
{
|
||
|
|
return ControlMap_extract( control, setup.baseField );
|
||
|
|
}
|
||
|
|
|
||
|
|
int ControlMap_overlayIndex( uint control, ControlMapSetup setup )
|
||
|
|
{
|
||
|
|
return ControlMap_extract( control, setup.overlayField );
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 0 is all base, 1 is all overlay. */
|
||
|
|
float ControlMap_blend( uint control, ControlMapSetup setup )
|
||
|
|
{
|
||
|
|
return ControlMap_extractUnit( control, setup.blendField );
|
||
|
|
}
|
||
|
|
|
||
|
|
/** The texel's uv rotation in radians. */
|
||
|
|
float ControlMap_angle( uint control, ControlMapSetup setup )
|
||
|
|
{
|
||
|
|
return ControlMap_extractCyclic( control, setup.angleField ) * ControlMap_TAU;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** The texel's uv scale multiplier. */
|
||
|
|
float ControlMap_scale( uint control, ControlMapSetup setup )
|
||
|
|
{
|
||
|
|
if ( setup.scaleField.y <= 0 )
|
||
|
|
{
|
||
|
|
return 1.0;
|
||
|
|
}
|
||
|
|
|
||
|
|
float scale = setup.scaleMin + float( ControlMap_extract( control, setup.scaleField ) ) * setup.scaleStep;
|
||
|
|
|
||
|
|
return abs( scale ) < 0.0001 ? 1.0 : scale;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ControlMap_hole( uint control, ControlMapSetup setup )
|
||
|
|
{
|
||
|
|
return ControlMap_extract( control, setup.holeField ) != 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ControlMap_navigable( uint control, ControlMapSetup setup )
|
||
|
|
{
|
||
|
|
return ControlMap_extract( control, setup.navigableField ) != 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/* READING
|
||
|
|
-------------------------------------------------------------------------
|
||
|
|
texelFetch only. A filtered read of this map would average bit patterns,
|
||
|
|
which is meaningless - a base index of 3 and one of 4 average to 3.5, and
|
||
|
|
half of texture 3 is not half of texture 4.
|
||
|
|
*/
|
||
|
|
|
||
|
|
uint ControlMap_read( sampler2D controlMap, ivec2 texel )
|
||
|
|
{
|
||
|
|
ivec2 maxTexel = textureSize( controlMap, 0 ) - 1;
|
||
|
|
|
||
|
|
return floatBitsToUint( texelFetch( controlMap, clamp( texel, ivec2( 0 ), maxTexel ), 0 ).r );
|
||
|
|
}
|
||
|
|
|
||
|
|
/** The one texel a world position sits in. The flags are read through this
|
||
|
|
rather than off the taps, because a bit cannot be blended: a fragment is
|
||
|
|
either in a hole or it is not.
|
||
|
|
*/
|
||
|
|
uint ControlMap_readAt( sampler2D controlMap, ControlMapSetup setup, vec3 worldPosition )
|
||
|
|
{
|
||
|
|
ivec2 resolution = textureSize( controlMap, 0 );
|
||
|
|
vec2 uv = ControlMap_uv( setup, worldPosition );
|
||
|
|
|
||
|
|
return ControlMap_read( controlMap, ivec2( floor( uv * vec2( resolution ) ) ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
/** The 2x2 neighbourhood and its bilinear weights.
|
||
|
|
|
||
|
|
The 0.5 bias is what makes the weights line up with what a hardware bilinear
|
||
|
|
filter would have done: a texture() call weights by the distance to the
|
||
|
|
surrounding texel <marked>centers</marked>, not to their corners.
|
||
|
|
*/
|
||
|
|
ControlMapTaps ControlMap_getTaps( sampler2D controlMap, ControlMapSetup setup, vec3 worldPosition )
|
||
|
|
{
|
||
|
|
ControlMapTaps taps;
|
||
|
|
|
||
|
|
taps.resolution = textureSize( controlMap, 0 );
|
||
|
|
|
||
|
|
vec2 uv = ControlMap_uv( setup, worldPosition );
|
||
|
|
vec2 texel = uv * vec2( taps.resolution );
|
||
|
|
|
||
|
|
if ( ! setup.bilinear )
|
||
|
|
{
|
||
|
|
taps.baseTexel = ivec2( floor( texel ) );
|
||
|
|
|
||
|
|
uint control = ControlMap_read( controlMap, taps.baseTexel );
|
||
|
|
|
||
|
|
taps.controls = uvec4( control );
|
||
|
|
taps.weights = vec4( 1.0, 0.0, 0.0, 0.0 );
|
||
|
|
|
||
|
|
return taps;
|
||
|
|
}
|
||
|
|
|
||
|
|
vec2 biased = texel - 0.5;
|
||
|
|
vec2 corner = floor( biased );
|
||
|
|
vec2 f = biased - corner;
|
||
|
|
|
||
|
|
taps.baseTexel = ivec2( corner );
|
||
|
|
|
||
|
|
taps.controls = uvec4(
|
||
|
|
ControlMap_read( controlMap, taps.baseTexel ),
|
||
|
|
ControlMap_read( controlMap, taps.baseTexel + ivec2( 1, 0 ) ),
|
||
|
|
ControlMap_read( controlMap, taps.baseTexel + ivec2( 0, 1 ) ),
|
||
|
|
ControlMap_read( controlMap, taps.baseTexel + ivec2( 1, 1 ) )
|
||
|
|
);
|
||
|
|
|
||
|
|
taps.weights = vec4(
|
||
|
|
( 1.0 - f.x ) * ( 1.0 - f.y ),
|
||
|
|
f.x * ( 1.0 - f.y ),
|
||
|
|
( 1.0 - f.x ) * f.y,
|
||
|
|
f.x * f.y
|
||
|
|
);
|
||
|
|
|
||
|
|
return taps;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/* THE TEXTURE SET
|
||
|
|
-------------------------------------------------------------------------
|
||
|
|
Two sampler2DArrays - albedo with its height in alpha, and normal with its
|
||
|
|
roughness in alpha - plus a 1-row parameter strip holding the per-texture
|
||
|
|
settings that cannot be per texel.
|
||
|
|
|
||
|
|
The strip is a texture rather than a uniform array because a uniform array
|
||
|
|
would have to be declared with a fixed length, which is exactly the kind of
|
||
|
|
hard-coded ceiling the variable bit layout exists to avoid. One texelFetch
|
||
|
|
per tap costs nothing next to the albedo and normal samples it feeds.
|
||
|
|
*/
|
||
|
|
|
||
|
|
/** ( meters per tile, detile amount, normal strength, roughness scale ) */
|
||
|
|
vec4 ControlMap_textureParams( sampler2D textureParams, ControlMapSetup setup, int id )
|
||
|
|
{
|
||
|
|
if ( ! setup.hasTextureParams )
|
||
|
|
{
|
||
|
|
return setup.defaultParams;
|
||
|
|
}
|
||
|
|
|
||
|
|
int maxIndex = textureSize( textureParams, 0 ).x - 1;
|
||
|
|
|
||
|
|
return texelFetch( textureParams, ivec2( clamp( id, 0, maxIndex ), 0 ), 0 );
|
||
|
|
}
|
||
|
|
|
||
|
|
/** The uv one texture is sampled at, and the two gradients that go with it.
|
||
|
|
|
||
|
|
The rotation pivots on the texel's own center, so a texel's pattern turns in
|
||
|
|
place instead of swinging around the map's origin - at the far edge of a
|
||
|
|
1000 m map, rotating around the origin would translate the pattern by
|
||
|
|
hundreds of meters and no two neighbouring texels would line up at all.
|
||
|
|
|
||
|
|
The gradients are analytic rather than left to the hardware. uv is an affine
|
||
|
|
function of worldPosition.xz here ( a scale and a rotation ), so its
|
||
|
|
jacobian is exactly that same scale and rotation applied to the world-space
|
||
|
|
derivative - no approximation. The hardware's own implicit derivative cannot
|
||
|
|
be used, because the uv jumps between texels wherever the scale or angle
|
||
|
|
changes, and the mip selected from that jump would be the coarsest one,
|
||
|
|
drawing a blurred line along every texel border.
|
||
|
|
*/
|
||
|
|
void ControlMap_textureUV(
|
||
|
|
ControlMapSetup setup,
|
||
|
|
uint control,
|
||
|
|
vec4 params,
|
||
|
|
vec3 worldPosition,
|
||
|
|
vec2 worldDDX,
|
||
|
|
vec2 worldDDY,
|
||
|
|
vec2 texelCenterXZ,
|
||
|
|
out vec2 uv,
|
||
|
|
out vec2 uvDDX,
|
||
|
|
out vec2 uvDDY
|
||
|
|
)
|
||
|
|
{
|
||
|
|
float metersPerTile = max( params.x, 0.0001 );
|
||
|
|
|
||
|
|
// Per-texture random rotation on top of the painted one. Without it a texture
|
||
|
|
// with any visible structure reads as a grid at tile scale; this is what
|
||
|
|
// Terrain3D calls detiling.
|
||
|
|
float detile = params.y * ControlMap_random( texelCenterXZ ) * ControlMap_TAU;
|
||
|
|
float angle = ControlMap_angle( control, setup ) + detile;
|
||
|
|
|
||
|
|
float k = ControlMap_scale( control, setup ) / metersPerTile;
|
||
|
|
|
||
|
|
uv = ControlMap_rotate( ( worldPosition.xz - texelCenterXZ ) * k, angle )
|
||
|
|
+ texelCenterXZ / metersPerTile;
|
||
|
|
|
||
|
|
uvDDX = ControlMap_rotate( worldDDX * k, angle );
|
||
|
|
uvDDY = ControlMap_rotate( worldDDY * k, angle );
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/* ACCUMULATION
|
||
|
|
-------------------------------------------------------------------------
|
||
|
|
Up to 8 albedo + 8 normal samples per fragment: 4 control taps, each with a
|
||
|
|
base and an overlay. That is the real cost of this technique and the reason
|
||
|
|
setup.bilinear exists as a switch.
|
||
|
|
|
||
|
|
Weighting is height-aware. A plain weighted average of two albedos is a
|
||
|
|
crossfade, which looks like a dissolve rather than like one ground meeting
|
||
|
|
another. Raising each weight by the albedo's alpha ( its height ) to a
|
||
|
|
sharpness power makes the taller texture win where it is tall, so the
|
||
|
|
transition follows the textures' own detail. This is the same family as
|
||
|
|
Terrain3D's exp2/log2 weighting, written out directly.
|
||
|
|
*/
|
||
|
|
void ControlMap_accumulate(
|
||
|
|
sampler2DArray albedoArray,
|
||
|
|
sampler2DArray normalArray,
|
||
|
|
sampler2D textureParams,
|
||
|
|
ControlMapSetup setup,
|
||
|
|
uint control,
|
||
|
|
int id,
|
||
|
|
float weight,
|
||
|
|
vec3 worldPosition,
|
||
|
|
vec2 worldDDX,
|
||
|
|
vec2 worldDDY,
|
||
|
|
vec2 texelCenterXZ,
|
||
|
|
inout vec3 albedo,
|
||
|
|
inout vec3 normal,
|
||
|
|
inout float roughness,
|
||
|
|
inout float total
|
||
|
|
)
|
||
|
|
{
|
||
|
|
if ( weight <= 0.0001 )
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
int maxId = textureSize( albedoArray, 0 ).z - 1;
|
||
|
|
int safeId = clamp( id, 0, max( maxId, 0 ) );
|
||
|
|
|
||
|
|
vec4 params = ControlMap_textureParams( textureParams, setup, safeId );
|
||
|
|
|
||
|
|
vec2 uv;
|
||
|
|
vec2 uvDDX;
|
||
|
|
vec2 uvDDY;
|
||
|
|
|
||
|
|
ControlMap_textureUV(
|
||
|
|
setup, control, params, worldPosition, worldDDX, worldDDY, texelCenterXZ,
|
||
|
|
uv, uvDDX, uvDDY
|
||
|
|
);
|
||
|
|
|
||
|
|
vec3 arrayUV = vec3( uv, float( safeId ) );
|
||
|
|
|
||
|
|
vec4 albedoHeight = textureGrad( albedoArray, arrayUV, uvDDX, uvDDY );
|
||
|
|
vec4 normalRough = textureGrad( normalArray, arrayUV, uvDDX, uvDDY );
|
||
|
|
|
||
|
|
float height = albedoHeight.a;
|
||
|
|
float w = pow( max( weight * ( 0.001 + height ), 0.0 ), setup.blendSharpness );
|
||
|
|
|
||
|
|
// The normal map was authored in the texture's own space, so the uv rotation
|
||
|
|
// has to be undone on it or a rotated texel would light as though its bumps
|
||
|
|
// had turned with the pattern - which they did, but the light did not.
|
||
|
|
vec3 tangentNormal = normalRough.rgb * 2.0 - 1.0;
|
||
|
|
tangentNormal.xy = ControlMap_rotate( tangentNormal.xy, -ControlMap_angle( control, setup ) );
|
||
|
|
tangentNormal.xy *= params.z;
|
||
|
|
|
||
|
|
albedo += albedoHeight.rgb * w;
|
||
|
|
normal += normalize( tangentNormal ) * w;
|
||
|
|
roughness += normalRough.a * params.w * w;
|
||
|
|
total += w;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** One control tap's contribution: its base texture, and its overlay when the
|
||
|
|
blend calls for one.
|
||
|
|
*/
|
||
|
|
void ControlMap_accumulateTap(
|
||
|
|
sampler2D controlMap,
|
||
|
|
sampler2DArray albedoArray,
|
||
|
|
sampler2DArray normalArray,
|
||
|
|
sampler2D textureParams,
|
||
|
|
ControlMapSetup setup,
|
||
|
|
ControlMapTaps taps,
|
||
|
|
uint control,
|
||
|
|
float weight,
|
||
|
|
ivec2 texelOffset,
|
||
|
|
vec3 worldPosition,
|
||
|
|
vec2 worldDDX,
|
||
|
|
vec2 worldDDY,
|
||
|
|
inout vec3 albedo,
|
||
|
|
inout vec3 normal,
|
||
|
|
inout float roughness,
|
||
|
|
inout float total
|
||
|
|
)
|
||
|
|
{
|
||
|
|
if ( weight <= 0.0001 )
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
vec2 texelCenterXZ = ControlMap_texelCenterXZ(
|
||
|
|
setup, taps.resolution, taps.baseTexel + texelOffset
|
||
|
|
);
|
||
|
|
|
||
|
|
int baseId = ControlMap_baseIndex( control, setup );
|
||
|
|
int overlayId = ControlMap_overlayIndex( control, setup );
|
||
|
|
float blend = ControlMap_blend( control, setup );
|
||
|
|
|
||
|
|
ControlMap_accumulate(
|
||
|
|
albedoArray, normalArray, textureParams, setup, control, baseId,
|
||
|
|
weight * ( 1.0 - blend ), worldPosition, worldDDX, worldDDY, texelCenterXZ,
|
||
|
|
albedo, normal, roughness, total
|
||
|
|
);
|
||
|
|
|
||
|
|
// Skipped when the two indices are the same, which is the unpainted case -
|
||
|
|
// otherwise every texel would pay for a second identical sample.
|
||
|
|
if ( blend > 0.0001 && overlayId != baseId )
|
||
|
|
{
|
||
|
|
ControlMap_accumulate(
|
||
|
|
albedoArray, normalArray, textureParams, setup, control, overlayId,
|
||
|
|
weight * blend, worldPosition, worldDDX, worldDDY, texelCenterXZ,
|
||
|
|
albedo, normal, roughness, total
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/* THE ENTRY POINT */
|
||
|
|
|
||
|
|
/** Shades one fragment from the control map.
|
||
|
|
|
||
|
|
worldDDX / worldDDY are the world-space XZ derivatives of the position -
|
||
|
|
dFdxCoarse( worldPosition ).xz and its Y counterpart. They are parameters
|
||
|
|
rather than taken here because a derivative is only valid in the fragment
|
||
|
|
stage and only on a value that varies linearly across the triangle, so the
|
||
|
|
consumer has to be the one to take them.
|
||
|
|
|
||
|
|
normal comes back in the texture's tangent space, which for a heightfield
|
||
|
|
over world XZ is the same thing as world space with Y up - so it can be
|
||
|
|
combined with the terrain's own normal by the caller.
|
||
|
|
|
||
|
|
hole and navigable are read from the single texel the fragment sits in, not
|
||
|
|
blended: they are bits, and a half-set bit is not a thing.
|
||
|
|
*/
|
||
|
|
void ControlMap_shade(
|
||
|
|
sampler2D controlMap,
|
||
|
|
sampler2DArray albedoArray,
|
||
|
|
sampler2DArray normalArray,
|
||
|
|
sampler2D textureParams,
|
||
|
|
ControlMapSetup setup,
|
||
|
|
vec3 worldPosition,
|
||
|
|
vec2 worldDDX,
|
||
|
|
vec2 worldDDY,
|
||
|
|
out vec3 albedo,
|
||
|
|
out vec3 normal,
|
||
|
|
out float roughness,
|
||
|
|
out bool hole,
|
||
|
|
out bool navigable
|
||
|
|
)
|
||
|
|
{
|
||
|
|
albedo = vec3( 0.5 );
|
||
|
|
normal = vec3( 0.0, 0.0, 1.0 );
|
||
|
|
roughness = 1.0;
|
||
|
|
|
||
|
|
uint flagControl = ControlMap_readAt( controlMap, setup, worldPosition );
|
||
|
|
|
||
|
|
hole = ControlMap_hole( flagControl, setup );
|
||
|
|
navigable = ControlMap_navigable( flagControl, setup );
|
||
|
|
|
||
|
|
ControlMapTaps taps = ControlMap_getTaps( controlMap, setup, worldPosition );
|
||
|
|
|
||
|
|
vec3 albedoSum = vec3( 0.0 );
|
||
|
|
vec3 normalSum = vec3( 0.0 );
|
||
|
|
float roughnessSum = 0.0;
|
||
|
|
float total = 0.0;
|
||
|
|
|
||
|
|
// Unrolled rather than looped: indexing a uvec4 with a loop variable is
|
||
|
|
// dynamic vector indexing, which not every target compiles, and the texel
|
||
|
|
// offsets want to be literals anyway.
|
||
|
|
ControlMap_accumulateTap(
|
||
|
|
controlMap, albedoArray, normalArray, textureParams, setup, taps,
|
||
|
|
taps.controls.x, taps.weights.x, ivec2( 0, 0 ),
|
||
|
|
worldPosition, worldDDX, worldDDY, albedoSum, normalSum, roughnessSum, total
|
||
|
|
);
|
||
|
|
|
||
|
|
ControlMap_accumulateTap(
|
||
|
|
controlMap, albedoArray, normalArray, textureParams, setup, taps,
|
||
|
|
taps.controls.y, taps.weights.y, ivec2( 1, 0 ),
|
||
|
|
worldPosition, worldDDX, worldDDY, albedoSum, normalSum, roughnessSum, total
|
||
|
|
);
|
||
|
|
|
||
|
|
ControlMap_accumulateTap(
|
||
|
|
controlMap, albedoArray, normalArray, textureParams, setup, taps,
|
||
|
|
taps.controls.z, taps.weights.z, ivec2( 0, 1 ),
|
||
|
|
worldPosition, worldDDX, worldDDY, albedoSum, normalSum, roughnessSum, total
|
||
|
|
);
|
||
|
|
|
||
|
|
ControlMap_accumulateTap(
|
||
|
|
controlMap, albedoArray, normalArray, textureParams, setup, taps,
|
||
|
|
taps.controls.w, taps.weights.w, ivec2( 1, 1 ),
|
||
|
|
worldPosition, worldDDX, worldDDY, albedoSum, normalSum, roughnessSum, total
|
||
|
|
);
|
||
|
|
|
||
|
|
if ( total <= 0.0 )
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
albedo = albedoSum / total;
|
||
|
|
roughness = clamp( roughnessSum / total, 0.0, 1.0 );
|
||
|
|
|
||
|
|
// Normalized rather than divided: the sum of weighted unit vectors points the
|
||
|
|
// right way regardless of the total, and a zero-length sum ( two opposed
|
||
|
|
// normals ) falls back to flat instead of to a NaN.
|
||
|
|
normal = length( normalSum ) < 0.0001 ? vec3( 0.0, 0.0, 1.0 ) : normalize( normalSum );
|
||
|
|
}
|