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 only 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. Only bit sizes are authored. 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 derived 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 both 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 count 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 ); } } }