Height maps: layer gizmos, image layer with mask/tiling, normal map pass
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f86625e1fb
commit
ae581c9a77
|
|
@ -0,0 +1,45 @@
|
||||||
|
using Godot;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
using Rokojori.Extensions;
|
||||||
|
namespace Rokojori
|
||||||
|
{
|
||||||
|
/** How an image layer reads a height out of a texture.
|
||||||
|
|
||||||
|
The single-channel entries take the height from that channel and cover the
|
||||||
|
whole rect at full weight. The paired entries take the height from the
|
||||||
|
colour channel and the <marked>coverage</marked> from alpha, so a cut-out
|
||||||
|
image only affects the map where it is opaque - which is what lets a
|
||||||
|
stamped shape with transparent corners blend in instead of writing a
|
||||||
|
square.
|
||||||
|
|
||||||
|
Coverage is not the same thing as height: under Add and Subtract it scales
|
||||||
|
the contribution, under Replace it is the crossfade back to what is
|
||||||
|
underneath. That is the same split the circle's falloff already has.
|
||||||
|
|
||||||
|
The int values are passed to HeightMapShape.gdshader as
|
||||||
|
shape_image_channel - do not renumber without updating the shader.
|
||||||
|
*/
|
||||||
|
public enum HeightMapChannel
|
||||||
|
{
|
||||||
|
R = 0,
|
||||||
|
G = 1,
|
||||||
|
B = 2,
|
||||||
|
A = 3,
|
||||||
|
|
||||||
|
RA = 4,
|
||||||
|
GA = 5,
|
||||||
|
BA = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class HeightMapChannelX
|
||||||
|
{
|
||||||
|
/** True for the paired entries, where alpha is read as coverage. */
|
||||||
|
public static bool UsesAlphaCoverage( this HeightMapChannel channel )
|
||||||
|
{
|
||||||
|
return channel == HeightMapChannel.RA
|
||||||
|
|| channel == HeightMapChannel.GA
|
||||||
|
|| channel == HeightMapChannel.BA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://d3nx7qd3h2eim
|
||||||
|
|
@ -124,7 +124,59 @@ namespace Rokojori
|
||||||
[Export]
|
[Export]
|
||||||
public DrawableTexture2D heightMapTarget;
|
public DrawableTexture2D heightMapTarget;
|
||||||
|
|
||||||
/** Materials to push terrainHeightMap / terrainCenterXZ / terrainSizeXZ /
|
/** World-space normals baked out of the finished height map, encoded as
|
||||||
|
n * 0.5 + 0.5 - see HeightMapNormals.gdshader.
|
||||||
|
|
||||||
|
Left empty, no normal pass runs at all and a terrain material has to
|
||||||
|
take its own derivative per fragment. Assigned, the map is rendered once
|
||||||
|
after every layer has drawn and pushed to `materials` as
|
||||||
|
terrainNormalMap, which is both cheaper per fragment and the only way the
|
||||||
|
smoothing below can exist - a consumer taking its own 1-texel central
|
||||||
|
difference has no way to be smoothed.
|
||||||
|
*/
|
||||||
|
[Export]
|
||||||
|
public DrawableTexture2D normalMapTarget;
|
||||||
|
|
||||||
|
float _normalRadius = 1;
|
||||||
|
|
||||||
|
/** How far apart the normal pass's taps are, in texels. Widening the stencil
|
||||||
|
low-passes the slope, which is what removes texel stair-stepping without
|
||||||
|
touching the height map itself.
|
||||||
|
*/
|
||||||
|
[Export]
|
||||||
|
public float normalRadius
|
||||||
|
{
|
||||||
|
get => _normalRadius;
|
||||||
|
set { _normalRadius = value; SetDirty(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
int _normalSamples = 1;
|
||||||
|
|
||||||
|
/** How many concentric stencils, at 1x / 2x / ... normalRadius, get
|
||||||
|
averaged. One wide stencil misses the detail between its taps; averaging
|
||||||
|
several keeps the fine slope and still drops the noise. Costs 8 taps
|
||||||
|
each, over the whole map.
|
||||||
|
*/
|
||||||
|
[Export( PropertyHint.Range, "1,8" )]
|
||||||
|
public int normalSamples
|
||||||
|
{
|
||||||
|
get => _normalSamples;
|
||||||
|
set { _normalSamples = value; SetDirty(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
float _normalStrength = 1;
|
||||||
|
|
||||||
|
/** Scales the slope before it is normalized - below 1 flattens the shading,
|
||||||
|
above 1 exaggerates it. Shading only; the geometry is unaffected.
|
||||||
|
*/
|
||||||
|
[Export]
|
||||||
|
public float normalStrength
|
||||||
|
{
|
||||||
|
get => _normalStrength;
|
||||||
|
set { _normalStrength = value; SetDirty(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Materials to push terrainHeightMap / terrainNormalMap / terrainCenterXZ / terrainSizeXZ /
|
||||||
terrainMinHeight / terrainMaxHeight onto after every render.
|
terrainMinHeight / terrainMaxHeight onto after every render.
|
||||||
|
|
||||||
This is the whole wiring between the generator and anything rendering the
|
This is the whole wiring between the generator and anything rendering the
|
||||||
|
|
@ -255,6 +307,33 @@ namespace Rokojori
|
||||||
return WorldBoxToRect( c - r, c + r );
|
return WorldBoxToRect( c - r, c + r );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The texel rect a rotated XZ quad covers - the axis-aligned bounds of its
|
||||||
|
four corners. A rotated layer therefore touches more texels than it
|
||||||
|
fills; the shader still rejects everything outside the quad itself.
|
||||||
|
*/
|
||||||
|
public Rect2I WorldQuadToRect( Vector2 center, Vector2 axisX, Vector2 axisZ, Vector2 size )
|
||||||
|
{
|
||||||
|
var x = axisX * ( size.X / 2 );
|
||||||
|
var z = axisZ * ( size.Y / 2 );
|
||||||
|
|
||||||
|
var a = center + x + z;
|
||||||
|
var b = center + x - z;
|
||||||
|
var c = center - x + z;
|
||||||
|
var d = center - x - z;
|
||||||
|
|
||||||
|
var min = new Vector2(
|
||||||
|
Mathf.Min( Mathf.Min( a.X, b.X ), Mathf.Min( c.X, d.X ) ),
|
||||||
|
Mathf.Min( Mathf.Min( a.Y, b.Y ), Mathf.Min( c.Y, d.Y ) )
|
||||||
|
);
|
||||||
|
|
||||||
|
var max = new Vector2(
|
||||||
|
Mathf.Max( Mathf.Max( a.X, b.X ), Mathf.Max( c.X, d.X ) ),
|
||||||
|
Mathf.Max( Mathf.Max( a.Y, b.Y ), Mathf.Max( c.Y, d.Y ) )
|
||||||
|
);
|
||||||
|
|
||||||
|
return WorldBoxToRect( min, max );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* HEIGHT NORMALIZATION
|
/* HEIGHT NORMALIZATION
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
|
@ -372,6 +451,8 @@ namespace Rokojori
|
||||||
renderedLayers++;
|
renderedLayers++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RenderNormalMap();
|
||||||
|
|
||||||
PushToMaterials();
|
PushToMaterials();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -410,8 +491,25 @@ namespace Rokojori
|
||||||
Called by layers after they have set up `shapeMaterial`; the rect and
|
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
|
blend-mode uniforms are set here, so a layer cannot get the world mapping
|
||||||
wrong.
|
wrong.
|
||||||
|
|
||||||
|
`source` and `mask` fill the shader's hint_blit_source0 and
|
||||||
|
hint_blit_source1 - an image layer passes its texture and optionally a
|
||||||
|
mask, a purely procedural shape passes neither. Both slots are always
|
||||||
|
bound, to the shared white 1x1 stand-in when a layer has nothing for
|
||||||
|
them: an unbound blit source arrives black, so "no mask" would otherwise
|
||||||
|
have to be a flag, while white is already a no-op multiplier.
|
||||||
*/
|
*/
|
||||||
public void DrawShape( Rect2I rect, HeightMapBlendMode blendMode )
|
public void DrawShape( Rect2I rect, HeightMapBlendMode blendMode )
|
||||||
|
{
|
||||||
|
DrawShape( rect, blendMode, null, null );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawShape( Rect2I rect, HeightMapBlendMode blendMode, Texture2D source )
|
||||||
|
{
|
||||||
|
DrawShape( rect, blendMode, source, null );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawShape( Rect2I rect, HeightMapBlendMode blendMode, Texture2D source, Texture2D mask )
|
||||||
{
|
{
|
||||||
if ( heightMapTarget == null || rect.Size.X <= 0 || rect.Size.Y <= 0 )
|
if ( heightMapTarget == null || rect.Size.X <= 0 || rect.Size.Y <= 0 )
|
||||||
{
|
{
|
||||||
|
|
@ -429,10 +527,14 @@ namespace Rokojori
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// A blit needs a source even when the shader samples none, so a shared
|
_blitSources[ 0 ] = source != null ? source : (Texture2D) dummySource;
|
||||||
// 1x1 stands in. The shape is entirely procedural from world XZ.
|
_blitSources[ 1 ] = mask != null ? mask : (Texture2D) dummySource;
|
||||||
scratch.BlitRect(
|
|
||||||
new Rect2I( 0, 0, rect.Size.X, rect.Size.Y ), dummySource, White, 0, material
|
// BlitRectMulti rather than BlitRect, because the explicit sources array
|
||||||
|
// is the documented way to fill more than source 0.
|
||||||
|
scratch.BlitRectMulti(
|
||||||
|
new Rect2I( 0, 0, rect.Size.X, rect.Size.Y ),
|
||||||
|
_blitSources, TextureStackCompositor.noExtraTargets, White, 0, material
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( _blendBlit == null )
|
if ( _blendBlit == null )
|
||||||
|
|
@ -450,6 +552,10 @@ namespace Rokojori
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Reused rather than allocated per blit - a drag re-renders every frame. */
|
||||||
|
readonly Godot.Collections.Array<Texture2D> _blitSources
|
||||||
|
= new Godot.Collections.Array<Texture2D>(){ null, null };
|
||||||
|
|
||||||
DrawableTexture2D _dummySource;
|
DrawableTexture2D _dummySource;
|
||||||
|
|
||||||
DrawableTexture2D dummySource
|
DrawableTexture2D dummySource
|
||||||
|
|
@ -530,7 +636,75 @@ namespace Rokojori
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Pushes the map and its world mapping onto every assigned material. */
|
/* THE NORMAL PASS
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
One full-map blit with the finished height map as the source. It runs
|
||||||
|
after the layers rather than per layer, because a normal is a derivative
|
||||||
|
of the composited result - taking it per layer and blending the results
|
||||||
|
would be wrong wherever two layers overlap.
|
||||||
|
|
||||||
|
Nothing clears the normal map: the shader writes every texel with blending
|
||||||
|
disabled, so the previous render is fully overwritten.
|
||||||
|
*/
|
||||||
|
|
||||||
|
HeightMapNormalsMaterial _normalsMaterial;
|
||||||
|
|
||||||
|
HeightMapNormalsMaterial normalsMaterial
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if ( _normalsMaterial == null )
|
||||||
|
{
|
||||||
|
_normalsMaterial = new HeightMapNormalsMaterial();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _normalsMaterial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawableTexture2D _setupNormalTarget;
|
||||||
|
Vector2I _setupNormalSize;
|
||||||
|
TextureStackFormat _setupNormalFormat;
|
||||||
|
|
||||||
|
/** Flat up, encoded - what an unrendered normal map should read as. */
|
||||||
|
static readonly Color FlatNormal = new Color( 0.5f, 1, 0.5f, 1 );
|
||||||
|
|
||||||
|
void RenderNormalMap()
|
||||||
|
{
|
||||||
|
if ( normalMapTarget == null || heightMapTarget == null )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var wanted = textureSize;
|
||||||
|
|
||||||
|
// Same allocation rule as the height map - Setup() reallocates and that
|
||||||
|
// emits the texture's changed signal, which rebuilds the inspector.
|
||||||
|
if ( _setupNormalTarget != normalMapTarget
|
||||||
|
|| _setupNormalSize != wanted
|
||||||
|
|| _setupNormalFormat != format )
|
||||||
|
{
|
||||||
|
normalMapTarget.Setup( wanted.X, wanted.Y, format.ToDrawableFormat(), FlatNormal, false );
|
||||||
|
|
||||||
|
_setupNormalTarget = normalMapTarget;
|
||||||
|
_setupNormalSize = wanted;
|
||||||
|
_setupNormalFormat = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
var material = normalsMaterial;
|
||||||
|
|
||||||
|
material.sizeXZ.Set( size );
|
||||||
|
material.minHeight.Set( minHeight );
|
||||||
|
material.maxHeight.Set( maxHeight );
|
||||||
|
material.radius.Set( normalRadius );
|
||||||
|
material.samples.Set( normalSamples );
|
||||||
|
material.strength.Set( normalStrength );
|
||||||
|
|
||||||
|
normalMapTarget.BlitRect( fullRect, heightMapTarget, White, 0, material );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Pushes the maps and their world mapping onto every assigned material. */
|
||||||
public void PushToMaterials()
|
public void PushToMaterials()
|
||||||
{
|
{
|
||||||
if ( materials == null )
|
if ( materials == null )
|
||||||
|
|
@ -548,6 +722,7 @@ namespace Rokojori
|
||||||
}
|
}
|
||||||
|
|
||||||
TerrainOffsetShader.heightMap.Set( material, heightMapTarget );
|
TerrainOffsetShader.heightMap.Set( material, heightMapTarget );
|
||||||
|
TerrainOffsetShader.normalMap.Set( material, normalMapTarget );
|
||||||
TerrainOffsetShader.centerXZ.Set( material, centerXZ );
|
TerrainOffsetShader.centerXZ.Set( material, centerXZ );
|
||||||
TerrainOffsetShader.sizeXZ.Set( material, size );
|
TerrainOffsetShader.sizeXZ.Set( material, size );
|
||||||
TerrainOffsetShader.minHeight.Set( material, minHeight );
|
TerrainOffsetShader.minHeight.Set( material, minHeight );
|
||||||
|
|
@ -556,15 +731,16 @@ namespace Rokojori
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Rough VRAM cost of the map itself, in bytes. The scratch buffer is
|
/** Rough VRAM cost of the maps, in bytes. The scratch buffer is rect-sized
|
||||||
rect-sized and pooled, so it is not counted - a layer covering the whole
|
and pooled, so it is not counted - a layer covering the whole map would
|
||||||
map would add one more of these.
|
add one more of these.
|
||||||
*/
|
*/
|
||||||
public long EstimateMemory()
|
public long EstimateMemory()
|
||||||
{
|
{
|
||||||
var r = textureSize;
|
var r = textureSize;
|
||||||
|
var maps = normalMapTarget != null ? 2 : 1;
|
||||||
|
|
||||||
return (long) r.X * r.Y * format.BytesPerPixel();
|
return (long) r.X * r.Y * format.BytesPerPixel() * maps;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,40 +7,31 @@ namespace Rokojori
|
||||||
{
|
{
|
||||||
/** A circular hill, crater or plateau, centered on the node's own position.
|
/** A circular hill, crater or plateau, centered on the node's own position.
|
||||||
|
|
||||||
Full `height` inside innerRadius, fading to nothing at outerRadius. The
|
Full height inside innerRadius, fading to nothing at outerRadius. The first
|
||||||
first primitive, and the template for the ones that follow: it computes the
|
primitive, and the template for the ones that follow: it computes the texel
|
||||||
texel rect it touches, configures the shared shape material and hands the
|
rect it touches, configures the shared shape material and hands the blit
|
||||||
blit back to the generator.
|
back to the generator.
|
||||||
|
|
||||||
|
Under Add and Subtract the height comes from `height` and is dragged with
|
||||||
|
the vertical gizmo handle; under Replace it is the node's own Y position,
|
||||||
|
and the circle writes that one flat level across the inner radius. A circle
|
||||||
|
has no per-texel amount of its own, so its Replace amplitude is always 0.
|
||||||
|
|
||||||
Only XZ position is read, not rotation or scale - a circle has no
|
Only XZ position is read, not rotation or scale - a circle has no
|
||||||
orientation, and scaling a radius through the node transform would make the
|
orientation, and scaling a radius through the node transform would make the
|
||||||
inspector's radius values lie. Later non-radial primitives will read the
|
inspector's radius values lie.
|
||||||
basis.
|
|
||||||
*/
|
*/
|
||||||
[Tool]
|
[Tool]
|
||||||
[GlobalClass]
|
[GlobalClass]
|
||||||
public partial class CircleHeightMapGeneratorLayer:HeightMapGeneratorLayer
|
public partial class CircleHeightMapGeneratorLayer:HeightMapGeneratorLayer
|
||||||
{
|
{
|
||||||
float _height = 10;
|
|
||||||
|
|
||||||
/** In meters. A height to add or subtract for Add/Subtract, the absolute
|
|
||||||
world height to flatten to for Replace.
|
|
||||||
*/
|
|
||||||
[Export]
|
|
||||||
public float height
|
|
||||||
{
|
|
||||||
get => _height;
|
|
||||||
set { _height = value; SetDirty(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
float _innerRadius = 10;
|
float _innerRadius = 10;
|
||||||
|
|
||||||
/** Radius in meters that gets the full height. */
|
/** Radius in meters that gets the full height. */
|
||||||
[Export]
|
[Export]
|
||||||
public float innerRadius
|
public float innerRadius
|
||||||
{
|
{
|
||||||
get => _innerRadius;
|
get => _innerRadius; set { _innerRadius = value; SetDirty(); }
|
||||||
set { _innerRadius = value; SetDirty(); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float _outerRadius = 20;
|
float _outerRadius = 20;
|
||||||
|
|
@ -49,8 +40,7 @@ namespace Rokojori
|
||||||
[Export]
|
[Export]
|
||||||
public float outerRadius
|
public float outerRadius
|
||||||
{
|
{
|
||||||
get => _outerRadius;
|
get => _outerRadius; set { _outerRadius = value; SetDirty(); }
|
||||||
set { _outerRadius = value; SetDirty(); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _smoothFalloff = true;
|
bool _smoothFalloff = true;
|
||||||
|
|
@ -61,8 +51,7 @@ namespace Rokojori
|
||||||
[Export]
|
[Export]
|
||||||
public bool smoothFalloff
|
public bool smoothFalloff
|
||||||
{
|
{
|
||||||
get => _smoothFalloff;
|
get => _smoothFalloff; set { _smoothFalloff = value; SetDirty(); }
|
||||||
set { _smoothFalloff = value; SetDirty(); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -96,9 +85,86 @@ namespace Rokojori
|
||||||
material.innerRadius.Set( inner );
|
material.innerRadius.Set( inner );
|
||||||
material.outerRadius.Set( outer );
|
material.outerRadius.Set( outer );
|
||||||
material.smoothFalloff.Set( smoothFalloff );
|
material.smoothFalloff.Set( smoothFalloff );
|
||||||
material.value.Set( NormalizeHeight( generator, height ) );
|
|
||||||
|
// Flat under Replace: the whole output is the node's Y, with the falloff
|
||||||
|
// as the crossfade. Under Add/Subtract the amplitude is the height.
|
||||||
|
SetShapeValue( generator, blendMode.IsAbsolute() ? 0 : height );
|
||||||
|
|
||||||
generator.DrawShape( rect, blendMode );
|
generator.DrawShape( rect, blendMode );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if TOOLS
|
||||||
|
|
||||||
|
/* GIZMOS
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
The two radius handles sit on different axes - inner on +X, outer on +Z -
|
||||||
|
so they never land on top of each other when the two radii are equal,
|
||||||
|
which is the one configuration a shared axis would make uneditable.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public const int HandleIdInnerRadius = HandleIdCustom;
|
||||||
|
public const int HandleIdOuterRadius = HandleIdCustom + 1;
|
||||||
|
|
||||||
|
protected override void DrawShapeGizmo( List<Vector3> lines, List<Vector3> handles, List<int> ids )
|
||||||
|
{
|
||||||
|
var x = ToGizmoLocal( Vector3.Right );
|
||||||
|
var z = ToGizmoLocal( Vector3.Back );
|
||||||
|
|
||||||
|
Gizmos.AddCircle( lines, Vector3.Zero, x * innerRadius, z * innerRadius, 48 );
|
||||||
|
Gizmos.AddCircle( lines, Vector3.Zero, x * outerRadius, z * outerRadius, 48 );
|
||||||
|
|
||||||
|
handles.Add( x * innerRadius );
|
||||||
|
ids.Add( HandleIdInnerRadius );
|
||||||
|
|
||||||
|
handles.Add( z * outerRadius );
|
||||||
|
ids.Add( HandleIdOuterRadius );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string GetShapeHandleName( int id )
|
||||||
|
{
|
||||||
|
return id == HandleIdInnerRadius ? "Inner Radius" : "Outer Radius";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Variant GetShapeHandleValue( int id )
|
||||||
|
{
|
||||||
|
return Variant.From( id == HandleIdInnerRadius ? innerRadius : outerRadius );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void SetShapeHandle( int id, Camera3D camera, Vector2 point )
|
||||||
|
{
|
||||||
|
Vector3 worldPoint;
|
||||||
|
|
||||||
|
if ( ! ProjectOntoGroundPlane( camera, point, dragStartPosition, out worldPoint ) )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var center = GlobalPosition;
|
||||||
|
var radius = new Vector2( worldPoint.X - center.X, worldPoint.Z - center.Z ).Length();
|
||||||
|
|
||||||
|
if ( id == HandleIdInnerRadius )
|
||||||
|
{
|
||||||
|
innerRadius = radius;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outerRadius = radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void RestoreShapeHandle( int id, Variant restore )
|
||||||
|
{
|
||||||
|
if ( id == HandleIdInnerRadius )
|
||||||
|
{
|
||||||
|
innerRadius = restore.AsSingle();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outerRadius = restore.AsSingle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,16 @@ namespace Rokojori
|
||||||
*/
|
*/
|
||||||
protected void SetDirty()
|
protected void SetDirty()
|
||||||
{
|
{
|
||||||
|
#if TOOLS
|
||||||
|
// A radius or a size edited in the inspector moves the gizmo handles, and
|
||||||
|
// nothing else would tell the editor to redraw them - a transform change
|
||||||
|
// does that on its own, an exported value does not.
|
||||||
|
if ( Engine.IsEditorHint() )
|
||||||
|
{
|
||||||
|
UpdateGizmos();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
var g = generator;
|
var g = generator;
|
||||||
|
|
||||||
if ( g == null )
|
if ( g == null )
|
||||||
|
|
|
||||||
|
|
@ -8,35 +8,273 @@ namespace Rokojori
|
||||||
/** A HeightMapDrawLayer that generates its content procedurally and blends it
|
/** A HeightMapDrawLayer that generates its content procedurally and blends it
|
||||||
onto the map.
|
onto the map.
|
||||||
|
|
||||||
Everything these layers have in common is the blend mode, which is also
|
What these layers have in common is the blend mode and a height, and the
|
||||||
what decides how a subclass reads its own height value: a delta for Add and
|
two are coupled: Add and Subtract read `height` as a <marked>delta</marked>
|
||||||
Subtract, an absolute world height for Replace - see
|
from whatever is underneath, so the node's own Y means nothing to them and
|
||||||
HeightMapBlendMode.IsAbsolute().
|
the height is dragged with its own gizmo handle instead. Replace reads the
|
||||||
|
node's <marked>Y position</marked> as the absolute world level, and
|
||||||
|
`height` only still matters for shapes that carry a per-texel amount - the
|
||||||
|
image layer's amplitude. See SetShapeValue().
|
||||||
|
|
||||||
Subclasses are expected to configure generator.shapeMaterial and then call
|
Subclasses are expected to configure generator.shapeMaterial, call
|
||||||
generator.DrawShape( rect, blendMode ), which keeps the world-to-texel
|
SetShapeValue() and then generator.DrawShape( rect, blendMode ), which
|
||||||
mapping and the blit itself in one place.
|
keeps the world-to-texel mapping and the blit itself in one place.
|
||||||
|
|
||||||
|
Under TOOLS this is also the gizmo base: it draws the height handle and
|
||||||
|
dispatches everything else to the shape's own handles, so a subclass only
|
||||||
|
describes its outline and says what its handles do.
|
||||||
*/
|
*/
|
||||||
[Tool]
|
[Tool]
|
||||||
public abstract partial class HeightMapGeneratorLayer:HeightMapDrawLayer
|
public abstract partial class HeightMapGeneratorLayer:HeightMapDrawLayer
|
||||||
|
#if TOOLS
|
||||||
|
, GizmoDrawerWithHandles
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
HeightMapBlendMode _blendMode = HeightMapBlendMode.Add;
|
HeightMapBlendMode _blendMode = HeightMapBlendMode.Add;
|
||||||
|
|
||||||
[Export]
|
[Export]
|
||||||
public HeightMapBlendMode blendMode
|
public HeightMapBlendMode blendMode
|
||||||
{
|
{
|
||||||
get => _blendMode;
|
get => _blendMode; set { _blendMode = value; SetDirty(); }
|
||||||
set { _blendMode = value; SetDirty(); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The layer's height in meters, turned into the normalized value the shape
|
float _height = 10;
|
||||||
shader wants - which conversion applies depends on the blend mode.
|
|
||||||
|
/** In meters. The height to add or subtract for Add/Subtract - the node's Y
|
||||||
|
is ignored there - and the amplitude on top of the node's Y for Replace,
|
||||||
|
which a flat shape like the circle simply does not use.
|
||||||
*/
|
*/
|
||||||
protected float NormalizeHeight( HeightMapGenerator generator, float worldHeight )
|
[Export]
|
||||||
|
public float height
|
||||||
{
|
{
|
||||||
return blendMode.IsAbsolute()
|
get => _height; set { _height = value; SetDirty(); }
|
||||||
? generator.NormalizeLevel( worldHeight )
|
}
|
||||||
: generator.NormalizeDelta( worldHeight );
|
|
||||||
|
/** Writes the two normalized height uniforms for this layer's blend mode.
|
||||||
|
|
||||||
|
`amplitude` is the per-texel height range the shape's own `amount` is
|
||||||
|
multiplied by. A shape with no per-texel amount passes 0 under Replace,
|
||||||
|
because its whole output is then the flat base level.
|
||||||
|
*/
|
||||||
|
protected void SetShapeValue( HeightMapGenerator generator, float amplitude )
|
||||||
|
{
|
||||||
|
var material = generator.shapeMaterial;
|
||||||
|
|
||||||
|
material.valueBase.Set( blendMode.IsAbsolute() ? generator.NormalizeLevel( GlobalPosition.Y ) : 0 );
|
||||||
|
material.valueScale.Set( generator.NormalizeDelta( amplitude ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if TOOLS
|
||||||
|
|
||||||
|
/* GIZMOS
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
Handle positions are in node-local space and the gizmo is drawn with the
|
||||||
|
node's full global transform, so every world-space offset goes through
|
||||||
|
ToGizmoLocal() - otherwise a rotated or scaled parent would move the
|
||||||
|
handles off the shape they are supposed to be editing.
|
||||||
|
|
||||||
|
Dragging reads the mouse ray against either the horizontal plane through
|
||||||
|
the layer ( radii, edges, corners ) or the vertical line through it
|
||||||
|
( the height ). There is no undo integration, matching SplinePoint - a
|
||||||
|
cancelled drag restores the values the drag started from, which is what
|
||||||
|
CommitHandle's `cancel` is for.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Every shape layer has a height handle at id 0; a subclass numbers its own
|
||||||
|
from HandleIdCustom upwards.
|
||||||
|
*/
|
||||||
|
public const int HandleIdHeight = 0;
|
||||||
|
public const int HandleIdCustom = 1;
|
||||||
|
|
||||||
|
/** Replace takes its level from the node's Y, which the normal move gizmo
|
||||||
|
already edits - so there is nothing for a height handle to do there.
|
||||||
|
*/
|
||||||
|
protected bool usesHeightHandle => ! blendMode.IsAbsolute();
|
||||||
|
|
||||||
|
/** The outline, the handles and their ids, all in node-local space. */
|
||||||
|
protected abstract void DrawShapeGizmo( List<Vector3> lines, List<Vector3> handles, List<int> ids );
|
||||||
|
|
||||||
|
protected virtual string GetShapeHandleName( int id ) { return ""; }
|
||||||
|
protected virtual Variant GetShapeHandleValue( int id ) { return Variant.From( 0 ); }
|
||||||
|
protected virtual void SetShapeHandle( int id, Camera3D camera, Vector2 point ) {}
|
||||||
|
protected virtual void RestoreShapeHandle( int id, Variant restore ) {}
|
||||||
|
|
||||||
|
/** A world-space offset from the node, as the gizmo's local space sees it. */
|
||||||
|
protected Vector3 ToGizmoLocal( Vector3 worldOffset )
|
||||||
|
{
|
||||||
|
var basis = GlobalTransform.Basis;
|
||||||
|
|
||||||
|
// A fully flattened basis has no inverse - draw unscaled rather than throw.
|
||||||
|
if ( Mathf.Abs( basis.Determinant() ) < 0.000001f )
|
||||||
|
{
|
||||||
|
return worldOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return basis.Inverse() * worldOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DrawGizmo( EditorNode3DGizmoPlugin gizmoPlugin, EditorNode3DGizmo gizmo )
|
||||||
|
{
|
||||||
|
gizmo.Clear();
|
||||||
|
|
||||||
|
var lines = new List<Vector3>();
|
||||||
|
var handles = new List<Vector3>();
|
||||||
|
var ids = new List<int>();
|
||||||
|
|
||||||
|
DrawShapeGizmo( lines, handles, ids );
|
||||||
|
|
||||||
|
if ( usesHeightHandle )
|
||||||
|
{
|
||||||
|
var top = ToGizmoLocal( new Vector3( 0, height, 0 ) );
|
||||||
|
|
||||||
|
// The stem is what makes the sign readable - a negative height hangs
|
||||||
|
// below the node instead of looking like a small positive one.
|
||||||
|
lines.Add( Vector3.Zero );
|
||||||
|
lines.Add( top );
|
||||||
|
|
||||||
|
handles.Add( top );
|
||||||
|
ids.Add( HandleIdHeight );
|
||||||
|
}
|
||||||
|
|
||||||
|
var material = gizmoPlugin.GetMaterial( "main", gizmo );
|
||||||
|
|
||||||
|
if ( lines.Count > 0 )
|
||||||
|
{
|
||||||
|
gizmo.AddLines( lines.ToArray(), material );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( handles.Count > 0 )
|
||||||
|
{
|
||||||
|
var handleMaterial = gizmoPlugin.GetMaterial( "handles", gizmo );
|
||||||
|
|
||||||
|
gizmo.AddHandles( handles.ToArray(), handleMaterial ?? material, ids.ToArray() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GetHandleName( EditorNode3DGizmo gizmo, int handleId, bool secondary )
|
||||||
|
{
|
||||||
|
return handleId == HandleIdHeight ? "Height" : GetShapeHandleName( handleId );
|
||||||
|
}
|
||||||
|
|
||||||
|
float _dragStartHeight;
|
||||||
|
Vector3 _dragStartPosition;
|
||||||
|
|
||||||
|
/** Where the layer sat when the drag started. The drag plane is anchored
|
||||||
|
here rather than on the live position, so an edge drag that moves the
|
||||||
|
node does not drag the plane out from under itself.
|
||||||
|
*/
|
||||||
|
protected Vector3 dragStartPosition => _dragStartPosition;
|
||||||
|
|
||||||
|
public Variant GetHandleValue( EditorNode3DGizmo gizmo, int handleId, bool secondary )
|
||||||
|
{
|
||||||
|
_dragStartHeight = height;
|
||||||
|
_dragStartPosition = GlobalPosition;
|
||||||
|
|
||||||
|
return handleId == HandleIdHeight ? Variant.From( height ) : GetShapeHandleValue( handleId );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetHandle( EditorNode3DGizmo gizmo, int id, bool secondary, Camera3D camera, Vector2 point )
|
||||||
|
{
|
||||||
|
if ( id == HandleIdHeight )
|
||||||
|
{
|
||||||
|
float along;
|
||||||
|
|
||||||
|
if ( ProjectOntoAxis( camera, point, _dragStartPosition, Vector3.Up, out along ) )
|
||||||
|
{
|
||||||
|
height = along;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetShapeHandle( id, camera, point );
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateGizmos();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CommitHandle( EditorNode3DGizmo gizmo, int id, bool secondary, Variant restore, bool cancel )
|
||||||
|
{
|
||||||
|
if ( ! cancel )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edge handles move the node as well as resizing it, so the position is
|
||||||
|
// restored for every handle, not just the ones that obviously moved it.
|
||||||
|
GlobalPosition = _dragStartPosition;
|
||||||
|
|
||||||
|
if ( id == HandleIdHeight )
|
||||||
|
{
|
||||||
|
height = _dragStartHeight;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RestoreShapeHandle( id, restore );
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateGizmos();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Where the mouse ray crosses the horizontal plane through `origin`. */
|
||||||
|
protected static bool ProjectOntoGroundPlane(
|
||||||
|
Camera3D camera, Vector2 point, Vector3 origin, out Vector3 worldPoint
|
||||||
|
)
|
||||||
|
{
|
||||||
|
worldPoint = origin;
|
||||||
|
|
||||||
|
var plane = new Godot.Plane( Vector3.Up, origin );
|
||||||
|
|
||||||
|
var intersection = plane.IntersectsRay(
|
||||||
|
camera.ProjectRayOrigin( point ), camera.ProjectRayNormal( point )
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( intersection == null )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
worldPoint = (Vector3) intersection;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** How far along `axis` from `origin` the mouse ray passes closest - the
|
||||||
|
standard closest-point-between-two-lines solve. The height handle slides
|
||||||
|
on a line rather than in a plane, and a plane would be unusable from a
|
||||||
|
top-down view anyway.
|
||||||
|
*/
|
||||||
|
protected static bool ProjectOntoAxis(
|
||||||
|
Camera3D camera, Vector2 point, Vector3 origin, Vector3 axis, out float along
|
||||||
|
)
|
||||||
|
{
|
||||||
|
along = 0;
|
||||||
|
|
||||||
|
var rayOrigin = camera.ProjectRayOrigin( point );
|
||||||
|
var rayDirection = camera.ProjectRayNormal( point );
|
||||||
|
|
||||||
|
var w = origin - rayOrigin;
|
||||||
|
|
||||||
|
var a = axis.Dot( axis );
|
||||||
|
var b = axis.Dot( rayDirection );
|
||||||
|
var c = rayDirection.Dot( rayDirection );
|
||||||
|
var d = axis.Dot( w );
|
||||||
|
var e = rayDirection.Dot( w );
|
||||||
|
|
||||||
|
var denominator = a * c - b * b;
|
||||||
|
|
||||||
|
// Looking straight down the axis - there is no unique closest point.
|
||||||
|
if ( Mathf.Abs( denominator ) < 0.000001f )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
along = ( b * e - c * d ) / denominator;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,418 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Godot;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
using Rokojori.Extensions;
|
||||||
|
namespace Rokojori
|
||||||
|
{
|
||||||
|
/** Draws a texture into the height map as a rectangle in the world.
|
||||||
|
|
||||||
|
The second primitive, and the first one that is not procedural: the
|
||||||
|
texture goes in as the blit's source and the shader samples it in the
|
||||||
|
rect's own space, so a rotated layer needs no matrix and no UV rect of its
|
||||||
|
own.
|
||||||
|
|
||||||
|
<marked>Placement</marked> is the node's XZ position, `size` in meters, and
|
||||||
|
the node's Y rotation. Scale is deliberately ignored, the same way the
|
||||||
|
circle ignores it - the size is in meters and the gizmo edits it directly,
|
||||||
|
and letting the transform scale it too would make the inspector's numbers
|
||||||
|
lie about the footprint.
|
||||||
|
|
||||||
|
<marked>Height</marked> follows the blend mode, as everywhere in this
|
||||||
|
family: Add and Subtract read `height` as the amplitude of the sampled
|
||||||
|
channel, so a white texel adds the full height and a black one adds
|
||||||
|
nothing; Replace puts the node's Y at a black texel and Y + height at a
|
||||||
|
white one, writing the image as an absolute landform.
|
||||||
|
|
||||||
|
<marked>Coverage</marked> is what `channel` splits off. A single-channel
|
||||||
|
entry covers the whole rect, which makes the texture's black regions flat
|
||||||
|
rather than absent. The paired entries read alpha as coverage instead, so
|
||||||
|
a cut-out stamp only touches the map where it is opaque and crossfades on
|
||||||
|
its own alpha edge.
|
||||||
|
*/
|
||||||
|
[Tool]
|
||||||
|
[GlobalClass]
|
||||||
|
public partial class ImageHeightMapGeneratorLayer:HeightMapGeneratorLayer
|
||||||
|
{
|
||||||
|
Texture2D _texture;
|
||||||
|
|
||||||
|
/** The height source. Sampled with linear filtering and no repeat - outside
|
||||||
|
the rect the layer contributes nothing at all.
|
||||||
|
*/
|
||||||
|
[Export]
|
||||||
|
public Texture2D texture
|
||||||
|
{
|
||||||
|
get => _texture; set { _texture = value; SetDirty(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 _size = new Vector2( 50, 50 );
|
||||||
|
|
||||||
|
/** The rect's extent in world meters, along the node's own X and Z axes. */
|
||||||
|
[Export]
|
||||||
|
public Vector2 size
|
||||||
|
{
|
||||||
|
get => _size; set { _size = value; SetDirty(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
HeightMapChannel _channel = HeightMapChannel.R;
|
||||||
|
|
||||||
|
/** Which channel carries the height, and whether alpha is read as coverage
|
||||||
|
- see HeightMapChannel.
|
||||||
|
*/
|
||||||
|
[Export]
|
||||||
|
public HeightMapChannel channel
|
||||||
|
{
|
||||||
|
get => _channel; set { _channel = value; SetDirty(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _infinite = false;
|
||||||
|
|
||||||
|
/** Tiles the layer across the <marked>whole map</marked> instead of stopping
|
||||||
|
at its rect.
|
||||||
|
|
||||||
|
`size` stops being the footprint and becomes the tiling cell, which is
|
||||||
|
what makes this worth having over just setting size to the map's: a base
|
||||||
|
noise layer wants a small cell repeated everywhere, and stretching one
|
||||||
|
copy over 400 m would give it no detail. The texture has to be seamless
|
||||||
|
for this to look right - the sampler wraps, so a non-seamless one shows
|
||||||
|
a grid of its own edges.
|
||||||
|
|
||||||
|
A mask is the natural companion: with this on, the mask stops tiling and
|
||||||
|
covers the map instead, so it can fade the noise out regionally.
|
||||||
|
*/
|
||||||
|
[Export]
|
||||||
|
public bool infinite
|
||||||
|
{
|
||||||
|
get => _infinite; set { _infinite = value; SetDirty(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Texture2D _mask;
|
||||||
|
|
||||||
|
/** An extra multiplier on the layer's coverage, on top of whatever the
|
||||||
|
channel already contributes.
|
||||||
|
|
||||||
|
It multiplies <marked>coverage</marked>, not height, which is what makes
|
||||||
|
it behave as a mask under every blend mode: a masked-out region falls
|
||||||
|
back to what is underneath rather than being flattened to the layer's
|
||||||
|
base level, exactly the way the circle's falloff does.
|
||||||
|
|
||||||
|
Mapped to the layer's own rect normally, and to the <marked>whole map</marked>
|
||||||
|
when `infinite` is on - a tiling layer's rect is a cell, and a mask
|
||||||
|
repeated per cell could not fade anything regionally. Either way it is
|
||||||
|
clamped, so outside its rect it extends its border value instead of
|
||||||
|
dropping to zero.
|
||||||
|
*/
|
||||||
|
[Export]
|
||||||
|
public Texture2D mask
|
||||||
|
{
|
||||||
|
get => _mask; set { _mask = value; SetDirty(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
HeightMapChannel _maskChannel = HeightMapChannel.A;
|
||||||
|
|
||||||
|
/** Which channel of the mask is read. The paired entries multiply the colour
|
||||||
|
channel by alpha, for a mask that carries both.
|
||||||
|
*/
|
||||||
|
[Export]
|
||||||
|
public HeightMapChannel maskChannel
|
||||||
|
{
|
||||||
|
get => _maskChannel; set { _maskChannel = value; SetDirty(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ORIENTATION
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
Taken from the basis rather than from Rotation.Y, so a layer under a
|
||||||
|
rotated parent still lines up: the two world axes are the basis axes
|
||||||
|
flattened into XZ. A basis tilted fully onto its side leaves one of them
|
||||||
|
with no horizontal component, which is why both have a fallback.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public Vector2 axisX
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var v = GlobalBasis.X;
|
||||||
|
var xz = new Vector2( v.X, v.Z );
|
||||||
|
|
||||||
|
return xz.LengthSquared() < 0.000001f ? new Vector2( 1, 0 ) : xz.Normalized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 axisZ
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var v = GlobalBasis.Z;
|
||||||
|
var xz = new Vector2( v.X, v.Z );
|
||||||
|
|
||||||
|
if ( xz.LengthSquared() < 0.000001f )
|
||||||
|
{
|
||||||
|
var x = axisX;
|
||||||
|
|
||||||
|
return new Vector2( -x.Y, x.X );
|
||||||
|
}
|
||||||
|
|
||||||
|
return xz.Normalized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 centerXZ
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var p = GlobalPosition;
|
||||||
|
|
||||||
|
return new Vector2( p.X, p.Z );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override void RenderToHeightMap( HeightMapGenerator generator )
|
||||||
|
{
|
||||||
|
if ( texture == null || size.X <= 0 || size.Y <= 0 )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var center = centerXZ;
|
||||||
|
var x = axisX;
|
||||||
|
var z = axisZ;
|
||||||
|
|
||||||
|
// Infinite covers everything, so there is nothing to clip against and the
|
||||||
|
// rect is simply the map.
|
||||||
|
var rect = infinite
|
||||||
|
? generator.fullRect
|
||||||
|
: generator.WorldQuadToRect( center, x, z, size );
|
||||||
|
|
||||||
|
if ( rect.Size.X <= 0 || rect.Size.Y <= 0 )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var material = generator.shapeMaterial;
|
||||||
|
|
||||||
|
material.Reset();
|
||||||
|
material.shapeType.Set( (int) HeightMapShapeType.Image );
|
||||||
|
material.center.Set( center );
|
||||||
|
material.axisX.Set( x );
|
||||||
|
material.axisZ.Set( z );
|
||||||
|
material.imageSize.Set( size );
|
||||||
|
material.imageChannel.Set( (int) channel );
|
||||||
|
material.imageTiling.Set( infinite );
|
||||||
|
|
||||||
|
material.maskChannel.Set( (int) maskChannel );
|
||||||
|
|
||||||
|
if ( infinite )
|
||||||
|
{
|
||||||
|
material.SetMaskRect(
|
||||||
|
generator.centerXZ, new Vector2( 1, 0 ), new Vector2( 0, 1 ), generator.size
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
material.SetMaskRect( center, x, z, size );
|
||||||
|
}
|
||||||
|
|
||||||
|
// The image has a per-texel amount under every blend mode, so unlike the
|
||||||
|
// circle its amplitude is the height even under Replace.
|
||||||
|
SetShapeValue( generator, height );
|
||||||
|
|
||||||
|
generator.DrawShape( rect, blendMode, texture, mask );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if TOOLS
|
||||||
|
|
||||||
|
/* GIZMOS
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
Four edge handles resize one dimension each, keeping the opposite edge
|
||||||
|
where it is - which means they move the node as well as changing `size`,
|
||||||
|
and that is the point: dragging an edge should feel like dragging an
|
||||||
|
edge, not like scaling around the center.
|
||||||
|
|
||||||
|
They sit a little outside the outline rather than on it, because a handle
|
||||||
|
exactly on the edge is hard to tell apart from the line it is on and
|
||||||
|
overlaps the corner handles at a small size.
|
||||||
|
|
||||||
|
The four corner handles rotate instead of resizing. A corner carries the
|
||||||
|
rect's diagonal direction, so pointing it at the mouse is the whole
|
||||||
|
interaction - no accumulated delta, no drag origin to get wrong.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public const int HandleIdEdgeFirst = HandleIdCustom;
|
||||||
|
public const int HandleIdCornerFirst = HandleIdCustom + 4;
|
||||||
|
|
||||||
|
const float MinHalfSize = 0.01f;
|
||||||
|
|
||||||
|
/** +1 for an edge on the axis's positive side, -1 for the negative one. */
|
||||||
|
static readonly float[] edgeSigns = { -1, 1, -1, 1 };
|
||||||
|
|
||||||
|
/** True when the edge is on the X axis, false for Z. */
|
||||||
|
static readonly bool[] edgeOnX = { true, true, false, false };
|
||||||
|
|
||||||
|
static readonly string[] edgeNames = { "Size -X", "Size +X", "Size -Z", "Size +Z" };
|
||||||
|
|
||||||
|
static readonly float[] cornerSignsX = { -1, 1, 1, -1 };
|
||||||
|
static readonly float[] cornerSignsZ = { -1, -1, 1, 1 };
|
||||||
|
|
||||||
|
float _editorHandleDistance = 0.08f;
|
||||||
|
|
||||||
|
/** How far outside the rect the edge handles sit, as a fraction of the
|
||||||
|
rect's longer side. A gizmo handle is a constant size on screen, so an
|
||||||
|
offset in pixels is not available here without the viewport camera - a
|
||||||
|
fraction keeps the handles clear of the outline at any scale instead.
|
||||||
|
*/
|
||||||
|
[Export]
|
||||||
|
public float editorHandleDistance
|
||||||
|
{
|
||||||
|
get => _editorHandleDistance; set { _editorHandleDistance = value; SetDirty(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
float handleOffset => editorHandleDistance * Mathf.Max( size.X, size.Y );
|
||||||
|
|
||||||
|
Vector3 axisX3 { get { var a = axisX; return new Vector3( a.X, 0, a.Y ); } }
|
||||||
|
Vector3 axisZ3 { get { var a = axisZ; return new Vector3( a.X, 0, a.Y ); } }
|
||||||
|
|
||||||
|
Vector3 EdgeAxis( int edge )
|
||||||
|
{
|
||||||
|
return edgeOnX[ edge ] ? axisX3 : axisZ3;
|
||||||
|
}
|
||||||
|
|
||||||
|
float EdgeHalfSize( int edge )
|
||||||
|
{
|
||||||
|
return ( edgeOnX[ edge ] ? size.X : size.Y ) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DrawShapeGizmo( List<Vector3> lines, List<Vector3> handles, List<int> ids )
|
||||||
|
{
|
||||||
|
var x = axisX3 * ( size.X / 2 );
|
||||||
|
var z = axisZ3 * ( size.Y / 2 );
|
||||||
|
|
||||||
|
var corners = new Vector3[ 4 ];
|
||||||
|
|
||||||
|
for ( var i = 0; i < 4; i++ )
|
||||||
|
{
|
||||||
|
corners[ i ] = ToGizmoLocal( x * cornerSignsX[ i ] + z * cornerSignsZ[ i ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( var i = 0; i < 4; i++ )
|
||||||
|
{
|
||||||
|
lines.Add( corners[ i ] );
|
||||||
|
lines.Add( corners[ ( i + 1 ) % 4 ] );
|
||||||
|
|
||||||
|
handles.Add( corners[ i ] );
|
||||||
|
ids.Add( HandleIdCornerFirst + i );
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = handleOffset;
|
||||||
|
|
||||||
|
for ( var i = 0; i < 4; i++ )
|
||||||
|
{
|
||||||
|
var out3 = EdgeAxis( i ) * edgeSigns[ i ] * ( EdgeHalfSize( i ) + offset );
|
||||||
|
|
||||||
|
handles.Add( ToGizmoLocal( out3 ) );
|
||||||
|
ids.Add( HandleIdEdgeFirst + i );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string GetShapeHandleName( int id )
|
||||||
|
{
|
||||||
|
return id >= HandleIdCornerFirst ? "Rotation" : edgeNames[ id - HandleIdEdgeFirst ];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Variant GetShapeHandleValue( int id )
|
||||||
|
{
|
||||||
|
return id >= HandleIdCornerFirst ? Variant.From( GlobalRotation.Y ) : Variant.From( size );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void SetShapeHandle( int id, Camera3D camera, Vector2 point )
|
||||||
|
{
|
||||||
|
Vector3 worldPoint;
|
||||||
|
|
||||||
|
if ( ! ProjectOntoGroundPlane( camera, point, dragStartPosition, out worldPoint ) )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( id >= HandleIdCornerFirst )
|
||||||
|
{
|
||||||
|
RotateToCorner( id - HandleIdCornerFirst, worldPoint );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DragEdge( id - HandleIdEdgeFirst, worldPoint );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Moves one edge to the mouse, keeping the opposite one fixed.
|
||||||
|
|
||||||
|
Everything is measured along the edge's axis from the current center:
|
||||||
|
the opposite edge sits at -sign * half, the dragged one lands where the
|
||||||
|
mouse is minus the handle's own outward offset, and the center ends up
|
||||||
|
halfway between the two. Recomputed from the live state each call rather
|
||||||
|
than from a drag origin, which is why repeatedly calling it converges
|
||||||
|
instead of drifting.
|
||||||
|
*/
|
||||||
|
void DragEdge( int edge, Vector3 worldPoint )
|
||||||
|
{
|
||||||
|
var axis = EdgeAxis( edge );
|
||||||
|
var sign = edgeSigns[ edge ];
|
||||||
|
var oldHalf = EdgeHalfSize( edge );
|
||||||
|
|
||||||
|
var along = ( worldPoint - GlobalPosition ).Dot( axis ) - sign * handleOffset;
|
||||||
|
|
||||||
|
var half = Mathf.Max( ( sign * along + oldHalf ) / 2, MinHalfSize );
|
||||||
|
|
||||||
|
GlobalPosition += axis * ( sign * half - sign * oldHalf );
|
||||||
|
|
||||||
|
size = edgeOnX[ edge ] ? new Vector2( half * 2, size.Y ) : new Vector2( size.X, half * 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Points the dragged corner at the mouse.
|
||||||
|
|
||||||
|
The corner's direction in the node's <marked>unrotated</marked> frame is
|
||||||
|
fixed by its signs and the rect's proportions, so the new yaw is just the
|
||||||
|
mouse's angle minus that one. Atan2( x, z ) is the angle convention a Y
|
||||||
|
rotation uses, so no conversion is needed either.
|
||||||
|
*/
|
||||||
|
void RotateToCorner( int corner, Vector3 worldPoint )
|
||||||
|
{
|
||||||
|
var delta = worldPoint - GlobalPosition;
|
||||||
|
|
||||||
|
if ( new Vector2( delta.X, delta.Z ).LengthSquared() < 0.000001f )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var localX = cornerSignsX[ corner ] * size.X / 2;
|
||||||
|
var localZ = cornerSignsZ[ corner ] * size.Y / 2;
|
||||||
|
|
||||||
|
var yaw = Mathf.Atan2( delta.X, delta.Z ) - Mathf.Atan2( localX, localZ );
|
||||||
|
|
||||||
|
var rotation = GlobalRotation;
|
||||||
|
|
||||||
|
GlobalRotation = new Vector3( rotation.X, yaw, rotation.Z );
|
||||||
|
|
||||||
|
SetDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void RestoreShapeHandle( int id, Variant restore )
|
||||||
|
{
|
||||||
|
if ( id >= HandleIdCornerFirst )
|
||||||
|
{
|
||||||
|
var rotation = GlobalRotation;
|
||||||
|
|
||||||
|
GlobalRotation = new Vector3( rotation.X, restore.AsSingle(), rotation.Z );
|
||||||
|
SetDirty();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size = restore.AsVector2();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://msavssuoeirh
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
// Bakes a world-space normal map out of a HeightMapGenerator's height map.
|
||||||
|
//
|
||||||
|
// One blit over the whole map with the height map as the source, run after every
|
||||||
|
// layer has drawn. The terrain material then samples this instead of taking its
|
||||||
|
// own central difference per fragment - getTerrainNormal in
|
||||||
|
// Shading/Library/Terrain.gdshaderinc - which both moves the work out of the
|
||||||
|
// fragment shader and makes the smoothing a property of the map rather than
|
||||||
|
// something every consumer has to agree on.
|
||||||
|
//
|
||||||
|
// Normals are stored encoded, n * 0.5 + 0.5, so the map works in an 8-bit format
|
||||||
|
// as well as a half one. They are world space, not tangent space: the map is a
|
||||||
|
// heightfield over world XZ, so there is no tangent frame to be relative to and
|
||||||
|
// nothing to reconstruct.
|
||||||
|
//
|
||||||
|
// SMOOTHING
|
||||||
|
// ---------
|
||||||
|
// A heightfield's normal is a derivative, so it amplifies whatever noise the
|
||||||
|
// height has - and a 1-texel central difference amplifies it most. Two knobs:
|
||||||
|
//
|
||||||
|
// normal_radius - how far apart the taps are, in texels. Widening the stencil
|
||||||
|
// low-passes the slope itself, which is what removes texel
|
||||||
|
// stair-stepping without touching the height map.
|
||||||
|
// normal_samples - how many concentric stencils at 1x, 2x ... the radius get
|
||||||
|
// averaged. One wide stencil misses detail between its taps;
|
||||||
|
// averaging several keeps the fine slope and still drops the
|
||||||
|
// noise.
|
||||||
|
//
|
||||||
|
// Each stencil is a full 3x3 Sobel rather than a 4-tap cross, because at a wide
|
||||||
|
// radius a cross only sees two lines through the texel and reads a diagonal
|
||||||
|
// ridge as flat.
|
||||||
|
|
||||||
|
shader_type texture_blit;
|
||||||
|
render_mode blend_disabled;
|
||||||
|
|
||||||
|
uniform sampler2D normal_height_map : hint_blit_source0, filter_linear, repeat_disable;
|
||||||
|
|
||||||
|
// The map's world extent in meters, which is what turns a height difference
|
||||||
|
// into a slope. Without it the normals would depend on the resolution.
|
||||||
|
uniform vec2 normal_size_xz = vec2( 100.0, 100.0 );
|
||||||
|
|
||||||
|
uniform float normal_min_height = 0.0;
|
||||||
|
uniform float normal_max_height = 100.0;
|
||||||
|
|
||||||
|
uniform float normal_radius = 1.0;
|
||||||
|
uniform int normal_samples = 1;
|
||||||
|
|
||||||
|
// Scales the slope before normalizing - below 1 flattens the shading, above 1
|
||||||
|
// exaggerates it. Does not touch the geometry, only the normal.
|
||||||
|
uniform float normal_strength = 1.0;
|
||||||
|
|
||||||
|
|
||||||
|
float heightMapNormals_height( vec2 uv )
|
||||||
|
{
|
||||||
|
return mix( normal_min_height, normal_max_height, texture( normal_height_map, uv ).r );
|
||||||
|
}
|
||||||
|
|
||||||
|
void blit()
|
||||||
|
{
|
||||||
|
vec2 mapSize = vec2( textureSize( normal_height_map, 0 ) );
|
||||||
|
vec2 texel = 1.0 / mapSize;
|
||||||
|
vec2 metersPerTexel = normal_size_xz / mapSize;
|
||||||
|
|
||||||
|
int samples = clamp( normal_samples, 1, 8 );
|
||||||
|
float radius = max( normal_radius, 0.25 );
|
||||||
|
|
||||||
|
vec2 gradient = vec2( 0.0, 0.0 );
|
||||||
|
|
||||||
|
for ( int i = 1; i <= samples; i++ )
|
||||||
|
{
|
||||||
|
float r = radius * float( i );
|
||||||
|
|
||||||
|
vec2 offset = texel * r;
|
||||||
|
vec2 meters = metersPerTexel * r;
|
||||||
|
|
||||||
|
float hMM = heightMapNormals_height( UV + vec2( -offset.x, -offset.y ) );
|
||||||
|
float hM0 = heightMapNormals_height( UV + vec2( -offset.x, 0.0 ) );
|
||||||
|
float hMP = heightMapNormals_height( UV + vec2( -offset.x, offset.y ) );
|
||||||
|
|
||||||
|
float h0M = heightMapNormals_height( UV + vec2( 0.0, -offset.y ) );
|
||||||
|
float h0P = heightMapNormals_height( UV + vec2( 0.0, offset.y ) );
|
||||||
|
|
||||||
|
float hPM = heightMapNormals_height( UV + vec2( offset.x, -offset.y ) );
|
||||||
|
float hP0 = heightMapNormals_height( UV + vec2( offset.x, 0.0 ) );
|
||||||
|
float hPP = heightMapNormals_height( UV + vec2( offset.x, offset.y ) );
|
||||||
|
|
||||||
|
// Sobel. The weights sum to 4 per side, so dividing by 8 * r-in-meters is
|
||||||
|
// the same central difference over 2r the 4-tap version would give.
|
||||||
|
gradient.x += ( ( hPM + 2.0 * hP0 + hPP ) - ( hMM + 2.0 * hM0 + hMP ) ) / ( 8.0 * meters.x );
|
||||||
|
gradient.y += ( ( hMP + 2.0 * h0P + hPP ) - ( hMM + 2.0 * h0M + hPM ) ) / ( 8.0 * meters.y );
|
||||||
|
}
|
||||||
|
|
||||||
|
gradient /= float( samples );
|
||||||
|
|
||||||
|
// The map's V runs along world +Z, so neither axis is flipped.
|
||||||
|
vec3 normal = normalize( vec3(
|
||||||
|
-gradient.x * normal_strength,
|
||||||
|
1.0,
|
||||||
|
-gradient.y * normal_strength
|
||||||
|
) );
|
||||||
|
|
||||||
|
COLOR0 = vec4( normal * 0.5 + 0.5, 1.0 );
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bfajwkc0lw7ve
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
using Godot;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
using Rokojori.Extensions;
|
||||||
|
namespace Rokojori
|
||||||
|
{
|
||||||
|
public class HeightMapNormalsShader
|
||||||
|
{
|
||||||
|
public static readonly CachedResource<Shader> shader = new CachedResource<Shader>(
|
||||||
|
"res://addons/rokojori_action_library/Runtime/Procedural/HeightMaps/Shaders/HeightMapNormals.gdshader"
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly Vector2PropertyName sizeXZ = Vector2PropertyName.Create( "normal_size_xz" );
|
||||||
|
public static readonly FloatPropertyName minHeight = FloatPropertyName.Create( "normal_min_height" );
|
||||||
|
public static readonly FloatPropertyName maxHeight = FloatPropertyName.Create( "normal_max_height" );
|
||||||
|
|
||||||
|
public static readonly FloatPropertyName radius = FloatPropertyName.Create( "normal_radius" );
|
||||||
|
public static readonly IntPropertyName samples = IntPropertyName.Create( "normal_samples" );
|
||||||
|
public static readonly FloatPropertyName strength = FloatPropertyName.Create( "normal_strength" );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Material for HeightMapNormals.gdshader.
|
||||||
|
|
||||||
|
One instance per HeightMapGenerator, like the shape material. The height
|
||||||
|
map is not a property here - it arrives as the blit's source, which is what
|
||||||
|
hint_blit_source0 binds to.
|
||||||
|
*/
|
||||||
|
[Tool]
|
||||||
|
public partial class HeightMapNormalsMaterial:CustomMaterial
|
||||||
|
{
|
||||||
|
public readonly CustomMaterialProperty<Vector2> sizeXZ;
|
||||||
|
public readonly CustomMaterialProperty<float> minHeight;
|
||||||
|
public readonly CustomMaterialProperty<float> maxHeight;
|
||||||
|
|
||||||
|
public readonly CustomMaterialProperty<float> radius;
|
||||||
|
public readonly CustomMaterialProperty<int> samples;
|
||||||
|
public readonly CustomMaterialProperty<float> strength;
|
||||||
|
|
||||||
|
public HeightMapNormalsMaterial()
|
||||||
|
{
|
||||||
|
Shader = HeightMapNormalsShader.shader.Get();
|
||||||
|
|
||||||
|
sizeXZ = new CustomMaterialProperty<Vector2>( this, HeightMapNormalsShader.sizeXZ );
|
||||||
|
minHeight = new CustomMaterialProperty<float>( this, HeightMapNormalsShader.minHeight );
|
||||||
|
maxHeight = new CustomMaterialProperty<float>( this, HeightMapNormalsShader.maxHeight );
|
||||||
|
|
||||||
|
radius = new CustomMaterialProperty<float>( this, HeightMapNormalsShader.radius );
|
||||||
|
samples = new CustomMaterialProperty<int>( this, HeightMapNormalsShader.samples );
|
||||||
|
strength = new CustomMaterialProperty<float>( this, HeightMapNormalsShader.strength );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cg4dtty0p55f8
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
// Content pass of a HeightMapGeneratorLayer's shape.
|
// Content pass of a HeightMapGeneratorLayer's shape.
|
||||||
//
|
//
|
||||||
// Writes one procedural shape's height contribution into the compositor's
|
// Writes one shape's height contribution into the compositor's scratch buffer
|
||||||
// scratch buffer with blending disabled. The scratch buffer is then blended onto
|
// with blending disabled. The scratch buffer is then blended onto the height map
|
||||||
// the height map by a plain BlitMaterial, which is where Add / Subtract / Replace
|
// by a plain BlitMaterial, which is where Add / Subtract / Replace actually
|
||||||
// actually happen - so this shader never reads the map and no ping-pong buffer is
|
// happen - so this shader never reads the map and no ping-pong buffer is
|
||||||
// involved.
|
// involved.
|
||||||
//
|
//
|
||||||
// Height lives in the R channel (G and B carry the same value so the map is
|
// Height lives in the R channel (G and B carry the same value so the map is
|
||||||
|
|
@ -16,6 +16,13 @@
|
||||||
// still drawn at its true position and size. ( The TextureStack had a bug here
|
// still drawn at its true position and size. ( The TextureStack had a bug here
|
||||||
// once - clipping the rect while keeping rect-local UV silently rescaled the
|
// once - clipping the rect while keeping rect-local UV silently rescaled the
|
||||||
// content. Deriving world space from the clipped rect cannot have that problem. )
|
// content. Deriving world space from the clipped rect cannot have that problem. )
|
||||||
|
//
|
||||||
|
// Every shape produces two values, because a falloff means two different things
|
||||||
|
// depending on the blend mode:
|
||||||
|
// amount - how much of the layer's height this texel gets, 0..1
|
||||||
|
// coverage - how much this texel is affected at all, 0..1
|
||||||
|
// Under Add/Subtract coverage scales the contribution; under Replace it is the
|
||||||
|
// alpha the Mix blend crossfades with.
|
||||||
|
|
||||||
shader_type texture_blit;
|
shader_type texture_blit;
|
||||||
render_mode blend_disabled;
|
render_mode blend_disabled;
|
||||||
|
|
@ -24,75 +31,195 @@ render_mode blend_disabled;
|
||||||
uniform vec2 shape_rect_world_min = vec2( 0.0, 0.0 );
|
uniform vec2 shape_rect_world_min = vec2( 0.0, 0.0 );
|
||||||
uniform vec2 shape_rect_world_size = vec2( 1.0, 1.0 );
|
uniform vec2 shape_rect_world_size = vec2( 1.0, 1.0 );
|
||||||
|
|
||||||
// HeightMapShapeType. Only Circle exists so far; the switch is the extension
|
// HeightMapShapeType - 0 Circle, 1 Image.
|
||||||
// point for the primitives that follow.
|
|
||||||
uniform int shape_type = 0;
|
uniform int shape_type = 0;
|
||||||
|
|
||||||
// HeightMapBlendMode - 0 Add, 1 Subtract, 2 Replace.
|
// HeightMapBlendMode - 0 Add, 1 Subtract, 2 Replace.
|
||||||
uniform int shape_blend_mode = 0;
|
uniform int shape_blend_mode = 0;
|
||||||
|
|
||||||
// Normalized 0..1 height, already converted by the generator: a delta of the
|
// Normalized height, split into two uniforms so one formula covers both
|
||||||
// min-to-max range for Add/Subtract, an absolute level for Replace.
|
// readings: the stored level is shape_value_base + shape_value_scale * amount.
|
||||||
uniform float shape_value = 0.0;
|
//
|
||||||
|
// Add/Subtract leave base at 0 and put the layer's delta in scale. Replace puts
|
||||||
|
// the node's own world height in base, and scale is the amplitude layered on
|
||||||
|
// top of it - which is 0 for a shape with no per-texel amount of its own, like
|
||||||
|
// the circle, and the image layer's height for one that has.
|
||||||
|
uniform float shape_value_base = 0.0;
|
||||||
|
uniform float shape_value_scale = 0.0;
|
||||||
|
|
||||||
|
// Circle.
|
||||||
uniform vec2 shape_center = vec2( 0.0, 0.0 );
|
uniform vec2 shape_center = vec2( 0.0, 0.0 );
|
||||||
uniform float shape_inner_radius = 0.0;
|
uniform float shape_inner_radius = 0.0;
|
||||||
uniform float shape_outer_radius = 1.0;
|
uniform float shape_outer_radius = 1.0;
|
||||||
uniform bool shape_smooth_falloff = true;
|
uniform bool shape_smooth_falloff = true;
|
||||||
|
|
||||||
|
// Image. The texture arrives as the blit's source 0; the layer's rect is
|
||||||
|
// described by its two world-space XZ axes and its size in meters, so a rotated
|
||||||
|
// layer needs no matrix and no UV rect.
|
||||||
|
//
|
||||||
|
// repeat_enable because a tiling layer relies on the sampler wrapping - which is
|
||||||
|
// also why nothing here calls fract(), that would put a filtering seam on every
|
||||||
|
// tile border. The non-tiling path emulates clamp-to-edge in UV instead.
|
||||||
|
uniform sampler2D shape_image : hint_blit_source0, filter_linear, repeat_enable;
|
||||||
|
uniform vec2 shape_axis_x = vec2( 1.0, 0.0 );
|
||||||
|
uniform vec2 shape_axis_z = vec2( 0.0, 1.0 );
|
||||||
|
uniform vec2 shape_size = vec2( 1.0, 1.0 );
|
||||||
|
uniform bool shape_image_tiling = false;
|
||||||
|
|
||||||
|
// HeightMapChannel - 0 R, 1 G, 2 B, 3 A, 4 RA, 5 GA, 6 BA.
|
||||||
|
uniform int shape_image_channel = 0;
|
||||||
|
|
||||||
|
// An extra multiplier on coverage, from source 1. An absent mask is bound to the
|
||||||
|
// shared white 1x1 stand-in, so "no mask" needs no flag - it just multiplies by
|
||||||
|
// 1 - and the mask is always clamped, so outside its own rect it extends its
|
||||||
|
// border value rather than disappearing.
|
||||||
|
uniform sampler2D shape_mask : hint_blit_source1, filter_linear, repeat_enable;
|
||||||
|
uniform int shape_mask_channel = 3;
|
||||||
|
uniform vec2 shape_mask_center = vec2( 0.0, 0.0 );
|
||||||
|
uniform vec2 shape_mask_axis_x = vec2( 1.0, 0.0 );
|
||||||
|
uniform vec2 shape_mask_axis_z = vec2( 0.0, 1.0 );
|
||||||
|
uniform vec2 shape_mask_size = vec2( 1.0, 1.0 );
|
||||||
|
|
||||||
|
|
||||||
/** 1 inside the inner radius, falling to 0 at the outer radius. */
|
/** 1 inside the inner radius, falling to 0 at the outer radius. */
|
||||||
float heightMapShape_circle( vec2 worldXZ )
|
void heightMapShape_circle( vec2 worldXZ, out float amount, out float coverage )
|
||||||
{
|
{
|
||||||
float distance = length( worldXZ - shape_center );
|
float distance = length( worldXZ - shape_center );
|
||||||
|
|
||||||
float inner = min( shape_inner_radius, shape_outer_radius );
|
float inner = min( shape_inner_radius, shape_outer_radius );
|
||||||
float outer = max( shape_inner_radius, shape_outer_radius );
|
float outer = max( shape_inner_radius, shape_outer_radius );
|
||||||
|
|
||||||
|
float weight;
|
||||||
|
|
||||||
// A zero-width band is a hard edge, not a division by zero.
|
// A zero-width band is a hard edge, not a division by zero.
|
||||||
if ( outer <= inner )
|
if ( outer <= inner )
|
||||||
{
|
{
|
||||||
return distance <= outer ? 1.0 : 0.0;
|
weight = distance <= outer ? 1.0 : 0.0;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
float t = clamp( ( distance - inner ) / ( outer - inner ), 0.0, 1.0 );
|
float t = clamp( ( distance - inner ) / ( outer - inner ), 0.0, 1.0 );
|
||||||
float weight = 1.0 - t;
|
|
||||||
|
weight = 1.0 - t;
|
||||||
|
|
||||||
if ( shape_smooth_falloff )
|
if ( shape_smooth_falloff )
|
||||||
{
|
{
|
||||||
weight = weight * weight * ( 3.0 - 2.0 * weight );
|
weight = weight * weight * ( 3.0 - 2.0 * weight );
|
||||||
}
|
}
|
||||||
|
|
||||||
return weight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float heightMapShape_weight( vec2 worldXZ )
|
amount = weight;
|
||||||
{
|
|
||||||
switch ( shape_type )
|
// The circle has no per-texel height of its own: under Replace it writes one
|
||||||
{
|
// flat level and the falloff is the crossfade, so it belongs in coverage;
|
||||||
case 0: return heightMapShape_circle( worldXZ );
|
// under Add/Subtract the falloff already scales the height through amount and
|
||||||
|
// coverage has to stay out of the way, or it would be applied twice.
|
||||||
|
coverage = shape_blend_mode == 2 ? weight : 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0.0;
|
float heightMapShape_channel( vec4 color, int channel )
|
||||||
|
{
|
||||||
|
if ( channel == 0 || channel == 4 ) { return color.r; }
|
||||||
|
if ( channel == 1 || channel == 5 ) { return color.g; }
|
||||||
|
if ( channel == 2 || channel == 6 ) { return color.b; }
|
||||||
|
|
||||||
|
return color.a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 0..1 across a rotated world-space XZ rect.
|
||||||
|
|
||||||
|
Image row 0 lands on the rect's -Z edge, so the texture reads upright when
|
||||||
|
the map is looked at from above.
|
||||||
|
*/
|
||||||
|
vec2 heightMapShape_rectUV( vec2 worldXZ, vec2 center, vec2 axisX, vec2 axisZ, vec2 size )
|
||||||
|
{
|
||||||
|
vec2 delta = worldXZ - center;
|
||||||
|
vec2 safeSize = max( size, vec2( 0.0001, 0.0001 ) );
|
||||||
|
|
||||||
|
return vec2( dot( delta, axisX ), dot( delta, axisZ ) ) / safeSize + 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 heightMapShape_clampToEdge( sampler2D source, vec2 uv )
|
||||||
|
{
|
||||||
|
vec2 halfTexel = 0.5 / vec2( textureSize( source, 0 ) );
|
||||||
|
|
||||||
|
return clamp( uv, halfTexel, 1.0 - halfTexel );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The layer's texture, sampled in the rotated rect's own space. */
|
||||||
|
void heightMapShape_image( vec2 worldXZ, out float amount, out float coverage )
|
||||||
|
{
|
||||||
|
amount = 0.0;
|
||||||
|
coverage = 0.0;
|
||||||
|
|
||||||
|
vec2 uv = heightMapShape_rectUV(
|
||||||
|
worldXZ, shape_center, shape_axis_x, shape_axis_z, shape_size
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! shape_image_tiling )
|
||||||
|
{
|
||||||
|
if ( uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sampler repeats for the tiling case, so clamp-to-edge has to be
|
||||||
|
// emulated here or a linear tap on the border would bleed the opposite edge
|
||||||
|
// of the texture in.
|
||||||
|
uv = heightMapShape_clampToEdge( shape_image, uv );
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 color = texture( shape_image, uv );
|
||||||
|
|
||||||
|
amount = heightMapShape_channel( color, shape_image_channel );
|
||||||
|
coverage = shape_image_channel >= 4 ? color.a : 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The mask's contribution to coverage. 1 everywhere when no mask is assigned,
|
||||||
|
because the stand-in bound in its place is white.
|
||||||
|
*/
|
||||||
|
float heightMapShape_mask( vec2 worldXZ )
|
||||||
|
{
|
||||||
|
vec2 uv = heightMapShape_rectUV(
|
||||||
|
worldXZ, shape_mask_center, shape_mask_axis_x, shape_mask_axis_z, shape_mask_size
|
||||||
|
);
|
||||||
|
|
||||||
|
vec4 color = texture( shape_mask, heightMapShape_clampToEdge( shape_mask, uv ) );
|
||||||
|
|
||||||
|
float value = heightMapShape_channel( color, shape_mask_channel );
|
||||||
|
|
||||||
|
return shape_mask_channel >= 4 ? value * color.a : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
void blit()
|
void blit()
|
||||||
{
|
{
|
||||||
vec2 worldXZ = shape_rect_world_min + UV * shape_rect_world_size;
|
vec2 worldXZ = shape_rect_world_min + UV * shape_rect_world_size;
|
||||||
|
|
||||||
float weight = heightMapShape_weight( worldXZ );
|
float amount = 0.0;
|
||||||
|
float coverage = 0.0;
|
||||||
|
|
||||||
|
switch ( shape_type )
|
||||||
|
{
|
||||||
|
case 0: heightMapShape_circle( worldXZ, amount, coverage ); break;
|
||||||
|
case 1: heightMapShape_image( worldXZ, amount, coverage ); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applied to coverage rather than to the height, so a masked-out region falls
|
||||||
|
// back to what is underneath under Replace instead of being flattened to the
|
||||||
|
// layer's base level.
|
||||||
|
coverage *= heightMapShape_mask( worldXZ );
|
||||||
|
|
||||||
if ( shape_blend_mode == 2 )
|
if ( shape_blend_mode == 2 )
|
||||||
{
|
{
|
||||||
// Replace: the height is flat and the falloff rides on alpha, so the Mix
|
// Replace: the height goes in RGB and the coverage rides on alpha, so the
|
||||||
// blend crossfades from the layer's height back to what is underneath.
|
// Mix blend crossfades from the layer's height back to what is underneath.
|
||||||
COLOR0 = vec4( vec3( shape_value ), weight );
|
COLOR0 = vec4( vec3( shape_value_base + shape_value_scale * amount ), coverage );
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Add / Subtract: alpha has to be 1, because the hardware blend multiplies
|
// Add / Subtract: alpha has to be 1, because the hardware blend multiplies
|
||||||
// the source by its own alpha before summing. The falloff is applied here
|
// the source by its own alpha before summing. Coverage is applied here
|
||||||
// instead.
|
// instead.
|
||||||
COLOR0 = vec4( vec3( shape_value * weight ), 1.0 );
|
COLOR0 = vec4( vec3( shape_value_scale * amount * coverage ), 1.0 );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ namespace Rokojori
|
||||||
*/
|
*/
|
||||||
public enum HeightMapShapeType
|
public enum HeightMapShapeType
|
||||||
{
|
{
|
||||||
Circle = 0
|
Circle = 0,
|
||||||
|
Image = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
public class HeightMapShapeShader
|
public class HeightMapShapeShader
|
||||||
|
|
@ -24,12 +25,26 @@ namespace Rokojori
|
||||||
|
|
||||||
public static readonly IntPropertyName shapeType = IntPropertyName.Create( "shape_type" );
|
public static readonly IntPropertyName shapeType = IntPropertyName.Create( "shape_type" );
|
||||||
public static readonly IntPropertyName blendMode = IntPropertyName.Create( "shape_blend_mode" );
|
public static readonly IntPropertyName blendMode = IntPropertyName.Create( "shape_blend_mode" );
|
||||||
public static readonly FloatPropertyName value = FloatPropertyName.Create( "shape_value" );
|
|
||||||
|
public static readonly FloatPropertyName valueBase = FloatPropertyName.Create( "shape_value_base" );
|
||||||
|
public static readonly FloatPropertyName valueScale = FloatPropertyName.Create( "shape_value_scale" );
|
||||||
|
|
||||||
public static readonly Vector2PropertyName center = Vector2PropertyName.Create( "shape_center" );
|
public static readonly Vector2PropertyName center = Vector2PropertyName.Create( "shape_center" );
|
||||||
public static readonly FloatPropertyName innerRadius = FloatPropertyName.Create( "shape_inner_radius" );
|
public static readonly FloatPropertyName innerRadius = FloatPropertyName.Create( "shape_inner_radius" );
|
||||||
public static readonly FloatPropertyName outerRadius = FloatPropertyName.Create( "shape_outer_radius" );
|
public static readonly FloatPropertyName outerRadius = FloatPropertyName.Create( "shape_outer_radius" );
|
||||||
public static readonly BoolPropertyName smoothFalloff = BoolPropertyName.Create( "shape_smooth_falloff" );
|
public static readonly BoolPropertyName smoothFalloff = BoolPropertyName.Create( "shape_smooth_falloff" );
|
||||||
|
|
||||||
|
public static readonly Vector2PropertyName axisX = Vector2PropertyName.Create( "shape_axis_x" );
|
||||||
|
public static readonly Vector2PropertyName axisZ = Vector2PropertyName.Create( "shape_axis_z" );
|
||||||
|
public static readonly Vector2PropertyName imageSize = Vector2PropertyName.Create( "shape_size" );
|
||||||
|
public static readonly IntPropertyName imageChannel = IntPropertyName.Create( "shape_image_channel" );
|
||||||
|
public static readonly BoolPropertyName imageTiling = BoolPropertyName.Create( "shape_image_tiling" );
|
||||||
|
|
||||||
|
public static readonly IntPropertyName maskChannel = IntPropertyName.Create( "shape_mask_channel" );
|
||||||
|
public static readonly Vector2PropertyName maskCenter = Vector2PropertyName.Create( "shape_mask_center" );
|
||||||
|
public static readonly Vector2PropertyName maskAxisX = Vector2PropertyName.Create( "shape_mask_axis_x" );
|
||||||
|
public static readonly Vector2PropertyName maskAxisZ = Vector2PropertyName.Create( "shape_mask_axis_z" );
|
||||||
|
public static readonly Vector2PropertyName maskSize = Vector2PropertyName.Create( "shape_mask_size" );
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Material for HeightMapShape.gdshader.
|
/** Material for HeightMapShape.gdshader.
|
||||||
|
|
@ -39,6 +54,9 @@ namespace Rokojori
|
||||||
between queued blits is safe - the earlier blits do not pick up the later
|
between queued blits is safe - the earlier blits do not pick up the later
|
||||||
values - which is why every layer can share one material instead of
|
values - which is why every layer can share one material instead of
|
||||||
allocating its own.
|
allocating its own.
|
||||||
|
|
||||||
|
The image texture is <marked>not</marked> a property here: it arrives as
|
||||||
|
the blit's source, which is what hint_blit_source0 binds to.
|
||||||
*/
|
*/
|
||||||
[Tool]
|
[Tool]
|
||||||
public partial class HeightMapShapeMaterial:CustomMaterial
|
public partial class HeightMapShapeMaterial:CustomMaterial
|
||||||
|
|
@ -48,13 +66,27 @@ namespace Rokojori
|
||||||
|
|
||||||
public readonly CustomMaterialProperty<int> shapeType;
|
public readonly CustomMaterialProperty<int> shapeType;
|
||||||
public readonly CustomMaterialProperty<int> blendMode;
|
public readonly CustomMaterialProperty<int> blendMode;
|
||||||
public readonly CustomMaterialProperty<float> value;
|
|
||||||
|
public readonly CustomMaterialProperty<float> valueBase;
|
||||||
|
public readonly CustomMaterialProperty<float> valueScale;
|
||||||
|
|
||||||
public readonly CustomMaterialProperty<Vector2> center;
|
public readonly CustomMaterialProperty<Vector2> center;
|
||||||
public readonly CustomMaterialProperty<float> innerRadius;
|
public readonly CustomMaterialProperty<float> innerRadius;
|
||||||
public readonly CustomMaterialProperty<float> outerRadius;
|
public readonly CustomMaterialProperty<float> outerRadius;
|
||||||
public readonly CustomMaterialProperty<bool> smoothFalloff;
|
public readonly CustomMaterialProperty<bool> smoothFalloff;
|
||||||
|
|
||||||
|
public readonly CustomMaterialProperty<Vector2> axisX;
|
||||||
|
public readonly CustomMaterialProperty<Vector2> axisZ;
|
||||||
|
public readonly CustomMaterialProperty<Vector2> imageSize;
|
||||||
|
public readonly CustomMaterialProperty<int> imageChannel;
|
||||||
|
public readonly CustomMaterialProperty<bool> imageTiling;
|
||||||
|
|
||||||
|
public readonly CustomMaterialProperty<int> maskChannel;
|
||||||
|
public readonly CustomMaterialProperty<Vector2> maskCenter;
|
||||||
|
public readonly CustomMaterialProperty<Vector2> maskAxisX;
|
||||||
|
public readonly CustomMaterialProperty<Vector2> maskAxisZ;
|
||||||
|
public readonly CustomMaterialProperty<Vector2> maskSize;
|
||||||
|
|
||||||
public HeightMapShapeMaterial()
|
public HeightMapShapeMaterial()
|
||||||
{
|
{
|
||||||
Shader = HeightMapShapeShader.shader.Get();
|
Shader = HeightMapShapeShader.shader.Get();
|
||||||
|
|
@ -64,12 +96,38 @@ namespace Rokojori
|
||||||
|
|
||||||
shapeType = new CustomMaterialProperty<int>( this, HeightMapShapeShader.shapeType );
|
shapeType = new CustomMaterialProperty<int>( this, HeightMapShapeShader.shapeType );
|
||||||
blendMode = new CustomMaterialProperty<int>( this, HeightMapShapeShader.blendMode );
|
blendMode = new CustomMaterialProperty<int>( this, HeightMapShapeShader.blendMode );
|
||||||
value = new CustomMaterialProperty<float>( this, HeightMapShapeShader.value );
|
|
||||||
|
valueBase = new CustomMaterialProperty<float>( this, HeightMapShapeShader.valueBase );
|
||||||
|
valueScale = new CustomMaterialProperty<float>( this, HeightMapShapeShader.valueScale );
|
||||||
|
|
||||||
center = new CustomMaterialProperty<Vector2>( this, HeightMapShapeShader.center );
|
center = new CustomMaterialProperty<Vector2>( this, HeightMapShapeShader.center );
|
||||||
innerRadius = new CustomMaterialProperty<float>( this, HeightMapShapeShader.innerRadius );
|
innerRadius = new CustomMaterialProperty<float>( this, HeightMapShapeShader.innerRadius );
|
||||||
outerRadius = new CustomMaterialProperty<float>( this, HeightMapShapeShader.outerRadius );
|
outerRadius = new CustomMaterialProperty<float>( this, HeightMapShapeShader.outerRadius );
|
||||||
smoothFalloff = new CustomMaterialProperty<bool>( this, HeightMapShapeShader.smoothFalloff );
|
smoothFalloff = new CustomMaterialProperty<bool>( this, HeightMapShapeShader.smoothFalloff );
|
||||||
|
|
||||||
|
axisX = new CustomMaterialProperty<Vector2>( this, HeightMapShapeShader.axisX );
|
||||||
|
axisZ = new CustomMaterialProperty<Vector2>( this, HeightMapShapeShader.axisZ );
|
||||||
|
imageSize = new CustomMaterialProperty<Vector2>( this, HeightMapShapeShader.imageSize );
|
||||||
|
imageChannel = new CustomMaterialProperty<int>( this, HeightMapShapeShader.imageChannel );
|
||||||
|
imageTiling = new CustomMaterialProperty<bool>( this, HeightMapShapeShader.imageTiling );
|
||||||
|
|
||||||
|
maskChannel = new CustomMaterialProperty<int>( this, HeightMapShapeShader.maskChannel );
|
||||||
|
maskCenter = new CustomMaterialProperty<Vector2>( this, HeightMapShapeShader.maskCenter );
|
||||||
|
maskAxisX = new CustomMaterialProperty<Vector2>( this, HeightMapShapeShader.maskAxisX );
|
||||||
|
maskAxisZ = new CustomMaterialProperty<Vector2>( this, HeightMapShapeShader.maskAxisZ );
|
||||||
|
maskSize = new CustomMaterialProperty<Vector2>( this, HeightMapShapeShader.maskSize );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The mask rect, for a layer that has no opinion about it: anything the
|
||||||
|
mask's own rect does not cover takes its border value, so a 1x1 rect
|
||||||
|
centered on the shape is a flat multiplier over the whole shape.
|
||||||
|
*/
|
||||||
|
public void SetMaskRect( Vector2 center, Vector2 axisX, Vector2 axisZ, Vector2 size )
|
||||||
|
{
|
||||||
|
maskCenter.Set( center );
|
||||||
|
maskAxisX.Set( axisX );
|
||||||
|
maskAxisZ.Set( axisZ );
|
||||||
|
maskSize.Set( size );
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Back to a shape that contributes nothing, so a layer that only sets some
|
/** Back to a shape that contributes nothing, so a layer that only sets some
|
||||||
|
|
@ -82,12 +140,23 @@ namespace Rokojori
|
||||||
{
|
{
|
||||||
shapeType.Set( (int) HeightMapShapeType.Circle );
|
shapeType.Set( (int) HeightMapShapeType.Circle );
|
||||||
blendMode.Set( (int) HeightMapBlendMode.Add );
|
blendMode.Set( (int) HeightMapBlendMode.Add );
|
||||||
value.Set( 0 );
|
|
||||||
|
valueBase.Set( 0 );
|
||||||
|
valueScale.Set( 0 );
|
||||||
|
|
||||||
center.Set( Vector2.Zero );
|
center.Set( Vector2.Zero );
|
||||||
innerRadius.Set( 0 );
|
innerRadius.Set( 0 );
|
||||||
outerRadius.Set( 0 );
|
outerRadius.Set( 0 );
|
||||||
smoothFalloff.Set( true );
|
smoothFalloff.Set( true );
|
||||||
|
|
||||||
|
axisX.Set( new Vector2( 1, 0 ) );
|
||||||
|
axisZ.Set( new Vector2( 0, 1 ) );
|
||||||
|
imageSize.Set( Vector2.One );
|
||||||
|
imageChannel.Set( (int) HeightMapChannel.R );
|
||||||
|
imageTiling.Set( false );
|
||||||
|
|
||||||
|
maskChannel.Set( (int) HeightMapChannel.A );
|
||||||
|
SetMaskRect( Vector2.Zero, new Vector2( 1, 0 ), new Vector2( 0, 1 ), Vector2.One );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ namespace Rokojori
|
||||||
|
|
||||||
@"
|
@"
|
||||||
uniform sampler2D terrainHeightMap;
|
uniform sampler2D terrainHeightMap;
|
||||||
|
uniform sampler2D terrainNormalMap;
|
||||||
uniform vec2 terrainCenterXZ = vec2( 0.0, 0.0 );
|
uniform vec2 terrainCenterXZ = vec2( 0.0, 0.0 );
|
||||||
uniform vec2 terrainSizeXZ = vec2( 100.0, 100.0 );
|
uniform vec2 terrainSizeXZ = vec2( 100.0, 100.0 );
|
||||||
uniform float terrainMinHeight = 0.0;
|
uniform float terrainMinHeight = 0.0;
|
||||||
|
|
|
||||||
|
|
@ -40,3 +40,39 @@ void addTerrainOffset(
|
||||||
_VERTEX = worldToLocal( worldVertex, _MODEL_MATRIX );
|
_VERTEX = worldToLocal( worldVertex, _MODEL_MATRIX );
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The world-space normal HeightMapGenerator baked into terrainNormalMap.
|
||||||
|
|
||||||
|
Stored encoded, n * 0.5 + 0.5, so the map also works in an 8-bit format - see
|
||||||
|
Procedural/HeightMaps/Shaders/HeightMapNormals.gdshader. World space, not
|
||||||
|
tangent space: a heightfield over world XZ has no tangent frame to be
|
||||||
|
relative to.
|
||||||
|
|
||||||
|
This replaces taking a central difference per fragment. Besides being
|
||||||
|
cheaper, it is the only way the smoothing can exist at all - a consumer
|
||||||
|
sampling its own two neighbours has nothing to widen the stencil with, and a
|
||||||
|
clipmap's triangles are far coarser than the map, so a vertex normal is not
|
||||||
|
an option either.
|
||||||
|
*/
|
||||||
|
vec3 getTerrainNormal(
|
||||||
|
sampler2D terrainNormalMap,
|
||||||
|
vec3 worldPosition,
|
||||||
|
vec2 terrainCenterXZ,
|
||||||
|
vec2 terrainSizeXZ
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// An unassigned sampler is Godot's 1x1 default and reads black, and
|
||||||
|
// normalize( black * 2 - 1 ) is a diagonal rather than up - so a material
|
||||||
|
// whose generator has no normalMapTarget would get wrong lighting instead of
|
||||||
|
// flat lighting. A real normal map is never 1x1, so the size is a safe test.
|
||||||
|
if ( textureSize( terrainNormalMap, 0 ).x <= 1 )
|
||||||
|
{
|
||||||
|
return vec3( 0.0, 1.0, 0.0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 mapOffset = terrainCenterXZ - terrainSizeXZ/2.0;
|
||||||
|
|
||||||
|
vec2 uv = ( worldPosition.xz - mapOffset ) / terrainSizeXZ;
|
||||||
|
|
||||||
|
return normalize( texture( terrainNormalMap, uv ).rgb * 2.0 - 1.0 );
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,12 @@ namespace Rokojori
|
||||||
public GizmoDrawerPlugin()
|
public GizmoDrawerPlugin()
|
||||||
{
|
{
|
||||||
CreateMaterial( "main", new Color( 1, 1, 1 , 1 ) );
|
CreateMaterial( "main", new Color( 1, 1, 1 , 1 ) );
|
||||||
|
|
||||||
|
// Handles need a material made by CreateHandleMaterial to render as the
|
||||||
|
// editor's billboarded, selectable dots - a plain line material draws
|
||||||
|
// them as single unclickable points. GetMaterial( "handles" ) is how a
|
||||||
|
// GizmoDrawerWithHandles reaches it.
|
||||||
|
CreateHandleMaterial( "handles" );
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string _GetGizmoName()
|
public override string _GetGizmoName()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue