Camera/Adjustments Udpates

This commit is contained in:
Rokojori 2026-07-31 18:06:46 +02:00
parent 72ab6d5df4
commit 5ede8f7dff
27 changed files with 708 additions and 33 deletions

View File

@ -0,0 +1,40 @@
#if TOOLS
using Godot;
using System.Collections.Generic;
namespace Rokojori;
[Tool]
public partial class DeduplicatingExportPlugin : EditorExportPlugin
{
private HashSet<string> _seenPaths = new HashSet<string>();
public override string _GetName() => "RokojoriDeduplicatingExport";
public override void _ExportBegin( string[] features, bool isDebug, string path, uint flags )
{
_seenPaths.Clear();
GD.Print( "[DeduplicatingExport] Export started: ", path );
}
public override void _ExportFile( string path, string type, string[] features )
{
if ( ! _seenPaths.Add( path ) )
{
GD.Print( "[DeduplicatingExport] SKIPPED (duplicate): ", path );
Skip();
return;
}
GD.Print( "[DeduplicatingExport] included: ", path );
}
public override void _ExportEnd()
{
GD.Print( "[DeduplicatingExport] Export ended. Total unique files: ", _seenPaths.Count );
}
}
#endif

View File

@ -0,0 +1 @@
uid://2vyvvmkop2ec

View File

