571 lines
16 KiB
C#
571 lines
16 KiB
C#
|
|
using System.Collections.Generic;
|
||
|
|
using Godot;
|
||
|
|
using System;
|
||
|
|
|
||
|
|
using Rokojori.Extensions;
|
||
|
|
namespace Rokojori
|
||
|
|
{
|
||
|
|
/** Renders a height map from the HeightMapDrawLayer nodes underneath it.
|
||
|
|
|
||
|
|
The node is the map's placement in the world: its global XZ is the map's
|
||
|
|
center, `size` is how many meters the map covers, and `resolution` is how
|
||
|
|
many texels that is. So a layer is positioned by <marked>moving it in the
|
||
|
|
3D viewport</marked> - the generator converts world XZ to texels, never the
|
||
|
|
other way round, and nothing in the system has a texel-space authoring
|
||
|
|
concept at all.
|
||
|
|
|
||
|
|
Height is stored normalized in the R channel and decoded by
|
||
|
|
addTerrainOffset ( Shading/Library/Terrain.gdshaderinc ) as
|
||
|
|
mix( minHeight, maxHeight, r ). minHeight/maxHeight are therefore part of
|
||
|
|
the contract between this node and the terrain material, which is what
|
||
|
|
PushToMaterials() exists to keep in sync.
|
||
|
|
|
||
|
|
Rendering is flag-driven, not immediate: a layer that moves or changes
|
||
|
|
calls SetDirty(), and the next _Process re-renders once. Several edits in
|
||
|
|
one frame - dragging a node, which moves it and re-emits every frame -
|
||
|
|
collapse into a single render. The render itself is GPU blits only, so at
|
||
|
|
the sizes this is meant for it is affordable per frame, which is what makes
|
||
|
|
dragging a layer around feel live in the editor.
|
||
|
|
|
||
|
|
Layers are collected by walking the children, so <marked>nesting is
|
||
|
|
grouping</marked>: put layers under a plain Node3D to move them together.
|
||
|
|
A nested HeightMapGenerator stops the walk - it owns its own map.
|
||
|
|
*/
|
||
|
|
[Tool]
|
||
|
|
[GlobalClass]
|
||
|
|
public partial class HeightMapGenerator:Node3D
|
||
|
|
{
|
||
|
|
static readonly Color White = new Color( 1, 1, 1, 1 );
|
||
|
|
static readonly Color Transparent = new Color( 0, 0, 0, 0 );
|
||
|
|
|
||
|
|
[ExportToolButton( "Render" )]
|
||
|
|
public Callable RenderButton => Callable.From( () => Render() );
|
||
|
|
|
||
|
|
|
||
|
|
Vector2 _size = new Vector2( 100, 100 );
|
||
|
|
|
||
|
|
/** The map's extent in world meters, on X and Z. */
|
||
|
|
[Export]
|
||
|
|
public Vector2 size
|
||
|
|
{
|
||
|
|
get => _size;
|
||
|
|
set { _size = value; SetDirty(); }
|
||
|
|
}
|
||
|
|
|
||
|
|
Vector2I _resolution = new Vector2I( 512, 512 );
|
||
|
|
|
||
|
|
/** The map's texture size. Combined with `size` this is the texel density -
|
||
|
|
512 over 100 m is roughly 0.2 m per texel.
|
||
|
|
*/
|
||
|
|
[Export]
|
||
|
|
public Vector2I resolution
|
||
|
|
{
|
||
|
|
get => _resolution;
|
||
|
|
set { _resolution = value; SetDirty(); }
|
||
|
|
}
|
||
|
|
|
||
|
|
float _minHeight = 0;
|
||
|
|
|
||
|
|
/** World height a stored 0 decodes to. Must match terrainMinHeight on the
|
||
|
|
material - PushToMaterials() does that.
|
||
|
|
*/
|
||
|
|
[Export]
|
||
|
|
public float minHeight
|
||
|
|
{
|
||
|
|
get => _minHeight;
|
||
|
|
set { _minHeight = value; SetDirty(); }
|
||
|
|
}
|
||
|
|
|
||
|
|
float _maxHeight = 100;
|
||
|
|
|
||
|
|
/** World height a stored 1 decodes to. Everything a layer writes is a
|
||
|
|
fraction of maxHeight - minHeight, so widening the range flattens the
|
||
|
|
existing layers rather than rescaling them.
|
||
|
|
*/
|
||
|
|
[Export]
|
||
|
|
public float maxHeight
|
||
|
|
{
|
||
|
|
get => _maxHeight;
|
||
|
|
set { _maxHeight = value; SetDirty(); }
|
||
|
|
}
|
||
|
|
|
||
|
|
float _baseHeight = 0;
|
||
|
|
|
||
|
|
/** The world height the map starts at before any layer draws. */
|
||
|
|
[Export]
|
||
|
|
public float baseHeight
|
||
|
|
{
|
||
|
|
get => _baseHeight;
|
||
|
|
set { _baseHeight = value; SetDirty(); }
|
||
|
|
}
|
||
|
|
|
||
|
|
TextureStackFormat _format = TextureStackFormat.RgbaHalf;
|
||
|
|
|
||
|
|
/** RgbaHalf by default, not Rgba8: 8 bits give 256 height steps over the
|
||
|
|
whole min-to-max range, which terraces visibly on any slope. Half is
|
||
|
|
smooth and still only 8 bytes per texel.
|
||
|
|
*/
|
||
|
|
[Export]
|
||
|
|
public TextureStackFormat format
|
||
|
|
{
|
||
|
|
get => _format;
|
||
|
|
set { _format = value; SetDirty(); }
|
||
|
|
}
|
||
|
|
|
||
|
|
/** The map itself. A DrawableTexture2D because that is what can be blitted
|
||
|
|
into; it is still a plain Texture2D to everything that samples it, so it
|
||
|
|
goes straight into a material's sampler slot.
|
||
|
|
|
||
|
|
Left empty, one is created on the first render - but then only this node
|
||
|
|
holds it, so the material has to be fed through `materials` below. Assign
|
||
|
|
a shared DrawableTexture2D here to reference the same map from a material
|
||
|
|
in the scene file instead.
|
||
|
|
*/
|
||
|
|
[Export]
|
||
|
|
public DrawableTexture2D heightMapTarget;
|
||
|
|
|
||
|
|
/** Materials to push terrainHeightMap / terrainCenterXZ / terrainSizeXZ /
|
||
|
|
terrainMinHeight / terrainMaxHeight onto after every render.
|
||
|
|
|
||
|
|
This is the whole wiring between the generator and anything rendering the
|
||
|
|
terrain - GeometryTerrainOffset's uniform block, by name. Without it the
|
||
|
|
material would silently keep the size and height range it was authored
|
||
|
|
with while the generator used different ones.
|
||
|
|
*/
|
||
|
|
[Export]
|
||
|
|
public Material[] materials = new Material[ 0 ];
|
||
|
|
|
||
|
|
/** Off makes the map update only on the Render button, for a map too big to
|
||
|
|
recompute while dragging.
|
||
|
|
*/
|
||
|
|
[Export]
|
||
|
|
public bool autoRender = true;
|
||
|
|
|
||
|
|
[ExportGroup( "Debug Info" )]
|
||
|
|
|
||
|
|
[Export]
|
||
|
|
public int renderedLayers;
|
||
|
|
|
||
|
|
|
||
|
|
/* THE DIRTY FLAG
|
||
|
|
------------------------------------------------------------------------
|
||
|
|
One bool, checked in _Process, rather than rendering straight out of the
|
||
|
|
setter. A drag emits a transform notification every frame and an inspector
|
||
|
|
slider emits one per pixel of mouse travel, so rendering per notification
|
||
|
|
would run several full renders per frame to show one result.
|
||
|
|
*/
|
||
|
|
|
||
|
|
bool _dirty = true;
|
||
|
|
|
||
|
|
public void SetDirty()
|
||
|
|
{
|
||
|
|
_dirty = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
public bool isDirty => _dirty;
|
||
|
|
|
||
|
|
public override void _Ready()
|
||
|
|
{
|
||
|
|
SetDirty();
|
||
|
|
}
|
||
|
|
|
||
|
|
public override void _Process( double delta )
|
||
|
|
{
|
||
|
|
if ( ! _dirty || ! autoRender )
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
Render();
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/* COORDINATES
|
||
|
|
------------------------------------------------------------------------
|
||
|
|
Texel (0,0) is the map's minimum XZ corner. addTerrainOffset computes its
|
||
|
|
UV as ( worldXZ - ( center - size/2 ) ) / size and samples with texture(),
|
||
|
|
whose (0,0) is the image's first texel - and BlitRect's Rect2I origin is
|
||
|
|
that same first texel, with no Y flip. So image +Y is world +Z throughout,
|
||
|
|
and nothing here needs to mirror anything.
|
||
|
|
*/
|
||
|
|
|
||
|
|
/** World XZ of the map's center - the node's own global position. */
|
||
|
|
public Vector2 centerXZ
|
||
|
|
{
|
||
|
|
get
|
||
|
|
{
|
||
|
|
var p = GlobalPosition;
|
||
|
|
return new Vector2( p.X, p.Z );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public Vector2 minXZ => centerXZ - size / 2;
|
||
|
|
|
||
|
|
public Vector2I textureSize => new Vector2I( Mathf.Max( 1, resolution.X ), Mathf.Max( 1, resolution.Y ) );
|
||
|
|
|
||
|
|
public Vector2 metersPerTexel
|
||
|
|
{
|
||
|
|
get
|
||
|
|
{
|
||
|
|
var r = textureSize;
|
||
|
|
return new Vector2( size.X / r.X, size.Y / r.Y );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public Rect2I fullRect => new Rect2I( 0, 0, textureSize.X, textureSize.Y );
|
||
|
|
|
||
|
|
public Vector2 TexelToWorldXZ( Vector2 texel )
|
||
|
|
{
|
||
|
|
var m = metersPerTexel;
|
||
|
|
var origin = minXZ;
|
||
|
|
|
||
|
|
return new Vector2( origin.X + texel.X * m.X, origin.Y + texel.Y * m.Y );
|
||
|
|
}
|
||
|
|
|
||
|
|
public Vector2 WorldXZToTexel( Vector2 worldXZ )
|
||
|
|
{
|
||
|
|
var m = metersPerTexel;
|
||
|
|
var origin = minXZ;
|
||
|
|
|
||
|
|
return new Vector2( ( worldXZ.X - origin.X ) / m.X, ( worldXZ.Y - origin.Y ) / m.Y );
|
||
|
|
}
|
||
|
|
|
||
|
|
/** The texel rect a world-space XZ box covers, grown to whole texels and
|
||
|
|
clipped to the map. Empty when the box misses the map entirely.
|
||
|
|
*/
|
||
|
|
public Rect2I WorldBoxToRect( Vector2 worldMin, Vector2 worldMax )
|
||
|
|
{
|
||
|
|
var min = WorldXZToTexel( worldMin );
|
||
|
|
var max = WorldXZToTexel( worldMax );
|
||
|
|
|
||
|
|
var x0 = Mathf.FloorToInt( Mathf.Min( min.X, max.X ) );
|
||
|
|
var y0 = Mathf.FloorToInt( Mathf.Min( min.Y, max.Y ) );
|
||
|
|
var x1 = Mathf.CeilToInt( Mathf.Max( min.X, max.X ) );
|
||
|
|
var y1 = Mathf.CeilToInt( Mathf.Max( min.Y, max.Y ) );
|
||
|
|
|
||
|
|
return new Rect2I( x0, y0, x1 - x0, y1 - y0 ).Intersection( fullRect );
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Convenience for a radially symmetric layer. */
|
||
|
|
public Rect2I WorldCircleToRect( Vector3 worldCenter, float worldRadius )
|
||
|
|
{
|
||
|
|
var c = new Vector2( worldCenter.X, worldCenter.Z );
|
||
|
|
var r = new Vector2( worldRadius, worldRadius );
|
||
|
|
|
||
|
|
return WorldBoxToRect( c - r, c + r );
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/* HEIGHT NORMALIZATION
|
||
|
|
------------------------------------------------------------------------
|
||
|
|
Layers author in meters; the map stores 0..1. These two are the only
|
||
|
|
conversion, and which one a layer uses is decided by its blend mode -
|
||
|
|
HeightMapBlendMode.IsAbsolute().
|
||
|
|
*/
|
||
|
|
|
||
|
|
public float heightRange
|
||
|
|
{
|
||
|
|
get
|
||
|
|
{
|
||
|
|
var range = maxHeight - minHeight;
|
||
|
|
|
||
|
|
return Mathf.Abs( range ) < 0.0001f ? 1 : range;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** A height difference in meters as a fraction of the range, for Add and
|
||
|
|
Subtract.
|
||
|
|
*/
|
||
|
|
public float NormalizeDelta( float worldHeightDelta )
|
||
|
|
{
|
||
|
|
return worldHeightDelta / heightRange;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** An absolute world height as a stored 0..1 value, for Replace. */
|
||
|
|
public float NormalizeLevel( float worldHeight )
|
||
|
|
{
|
||
|
|
return ( worldHeight - minHeight ) / heightRange;
|
||
|
|
}
|
||
|
|
|
||
|
|
public float baseLevel => NormalizeLevel( baseHeight );
|
||
|
|
|
||
|
|
|
||
|
|
/* RENDERING */
|
||
|
|
|
||
|
|
HeightMapShapeMaterial _shapeMaterial;
|
||
|
|
|
||
|
|
/** The shared content-pass material. Layers configure it in
|
||
|
|
RenderToHeightMap() and then call DrawShape(), rather than allocating a
|
||
|
|
material each.
|
||
|
|
*/
|
||
|
|
public HeightMapShapeMaterial shapeMaterial
|
||
|
|
{
|
||
|
|
get
|
||
|
|
{
|
||
|
|
if ( _shapeMaterial == null )
|
||
|
|
{
|
||
|
|
_shapeMaterial = new HeightMapShapeMaterial();
|
||
|
|
}
|
||
|
|
|
||
|
|
return _shapeMaterial;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
BlitMaterial _blendBlit;
|
||
|
|
BlitMaterial _copyBlit;
|
||
|
|
|
||
|
|
/** Straight overwrite, for clearing. */
|
||
|
|
BlitMaterial copyBlit
|
||
|
|
{
|
||
|
|
get
|
||
|
|
{
|
||
|
|
if ( _copyBlit == null )
|
||
|
|
{
|
||
|
|
_copyBlit = new BlitMaterial();
|
||
|
|
_copyBlit.BlendMode = BlitMaterial.BlendModeEnum.Disabled;
|
||
|
|
}
|
||
|
|
|
||
|
|
return _copyBlit;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
readonly List<HeightMapDrawLayer> _layers = new List<HeightMapDrawLayer>();
|
||
|
|
|
||
|
|
bool _rendering = false;
|
||
|
|
|
||
|
|
/** Clears the map to baseHeight and lets every visible layer underneath draw
|
||
|
|
into it, in tree order - so the layer listed last wins, the same way the
|
||
|
|
3D viewport draws it.
|
||
|
|
*/
|
||
|
|
public void Render()
|
||
|
|
{
|
||
|
|
// A layer that calls SetDirty() from inside its own draw - a transform
|
||
|
|
// notification from a tool script, say - would otherwise recurse.
|
||
|
|
if ( _rendering )
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
_dirty = false;
|
||
|
|
_rendering = true;
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
PrepareTarget();
|
||
|
|
|
||
|
|
_layers.Clear();
|
||
|
|
CollectLayers( this, _layers );
|
||
|
|
|
||
|
|
renderedLayers = 0;
|
||
|
|
|
||
|
|
for ( var i = 0; i < _layers.Count; i++ )
|
||
|
|
{
|
||
|
|
var layer = _layers[ i ];
|
||
|
|
|
||
|
|
if ( ! layer.IsVisibleInTree() )
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
layer.RenderToHeightMap( this );
|
||
|
|
|
||
|
|
renderedLayers++;
|
||
|
|
}
|
||
|
|
|
||
|
|
PushToMaterials();
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
_rendering = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void CollectLayers( Node node, List<HeightMapDrawLayer> into )
|
||
|
|
{
|
||
|
|
foreach ( var child in node.GetChildren() )
|
||
|
|
{
|
||
|
|
// Its own map, its own layers.
|
||
|
|
if ( child is HeightMapGenerator )
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( child is HeightMapDrawLayer layer )
|
||
|
|
{
|
||
|
|
into.Add( layer );
|
||
|
|
}
|
||
|
|
|
||
|
|
CollectLayers( child, into );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** One blit per layer pair: the shape into a rect-sized scratch buffer with
|
||
|
|
blending disabled, then the scratch onto the map with the blend mode's
|
||
|
|
hardware blend.
|
||
|
|
|
||
|
|
The scratch buffer is rect-sized rather than full-size because BlitRect
|
||
|
|
stretches its whole source across the destination rect - a full-size
|
||
|
|
source would be squeezed into the rect.
|
||
|
|
|
||
|
|
Called by layers after they have set up `shapeMaterial`; the rect and
|
||
|
|
blend-mode uniforms are set here, so a layer cannot get the world mapping
|
||
|
|
wrong.
|
||
|
|
*/
|
||
|
|
public void DrawShape( Rect2I rect, HeightMapBlendMode blendMode )
|
||
|
|
{
|
||
|
|
if ( heightMapTarget == null || rect.Size.X <= 0 || rect.Size.Y <= 0 )
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var material = shapeMaterial;
|
||
|
|
var meters = metersPerTexel;
|
||
|
|
|
||
|
|
material.rectWorldMin.Set( TexelToWorldXZ( new Vector2( rect.Position.X, rect.Position.Y ) ) );
|
||
|
|
material.rectWorldSize.Set( new Vector2( rect.Size.X * meters.X, rect.Size.Y * meters.Y ) );
|
||
|
|
material.blendMode.Set( (int) blendMode );
|
||
|
|
|
||
|
|
var scratch = TextureStackBufferPool.shared.Rent( rect.Size.X, rect.Size.Y, format, Transparent );
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
// A blit needs a source even when the shader samples none, so a shared
|
||
|
|
// 1x1 stands in. The shape is entirely procedural from world XZ.
|
||
|
|
scratch.BlitRect(
|
||
|
|
new Rect2I( 0, 0, rect.Size.X, rect.Size.Y ), dummySource, White, 0, material
|
||
|
|
);
|
||
|
|
|
||
|
|
if ( _blendBlit == null )
|
||
|
|
{
|
||
|
|
_blendBlit = new BlitMaterial();
|
||
|
|
}
|
||
|
|
|
||
|
|
_blendBlit.BlendMode = blendMode.ToBlitBlendMode();
|
||
|
|
|
||
|
|
heightMapTarget.BlitRect( rect, scratch, White, 0, _blendBlit );
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
TextureStackBufferPool.shared.Return( scratch );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
DrawableTexture2D _dummySource;
|
||
|
|
|
||
|
|
DrawableTexture2D dummySource
|
||
|
|
{
|
||
|
|
get
|
||
|
|
{
|
||
|
|
if ( _dummySource == null )
|
||
|
|
{
|
||
|
|
_dummySource = new DrawableTexture2D();
|
||
|
|
_dummySource.Setup( 1, 1, DrawableTexture2D.DrawableFormat.Rgba8, White, false );
|
||
|
|
}
|
||
|
|
|
||
|
|
return _dummySource;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/* PREPARING THE MAP
|
||
|
|
------------------------------------------------------------------------
|
||
|
|
Setup() reallocates, and reallocating makes the engine emit the texture's
|
||
|
|
changed signal - which rebuilds the inspector. So it runs only when the
|
||
|
|
allocation actually has to change, and every other render clears by
|
||
|
|
blitting a 1x1 base-height texture across the map with blending disabled.
|
||
|
|
Same pixels, no reallocation, no signal. This is also what takes the one
|
||
|
|
expensive fixed cost out of a re-render and makes rendering per frame
|
||
|
|
affordable - the same finding the TextureStack compositor landed on.
|
||
|
|
*/
|
||
|
|
|
||
|
|
DrawableTexture2D _setupTarget;
|
||
|
|
Vector2I _setupSize;
|
||
|
|
TextureStackFormat _setupFormat;
|
||
|
|
|
||
|
|
DrawableTexture2D _clearSource;
|
||
|
|
Color _clearSourceColor;
|
||
|
|
TextureStackFormat _clearSourceFormat;
|
||
|
|
|
||
|
|
void PrepareTarget()
|
||
|
|
{
|
||
|
|
if ( heightMapTarget == null )
|
||
|
|
{
|
||
|
|
heightMapTarget = new DrawableTexture2D();
|
||
|
|
}
|
||
|
|
|
||
|
|
var level = baseLevel;
|
||
|
|
var clearColor = new Color( level, level, level, 1 );
|
||
|
|
var wanted = textureSize;
|
||
|
|
|
||
|
|
if ( _setupTarget != heightMapTarget || _setupSize != wanted || _setupFormat != format )
|
||
|
|
{
|
||
|
|
heightMapTarget.Setup( wanted.X, wanted.Y, format.ToDrawableFormat(), clearColor, false );
|
||
|
|
|
||
|
|
_setupTarget = heightMapTarget;
|
||
|
|
_setupSize = wanted;
|
||
|
|
_setupFormat = format;
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
ClearTarget( clearColor );
|
||
|
|
}
|
||
|
|
|
||
|
|
void ClearTarget( Color clearColor )
|
||
|
|
{
|
||
|
|
if ( _clearSource == null || _clearSourceColor != clearColor || _clearSourceFormat != format )
|
||
|
|
{
|
||
|
|
if ( _clearSource == null )
|
||
|
|
{
|
||
|
|
_clearSource = new DrawableTexture2D();
|
||
|
|
}
|
||
|
|
|
||
|
|
_clearSource.Setup( 1, 1, format.ToDrawableFormat(), clearColor, false );
|
||
|
|
|
||
|
|
_clearSourceColor = clearColor;
|
||
|
|
_clearSourceFormat = format;
|
||
|
|
}
|
||
|
|
|
||
|
|
heightMapTarget.BlitRect( fullRect, _clearSource, White, 0, copyBlit );
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/** Pushes the map and its world mapping onto every assigned material. */
|
||
|
|
public void PushToMaterials()
|
||
|
|
{
|
||
|
|
if ( materials == null )
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
for ( var i = 0; i < materials.Length; i++ )
|
||
|
|
{
|
||
|
|
var material = materials[ i ];
|
||
|
|
|
||
|
|
if ( material == null )
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
TerrainOffsetShader.heightMap.Set( material, heightMapTarget );
|
||
|
|
TerrainOffsetShader.centerXZ.Set( material, centerXZ );
|
||
|
|
TerrainOffsetShader.sizeXZ.Set( material, size );
|
||
|
|
TerrainOffsetShader.minHeight.Set( material, minHeight );
|
||
|
|
TerrainOffsetShader.maxHeight.Set( material, maxHeight );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/** Rough VRAM cost of the map itself, in bytes. The scratch buffer is
|
||
|
|
rect-sized and pooled, so it is not counted - a layer covering the whole
|
||
|
|
map would add one more of these.
|
||
|
|
*/
|
||
|
|
public long EstimateMemory()
|
||
|
|
{
|
||
|
|
var r = textureSize;
|
||
|
|
|
||
|
|
return (long) r.X * r.Y * format.BytesPerPixel();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|