433 lines
16 KiB
C#
433 lines
16 KiB
C#
using Godot;
|
|
using System;
|
|
|
|
using Rokojori.Extensions;
|
|
namespace Rokojori
|
|
{
|
|
/** How one control-map texel's 32 bits are divided up.
|
|
|
|
Terrain3D fixes this in shader macros. Here it is a Resource, for one
|
|
reason: it is the <marked>only</marked> description of the layout, and both
|
|
sides read it. The pack pass and the decoders in
|
|
Shading/Library/ControlMap.gdshaderinc ( through PushToMaterial ) cannot
|
|
drift apart, because neither of them knows an offset - they are both told.
|
|
|
|
Hard-coding the offsets would be an optimization, and the cost it buys is
|
|
a handful of shifts per fragment against the certainty that a layout change
|
|
means editing two files that have to agree.
|
|
|
|
<marked>Only bit sizes are authored.</marked> Offsets are derived by packing
|
|
the fields from bit 0 upwards in a fixed order, so a layout is always
|
|
internally valid and a widened field pushes the rest along rather than
|
|
silently overlapping one.
|
|
|
|
THE WIDTHS, AND WHY THEY ARE BETTER THAN TERRAIN3D'S
|
|
------------------------------------------------------------------------
|
|
The default adds up to 27 of 32 bits - the same budget Terrain3D spends -
|
|
but buys 32 rotation steps instead of 16 and 16 scale steps instead of 8,
|
|
because the base/overlay blend drops from 8 bits to 6.
|
|
|
|
That is affordable here and not there: with the weight-stack edit format the
|
|
blend is <marked>derived</marked> at pack time from the two largest texture
|
|
weights rather than authored by hand, and 64 steps of crossfade is already
|
|
past what a ground transition can show. Terrain3D has to leave room for
|
|
someone painting a blend value directly.
|
|
|
|
The 5 spare bits are deliberately left spare. A custom channel should be
|
|
easy to add; none is built.
|
|
|
|
OVERFLOW IS THE USER'S PROBLEM
|
|
------------------------------------------------------------------------
|
|
A layout past 32 bits warns and lets the high fields truncate. It does not
|
|
clamp and it does not rearrange, because either one would silently give the
|
|
user a layout they did not ask for. Safe to be this blunt because nothing
|
|
is ever persisted as control-map pixel data - the map is generated from the
|
|
layer stack every session, so re-rendering is the entire migration.
|
|
*/
|
|
[Tool]
|
|
[GlobalClass]
|
|
public partial class ControlMapFormat:Resource
|
|
{
|
|
public const int TotalBits = 32;
|
|
|
|
/* THE AUTHORED WIDTHS
|
|
------------------------------------------------------------------------
|
|
FIELD ORDER is low bits first: base, overlay, blend, rotation, scale,
|
|
hole, navigable. The order itself is arbitrary - nothing reads a field
|
|
without being told where it is - but it is fixed rather than exported,
|
|
because a reorderable order is a second thing that could disagree with
|
|
itself and buys nothing.
|
|
*/
|
|
|
|
BitSizeEnum _numTexturesBits = BitSizeEnum.Bits5_32Values;
|
|
|
|
/** How many ground textures an index can reach, for <marked>both</marked>
|
|
the base and the overlay index.
|
|
|
|
One width rather than two, even though Terrain3D-style layouts sometimes
|
|
narrow the overlay to buy bits back. Two widths that can disagree is a
|
|
state where a texture is paintable as a base and not as an overlay, which
|
|
is a bug report waiting to happen for a few bits that the blend field
|
|
already gave back.
|
|
*/
|
|
[Export]
|
|
public BitSizeEnum numTexturesBits
|
|
{
|
|
get => _numTexturesBits;
|
|
set { _numTexturesBits = value; EmitChanged(); }
|
|
}
|
|
|
|
BitSizeEnum _textureBlendBits = BitSizeEnum.Bits6_64Values;
|
|
|
|
/** Resolution of the base-to-overlay crossfade. Derived at pack time from
|
|
the weight stack, not authored, which is why 6 bits is enough.
|
|
*/
|
|
[Export]
|
|
public BitSizeEnum textureBlendBits
|
|
{
|
|
get => _textureBlendBits;
|
|
set { _textureBlendBits = value; EmitChanged(); }
|
|
}
|
|
|
|
BitSizeEnum _rotationBits = BitSizeEnum.Bits5_32Values;
|
|
|
|
/** How many uv rotation steps over a full turn. 5 bits is 32 steps of
|
|
11.25 degrees.
|
|
*/
|
|
[Export]
|
|
public BitSizeEnum rotationBits
|
|
{
|
|
get => _rotationBits;
|
|
set { _rotationBits = value; EmitChanged(); }
|
|
}
|
|
|
|
BitSizeEnum _scaleBits = BitSizeEnum.Bits4_16Values;
|
|
|
|
/** How many uv scale steps. The value is scaleMin + raw * scaleStep. */
|
|
[Export]
|
|
public BitSizeEnum scaleBits
|
|
{
|
|
get => _scaleBits;
|
|
set { _scaleBits = value; EmitChanged(); }
|
|
}
|
|
|
|
bool _holeFlag = true;
|
|
|
|
/** Whether the terrain is absent here. The consumer discards or writes a
|
|
NaN vertex; see the preview shader.
|
|
|
|
A bool rather than a width, because a 3-bit hole flag was never a
|
|
meaningful thing to be able to ask for.
|
|
*/
|
|
[Export]
|
|
public bool holeFlag
|
|
{
|
|
get => _holeFlag;
|
|
set { _holeFlag = value; EmitChanged(); }
|
|
}
|
|
|
|
bool _navigationFlag = true;
|
|
|
|
/** Whether the terrain is walkable here. Never reaches the shader's output -
|
|
it is read by the navmesh bake and by the flag read in ControlMap_shade.
|
|
*/
|
|
[Export]
|
|
public bool navigationFlag
|
|
{
|
|
get => _navigationFlag;
|
|
set { _navigationFlag = value; EmitChanged(); }
|
|
}
|
|
|
|
[ExportGroup( "Scale Mapping" )]
|
|
|
|
float _scaleMin = 1;
|
|
|
|
/** The uv multiplier a raw scale of 0 means. Deliberately 1 by default, so
|
|
an all-zero control map - which is what a freshly allocated one is -
|
|
renders every texture at its authored size rather than at some arbitrary
|
|
step. Every other field is neutral at 0 for the same reason.
|
|
*/
|
|
[Export]
|
|
public float scaleMin
|
|
{
|
|
get => _scaleMin;
|
|
set { _scaleMin = value; EmitChanged(); }
|
|
}
|
|
|
|
float _scaleStep = 0.25f;
|
|
|
|
/** How much one scale step adds to the multiplier. With the default 4 bits
|
|
and 0.25, the range is 1.0 to 4.75 - finer tiling only. Set a negative
|
|
step with a higher scaleMin to tile coarser instead.
|
|
*/
|
|
[Export]
|
|
public float scaleStep
|
|
{
|
|
get => _scaleStep;
|
|
set { _scaleStep = value; EmitChanged(); }
|
|
}
|
|
|
|
|
|
/* DERIVED WIDTHS
|
|
------------------------------------------------------------------------
|
|
The authored values as plain bit counts. Named ...BitCount throughout so
|
|
there is never a question of which of the two a name means.
|
|
*/
|
|
|
|
public int baseBitCount => (int) numTexturesBits;
|
|
public int overlayBitCount => (int) numTexturesBits;
|
|
public int blendBitCount => (int) textureBlendBits;
|
|
public int rotationBitCount => (int) rotationBits;
|
|
public int scaleBitCount => (int) scaleBits;
|
|
public int holeBitCount => holeFlag ? 1 : 0;
|
|
public int navigableBitCount => navigationFlag ? 1 : 0;
|
|
|
|
|
|
/* DERIVED OFFSETS
|
|
------------------------------------------------------------------------
|
|
Each field is ( offset, bits ), which is exactly the ivec2 the shader
|
|
takes. A bit count of 0 gives a field the shader reads as absent.
|
|
*/
|
|
|
|
public int baseOffset => 0;
|
|
public int overlayOffset => baseOffset + baseBitCount;
|
|
public int blendOffset => overlayOffset + overlayBitCount;
|
|
public int rotationOffset => blendOffset + blendBitCount;
|
|
public int scaleOffset => rotationOffset + rotationBitCount;
|
|
public int holeOffset => scaleOffset + scaleBitCount;
|
|
public int navigableOffset => holeOffset + holeBitCount;
|
|
|
|
public int usedBits => navigableOffset + navigableBitCount;
|
|
|
|
/** True when the layout does not fit. Nothing enforces it - the widths are
|
|
individually legal but their sum is not checked - so the generator checks
|
|
this and warns rather than silently writing a flag into nowhere.
|
|
*/
|
|
public bool isOverflowing => usedBits > TotalBits;
|
|
|
|
public Vector2I baseField => new Vector2I( baseOffset, baseBitCount );
|
|
public Vector2I overlayField => new Vector2I( overlayOffset, overlayBitCount );
|
|
public Vector2I blendField => new Vector2I( blendOffset, blendBitCount );
|
|
public Vector2I rotationField => new Vector2I( rotationOffset, rotationBitCount );
|
|
public Vector2I scaleField => new Vector2I( scaleOffset, scaleBitCount );
|
|
public Vector2I holeField => new Vector2I( holeOffset, holeBitCount );
|
|
public Vector2I navigableField => new Vector2I( navigableOffset, navigableBitCount );
|
|
|
|
/** How many textures an index can address. */
|
|
public int maxTextures => baseBitCount <= 0 ? 1 : 1 << baseBitCount;
|
|
|
|
|
|
/* PACKING
|
|
------------------------------------------------------------------------
|
|
The CPU half. Mirrors ControlMap_extract and the decoders on top of it -
|
|
and mirrors them by construction, since both are driven by the same
|
|
offsets above.
|
|
|
|
The pack pass itself is a compute shader and does none of this; these stay
|
|
because a seeded test, a navmesh query and a debug pattern all need to
|
|
speak the format without a GPU dispatch.
|
|
*/
|
|
|
|
static uint Mask( int bits )
|
|
{
|
|
if ( bits <= 0 )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return bits >= 32 ? uint.MaxValue : ( 1u << bits ) - 1u;
|
|
}
|
|
|
|
static uint Write( uint control, int offset, int bits, int value )
|
|
{
|
|
if ( bits <= 0 )
|
|
{
|
|
return control;
|
|
}
|
|
|
|
var mask = Mask( bits );
|
|
var raw = (uint) Mathf.Clamp( value, 0, (int) mask ) & mask;
|
|
|
|
return control | ( raw << offset );
|
|
}
|
|
|
|
public int Read( uint control, Vector2I field )
|
|
{
|
|
if ( field.Y <= 0 )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return (int) ( ( control >> field.X ) & Mask( field.Y ) );
|
|
}
|
|
|
|
/** A blend of 0..1 as its raw field value. The maximum maps to the field's
|
|
maximum, so both endpoints are reachable.
|
|
*/
|
|
public int BlendToRaw( float blend )
|
|
{
|
|
if ( blendBitCount <= 0 )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return Mathf.RoundToInt( Mathf.Clamp( blend, 0, 1 ) * Mask( blendBitCount ) );
|
|
}
|
|
|
|
public float RawToBlend( int raw )
|
|
{
|
|
return blendBitCount <= 0 ? 0 : raw / (float) Mask( blendBitCount );
|
|
}
|
|
|
|
/** A rotation in radians as its raw field value. Divided by the step
|
|
<marked>count</marked> rather than by the maximum, because the quantity
|
|
wraps - mapping the last step onto a full turn would make it a duplicate
|
|
of 0 and waste it.
|
|
*/
|
|
public int RotationToRaw( float radians )
|
|
{
|
|
if ( rotationBitCount <= 0 )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var steps = 1 << rotationBitCount;
|
|
var turns = radians / Mathf.Tau;
|
|
|
|
// Mathf.PosMod so a negative angle wraps rather than clamping to 0.
|
|
return Mathf.PosMod( Mathf.RoundToInt( turns * steps ), steps );
|
|
}
|
|
|
|
public float RawToRotation( int raw )
|
|
{
|
|
return rotationBitCount <= 0 ? 0 : raw / (float) ( 1 << rotationBitCount ) * Mathf.Tau;
|
|
}
|
|
|
|
/** A uv multiplier as its raw field value, inverting the shader's
|
|
scaleMin + raw * scaleStep.
|
|
*/
|
|
public int ScaleToRaw( float scale )
|
|
{
|
|
if ( scaleBitCount <= 0 || Mathf.Abs( scaleStep ) < 0.000001f )
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return Mathf.RoundToInt( ( scale - scaleMin ) / scaleStep );
|
|
}
|
|
|
|
public float RawToScale( int raw )
|
|
{
|
|
return scaleBitCount <= 0 ? 1 : scaleMin + raw * scaleStep;
|
|
}
|
|
|
|
/** One texel's control value.
|
|
|
|
blend is 0..1, rotation is radians, scale is the uv multiplier - the same
|
|
units the decoders hand back, so a caller never sees a raw field value
|
|
unless it wants to.
|
|
*/
|
|
public uint Pack(
|
|
int baseIndex, int overlayIndex, float blend, float rotation, float scale,
|
|
bool hole = false, bool navigable = true
|
|
)
|
|
{
|
|
return PackRaw(
|
|
baseIndex, overlayIndex,
|
|
BlendToRaw( blend ), RotationToRaw( rotation ), ScaleToRaw( scale ),
|
|
hole, navigable
|
|
);
|
|
}
|
|
|
|
/** Pack from raw field values, for a caller that is stepping a field rather
|
|
than expressing a quantity - a debug pattern walking every rotation, for
|
|
instance.
|
|
*/
|
|
public uint PackRaw(
|
|
int baseIndex, int overlayIndex, int blendRaw, int rotationRaw, int scaleRaw,
|
|
bool hole = false, bool navigable = true
|
|
)
|
|
{
|
|
uint control = 0;
|
|
|
|
control = Write( control, baseOffset, baseBitCount, baseIndex );
|
|
control = Write( control, overlayOffset, overlayBitCount, overlayIndex );
|
|
control = Write( control, blendOffset, blendBitCount, blendRaw );
|
|
control = Write( control, rotationOffset, rotationBitCount, rotationRaw );
|
|
control = Write( control, scaleOffset, scaleBitCount, scaleRaw );
|
|
control = Write( control, holeOffset, holeBitCount, hole ? 1 : 0 );
|
|
control = Write( control, navigableOffset, navigableBitCount, navigable ? 1 : 0 );
|
|
|
|
return control;
|
|
}
|
|
|
|
/** The neutral texel: texture 0, no overlay, no rotation, scale 1, not a
|
|
hole, navigable. Identical to PackRaw with every raw value 0 except the
|
|
navigable bit, which is what makes a zero-filled map sane.
|
|
*/
|
|
public uint packedDefault => PackRaw( 0, 0, 0, 0, 0, false, true );
|
|
|
|
|
|
/* STORAGE
|
|
------------------------------------------------------------------------
|
|
The map is a single-channel float texture, so a control value travels as
|
|
the float with those exact bits. Not a conversion - a reinterpretation,
|
|
undone on the GPU by floatBitsToUint.
|
|
|
|
R32F and not R32UI, and this is not a preference: Godot's Image has no
|
|
integer formats, and texture_rd_create maps an RD format to an
|
|
Image::Format before wrapping it - so no integer-format texture can ever
|
|
become a Texture2D. Measured in the phase 0 probe. imageStore to an r32f
|
|
target through uintBitsToFloat was measured bit-exact for both NaN
|
|
patterns, so the reinterpretation is safe through a compute write.
|
|
|
|
Which is also why the map must never be filtered, mipmapped or stored in
|
|
any format that renormalizes: a control value is frequently a NaN or a
|
|
denormal bit pattern as a float, and anything that does arithmetic on it
|
|
is free to flush it to zero.
|
|
*/
|
|
|
|
public static float ToStorage( uint control )
|
|
{
|
|
return BitConverter.Int32BitsToSingle( unchecked( (int) control ) );
|
|
}
|
|
|
|
public static uint FromStorage( float stored )
|
|
{
|
|
return unchecked( (uint) BitConverter.SingleToInt32Bits( stored ) );
|
|
}
|
|
|
|
|
|
/** Pushes the layout and its value mapping onto a material declaring
|
|
ControlMapShader's block.
|
|
*/
|
|
public void PushToMaterial( Material material )
|
|
{
|
|
if ( material == null )
|
|
{
|
|
return;
|
|
}
|
|
|
|
ControlMapShader.baseField.Set( material, baseField );
|
|
ControlMapShader.overlayField.Set( material, overlayField );
|
|
ControlMapShader.blendField.Set( material, blendField );
|
|
|
|
// The one place the two vocabularies meet. The shader side calls this the
|
|
// angle field, because what it decodes is radians; the authoring side
|
|
// calls it rotation, because what the user picks is how finely a texel can
|
|
// be turned. Mapped here rather than renamed on either side - the include
|
|
// is a render-side contract that predates this resource, and its symbols
|
|
// are deliberately left untouched so that the first time Godot compiles it
|
|
// is a test of the include alone.
|
|
ControlMapShader.angleField.Set( material, rotationField );
|
|
|
|
ControlMapShader.scaleField.Set( material, scaleField );
|
|
ControlMapShader.holeField.Set( material, holeField );
|
|
ControlMapShader.navigableField.Set( material, navigableField );
|
|
|
|
ControlMapShader.scaleMin.Set( material, scaleMin );
|
|
ControlMapShader.scaleStep.Set( material, scaleStep );
|
|
}
|
|
}
|
|
}
|