540 lines
17 KiB
C#
540 lines
17 KiB
C#
|
|
using System.Collections.Generic;
|
||
|
|
using Godot;
|
||
|
|
using System;
|
||
|
|
|
||
|
|
using Rokojori.Extensions;
|
||
|
|
namespace Rokojori
|
||
|
|
{
|
||
|
|
/** Turns a Spline into the ribbon mesh that HeightMapMeshAtlas rasterizes.
|
||
|
|
|
||
|
|
This is the line-renderer half of the spline layer: a quad strip walked
|
||
|
|
along the curve, widened to the per-point `width` data, with one span of
|
||
|
|
the strip per texture instance. It is the same technique LineVFX uses,
|
||
|
|
with two differences - the strip is flat in XZ because the map is a height
|
||
|
|
field, and the deformation happens here on the CPU rather than in a shader,
|
||
|
|
because there is nothing to animate and the vertex count is in the
|
||
|
|
hundreds.
|
||
|
|
|
||
|
|
<marked>Stations are spaced by arc length</marked>, not by point index. A
|
||
|
|
rational bezier does not travel at a constant rate along its parameter, so
|
||
|
|
stepping the parameter evenly would both bunch the geometry up in the tight
|
||
|
|
parts of the curve and stretch the texture there - see
|
||
|
|
SplineArcLengthTable, which exists for this.
|
||
|
|
|
||
|
|
Everything is emitted in <marked>atlas texel coordinates</marked>: X is the
|
||
|
|
atlas column, Y is the negated atlas row, Z is the normalized height. That
|
||
|
|
makes the mesh independent of the atlas's final size - only the camera
|
||
|
|
depends on it - and it is what lets the atlas pack regions first and size
|
||
|
|
itself afterwards. Negated Y because image row 0 is at the top of the
|
||
|
|
camera's view, so rows have to grow downwards in world space.
|
||
|
|
*/
|
||
|
|
public class HeightMapRibbonBuilder
|
||
|
|
{
|
||
|
|
/** Vertex rows per span. Fixed rather than derived from curvature, because
|
||
|
|
the strip is rasterized into texels and a span is short - one texture
|
||
|
|
instance - so the chord error is already far below a texel for anything
|
||
|
|
that is not a hairpin.
|
||
|
|
*/
|
||
|
|
public int rowsPerSpan = 24;
|
||
|
|
|
||
|
|
/** How much arc length one copy of the middle texture covers. 0 maps one
|
||
|
|
copy onto each spline segment instead, which keeps the authoring simple
|
||
|
|
at the cost of stretching the texture on an unevenly spaced spline.
|
||
|
|
*/
|
||
|
|
public float middleTileLength = 0;
|
||
|
|
|
||
|
|
/** Atlas slots, as HeightMapRibbonAtlas packs them: 0 start, 1 middle,
|
||
|
|
2 end, in one horizontal row of square slots.
|
||
|
|
*/
|
||
|
|
public int slots = 3;
|
||
|
|
public int slotSize = 512;
|
||
|
|
|
||
|
|
public const int SlotStart = 0;
|
||
|
|
public const int SlotMiddle = 1;
|
||
|
|
public const int SlotEnd = 2;
|
||
|
|
|
||
|
|
const int MaxRepeatsPerSegment = 64;
|
||
|
|
|
||
|
|
|
||
|
|
/* THE SPLINE STATE
|
||
|
|
------------------------------------------------------------------------
|
||
|
|
Resolved once per render by Prepare(), because the curve, the length table
|
||
|
|
and the widths are all needed twice - once to find the layer's texel rect
|
||
|
|
and once to build the mesh into the region that rect got.
|
||
|
|
*/
|
||
|
|
|
||
|
|
SplineCurve _curve;
|
||
|
|
SplineArcLengthTable _table;
|
||
|
|
float[] _widths;
|
||
|
|
bool _closed;
|
||
|
|
|
||
|
|
public float length => _table == null ? 0 : _table.length;
|
||
|
|
public int numSegments => _table == null ? 0 : _table.numSegments;
|
||
|
|
public SplineArcLengthTable table => _table;
|
||
|
|
|
||
|
|
/** Reads the spline into a curve, an arc-length table and a per-curve-point
|
||
|
|
width array. False when there is nothing to draw.
|
||
|
|
|
||
|
|
The width array is sized to the <marked>curve's</marked> points, not the
|
||
|
|
spline's: a closed spline's curve carries one extra point duplicating
|
||
|
|
index 0, so the last entry duplicates the first width too and the seam
|
||
|
|
interpolates instead of jumping.
|
||
|
|
*/
|
||
|
|
public bool Prepare( Spline spline, string widthDataName, float defaultWidth, int samplesPerSegment )
|
||
|
|
{
|
||
|
|
_curve = null;
|
||
|
|
_table = null;
|
||
|
|
_widths = null;
|
||
|
|
|
||
|
|
if ( spline == null )
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// The curve is cached inside the Spline and nothing invalidates it when a
|
||
|
|
// point moves outside the editor's gizmo pass, so it is dropped here.
|
||
|
|
spline.ClearCurveCache();
|
||
|
|
|
||
|
|
var points = spline.GetPoints();
|
||
|
|
|
||
|
|
if ( points.Count < 2 )
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
_closed = spline.closed;
|
||
|
|
_curve = spline.GetCurve();
|
||
|
|
|
||
|
|
if ( _curve == null || _curve.points.Count < 2 )
|
||
|
|
{
|
||
|
|
_curve = null;
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
_table = SplineArcLengthTable.Create( _curve, samplesPerSegment );
|
||
|
|
|
||
|
|
if ( _table.length <= 0.000001f )
|
||
|
|
{
|
||
|
|
_curve = null;
|
||
|
|
_table = null;
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
_widths = new float[ _curve.points.Count ];
|
||
|
|
|
||
|
|
for ( var i = 0; i < _widths.Length; i++ )
|
||
|
|
{
|
||
|
|
var point = points[ i % points.Count ];
|
||
|
|
|
||
|
|
_widths[ i ] = Mathf.Max( 0, spline.ResolveFloatData( point, widthDataName, defaultWidth ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Linearly interpolated between the two surrounding points, which is what
|
||
|
|
"the values are interpolated from one point to the other" means - the
|
||
|
|
width follows the point spacing, not the curve's own parameter speed.
|
||
|
|
*/
|
||
|
|
public float WidthAtPointIndex( float pointIndex )
|
||
|
|
{
|
||
|
|
var maxIndex = _widths.Length - 1;
|
||
|
|
var clamped = Mathf.Clamp( pointIndex, 0, maxIndex );
|
||
|
|
|
||
|
|
var lower = Mathf.FloorToInt( clamped );
|
||
|
|
var upper = Mathf.Min( lower + 1, maxIndex );
|
||
|
|
|
||
|
|
return Mathf.Lerp( _widths[ lower ], _widths[ upper ], clamped - lower );
|
||
|
|
}
|
||
|
|
|
||
|
|
public float WidthAtDistance( float distance )
|
||
|
|
{
|
||
|
|
return WidthAtPointIndex( _table.PointIndexAtDistance( distance ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
float tangentDelta => Mathf.Max( 0.01f, _table.length * 0.001f );
|
||
|
|
|
||
|
|
/** The ribbon's world XZ bounds, including the half width at every station.
|
||
|
|
|
||
|
|
Sampled at the same density the mesh will use, so the rect cannot end up
|
||
|
|
smaller than the geometry that goes into it.
|
||
|
|
*/
|
||
|
|
public bool GetWorldBounds( out Vector2 min, out Vector2 max )
|
||
|
|
{
|
||
|
|
min = Vector2.Zero;
|
||
|
|
max = Vector2.Zero;
|
||
|
|
|
||
|
|
if ( _table == null )
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
var total = _table.length;
|
||
|
|
var stations = Mathf.Max( 2, numSegments * Mathf.Max( 2, rowsPerSpan ) );
|
||
|
|
var first = true;
|
||
|
|
|
||
|
|
for ( var i = 0; i < stations; i++ )
|
||
|
|
{
|
||
|
|
var distance = total * i / (float) ( stations - 1 );
|
||
|
|
var position = _table.PositionAtDistance( distance );
|
||
|
|
var halfWidth = WidthAtDistance( distance ) / 2;
|
||
|
|
|
||
|
|
var center = new Vector2( position.X, position.Z );
|
||
|
|
var radius = new Vector2( halfWidth, halfWidth );
|
||
|
|
|
||
|
|
if ( first )
|
||
|
|
{
|
||
|
|
min = center - radius;
|
||
|
|
max = center + radius;
|
||
|
|
first = false;
|
||
|
|
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
min = min.Min( center - radius );
|
||
|
|
max = max.Max( center + radius );
|
||
|
|
}
|
||
|
|
|
||
|
|
return ! first;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/* THE MESH */
|
||
|
|
|
||
|
|
readonly List<Vector3> _vertices = new List<Vector3>();
|
||
|
|
readonly List<Vector2> _uvs = new List<Vector2>();
|
||
|
|
readonly List<Vector2> _uv2s = new List<Vector2>();
|
||
|
|
readonly List<int> _indices = new List<int>();
|
||
|
|
|
||
|
|
long _signature;
|
||
|
|
|
||
|
|
/** Changes whenever the emitted geometry does. The atlas compares these to
|
||
|
|
decide whether the GPU pass has to run at all - moving a circle layer in
|
||
|
|
the same map then costs no draw.
|
||
|
|
*/
|
||
|
|
public long signature => _signature;
|
||
|
|
|
||
|
|
/** One ArrayMesh, one surface. The texture choice is in the UVs rather than
|
||
|
|
in a material, so a ribbon crossing from the start texture into the
|
||
|
|
middle one is still a single draw.
|
||
|
|
|
||
|
|
`rect` is the layer's texel rect in the map, `regionOrigin` where that
|
||
|
|
rect was packed into the atlas. The two have the same size by
|
||
|
|
definition, which is what keeps the later blit a 1:1 texel copy.
|
||
|
|
*/
|
||
|
|
public ArrayMesh BuildMesh( HeightMapGenerator generator, Rect2I rect, Vector2I regionOrigin )
|
||
|
|
{
|
||
|
|
_vertices.Clear();
|
||
|
|
_uvs.Clear();
|
||
|
|
_uv2s.Clear();
|
||
|
|
_indices.Clear();
|
||
|
|
|
||
|
|
_signature = HashSeed;
|
||
|
|
|
||
|
|
if ( _table == null )
|
||
|
|
{
|
||
|
|
_signature = 0;
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
HashLong( regionOrigin.X );
|
||
|
|
HashLong( regionOrigin.Y );
|
||
|
|
HashLong( rect.Size.X );
|
||
|
|
HashLong( rect.Size.Y );
|
||
|
|
|
||
|
|
// World XZ -> map texel -> atlas texel. The map texel offset of the rect
|
||
|
|
// is subtracted, so a layer whose rect the map's edge clipped still lands
|
||
|
|
// at the region's origin.
|
||
|
|
var texelOffset = new Vector2(
|
||
|
|
regionOrigin.X - rect.Position.X,
|
||
|
|
regionOrigin.Y - rect.Position.Y
|
||
|
|
);
|
||
|
|
|
||
|
|
var segments = numSegments;
|
||
|
|
|
||
|
|
for ( var segment = 0; segment < segments; segment++ )
|
||
|
|
{
|
||
|
|
var slot = SlotFor( segment, segments );
|
||
|
|
|
||
|
|
var segmentStart = _table.DistanceAtPointIndex( segment );
|
||
|
|
var segmentEnd = _table.DistanceAtPointIndex( segment + 1 );
|
||
|
|
var segmentLength = segmentEnd - segmentStart;
|
||
|
|
|
||
|
|
if ( segmentLength <= 0.000001f )
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
var repeats = RepeatsFor( slot, segmentLength );
|
||
|
|
|
||
|
|
for ( var repeat = 0; repeat < repeats; repeat++ )
|
||
|
|
{
|
||
|
|
var from = Mathf.Lerp( segmentStart, segmentEnd, repeat / (float) repeats );
|
||
|
|
var to = Mathf.Lerp( segmentStart, segmentEnd, ( repeat + 1 ) / (float) repeats );
|
||
|
|
|
||
|
|
BuildSpan( generator, texelOffset, slot, from, to );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( _indices.Count == 0 )
|
||
|
|
{
|
||
|
|
_signature = 0;
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Constant and facing the camera. The material is unshaded, so nothing
|
||
|
|
// reads them - they are here because a surface without normals is an
|
||
|
|
// unusual vertex format to hand a spatial shader, and the array costs
|
||
|
|
// nothing at these vertex counts.
|
||
|
|
var normals = new Vector3[ _vertices.Count ];
|
||
|
|
|
||
|
|
for ( var i = 0; i < normals.Length; i++ )
|
||
|
|
{
|
||
|
|
normals[ i ] = Vector3.Back;
|
||
|
|
}
|
||
|
|
|
||
|
|
var arrays = new Godot.Collections.Array();
|
||
|
|
arrays.Resize( (int) Mesh.ArrayType.Max );
|
||
|
|
|
||
|
|
arrays[ (int) Mesh.ArrayType.Vertex ] = _vertices.ToArray();
|
||
|
|
arrays[ (int) Mesh.ArrayType.Normal ] = normals;
|
||
|
|
arrays[ (int) Mesh.ArrayType.TexUV ] = _uvs.ToArray();
|
||
|
|
arrays[ (int) Mesh.ArrayType.TexUV2 ] = _uv2s.ToArray();
|
||
|
|
arrays[ (int) Mesh.ArrayType.Index ] = _indices.ToArray();
|
||
|
|
|
||
|
|
var mesh = new ArrayMesh();
|
||
|
|
|
||
|
|
mesh.AddSurfaceFromArrays( Mesh.PrimitiveType.Triangles, arrays );
|
||
|
|
|
||
|
|
return mesh;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** First segment takes the start texture, last one the end texture, the
|
||
|
|
rest the middle one.
|
||
|
|
|
||
|
|
A closed spline has no first or last segment to speak of - its ends meet
|
||
|
|
- so it is all middle, which also keeps the seam from showing two
|
||
|
|
different textures.
|
||
|
|
*/
|
||
|
|
int SlotFor( int segment, int segments )
|
||
|
|
{
|
||
|
|
if ( _closed )
|
||
|
|
{
|
||
|
|
return SlotMiddle;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( segment == 0 )
|
||
|
|
{
|
||
|
|
return SlotStart;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( segment == segments - 1 )
|
||
|
|
{
|
||
|
|
return SlotEnd;
|
||
|
|
}
|
||
|
|
|
||
|
|
return SlotMiddle;
|
||
|
|
}
|
||
|
|
|
||
|
|
int RepeatsFor( int slot, float segmentLength )
|
||
|
|
{
|
||
|
|
if ( slot != SlotMiddle || middleTileLength <= 0.000001f )
|
||
|
|
{
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
var repeats = Mathf.RoundToInt( segmentLength / middleTileLength );
|
||
|
|
|
||
|
|
return Mathf.Clamp( repeats, 1, MaxRepeatsPerSegment );
|
||
|
|
}
|
||
|
|
|
||
|
|
/** One texture instance: a quad strip from `from` to `to` in arc length,
|
||
|
|
with V running 0..1 across it and U running 0..1 across the width.
|
||
|
|
|
||
|
|
Spans do not share their boundary rows. The two rows at a seam are
|
||
|
|
emitted twice, once with V = 1 and once with V = 0, which is what lets
|
||
|
|
the texture restart exactly without any fract() in the sampler and
|
||
|
|
therefore without a filtering seam.
|
||
|
|
*/
|
||
|
|
void BuildSpan( HeightMapGenerator generator, Vector2 texelOffset, int slot, float from, float to )
|
||
|
|
{
|
||
|
|
var rows = Mathf.Max( 1, rowsPerSpan );
|
||
|
|
var delta = tangentDelta;
|
||
|
|
var total = _table.length;
|
||
|
|
|
||
|
|
var firstVertex = _vertices.Count;
|
||
|
|
var lastNormal = new Vector2( 1, 0 );
|
||
|
|
|
||
|
|
for ( var row = 0; row <= rows; row++ )
|
||
|
|
{
|
||
|
|
var v = row / (float) rows;
|
||
|
|
var distance = Mathf.Lerp( from, to, v );
|
||
|
|
|
||
|
|
var position = _table.PositionAtDistance( distance );
|
||
|
|
var direction = _table.DirectionAtDistance( distance, delta );
|
||
|
|
|
||
|
|
var directionXZ = new Vector2( direction.X, direction.Z );
|
||
|
|
|
||
|
|
// A vertical tangent has no XZ direction to be perpendicular to, so the
|
||
|
|
// previous row's normal is carried over rather than snapping.
|
||
|
|
if ( directionXZ.LengthSquared() > 0.000001f )
|
||
|
|
{
|
||
|
|
directionXZ = directionXZ.Normalized();
|
||
|
|
lastNormal = new Vector2( -directionXZ.Y, directionXZ.X );
|
||
|
|
}
|
||
|
|
|
||
|
|
var normal = lastNormal;
|
||
|
|
var halfWidth = WidthAtDistance( distance ) / 2;
|
||
|
|
var center = new Vector2( position.X, position.Z );
|
||
|
|
|
||
|
|
var level = Mathf.Clamp( generator.NormalizeLevel( position.Y ), 0, 1 );
|
||
|
|
var w = total <= 0.000001f ? 0 : distance / total;
|
||
|
|
|
||
|
|
AddVertex( generator, texelOffset, center - normal * halfWidth, level, slot, 0, v, w );
|
||
|
|
AddVertex( generator, texelOffset, center + normal * halfWidth, level, slot, 1, v, w );
|
||
|
|
}
|
||
|
|
|
||
|
|
for ( var row = 0; row < rows; row++ )
|
||
|
|
{
|
||
|
|
var a = firstVertex + row * 2;
|
||
|
|
var b = a + 1;
|
||
|
|
var c = a + 2;
|
||
|
|
var d = a + 3;
|
||
|
|
|
||
|
|
_indices.Add( a ); _indices.Add( b ); _indices.Add( c );
|
||
|
|
_indices.Add( b ); _indices.Add( d ); _indices.Add( c );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void AddVertex(
|
||
|
|
HeightMapGenerator generator, Vector2 texelOffset, Vector2 worldXZ,
|
||
|
|
float level, int slot, float u, float v, float w
|
||
|
|
)
|
||
|
|
{
|
||
|
|
var texel = generator.WorldXZToTexel( worldXZ ) + texelOffset;
|
||
|
|
|
||
|
|
// Negated Y: the camera looks at the atlas from the front with Y up, so
|
||
|
|
// atlas row 0 has to sit at the top of its view.
|
||
|
|
var vertex = new Vector3( texel.X, -texel.Y, level );
|
||
|
|
|
||
|
|
_vertices.Add( vertex );
|
||
|
|
_uvs.Add( AtlasUV( slot, u, v ) );
|
||
|
|
_uv2s.Add( new Vector2( w, 0 ) );
|
||
|
|
|
||
|
|
HashFloat( vertex.X );
|
||
|
|
HashFloat( vertex.Y );
|
||
|
|
HashFloat( vertex.Z );
|
||
|
|
HashFloat( u );
|
||
|
|
HashFloat( v );
|
||
|
|
HashFloat( w );
|
||
|
|
HashLong( slot );
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Slot-local 0..1 mapped into the slot's own texels, inset by half a texel
|
||
|
|
on every side.
|
||
|
|
|
||
|
|
The inset is what keeps a linear tap at a slot's border from bleeding the
|
||
|
|
neighbouring slot in - the slots sit directly against each other, so the
|
||
|
|
start texture's edge is the middle texture's edge.
|
||
|
|
*/
|
||
|
|
Vector2 AtlasUV( int slot, float u, float v )
|
||
|
|
{
|
||
|
|
var size = Mathf.Max( 2, slotSize );
|
||
|
|
var width = Mathf.Max( 1, slots ) * size;
|
||
|
|
|
||
|
|
var x = ( slot * size + 0.5f + u * ( size - 1 ) ) / width;
|
||
|
|
var y = ( 0.5f + v * ( size - 1 ) ) / size;
|
||
|
|
|
||
|
|
return new Vector2( x, y );
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/* THE LEVEL LOOKUP
|
||
|
|
------------------------------------------------------------------------
|
||
|
|
The buffer the mesh writes has three usable channels and four things to
|
||
|
|
carry, so the height level does not fit in it - an opaque spatial material
|
||
|
|
cannot write alpha, and making the material transparent to get the channel
|
||
|
|
back would blend the other three by it and skip the depth write.
|
||
|
|
|
||
|
|
So the level travels as a function of W instead: a 1D table the blit
|
||
|
|
samples with the W it read out of the buffer. That works because a
|
||
|
|
ribbon's level only ever varies <marked>along</marked> the line, never
|
||
|
|
across it, and W is exactly the along-coordinate. The table is float, so
|
||
|
|
the height keeps its full precision regardless of what the render target
|
||
|
|
turned out to be.
|
||
|
|
*/
|
||
|
|
|
||
|
|
ImageTexture _levelTexture;
|
||
|
|
Image _levelImage;
|
||
|
|
int _levelSize;
|
||
|
|
|
||
|
|
public Texture2D levelTexture => _levelTexture;
|
||
|
|
|
||
|
|
/** Rebuilt every render - it is one row of a few hundred texels, and
|
||
|
|
tracking what would have invalidated it costs more than redoing it.
|
||
|
|
*/
|
||
|
|
public void BuildLevels( HeightMapGenerator generator, int size )
|
||
|
|
{
|
||
|
|
if ( _table == null )
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var wanted = Mathf.Clamp( size, 2, 4096 );
|
||
|
|
|
||
|
|
if ( _levelImage == null || _levelSize != wanted )
|
||
|
|
{
|
||
|
|
// Rgbaf rather than a packed byte format: this is the one value in the
|
||
|
|
// whole path that may not be quantized, since the map itself is half
|
||
|
|
// float precisely because 8 bits terrace a slope visibly.
|
||
|
|
_levelImage = Image.CreateEmpty( wanted, 1, false, Image.Format.Rgbaf );
|
||
|
|
_levelTexture = null;
|
||
|
|
_levelSize = wanted;
|
||
|
|
}
|
||
|
|
|
||
|
|
var total = _table.length;
|
||
|
|
|
||
|
|
for ( var i = 0; i < wanted; i++ )
|
||
|
|
{
|
||
|
|
var distance = total * i / (float) ( wanted - 1 );
|
||
|
|
var position = _table.PositionAtDistance( distance );
|
||
|
|
var level = Mathf.Clamp( generator.NormalizeLevel( position.Y ), 0, 1 );
|
||
|
|
|
||
|
|
_levelImage.SetPixel( i, 0, new Color( level, level, level, 1 ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( _levelTexture == null )
|
||
|
|
{
|
||
|
|
_levelTexture = ImageTexture.CreateFromImage( _levelImage );
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
_levelTexture.Update( _levelImage );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/* HASHING
|
||
|
|
------------------------------------------------------------------------
|
||
|
|
FNV-1a over the raw bits, so two runs that produce the same geometry
|
||
|
|
produce the same signature and the GPU pass can be skipped.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const long HashSeed = unchecked( (long) 0xcbf29ce484222325 );
|
||
|
|
|
||
|
|
void HashLong( long value )
|
||
|
|
{
|
||
|
|
_signature = unchecked( ( _signature ^ value ) * 0x100000001b3 );
|
||
|
|
}
|
||
|
|
|
||
|
|
void HashFloat( float value )
|
||
|
|
{
|
||
|
|
HashLong( BitConverter.SingleToInt32Bits( value ) );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|