@ -64,6 +64,7 @@ public partial class RokojoriPlugin: EditorPlugin
{ {
GizmoDrawerPlugin gizmoDrawerPlugin = new GizmoDrawerPlugin(); GizmoDrawerPlugin gizmoDrawerPlugin = new GizmoDrawerPlugin();
GodotEditorInspectorTools inspectorTools = new GodotEditorInspectorTools(); GodotEditorInspectorTools inspectorTools = new GodotEditorInspectorTools();
DeduplicatingExportPlugin deduplicatingExportPlugin = new DeduplicatingExportPlugin();
public static readonly string path = "res://addons/rokojori_action_library"; public static readonly string path = "res://addons/rokojori_action_library";
@ -163,6 +164,7 @@ public partial class RokojoriPlugin: EditorPlugin
this.LogInfo(); this.LogInfo();
AddNode3DGizmoPlugin( gizmoDrawerPlugin ); AddNode3DGizmoPlugin( gizmoDrawerPlugin );
AddExportPlugin( deduplicatingExportPlugin );
inspectorTools.rokojoriPlugin = this; inspectorTools.rokojoriPlugin = this;
inspectorTools.AddPlugins(); inspectorTools.AddPlugins();
@ -173,6 +175,7 @@ public partial class RokojoriPlugin: EditorPlugin
this.LogInfo(); this.LogInfo();
RemoveNode3DGizmoPlugin( gizmoDrawerPlugin ); RemoveNode3DGizmoPlugin( gizmoDrawerPlugin );
RemoveExportPlugin( deduplicatingExportPlugin );
inspectorTools.rokojoriPlugin = this; inspectorTools.rokojoriPlugin = this;
inspectorTools.RemovePlugins(); inspectorTools.RemovePlugins();

View File

@ -0,0 +1,51 @@
using Rokojori;
using Godot;
namespace Rokojori;
[GlobalClass,Tool]
public partial class AdjustmentsDesigner: Node
{
[Export]
public AdjustmentsStack stack = new AdjustmentsStack();
[Export]
public bool autoUpdate = false;
[Export]
public WorldEnvironment environment;
[ExportToolButton( "Update Adjustments" )]
public Callable updateAdjustmentsButton => Callable.From(
()=>
{
ProcessStack();
}
);
public override void _Process( double delta )
{
if ( ! Engine.IsEditorHint() )
{
return;
}
if ( ! autoUpdate )
{
return;
}
ProcessStack();
}
void ProcessStack()
{
stack.GenerateAdjustmentsTexture();
if ( environment != null )
{
environment.Environment.AdjustmentColorCorrection = stack.adjustmentsTexture;
}
}
}

View File

@ -0,0 +1 @@
uid://8yy5v376kadx

View File

@ -0,0 +1,58 @@
using Rokojori;
using Godot;
namespace Rokojori;
[GlobalClass,Tool]
public abstract partial class AdjustmentsFilter: Resource
{
[Export( PropertyHint.Range, "0,1" )]
public float amount;
[Export]
public bool enabled = true;
[Export]
public AdjustmentsMask[] masks;
public virtual void PrepareFilter()
{
}
public Color GetFiltered( Color rgb )
{
if ( ! enabled )
{
return rgb ;
}
var filtered = _FilterColor( rgb );
var mask = amount;
if ( masks != null && masks.Length > 0 )
{
var maskStack = 0f;
for ( int i = 0; i < masks.Length; i++ )
{
var maskIndex = ( masks.Length - 1 ) - i ;
var maskValue = masks[ maskIndex ].GetMask( rgb );
var maskBefore = i == 0 ? null : masks[ maskIndex + 1 ];
maskStack = AdjustmentsMask.Blend( maskBefore, maskStack, masks[ maskIndex ], maskValue );
}
mask *= maskStack;
}
return rgb.Lerp( filtered, mask );
}
abstract protected Color _FilterColor( Color rgb );
}

View File

@ -0,0 +1 @@
uid://3orxcbnth5i6

View File

@ -0,0 +1,63 @@
using Rokojori;
using Godot;
namespace Rokojori;
[GlobalClass,Tool]
public abstract partial class AdjustmentsMask: Resource
{
public enum Mode
{
Add,
Subtract,
Multiply,
Divide,
Max,
Min
}
[Export]
public Mode mode = Mode.Multiply;
public abstract float GetMask( Color rgb );
public static float Blend( AdjustmentsMask bottom, float bottomMask, AdjustmentsMask top, float topMask )
{
if ( bottom == null )
{
return bottomMask;
}
if ( Mode.Add == top.mode )
{
return bottomMask + topMask;
}
if ( Mode.Subtract == top.mode )
{
return bottomMask - topMask;
}
if ( Mode.Multiply == top.mode )
{
return bottomMask * topMask;
}
if ( Mode.Divide == top.mode )
{
return bottomMask / topMask;
}
if ( Mode.Max == top.mode )
{
return Mathf.Max( bottomMask, topMask );
}
if ( Mode.Min == top.mode )
{
return Mathf.Min( bottomMask, topMask );
}
return 0;
}
}

View File

@ -0,0 +1 @@
uid://b7m05qa3ygvy2

View File

@ -0,0 +1,81 @@
using Rokojori;
using Godot;
namespace Rokojori;
[GlobalClass,Tool]
public partial class AdjustmentsStack: Resource
{
[Export]
public AdjustmentsFilter[] filters = [];
[Export( PropertyHint.Range, "0,1" )]
public float amount = 1;
[Export]
public bool enabled = true;
[Export]
public int adjustmentSize = 33;
[Export]
public Texture3D adjustmentsTexture;
public void GenerateAdjustmentsTexture()
{
filters.ForEach( f => f.PrepareFilter() );
var luts = new Godot.Collections.Array<Image>();
for ( int i = 0; i < adjustmentSize; i++ )
{
luts.Add( Image.CreateEmpty( adjustmentSize, adjustmentSize, false, Image.Format.Rgb8 ) );
}
for ( int bi = 0; bi < adjustmentSize; bi++ )
{
var image = luts[ bi ];
var b = bi / ( adjustmentSize - 1f );
for ( int ri = 0; ri < adjustmentSize; ri++ )
{
var r = ri / ( adjustmentSize - 1f );
for ( int gi = 0; gi < adjustmentSize; gi++ )
{
var g = gi / ( adjustmentSize - 1f );
var filtered = GetFilteredColor( new Color( r, g, b ) );
image.SetPixel( ri, gi, filtered );
}
}
}
var texture = new ImageTexture3D();
if ( adjustmentsTexture is ImageTexture3D existing && existing.GetWidth() == adjustmentSize )
{
existing.Update( luts );
}
else
{
texture.Create( Image.Format.Rgb8, adjustmentSize, adjustmentSize, adjustmentSize, false, luts );
adjustmentsTexture = texture;
}
}
public Color GetFilteredColor( Color rgb )
{
if ( ! enabled )
{
return rgb;
}
var originalRGB = rgb;
filters.ForEach( f => { rgb = f.GetFiltered( rgb ); } );
return originalRGB.Lerp( rgb, amount );
}
}

View File

@ -0,0 +1 @@
uid://bdubaufaxcujo

View File

@ -0,0 +1,67 @@
using Rokojori;
using Godot;
namespace Rokojori;
[GlobalClass,Tool]
public partial class AdjustmentsCurvesFilter: AdjustmentsFilter
{
[Export]
public ColorChannelType driverChannel = ColorChannelType.Lightness;
[Export]
public ColorChannelType targetChannel = ColorChannelType.Lightness;
[Export]
public Curve curve;
public enum MappingMode
{
Auto,
Map,
Add
}
[Export]
public MappingMode mappingMode = MappingMode.Auto;
protected override Color _FilterColor( Color rgb )
{
var driverValue = ColorX.GetNormalizedChannel( rgb, driverChannel );
var targetValue = curve.Sample( driverValue );
var mapping = ResolveMappingMode();
if ( MappingMode.Map == mapping )
{
return ColorX.SetNormalizedChannel( rgb, targetChannel, targetValue );
}
else if ( MappingMode.Add == mapping )
{
var sourceValue = ColorX.GetNormalizedChannel( rgb, targetChannel );
if ( MappingMode.Auto == mappingMode )
{
var min = curve.MinValue;
var max = curve.MaxValue;
targetValue = MathX.MapClamped( targetValue, min, max, -1, 1 );
}
return ColorX.SetNormalizedChannel( rgb, targetChannel, targetValue + sourceValue );
}
return rgb;
}
MappingMode ResolveMappingMode()
{
if ( MappingMode.Auto == mappingMode )
{
return driverChannel == targetChannel ? MappingMode.Map : MappingMode.Add;
}
return mappingMode;
}
}

View File

@ -0,0 +1 @@
uid://bhegakbwte7hf

View File

@ -0,0 +1,71 @@
using Rokojori;
using Godot;
namespace Rokojori;
[GlobalClass,Tool]
public partial class AdjustmentsHueShifter: AdjustmentsFilter
{
[Export]
public AdjustmentsHueShifterBand band0 = new AdjustmentsHueShifterBand();
[Export]
public AdjustmentsHueShifterBand band1 = new AdjustmentsHueShifterBand();
[Export]
public AdjustmentsHueShifterBand band2 = new AdjustmentsHueShifterBand();
AdjustmentsHueShifterBand combined = new AdjustmentsHueShifterBand();
[Export]
public bool debug = false;
[Export]
public int numZero = 0;
public override void PrepareFilter()
{
numZero = 0;
}
protected override Color _FilterColor( Color rgb )
{
var mask0 = band0 == null ? 0f : band0.GetMask( rgb );
var mask1 = band1 == null ? 0f : band1.GetMask( rgb );
var mask2 = band2 == null ? 0f : band2.GetMask( rgb );
var sum = mask0 + mask1 + mask2;
if ( sum <= 0 )
{
if ( debug )
{
numZero ++;
// RJLog.Log( "Sum to small of masks", mask0, mask1, mask2 );
}
return rgb;
}
combined.ResetValues();
combined.AddChanges( band0, mask0 );
combined.AddChanges( band1, mask1 );
combined.AddChanges( band2, mask2 );
// RJLog.Log( "mask", mask0, mask1, mask2,
// "hue", combined.hueOffset,
// "temp", combined.tempratureOffset,
// "sat", combined.saturationOffset,
// "light", combined.lightnessOffset
// );
return combined.ProcessColor( rgb );
}
}

View File

@ -0,0 +1 @@
uid://b06d5aqfu7aoq

View File

@ -0,0 +1,88 @@
using Rokojori;
using Godot;
namespace Rokojori;
[GlobalClass,Tool]
public partial class AdjustmentsHueShifterBand: Resource
{
[Export( PropertyHint.Range, "-60,420")]
public float sourceHue = 0;
[Export( PropertyHint.Range, "0,360")]
public float sourceWidth = 30;
[Export( PropertyHint.Range, "0,1")]
public float amount = 1f;
[Export( PropertyHint.Range, "-180,180")]
public float hueOffset = 0;
[Export( PropertyHint.Range, "-180,180")]
public float tempratureOffset = 0;
[Export( PropertyHint.Range, "-100,100")]
public float saturationOffset = 0;
[Export( PropertyHint.Range, "-100,100")]
public float lightnessOffset = 0;
public void ResetValues()
{
hueOffset = 0;
tempratureOffset = 0;
saturationOffset = 0;
lightnessOffset = 0;
}
[Export]
public float fixedMask = 0;
public float GetMask( Color color )
{
if ( fixedMask > 0 )
{
return fixedMask * amount;
}
var hue = HSLColor.FromRGBA( color ).h;
var difference = Mathf.Abs( MathX.AngleDifferenceDegrees( sourceHue, hue ) );
return MathX.Clamp01( 1f - Mathf.Min( 1f, difference / sourceWidth ) ) * amount;
}
public void AddChanges( AdjustmentsHueShifterBand band, float mask )
{
if ( band == null || mask == 0 )
{
return;
}
hueOffset += band.hueOffset * mask;
tempratureOffset += band.tempratureOffset * mask;
saturationOffset += band.saturationOffset * mask;
lightnessOffset += band.lightnessOffset * mask;
}
public Color ProcessColor( Color color )
{
var hsl = HSLColor.FromRGBA( color );
hsl.h = MathX.Repeat( hsl.h + hueOffset, 360f );
hsl = hsl.ShiftTemperature( tempratureOffset, saturationOffset / 100f, lightnessOffset/ 100f );
return hsl;
}
}

View File

@ -0,0 +1 @@
uid://cmddo8mo0qulp

View File

@ -0,0 +1,30 @@
using Rokojori;
using Godot;
namespace Rokojori;
[GlobalClass,Tool]
public partial class AdjustmentstGradientTint: AdjustmentsFilter
{
[Export]
public GradientTexture1D gradient;
[Export]
public LumaMode lumaMode = LumaMode.Lightness;
[Export]
public ColorBlendModeType colorBlendMode = ColorBlendModeType.Alpha;
protected override Color _FilterColor( Color rgb )
{
var luma = Luma.Get( rgb, lumaMode );
var tint = gradient.Gradient.Sample( luma );
var amount = tint.A;
rgb.A = 1.0f;
return ColorBlendMode.Blend( colorBlendMode, rgb, tint );
}
}

View File

@ -0,0 +1 @@
uid://c5wqcbyxi6fgf

View File

@ -22,6 +22,7 @@ namespace Rokojori
[Export] [Export]
public float pitch; public float pitch;
[Export] [Export]
public ThirdPersonCameraSettings settings; public ThirdPersonCameraSettings settings;
@ -99,11 +100,15 @@ namespace Rokojori
var smoothedPitch = Smoothing.Apply( settings.pitchSmoothing, pitch, delta ); var smoothedPitch = Smoothing.Apply( settings.pitchSmoothing, pitch, delta );
var pitchState = MathX.NormalizeClamped( pitch, settings.minPitch, settings.maxPitch ); var pitchState = MathX.NormalizeClamped( pitch, settings.minPitch, settings.maxPitch );
var distance = settings.distanceForPitch.Sample( pitchState ) * settings.distanceScale; var distance = settings.distanceForPitch.Sample( pitchState ) * settings.distanceScale;
var smoothedDistance = Smoothing.Apply( settings.distanceSmoothing, distance, delta );
FOV = settings.fovOffset + ( settings.fovForPitch?.Sample( pitchState ) ?? 0f ); FOV = settings.fovOffset + ( settings.fovForPitch?.Sample( pitchState ) ?? 0f );
cameraPosition = targetPosition + Math3D.YawPitchRotation( smoothedYaw, smoothedPitch ) * Vector3.Forward * distance; cameraPosition = targetPosition + Math3D.YawPitchRotation( smoothedYaw, smoothedPitch ) * Vector3.Forward * smoothedDistance;
cameraForward = ( targetPosition - cameraPosition ).Normalized(); cameraForward = ( targetPosition - cameraPosition ).Normalized();
// LookAt( targetPosition, Vector3.Up, true ); // LookAt( targetPosition, Vector3.Up, true );

View File

@ -12,14 +12,16 @@ namespace Rokojori
[Export] [Export]
public Smoothing targetFollowSmoothing = new FrameSmoothing(); public Smoothing targetFollowSmoothing = new FrameSmoothing();
[Export] [Export]
public Smoothing yawSmoothing = new FrameSmoothing(); public Smoothing yawSmoothing = new FrameSmoothing();
[Export] [Export]
public Smoothing pitchSmoothing = new FrameSmoothing(); public Smoothing pitchSmoothing = new FrameSmoothing();
[Export]
public Smoothing distanceSmoothing = new FrameSmoothing();
[Export] [Export]
public ThirdPersonCameraData[] deviceData = []; public ThirdPersonCameraData[] deviceData = [];

View File

@ -8,7 +8,16 @@ namespace Rokojori
Alpha, Alpha,
Add, Add,
Multiply, Multiply,
Replace
Screen,
Overlay,
HardLight,
SoftLight,
Color,
Hue,
Saturation
} }
public class ColorBlendMode public class ColorBlendMode
@ -17,41 +26,60 @@ namespace Rokojori
{ {
switch ( type ) switch ( type )
{ {
case ColorBlendModeType.Alpha: return Alpha( bottom, top ); case ColorBlendModeType.Alpha: return ColorX.Blend( bottom, top );
case ColorBlendModeType.Multiply: return Multiply( bottom, top ); case ColorBlendModeType.Multiply: return ColorX.BlendMultiply( bottom, top );
case ColorBlendModeType.Add: return Add( bottom, top ); case ColorBlendModeType.Add: return ColorX.BlendAdd( bottom, top );
case ColorBlendModeType.Replace: return Replace( bottom, top );
case ColorBlendModeType.Screen: return ColorX.BlendScreen( bottom, top );
case ColorBlendModeType.Overlay: return ColorX.BlendScreen( bottom, top );
case ColorBlendModeType.HardLight: return ColorX.BlendHardLight( bottom, top );
case ColorBlendModeType.SoftLight: return ColorX.BlendSoftLight( bottom, top );
case ColorBlendModeType.Color: return ColorX.BlendColor( bottom, top );
case ColorBlendModeType.Hue: return ColorX.BlendHue( bottom, top );
case ColorBlendModeType.Saturation: return ColorX.BlendSaturation( bottom, top );
} }
return new Color( 1, 1, 1, 1 ); return new Color( 1, 1, 1, 1 );
} }
public static Color Alpha( Color bottom, Color top ) // public static Color Alpha( Color bottom, Color top )
{ // {
var a = top.A + bottom.A * ( 1f - top.A ); // var a = top.A + bottom.A * ( 1f - top.A );
var colorBottom = ColorX.RGB( bottom ); // var colorBottom = ColorX.RGB( bottom );
var colorTop = ColorX.RGB( top ); // var colorTop = ColorX.RGB( top );
var colorRGB = ( colorTop * top.A + colorBottom * bottom.A * ( 1f - top.A ) ) / a; // var colorRGB = ( colorTop * top.A + colorBottom * bottom.A * ( 1f - top.A ) ) / a;
return ColorX.Create( colorRGB, a ); // return ColorX.Create( colorRGB, a );
} // }
public static Color Multiply( Color bottom, Color top ) // Color Multiply( Color bottom, Color top )
{ // {
return bottom * top; // if ( bottom.A <= 0.0 )
} // {
// return top;
// }
public static Color Add( Color bottom, Color top ) // return Alpha( bottom, MultiplyFunction( bottom.rgb, top.rgb ), top.a ) );
{ // }
return bottom + top;
}
public static Color Replace( Color bottom, Color top ) // public static Color MultiplyFunction( Color bottom, Color top )
{ // {
return top; // return bottom * top;
} // }
// public static Color AddFunction( Color bottom, Color top )
// {
// return bottom + top;
// }
// public static Color ReplaceFunction( Color bottom, Color top )
// {
// return top;
// }
} }
} }

