using Godot; using System; using Rokojori.Extensions; namespace Rokojori { /** One ground texture in a ControlMapTerrain's set. Its position in the terrain's `textures` array is its index, and that index is what a control-map texel stores. So reordering the array repaints the terrain - worth knowing before dragging entries around. Channel packing. Two textures per entry, not four: albedo RGB + height in alpha, and normal RGB + roughness in alpha. The packing is not an optimization here, it is the only affordable shape - a fragment reads up to 8 of these ( 4 control taps, base and overlay each ), so every extra sampler is 8 more fetches. Height has to be in alpha because the blend weighting needs it at the same time as the albedo, and roughness rides along with the normal because nothing needs it alone. */ [Tool] [GlobalClass] public partial class ControlMapTexture:Resource { Texture2D _albedo; /** RGB albedo, alpha = height. The height drives the blend weighting, so an entry with a flat alpha of 1 crossfades while one with real height interlocks with its neighbours. A texture with no alpha channel at all becomes opaque on import and therefore crossfades, which is the sane fallback rather than a special case. */ [Export] public Texture2D albedo { get => _albedo; set { _albedo = value; EmitChanged(); } } Texture2D _normal; /** RGB tangent-space normal, alpha = roughness. Left empty, the entry gets a flat normal and full roughness. */ [Export] public Texture2D normal { get => _normal; set { _normal = value; EmitChanged(); } } float _metersPerTile = 4; /** How many world meters one tile of this texture covers, before the per-texel scale multiplies it. This is the per-texture setting that matters most and the reason the parameter strip exists at all - gravel and a cliff face cannot share a tile size. */ [Export] public float metersPerTile { get => _metersPerTile; set { _metersPerTile = Mathf.Max( 0.01f, value ); EmitChanged(); } } float _detile = 0; /** Random rotation per control texel, 0 to 1 of a full turn, on top of the painted rotation. A texture with any visible structure reads as a grid at tile scale once it covers a large area; rotating each texel's copy breaks that up. Costs nothing - it is one hash and the rotation was already being applied - but it fights with a texture that has a direction of its own, so it is per texture rather than global. */ [Export( PropertyHint.Range, "0,1" )] public float detile { get => _detile; set { _detile = Mathf.Clamp( value, 0, 1 ); EmitChanged(); } } float _normalStrength = 1; /** Scales the normal map's XY before it is normalized. */ [Export] public float normalStrength { get => _normalStrength; set { _normalStrength = value; EmitChanged(); } } float _roughness = 1; /** Multiplies the roughness read from the normal map's alpha. */ [Export] public float roughness { get => _roughness; set { _roughness = value; EmitChanged(); } } /** The entry as the parameter strip's texel, in the order the shader's ControlMap_textureParams unpacks it. */ public Color ToParams() { return new Color( metersPerTile, detile, normalStrength, roughness ); } } }