396 lines
12 KiB
C#
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 );
|
|
}
|
|
}
|
|
}
|