View File

@ -11,7 +11,7 @@ namespace Rokojori
Blue, Blue,
Hue, Hue,
Saturation, Saturation,
Luminance, Lightness,
Alpha Alpha
} }
@ -77,7 +77,7 @@ namespace Rokojori
return $"RGBtoHSL( {source} ).y"; return $"RGBtoHSL( {source} ).y";
} }
if ( ColorChannelType.Luminance == colorChannelType ) if ( ColorChannelType.Lightness == colorChannelType )
{ {
return $"RGBtoHSL( {source} ).u"; return $"RGBtoHSL( {source} ).u";
} }

View File

@ -40,6 +40,18 @@ namespace Rokojori
return new Vector3( rgba.X, rgba.Y, rgba.Z ); return new Vector3( rgba.X, rgba.Y, rgba.Z );
} }
public static float GetNormalizedChannel( this Color color, ColorChannelType colorChannelType )
{
var channel = color.GetChannel( colorChannelType );
if ( ColorChannelType.Hue == colorChannelType )
{
channel /= 360.0f;
}
return channel;
}
public static float GetChannel( this Color color, ColorChannelType colorChannelType ) public static float GetChannel( this Color color, ColorChannelType colorChannelType )
{ {
if ( ColorChannelType.Red == colorChannelType ) if ( ColorChannelType.Red == colorChannelType )
@ -75,7 +87,7 @@ namespace Rokojori
return hsl.s; return hsl.s;
} }
if ( ColorChannelType.Luminance == colorChannelType ) if ( ColorChannelType.Lightness == colorChannelType )
{ {
return hsl.l; return hsl.l;
} }
@ -84,6 +96,16 @@ namespace Rokojori
} }
public static Color SetNormalizedChannel( this Color color, ColorChannelType colorChannelType, float value )
{
if ( ColorChannelType.Hue == colorChannelType )
{
value *= 360.0f;
}
return color.SetChannel( colorChannelType, value );
}
public static Color SetChannel( this Color color, ColorChannelType colorChannelType, float value ) public static Color SetChannel( this Color color, ColorChannelType colorChannelType, float value )
{ {
if ( ColorChannelType.Red == colorChannelType ) if ( ColorChannelType.Red == colorChannelType )
@ -121,7 +143,7 @@ namespace Rokojori
color = hsl; color = hsl;
} }
if ( ColorChannelType.Luminance == colorChannelType ) if ( ColorChannelType.Lightness == colorChannelType )
{ {
hsl.l = value; hsl.l = value;
color = hsl; color = hsl;

53
Runtime/Colors/Luma.cs Normal file
View File

@ -0,0 +1,53 @@
using Godot;
using System.Collections.Generic;
using Rokojori.Extensions;
namespace Rokojori;
public enum LumaMode
{
Lightness,
Luminosity_709,
Luminosity_601,
Average,
Min,
Max
}
public class Luma
{
public static float Get( Color rgb, LumaMode mode )
{
if ( LumaMode.Lightness == mode )
{
return HSLColor.FromRGBA( rgb ).l;
}
if ( LumaMode.Luminosity_709 == mode )
{
return rgb.R * 0.2126f + rgb.G * 0.7152f + rgb.B * 0.0722f;
}
if ( LumaMode.Luminosity_601 == mode )
{
return rgb.R * 0.299f + rgb.G * 0.587f + rgb.B * 0.114f;
}
if ( LumaMode.Average == mode )
{
return ( rgb.R + rgb.G + rgb.B ) / 3f;
}
if ( LumaMode.Min == mode )
{
return MathX.Min( rgb.R, rgb.G, rgb.B );
}
if ( LumaMode.Max == mode )
{
return MathX.Max( rgb.R, rgb.G, rgb.B );
}
return 0;
}
}

View File

@ -0,0 +1 @@
uid://wxxtcur80snr

View File

@ -245,7 +245,10 @@ namespace Rokojori
} }
public static float AngleDifferenceDegrees( float degreesA, float degreesB )
{
return Mathf.AngleDifference( degreesA * DegreesToRadians, degreesB * DegreesToRadians ) * RadiansToDegrees;
}
public static float AngleDelta( float degreesA, float degreesB) public static float AngleDelta( float degreesA, float degreesB)
{ {