Texture Combiner/Grass Updates

This commit is contained in:
Josef 2025-02-19 13:12:12 +01:00
parent 756b1d2a9d
commit 8f2d50ff71
12 changed files with 723 additions and 35 deletions

View File

@ -4,6 +4,8 @@ namespace Rokojori
{
public static class ColorX
{
public static float AlphaMultipliedR( this Color c )
{
return c.R * c.A;
@ -19,20 +21,103 @@ namespace Rokojori
return c.B * c.A;
}
public static float pmR( this Color c ) => c.AlphaMultipliedR();
public static float pmG( this Color c ) => c.AlphaMultipliedG();
public static float pmB( this Color c ) => c.AlphaMultipliedB();
static float Clamp( float f )
{
return MathX.Clamp01( f );
}
public static Color Blend( Color bottom, Color top )
{
var fade = ( 1f - top.A );
var a0 = top.A + bottom.A * fade;
var a = top.A + bottom.A * fade;
var xR = top.AlphaMultipliedR() + bottom.AlphaMultipliedR() * fade;
var xG = top.AlphaMultipliedG() + bottom.AlphaMultipliedG() * fade;
var xB = top.AlphaMultipliedB() + bottom.AlphaMultipliedB() * fade;
var r = top.pmR() + bottom.pmR() * fade;
var g = top.pmG() + bottom.pmG() * fade;
var b = top.pmB() + bottom.pmB() * fade;
var normalizer = 1f / a0;
return new Color( xR, xG, xB, 1 ) * normalizer;
return new Color( r, g, b, 1 ) / a;
}
public static Color PreMultipliedSingleComponentBlendMode( Color bottom, Color top, System.Func<float,float,float, float> blending )
{
var fade = ( 1f - top.A );
var a = top.A + bottom.A * fade;
var r = blending( bottom.pmR(), top.pmR(), fade );
var g = blending( bottom.pmG(), top.pmG(), fade );
var b = blending( bottom.pmB(), top.pmB(), fade );
return new Color( r, g, b, 1 ) / a;
}
public static Color BlendMultiply( Color bottom, Color top )
{
return PreMultipliedSingleComponentBlendMode( bottom, top,
( a, b, f)=>
{
return a * b;
}
);
}
public static Color BlendScreen( Color bottom, Color top )
{
return PreMultipliedSingleComponentBlendMode( bottom, top,
( a, b, f)=>
{
return 1f - ( 1f - a ) * ( 1f - b);
}
);
}
public static Color BlendOverlay( Color bottom, Color top )
{
return PreMultipliedSingleComponentBlendMode( bottom, top,
( a, b, f)=>
{
return a < 0.5f ? (2.0f * a * b) : (1.0f - 2.0f * (1.0f - a) * (1.0f - b));
}
);
}
public static Color BlendHardLight( Color bottom, Color top )
{
return PreMultipliedSingleComponentBlendMode( bottom, top,
( a, b, f)=>
{
return b < 0.5f ? (2.0f * a * b) : (1.0f - 2.0f * (1.0f - a) * (1.0f - b)) ;
}
);
}
public static Color BlendSoftLight( Color bottom, Color top )
{
return PreMultipliedSingleComponentBlendMode( bottom, top,
( a, b, f)=>
{
return b < 0.5f ? (2.0f * a * b) : (1.0f - 2.0f * (1.0f - a) * (1.0f - b)) ;
}
);
}
public static Color BlendAdd( Color bottom, Color top )
{
return PreMultipliedSingleComponentBlendMode( bottom, top,
( b, t, f)=>
{
return b + t;
}
);
}
public static Color Lerp( Color a, Color b, float amount )
{
return a.Lerp( b, amount );

View File

@ -133,6 +133,9 @@ namespace Rokojori
[Export]
public Curve rolling = MathX.Curve( 0f );
[Export]
public Curve rolling2 = null;
[ExportGroup( "Blade Offset & Scale")]
@ -619,11 +622,18 @@ namespace Rokojori
outputSpline.AutoOrientateByTangents( Vector3.Back );
var rollingMix = random.Next();
var otherRolling = rolling2 == null ? rolling : rolling2;
for ( int i = 0; i < outputPoints.Count - 1; i++ )
{
var t = i / (float)( outputPoints.Count - 2 );
var rotation = rolling.Sample( t );
RollSpline( outputPoints, i, rotation );
var rotation2 = otherRolling.Sample( t );
var mixedRotation = Mathf.Lerp( rotation, rotation2, rollingMix );
RollSpline( outputPoints, i, mixedRotation );
}
var modifier = new SplinesDeformModifier();

View File

@ -53,8 +53,9 @@ void vertex()
float strength = texture( windNoise, windUV + windNoiseStrengthOffset ).r * windStrength;
vec2 circle = onCircle( angle ) * strength;
VERTEX = worldToLocal( worldVertex + vec3( circle.x, 0, circle.y ) * windAmount, MODEL_MATRIX );
VERTEX.y = mix( VERTEX.y, max( 0, VERTEX.y - strength * windAmount), windHeightCompensation * 2.0f );
float minY = min( VERTEX.y, 0 );
// VERTEX.y = mix( VERTEX.y, max( 0, VERTEX.y - strength * windAmount), windHeightCompensation * 2.0f );
VERTEX.y = mix( VERTEX.y, max( minY, VERTEX.y - strength * windAmount), windHeightCompensation * 4.0f );
}
void fragment()

View File

@ -0,0 +1,61 @@
using System.Collections;
using System.Collections.Generic;
using Godot;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Rokojori
{
[Tool]
[GlobalClass]
public partial class TextureCombinerTextureLayer:TextureCombinerLayer
{
[Export]
public Texture2D texture;
[Export]
public Vector2 tiling = Vector2.One;
[Export]
public Vector2 offset = Vector2.Zero;
[Export]
public bool updateTexture;
TextureCombinerBuffer textureBuffer;
public override async Task Process( TextureCombinerProcessingRect processingRect )
{
var time = Async.StartTimer();
if ( updateTexture || textureBuffer == null )
{
textureBuffer = TextureCombinerBuffer.From( texture );
time = await Async.WaitIfExceeded( time );
}
for ( int i = 0; i < processingRect.processingWidth; i++ )
{
for ( int j = 0; j < processingRect.processingHeight; j++ )
{
var uv = processingRect.GetRelativeUV( i, j );
uv *= tiling;
uv += offset;
uv = uv.PosMod( 1f );
var color = textureBuffer.SampleBilinearUV( uv );
processingRect.SetOutputRelative( i, j, color );
}
}
return;
}
}
}

View File

@ -2,7 +2,8 @@ using System.Collections;
using System.Collections.Generic;
using Godot;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Rokojori
@ -12,6 +13,199 @@ namespace Rokojori
public partial class TextureCombiner:Resource
{
[Export]
public TextureCombinerLayer[] layers;
public Texture2D textureOutput;
[Export]
public bool createMipmaps = true;
[Export]
public TextureCombinerMaterialOutput materialOutput;
[Export]
public bool create
{
get => false;
set { if ( ! value ) return; CreateTexture(); }
}
[Export]
public TextureCombinerStack layerStack;
[Export]
public int width = 1024;
[Export]
public int height = 1024;
[ExportGroup( "Processing Settings")]
[Export]
bool _creatingTexture = false;
[Export]
int _blockSize = 512;
async void CreateTexture()
{
if ( _creatingTexture )
{
return;
}
var hRatio = width / (float)height;
var waitImage = Image.CreateEmpty( 32, Mathf.Max( 1, Mathf.RoundToInt( 32 / hRatio ) ), false, Image.Format.Rgba8 );
waitImage.Fill( new Color( 1, 0, 1 ));
textureOutput = ImageTexture.CreateFromImage( waitImage );
_creatingTexture = true;
var context = new TextureCombinerContext();
context.combiner = this;
context.imageWidth = width;
context.imageHeight = height;
outputBuffer = TextureCombinerBuffer.Create( width, height );
inputBuffer = TextureCombinerBuffer.Create( width, height );
var first = true;
for ( int i = 0; i < layerStack.layers.Length; i++ )
{
var layerIndex = ( layerStack.layers.Length - 1 ) - i;
var layer = layerStack.layers[ layerIndex ];
if ( ! layer.visible )
{
continue;
}
await ProcessLayer( layer, context );
if ( first )
{
var b = inputBuffer;
inputBuffer = outputBuffer;
outputBuffer = b;
first = false;
}
else
{
await BlendLayer( layer, context );
}
}
var image = Image.CreateEmpty( width, height, createMipmaps, Image.Format.Rgba8 );
inputBuffer.CopyTo( 0, 0, 0, 0, width, height, image );
image.GenerateMipmaps();
textureOutput = ImageTexture.CreateFromImage( image );
if ( materialOutput != null &&
materialOutput.material != null &&
materialOutput.textureName != null
)
{
materialOutput.textureName.Set( materialOutput.material, textureOutput );
}
_creatingTexture = false;
}
async Task ProcessLayer( TextureCombinerLayer layer, TextureCombinerContext context )
{
var blockSize = _blockSize;
var blocksX = Mathf.CeilToInt( width / (float) blockSize );
var blocksY = Mathf.CeilToInt( height / (float) blockSize );
var time = Async.StartTimer();
for ( int i = 0; i < blocksX; i++ )
{
var startX = i * blockSize;
var endX = Mathf.Min( startX + blockSize, width );
for ( int j = 0; j < blocksY; j++ )
{
var startY = j * blockSize;
var endY = Mathf.Min( startY + blockSize, height );
var rect = new TextureCombinerProcessingRect();
rect.processingX = startX;
rect.processingY = startY;
rect.processingWidth = endX - startX;
rect.processingHeight = endY - startY;
rect.context = context;
rect.inputBuffer = inputBuffer;
rect.outputBuffer = outputBuffer;
await layer.Process( rect );
time = await Async.WaitIfExceeded( time );
}
}
}
async Task BlendLayer( TextureCombinerLayer layer, TextureCombinerContext context )
{
var blockSize = _blockSize;
var blocksX = Mathf.CeilToInt( width / (float) blockSize );
var blocksY = Mathf.CeilToInt( height / (float) blockSize );
var time = Async.StartTimer();
TextureCombinerBuffer maskBuffer = null;
if ( layer.opacityMask != null )
{
maskBuffer = TextureCombinerBuffer.From( layer.opacityMask ).Resize( context.imageWidth, context.imageHeight );
}
for ( int i = 0; i < blocksX; i++ )
{
var startX = i * blockSize;
var endX = Mathf.Min( startX + blockSize, width );
for ( int j = 0; j < blocksY; j++ )
{
var startY = j * blockSize;
var endY = Mathf.Min( startY + blockSize, width );
var rect = new TextureCombinerProcessingRect();
if ( layer.opacityMask == null )
{
TextureCombinerBlendModeAlgorithm.Blend( layer.blendMode,
startX, startY, endX - startX, endY - startY, layer.opacity,
inputBuffer, outputBuffer, inputBuffer
);
}
else
{
TextureCombinerBlendModeAlgorithm.BlendMasked( layer.blendMode,
startX, startY, endX - startX, endY - startY, layer.opacity, maskBuffer,
inputBuffer, outputBuffer, inputBuffer
);
}
time = await Async.WaitIfExceeded( time );
}
}
}
TextureCombinerBuffer outputBuffer;
TextureCombinerBuffer inputBuffer;
}
}

View File

@ -11,23 +11,53 @@ namespace Rokojori
{
Normal,
Add,
Multiply
Multiply,
Screen,
Overlay,
HardLight,
SoftLight
}
public class TextureCombinerBlendModeAlgorithm
{
public static void Blend( TextureCombinerBlendMode blendMode, int x, int y, int w, int h, float topOpacity,
TextureBuffer<Color> bottom, TextureBuffer<Color> top, TextureBuffer<Color> output
TextureCombinerBuffer bottom, TextureCombinerBuffer top, TextureCombinerBuffer output
)
{
if ( TextureCombinerBlendMode.Normal == blendMode )
{
BlendNormal( x, y, w, h, topOpacity, bottom, top, output );
BlendMode( ColorX.Blend, x, y, w, h, topOpacity, bottom, top, output );
}
else if ( TextureCombinerBlendMode.Add == blendMode )
{
BlendMode( ColorX.BlendAdd, x, y, w, h, topOpacity, bottom, top, output );
}
else if ( TextureCombinerBlendMode.Multiply == blendMode )
{
BlendMode( ColorX.BlendMultiply, x, y, w, h, topOpacity, bottom, top, output );
}
else if ( TextureCombinerBlendMode.Screen == blendMode )
{
BlendMode( ColorX.BlendScreen, x, y, w, h, topOpacity, bottom, top, output );
}
else if ( TextureCombinerBlendMode.Overlay == blendMode )
{
BlendMode( ColorX.BlendOverlay, x, y, w, h, topOpacity, bottom, top, output );
}
else if ( TextureCombinerBlendMode.HardLight == blendMode )
{
BlendMode( ColorX.BlendHardLight, x, y, w, h, topOpacity, bottom, top, output );
}
else if ( TextureCombinerBlendMode.SoftLight == blendMode )
{
BlendMode( ColorX.BlendSoftLight, x, y, w, h, topOpacity, bottom, top, output );
}
}
static void BlendNormal( int x, int y, int w, int h, float topOpacity,
TextureBuffer<Color> bottom, TextureBuffer<Color> top, TextureBuffer<Color> output )
static void BlendMode( System.Func<Color,Color,Color> blender, int x, int y, int w, int h, float topOpacity,
TextureCombinerBuffer bottom, TextureCombinerBuffer top, TextureCombinerBuffer output )
{
for ( int i = 0; i < w; i++ )
{
@ -39,10 +69,78 @@ namespace Rokojori
var index = output.ComputeIndexFromPosition( rx, ry );
var topColor = top.GetIndexed( index ).FadeAlpha( topOpacity );
var topColor = top.GetIndexed( index );
var bottomColor = bottom.GetIndexed( index );
var outputColor = bottomColor.Blend( topColor );
var outputColor = blender( bottomColor, topColor );
outputColor = bottomColor.Blend( outputColor.FadeAlpha( topOpacity ) );
output.SetIndexed( index, outputColor );
}
}
}
public static void BlendMasked( TextureCombinerBlendMode blendMode, int x, int y, int w, int h, float topOpacity,
TextureCombinerBuffer mask,
TextureCombinerBuffer bottom, TextureCombinerBuffer top, TextureCombinerBuffer output
)
{
if ( TextureCombinerBlendMode.Normal == blendMode )
{
BlendModeMasked( ColorX.Blend, x, y, w, h, topOpacity, mask, bottom, top, output );
}
else if ( TextureCombinerBlendMode.Add == blendMode )
{
BlendModeMasked( ColorX.BlendAdd, x, y, w, h, topOpacity, mask, bottom, top, output );
}
else if ( TextureCombinerBlendMode.Multiply == blendMode )
{
BlendModeMasked( ColorX.BlendMultiply, x, y, w, h, topOpacity, mask, bottom, top, output );
}
else if ( TextureCombinerBlendMode.Screen == blendMode )
{
BlendModeMasked( ColorX.BlendScreen, x, y, w, h, topOpacity, mask, bottom, top, output );
}
else if ( TextureCombinerBlendMode.Overlay == blendMode )
{
BlendModeMasked( ColorX.BlendOverlay, x, y, w, h, topOpacity, mask, bottom, top, output );
}
else if ( TextureCombinerBlendMode.HardLight == blendMode )
{
BlendModeMasked( ColorX.BlendHardLight, x, y, w, h, topOpacity, mask, bottom, top, output );
}
else if ( TextureCombinerBlendMode.SoftLight == blendMode )
{
BlendModeMasked( ColorX.BlendSoftLight, x, y, w, h, topOpacity, mask, bottom, top, output );
}
}
static void BlendModeMasked( System.Func<Color,Color,Color> blender, int x, int y, int w, int h, float topOpacity,
TextureCombinerBuffer mask,
TextureCombinerBuffer bottom, TextureCombinerBuffer top, TextureCombinerBuffer output )
{
for ( int i = 0; i < w; i++ )
{
var rx = x + i;
for ( int j = 0; j < h; j++ )
{
var ry = y + j;
var index = output.ComputeIndexFromPosition( rx, ry );
var topColor = top.GetIndexed( index );
var bottomColor = bottom.GetIndexed( index );
var outputColor = blender( bottomColor, topColor );
var maskOpacity = mask.GetIndexed( index );
outputColor = bottomColor.Blend( outputColor.FadeAlpha( topOpacity * maskOpacity.R ) );
output.SetIndexed( index, outputColor );

View File

@ -7,7 +7,7 @@ using System;
namespace Rokojori
{
public class TextureBuffer<T>
public class TextureCombinerBuffer
{
int _width;
public int width => _width;
@ -15,43 +15,150 @@ namespace Rokojori
int _height;
public int height => _height;
T[] _pixels;
Color[] _pixels;
public static TextureBuffer<T> Create( int w, int h )
public static TextureCombinerBuffer Create( int w, int h )
{
var tb = new TextureBuffer<T>();
var tb = new TextureCombinerBuffer();
tb._height = h;
tb._width = w;
tb._pixels = new T[ w * h ];
tb._pixels = new Color[ w * h ];
return tb;
}
public static TextureCombinerBuffer From( Texture2D texture )
{
var buffer = Create( texture.GetWidth(), texture.GetHeight() );
buffer.CopyFrom( texture.GetImage(), 0, 0, 0, 0, texture.GetWidth(), texture.GetHeight() );
return buffer;
}
public TextureCombinerBuffer Resize( int w, int h )
{
var buffer = Create( w, h );
for ( int i = 0; i < w; i++ )
{
for ( int j = 0; j < h; j++ )
{
var uv = new Vector2( i / (float) w, j / (float) h );
var pixel = SampleBilinearUV( uv );
buffer.SetAt( i, j, pixel );
}
}
return buffer;
}
public void CopyFrom( Image image, int sourceX, int sourceY, int ownX, int ownY, int w, int h )
{
for ( int i = 0; i < w; i++ )
{
for ( int j = 0; j < h; j++ )
{
var sourcePixel = image.GetPixel( sourceX + i, sourceY + j );
SetAt( ownX + i, ownY + j, sourcePixel );
}
}
}
public void CopyTo( int ownX, int ownY, int targetX, int targetY, int w, int h, TextureCombinerBuffer output )
{
for ( int i = 0; i < w; i++ )
{
for ( int j = 0; j < h; j++ )
{
var ownPixel = GetAt( ownX + i, ownY + j );
output.SetAt( targetX + i, targetY + j, ownPixel );
}
}
}
public void CopyTo( int ownX, int ownY, int targetX, int targetY, int w, int h, Image image )
{
for ( int i = 0; i < w; i++ )
{
for ( int j = 0; j < h; j++ )
{
var ownPixel = GetAt( ownX + i, ownY + j );
image.SetPixel( targetX + i, targetY + j, ownPixel );
}
}
}
public int ComputeIndexFromPosition( int x, int y )
{
return x + y * _width;
}
public void SetAt( int x, int y, T value )
public void SetAt( int x, int y, Color value )
{
SetIndexed( ComputeIndexFromPosition( x, y ), value );
}
public void SetIndexed( int index, T value )
public void SetIndexed( int index, Color value )
{
_pixels[ index ] = value;
}
public T GetAt( int x, int y )
public Color GetAt( int x, int y )
{
return GetIndexed( ComputeIndexFromPosition( x, y ) );
}
public T GetIndexed( int index )
public Color GetIndexed( int index )
{
return _pixels[ index ];
}
public Color SampleNearestUV( Vector2 uv )
{
return SampleNearestImage( uv.X * width, uv.Y * height );
}
public Color SampleNearestImage( float imageDimensionsX, float imageDimensionsY )
{
return GetAt( Mathf.RoundToInt( imageDimensionsX ), Mathf.RoundToInt( imageDimensionsY ) );
}
public Color SampleBilinearUV( Vector2 uv )
{
return SampleBilinearImage( uv.X * width, uv.Y * height );
}
public Color SampleBilinearImage( float imageDimensionsX, float imageDimensionsY )
{
var lowX = Mathf.FloorToInt( imageDimensionsX );
var highX = Mathf.Min( lowX + 1, width - 1 );
var lerpX = imageDimensionsX - lowX;
var lowY = Mathf.FloorToInt( imageDimensionsY );
var highY = Mathf.Min( lowY + 1, height - 1 );
var lerpY = imageDimensionsY - lowY;
var ll = GetAt( lowX, lowY );
var hl = GetAt( highX, lowY );
var lh = GetAt( lowX, highY );
var hh = GetAt( highX, highY );
var upper = ll.Lerp( hl, lerpX );
var lower = lh.Lerp( hh, lerpX );
return upper.Lerp( lower, lerpY );
}
}
}

View File

@ -19,7 +19,13 @@ namespace Rokojori
[Export( PropertyHint.Range, "0,1")]
public float opacity = 1;
public async Task Process( TextureCombinerProcessingRect processingRect )
[Export]
public Texture2D opacityMask;
[Export]
public bool visible = true;
public virtual async Task Process( TextureCombinerProcessingRect processingRect )
{
return;
}

View File

@ -0,0 +1,21 @@
using System.Collections;
using System.Collections.Generic;
using Godot;
using System;
namespace Rokojori
{
[Tool]
[GlobalClass]
public partial class TextureCombinerMaterialOutput:Resource
{
[Export]
public Material material;
[Export]
public Texture2DPropertyName textureName;
}
}

View File

@ -2,7 +2,8 @@ using System.Collections;
using System.Collections.Generic;
using Godot;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Rokojori
@ -12,10 +13,41 @@ namespace Rokojori
public TextureCombinerContext context;
public int processingX = 0;
public int processingY = 0;
public int processingWidht = 0;
public int processingWidth = 0;
public int processingHeight = 0;
public TextureBuffer<Color> output;
public Vector2 GetUV( int x, int y )
{
return new Vector2( x / (float) context.imageWidth, y / (float) context.imageHeight );
}
public Vector2 GetRelativeUV( int x, int y )
{
return GetUV( x + processingX, y + processingY );
}
public void SetOutputRelative( int x, int y, Color c )
{
outputBuffer.SetAt( x + processingX, y + processingY, c );
}
public Vector2 uvStart => GetUV( processingX, processingY );
public Vector2 uvEnd => GetUV( processingX + processingWidth, processingY + processingHeight );
public TextureCombinerBuffer inputBuffer;
public TextureCombinerBuffer outputBuffer;
public async Task CopyToFinalOutput( TextureCombinerBuffer bottomBuffer, TextureCombinerBuffer image )
{
var time = Async.StartTimer();
bottomBuffer.CopyTo( 0, 0, processingX, processingY, processingWidth, processingHeight, image );
time = await Async.WaitIfExceeded( time );
return;
}
}
}

View File

@ -0,0 +1,17 @@
using System.Collections;
using System.Collections.Generic;
using Godot;
using System;
namespace Rokojori
{
[Tool]
[GlobalClass]
public partial class TextureCombinerStack:Resource
{
[Export]
public TextureCombinerLayer[] layers;
}
}

56
Runtime/Tools/Async.cs Normal file
View File

@ -0,0 +1,56 @@
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System;
using Godot;
using System;
using System.Threading.Tasks;
namespace Rokojori
{
public class Async
{
public const double waitTime = 1.0 / 60.0;
public static async Task Wait( double waitTime = Async.waitTime )
{
await Task.Delay( (int)( waitTime * 1000 ) );
return;
}
public static double StartTimer()
{
return Time.GetTicksMsec() / 1000.0;
}
public static async Task<double> WaitIfExceeded( double last, double waitTime = Async.waitTime )
{
var now = Time.GetTicksMsec() / 1000.0;
if ( ( now - last ) > waitTime )
{
await Task.Delay( (int)( waitTime * 1000 ) );
return Time.GetTicksMsec() / 1000.0;;
}
return last;
}
public static double DoIfExceeded( double last, System.Action action, double waitTime = Async.waitTime )
{
var now = Time.GetTicksMsec() / 1000.0;
if ( ( now - last ) > waitTime )
{
action();
}
return Time.GetTicksMsec() / 1000.0;;
}
}
}