rokojori_action_library/Runtime/Procedural/HeightMaps/Mesh/HeightMapMeshAtlas.cs

396 lines
12 KiB
C#

using System.Collections.Generic;
using Godot;
using System;
using Rokojori.Extensions;
namespace Rokojori
{
/** The one render pass in the height map system that rasterizes meshes instead
of blitting rects.
It exists because the blit path cannot do two things a spline layer needs.
It has no <marked>Max</marked> blend mode - the native modes are Mix, Add,
Sub and Mul - so two overlapping pieces of the same ribbon cannot resolve
to the higher one; and nearest-segment math in a shader is discontinuous
along the medial axis of a curve, so an analytic ribbon would tear its UVs
at inner corners and pinch on sharp turns. A mesh overdraws and lets the
depth test sort it out, which gives Max for free and makes corners a
non-problem.
<marked>One viewport, one draw, any number of layers.</marked> Each layer
allocates a region the size of its own texel rect, the regions are shelf
packed into this viewport's texture, and every ribbon is drawn into its own
region in a single pass. A ribbon never reads the height map, so this pass
has no ordering relationship with the layer loop at all - it runs before
it, once, and the layers then blit their regions out in tree order.
<marked>It only draws when something changed.</marked> Every layer's
geometry contributes to a signature; if the signature matches the last
pass, the viewport is left alone and its texture is reused. So dragging a
circle layer in the same map costs no mesh pass, and dragging a spline
point costs exactly one.
The draw is <marked>one frame behind</marked>. Nothing here calls
RenderingServer.ForceDraw(): it is documented as internal and can crash
when called from inside an existing draw, and a stale frame is cheap to
fix - the generator re-flags itself whenever this pass queued a redraw, so
the next render blits the fresh atlas. During a drag every frame is one
frame old, which is invisible; a single Render() press settles after one
extra frame.
*/
[Tool]
public partial class HeightMapMeshAtlas:SubViewport
{
/** The widest shelf the packer will use, and the cap on the viewport's own
size. Regions wider than this cannot be packed at all.
*/
public int maxSize = 4096;
/** Asks for a 16-bit float render target rather than an 8-bit one.
This matters more than it looks: the pass writes UVs and a position
along the line, not a colour, and an 8-bit target would quantize the UV
to 256 steps across a texture and - because a low dynamic range target
is also converted to sRGB on the way out - bend the values on top of
that. See HeightMapGenerator.meshAtlasSrgbDecode for the other half of
that question.
*/
public bool hdr = true;
/** Rounded up to this, and never shrunk. A drag changes the packing every
frame, and resizing a viewport throws its contents away - so the size
only ever grows, and in coarse steps.
*/
const int SizeGranularity = 256;
const int MinSize = SizeGranularity;
/** Normalized heights are 0..1, so the camera only has to cover that plus
clearance on both sides.
*/
const float CameraDistance = 2;
const float CameraNear = 0.05f;
const float CameraFar = 4;
Camera3D _camera;
readonly List<MeshInstance3D> _instances = new List<MeshInstance3D>();
int _usedInstances;
Vector2I _atlasSize = new Vector2I( MinSize, MinSize );
int _shelfX;
int _shelfY;
int _shelfHeight;
Vector2I _needed;
long _signature;
long _drawnSignature = -1;
bool _configured;
public Vector2I atlasSize => _atlasSize;
/** The packed buffer. A plain Texture2D to a sampler, so it goes straight
into a blit source slot.
*/
public Texture2D atlasTexture => GetTexture();
/** Region rect to UV, for the shader that reads a layer's region back out.
Normalized against the atlas rather than the region, because the sampler
works in whole-texture UV.
*/
public Rect2 RegionUV( Rect2I region )
{
var size = new Vector2( Mathf.Max( 1, _atlasSize.X ), Mathf.Max( 1, _atlasSize.Y ) );
return new Rect2(
new Vector2( region.Position.X, region.Position.Y ) / size,
new Vector2( region.Size.X, region.Size.Y ) / size
);
}
/* SETUP
------------------------------------------------------------------------
OwnWorld3D is the one setting that is not an optimisation: without it the
viewport renders the scene the generator lives in, and the atlas would
come back full of terrain.
The explicit Environment is there to pin the tonemapper to Linear. The
pass writes data, not light, so anything the post stack would normally do
to a colour is damage here.
*/
void Configure()
{
if ( _configured )
{
return;
}
_configured = true;
OwnWorld3D = true;
TransparentBg = true;
Msaa3D = Msaa.Disabled;
ScreenSpaceAA = ScreenSpaceAAEnum.Disabled;
UseTaa = false;
UseDebanding = false;
UseHdr2D = hdr;
PositionalShadowAtlasSize = 0;
AudioListenerEnable3D = false;
RenderTargetUpdateMode = UpdateMode.Disabled;
Size = _atlasSize;
var environment = new Godot.Environment();
environment.BackgroundMode = Godot.Environment.BGMode.Color;
environment.BackgroundColor = new Color( 0, 0, 0, 0 );
environment.AmbientLightSource = Godot.Environment.AmbientSource.Disabled;
environment.TonemapMode = Godot.Environment.ToneMapper.Linear;
environment.TonemapExposure = 1;
environment.TonemapWhite = 1;
environment.SsaoEnabled = false;
environment.SsilEnabled = false;
environment.SdfgiEnabled = false;
environment.GlowEnabled = false;
environment.FogEnabled = false;
environment.VolumetricFogEnabled = false;
environment.AdjustmentEnabled = false;
var world = FindWorld3D();
if ( world != null )
{
world.Environment = environment;
}
_camera = new Camera3D();
_camera.Projection = Camera3D.ProjectionType.Orthogonal;
_camera.Near = CameraNear;
_camera.Far = CameraFar;
_camera.Environment = environment;
AddChild( _camera );
}
/** Places the camera so that one world unit is one atlas texel and texel
(0,0) is the texture's first texel.
With KeepAspect = Height the orthogonal size is the vertical extent, so
setting it to the atlas's height makes the horizontal extent come out as
the atlas's width on its own - no aspect correction anywhere, and the
ribbon builder can emit plain texel coordinates.
*/
void PlaceCamera()
{
_camera.KeepAspect = Camera3D.KeepAspectEnum.Height;
_camera.Size = Mathf.Max( 1, _atlasSize.Y );
_camera.Position = new Vector3(
_atlasSize.X / 2f, -_atlasSize.Y / 2f, CameraDistance
);
_camera.Rotation = Vector3.Zero;
}
/* PACKING */
/** Starts a pass. Every layer that wants a region has to allocate it after
this and before Flush().
*/
public void Begin()
{
Configure();
// Re-applied rather than only set up front, so flipping it in the
// inspector during the live test actually does something. Changing the
// target's format throws its contents away, hence the invalidation.
if ( UseHdr2D != hdr )
{
UseHdr2D = hdr;
_drawnSignature = -1;
}
_shelfX = 0;
_shelfY = 0;
_shelfHeight = 0;
_needed = Vector2I.Zero;
_usedInstances = 0;
_signature = 0;
}
/** A region of exactly this size, or a zero-size rect when it does not fit.
Shelf packing: fill a row left to right, start a new row when the next
region would overflow the maximum width. Simple and good enough because
the regions are a handful of similar-sized rects, not a sprite sheet.
The size is the layer's own texel rect, unrounded - the later blit is a
1:1 texel copy out of this region, and anything else would resample the
UVs it is reading.
*/
public Rect2I Allocate( Vector2I size )
{
if ( size.X <= 0 || size.Y <= 0 || size.X > maxSize || size.Y > maxSize )
{
return new Rect2I( 0, 0, 0, 0 );
}
if ( _shelfX + size.X > maxSize )
{
_shelfY += _shelfHeight;
_shelfX = 0;
_shelfHeight = 0;
}
if ( _shelfY + size.Y > maxSize )
{
return new Rect2I( 0, 0, 0, 0 );
}
var region = new Rect2I( _shelfX, _shelfY, size.X, size.Y );
_shelfX += size.X;
_shelfHeight = Mathf.Max( _shelfHeight, size.Y );
_needed = new Vector2I(
Mathf.Max( _needed.X, _shelfX ),
Mathf.Max( _needed.Y, _shelfY + _shelfHeight )
);
return region;
}
/** Hands a built ribbon over. Meshes are drawn in submission order, which
has no effect on the result - the depth test decides, not the order.
`region` is the rect Allocate() returned. It is handed to the material so
the shader can clip to it: a layer's texel rect is clipped to the map, so
a spline leaving the map has geometry outside its own region, and the
region next to it belongs to another layer.
*/
public void Submit( ArrayMesh mesh, Rect2I region, long signature )
{
if ( mesh == null )
{
return;
}
var instance = InstanceAt( _usedInstances );
instance.Mesh = mesh;
instance.Visible = true;
( (HeightMapRibbonMaterial) instance.MaterialOverride ).SetRegion( region );
_usedInstances++;
_signature = unchecked( ( _signature ^ signature ) * 0x100000001b3 );
}
MeshInstance3D InstanceAt( int index )
{
while ( _instances.Count <= index )
{
var created = new MeshInstance3D();
created.MaterialOverride = new HeightMapRibbonMaterial();
created.GIMode = GeometryInstance3D.GIModeEnum.Disabled;
created.CastShadow = GeometryInstance3D.ShadowCastingSetting.Off;
AddChild( created );
_instances.Add( created );
}
return _instances[ index ];
}
/** Sizes the viewport, hides the unused instances and queues the draw.
True when a draw was queued - which is the generator's signal to flag
itself again, so the next render blits an atlas that is actually up to
date rather than one frame behind.
*/
public bool Flush()
{
Configure();
for ( var i = _usedInstances; i < _instances.Count; i++ )
{
_instances[ i ].Visible = false;
_instances[ i ].Mesh = null;
}
if ( _usedInstances == 0 )
{
RenderTargetUpdateMode = UpdateMode.Disabled;
_drawnSignature = 0;
return false;
}
var wanted = WantedSize();
if ( wanted != _atlasSize )
{
_atlasSize = wanted;
Size = wanted;
// A resize throws the render target's contents away, so the previous
// draw cannot be reused no matter what the geometry did.
_drawnSignature = -1;
}
PlaceCamera();
// Folded in because the region coordinates the meshes were built against
// only mean anything relative to this size.
var signature = unchecked( ( _signature ^ ( _atlasSize.X * 65537L + _atlasSize.Y ) ) * 0x100000001b3 );
if ( signature == _drawnSignature )
{
RenderTargetUpdateMode = UpdateMode.Disabled;
return false;
}
_drawnSignature = signature;
RenderTargetUpdateMode = UpdateMode.Once;
return true;
}
Vector2I WantedSize()
{
var width = Mathf.Max( MinSize, Grow( _needed.X ) );
var height = Mathf.Max( MinSize, Grow( _needed.Y ) );
// Grow-only, so a drag that momentarily needs less does not resize and
// throw the contents away.
return new Vector2I(
Mathf.Min( maxSize, Mathf.Max( width, _atlasSize.X ) ),
Mathf.Min( maxSize, Mathf.Max( height, _atlasSize.Y ) )
);
}
static int Grow( int value )
{
var steps = Mathf.CeilToInt( value / (float) SizeGranularity );
return Mathf.Max( 1, steps ) * SizeGranularity;
}
/** Rough VRAM cost of the atlas, in bytes. */
public long EstimateMemory()
{
return (long) _atlasSize.X * _atlasSize.Y * ( hdr ? 8 : 4 );
}
}
}