diff --git a/.gitignore b/.gitignore index 0203353..3f5c20f 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ project.lock.json project.fragment.lock.json artifacts/ **/Properties/launchSettings.json +dotnet-tools.json *_i.c *_p.c diff --git a/Examples/Audio/AmpEnvelope.cs b/Examples/Audio/AmpEnvelope.cs new file mode 100644 index 0000000..396b3e2 --- /dev/null +++ b/Examples/Audio/AmpEnvelope.cs @@ -0,0 +1,263 @@ +/******************************************************************************************* +* +* raylib [audio] example - amp envelope +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Arbinda Rizki Muhammad (@arbipink) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2026 Arbinda Rizki Muhammad (@arbipink) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Audio; + +public unsafe partial class AmpEnvelope : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int BUFFER_SIZE = 4096; + private const int SAMPLE_RATE = 44100; + + // Wave state + private enum ADSRState + { + Idle, + Attack, + Decay, + Sustain, + Release + } + + // Grouping all ADSR parameters and state into a struct + private struct Envelope + { + public float AttackTime; + public float DecayTime; + public float SustainLevel; + public float ReleaseTime; + public float CurrentValue; + public ADSRState State; + } + + public string Name => "Audio / Amp Envelope"; + + public string Title => "raylib [audio] example - amp envelope"; + + private float[] buffer; + private AudioStream stream; + private float audioTime; + private Envelope env; + + public void Init() + { + InitAudioDevice(); + + // Set the number of samples the stream will keep in memory at a time to BUFFER_SIZE + SetAudioStreamBufferSizeDefault(BUFFER_SIZE); + buffer = new float[BUFFER_SIZE]; + + // Init raw audio stream (sample rate: 44100, sample size: 32bit-float, channels: 1-mono) + stream = LoadAudioStream(SAMPLE_RATE, 32, 1); + + // Init Phase + audioTime = 0.0f; + + // Initialize the struct + env = new Envelope + { + AttackTime = 1.0f, + DecayTime = 1.0f, + SustainLevel = 0.5f, + ReleaseTime = 1.0f, + CurrentValue = 0.0f, + State = ADSRState.Idle + }; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.Space)) env.State = ADSRState.Attack; + + if (IsKeyReleased(KeyboardKey.Space) && (env.State != ADSRState.Idle)) env.State = ADSRState.Release; + + if (IsAudioStreamProcessed(stream)) + { + if ((env.State != ADSRState.Idle) || (env.CurrentValue > 0.0f)) + { + for (int i = 0; i < BUFFER_SIZE; i++) + { + UpdateEnvelope(ref env); + FillAudioBuffer(i, buffer, env.CurrentValue, ref audioTime); + } + } + else + { + // Clear buffer if silent to avoid looping noise + for (int i = 0; i < BUFFER_SIZE; i++) buffer[i] = 0; + audioTime = 0.0f; + } + + fixed (float* bufferPtr = buffer) + { + UpdateAudioStream(stream, bufferPtr, BUFFER_SIZE); + } + } + + if (!IsAudioStreamPlaying(stream)) PlayAudioStream(stream); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + // NOTE: raygui is not bound in raylib-cs, so the sliders below are left as reference. + // The envelope keeps its default parameters (Attack/Decay/Release = 1.0s, Sustain = 0.5). + //GuiSliderBar(new Rectangle( 100, 60, 400, 30 ), "Attack (s)", TextFormat("%2.2fs", env.AttackTime), ref env.AttackTime, 0.1f, 3.0f); + //GuiSliderBar(new Rectangle( 100, 100, 400, 30 ), "Decay (s)", TextFormat("%2.2fs", env.DecayTime), ref env.DecayTime, 0.1f, 3.0f); + //GuiSliderBar(new Rectangle( 100, 140, 400, 30 ), "Sustain", TextFormat("%2.2f", env.SustainLevel), ref env.SustainLevel, 0.0f, 1.0f); + //GuiSliderBar(new Rectangle( 100, 180, 400, 30 ), "Release (s)", TextFormat("%2.2fs", env.ReleaseTime), ref env.ReleaseTime, 0.1f, 3.0f); + + DrawADSRGraph(ref env, new Rectangle(100, 250, 400, 100)); + + DrawCircleV(new Vector2(520, 350 - (env.CurrentValue * 100)), 5, Color.Maroon); + DrawText($"Current Gain: {env.CurrentValue:F2}", 535, (int)(345 - (env.CurrentValue * 100)), 10, Color.Maroon); + + DrawText("Press SPACE to PLAY the sound!", 200, 400, 20, Color.LightGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadAudioStream(stream); + CloseAudioDevice(); + } + + //------------------------------------------------------------------------------------ + // Module Functions Definition + //------------------------------------------------------------------------------------ + private static void FillAudioBuffer(int i, float[] buffer, float envelopeValue, ref float audioTime) + { + int frequency = 440; + buffer[i] = envelopeValue * MathF.Sin(2.0f * MathF.PI * frequency * audioTime); + audioTime += (1.0f / SAMPLE_RATE); + } + + private static void UpdateEnvelope(ref Envelope env) + { + // Calculate the time delta for ONE sample (1/44100) + float sampleTime = 1.0f / SAMPLE_RATE; + + switch (env.State) + { + case ADSRState.Attack: + { + env.CurrentValue += (1.0f / env.AttackTime) * sampleTime; + if (env.CurrentValue >= 1.0f) + { + env.CurrentValue = 1.0f; + env.State = ADSRState.Decay; + } + } + break; + case ADSRState.Decay: + { + env.CurrentValue -= ((1.0f - env.SustainLevel) / env.DecayTime) * sampleTime; + if (env.CurrentValue <= env.SustainLevel) + { + env.CurrentValue = env.SustainLevel; + env.State = ADSRState.Sustain; + } + } + break; + case ADSRState.Sustain: + { + env.CurrentValue = env.SustainLevel; + } + break; + case ADSRState.Release: + { + env.CurrentValue -= (env.SustainLevel / env.ReleaseTime) * sampleTime; + if (env.CurrentValue <= 0.001f) // Use a small threshold to avoid infinite tail + { + env.CurrentValue = 0.0f; + env.State = ADSRState.Idle; + } + } + break; + default: break; + } + } + + private static void DrawADSRGraph(ref Envelope env, Rectangle bounds) + { + DrawRectangleRec(bounds, Fade(Color.LightGray, 0.3f)); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + + // Fixed visual width for sustain stage since it's an amplitude not a time value + float sustainWidth = 1.0f; + + // Total time to visualize (sum of A, D, R + a padding for Sustain) + float totalTime = env.AttackTime + env.DecayTime + sustainWidth + env.ReleaseTime; + + float scaleX = bounds.Width / totalTime; + float scaleY = bounds.Height; + + Vector2 start = new Vector2(bounds.X, bounds.Y + bounds.Height); + Vector2 peak = new Vector2(start.X + (env.AttackTime * scaleX), bounds.Y); + Vector2 sustain = new Vector2(peak.X + (env.DecayTime * scaleX), bounds.Y + (1.0f - env.SustainLevel) * scaleY); + Vector2 rel = new Vector2(sustain.X + (sustainWidth * scaleX), sustain.Y); + Vector2 end = new Vector2(rel.X + (env.ReleaseTime * scaleX), bounds.Y + bounds.Height); + + DrawLineV(start, peak, Color.SkyBlue); + DrawLineV(peak, sustain, Color.Blue); + DrawLineV(sustain, rel, Color.DarkBlue); + DrawLineV(rel, end, Color.Orange); + + DrawText("ADSR Visualizer", (int)bounds.X, (int)bounds.Y - 20, 10, Color.DarkGray); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [audio] example - amp envelope"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new AmpEnvelope(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Audio/MixedProcessor.cs b/Examples/Audio/MixedProcessor.cs new file mode 100644 index 0000000..fa85528 --- /dev/null +++ b/Examples/Audio/MixedProcessor.cs @@ -0,0 +1,159 @@ +/******************************************************************************************* +* +* raylib [audio] example - mixed processor +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 4.2, last time updated with raylib 4.2 +* +* Example contributed by hkc (@hatkidchan) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2023-2025 hkc (@hatkidchan) +* +********************************************************************************************/ + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static Raylib_cs.Raylib; + +namespace Examples.Audio; + +[ExcludeFromBrowser("AttachAudioMixedProcessor callback is unreliable on the wasm audio backend")] +public unsafe partial class MixedProcessor : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Audio / Mixed Processor"; + + public string Title => "raylib [audio] example - mixed processor"; + + private static float exponent = 1.0f; // Audio exponentiation value + private static readonly float[] averageVolume = new float[400]; // Average volume history + + private Music music; + private Sound sound; + + //------------------------------------------------------------------------------------ + // Audio processing function + //------------------------------------------------------------------------------------ + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void ProcessAudio(void* buffer, uint frames) + { + float* samples = (float*)buffer; // Samples internally stored as s + float average = 0.0f; // Temporary average volume + + for (uint frame = 0; frame < frames; frame++) + { + float* left = &samples[frame * 2 + 0]; + float* right = &samples[frame * 2 + 1]; + + *left = MathF.Pow(MathF.Abs(*left), exponent) * ((*left < 0.0f) ? -1.0f : 1.0f); + *right = MathF.Pow(MathF.Abs(*right), exponent) * ((*right < 0.0f) ? -1.0f : 1.0f); + + average += MathF.Abs(*left) / frames; // accumulating average volume + average += MathF.Abs(*right) / frames; + } + + // Moving history to the left + for (int i = 0; i < 399; i++) averageVolume[i] = averageVolume[i + 1]; + + averageVolume[399] = average; // Adding last average value + } + + public void Init() + { + InitAudioDevice(); // Initialize audio device + + exponent = 1.0f; + Array.Clear(averageVolume, 0, averageVolume.Length); + + AttachAudioMixedProcessor(&ProcessAudio); + + music = LoadMusicStream("resources/audio/country.mp3"); + sound = LoadSound("resources/audio/coin.wav"); + + PlayMusicStream(music); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateMusicStream(music); // Update music buffer with new stream data + + // Modify processing variables + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.Left)) exponent -= 0.05f; + if (IsKeyPressed(KeyboardKey.Right)) exponent += 0.05f; + + if (exponent <= 0.5f) exponent = 0.5f; + if (exponent >= 3.0f) exponent = 3.0f; + + if (IsKeyPressed(KeyboardKey.Space)) PlaySound(sound); + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + DrawText("MUSIC SHOULD BE PLAYING!", 255, 150, 20, Color.LightGray); + + DrawText($"EXPONENT = {exponent:F2}", 215, 180, 20, Color.LightGray); + + DrawRectangle(199, 199, 402, 34, Color.LightGray); + for (int i = 0; i < 400; i++) + { + DrawLine(201 + i, 232 - (int)(averageVolume[i] * 32), 201 + i, 232, Color.Maroon); + } + DrawRectangleLines(199, 199, 402, 34, Color.Gray); + + DrawText("PRESS SPACE TO PLAY OTHER SOUND", 200, 250, 20, Color.LightGray); + DrawText("USE LEFT AND RIGHT ARROWS TO ALTER DISTORTION", 140, 280, 20, Color.LightGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadMusicStream(music); // Unload music stream buffers from RAM + + DetachAudioMixedProcessor(&ProcessAudio); // Disconnect audio processor + + CloseAudioDevice(); // Close audio device (music streaming is automatically stopped) + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [audio] example - mixed processor"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new MixedProcessor(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Audio/RawStream.cs b/Examples/Audio/RawStream.cs new file mode 100644 index 0000000..08f44b0 --- /dev/null +++ b/Examples/Audio/RawStream.cs @@ -0,0 +1,182 @@ +/******************************************************************************************* +* +* raylib [audio] example - raw stream +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 1.6, last time updated with raylib 6.0 +* +* Example created by Ramon Santamaria (@raysan5) and reviewed by James Hofmann (@triplefox) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2015-2026 Ramon Santamaria (@raysan5) and James Hofmann (@triplefox) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Audio; + +public unsafe partial class RawStream : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int BUFFER_SIZE = 4096; + private const int SAMPLE_RATE = 44100; + + public string Name => "Audio / Raw Stream"; + + public string Title => "raylib [audio] example - raw stream"; + + public int TargetFps => 30; + + private float[] buffer; + private AudioStream stream; + private float pan; + private int sineFrequency; + private int newSineFrequency; + private int sineIndex; + private double sineStartTime; + + public void Init() + { + InitAudioDevice(); + + // Set the number of samples the stream will keep in memory at a time to BUFFER_SIZE + SetAudioStreamBufferSizeDefault(BUFFER_SIZE); + buffer = new float[BUFFER_SIZE]; + + // Init raw audio stream (sample rate: 44100, sample size: 32bit-float, channels: 1-mono) + stream = LoadAudioStream(SAMPLE_RATE, 32, 1); + pan = 0.0f; + SetAudioStreamPan(stream, pan); + PlayAudioStream(stream); + + sineFrequency = 440; + newSineFrequency = 440; + sineIndex = 0; + sineStartTime = 0.0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + + if (IsKeyDown(KeyboardKey.Up)) + { + newSineFrequency += 10; + if (newSineFrequency > 12500) newSineFrequency = 12500; + } + + if (IsKeyDown(KeyboardKey.Down)) + { + newSineFrequency -= 10; + if (newSineFrequency < 20) newSineFrequency = 20; + } + + if (IsKeyDown(KeyboardKey.Left)) + { + pan -= 0.01f; + if (pan < -1.0f) pan = -1.0f; + SetAudioStreamPan(stream, pan); + } + + if (IsKeyDown(KeyboardKey.Right)) + { + pan += 0.01f; + if (pan > 1.0f) pan = 1.0f; + SetAudioStreamPan(stream, pan); + } + + if (IsAudioStreamProcessed(stream)) + { + for (int i = 0; i < BUFFER_SIZE; i++) + { + int wl = SAMPLE_RATE / sineFrequency; + buffer[i] = MathF.Sin(2 * MathF.PI * sineIndex / wl); + sineIndex++; + + if (sineIndex >= wl) + { + sineFrequency = newSineFrequency; + sineIndex = 0; + sineStartTime = GetTime(); + } + } + + fixed (float* bufferPtr = buffer) + { + UpdateAudioStream(stream, bufferPtr, BUFFER_SIZE); + } + } + + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText($"sine frequency: {sineFrequency}", screenWidth - 220, 10, 20, Color.Red); + DrawText($"pan: {pan:F2}", screenWidth - 220, 30, 20, Color.Red); + DrawText("Up/down to change frequency", 10, 10, 20, Color.DarkGray); + DrawText("Left/right to pan", 10, 30, 20, Color.DarkGray); + + int windowStart = (int)((GetTime() - sineStartTime) * SAMPLE_RATE); + int windowSize = (int)(0.1f * SAMPLE_RATE); + int wavelength = SAMPLE_RATE / sineFrequency; + + // Draw a sine wave with the same frequency as the one being sent to the audio stream + for (int i = 0; i < screenWidth; i++) + { + int t0 = windowStart + i * windowSize / screenWidth; + int t1 = windowStart + (i + 1) * windowSize / screenWidth; + Vector2 startPos = new Vector2(i, 250 + 50 * MathF.Sin(2 * MathF.PI * t0 / wavelength)); + Vector2 endPos = new Vector2(i + 1, 250 + 50 * MathF.Sin(2 * MathF.PI * t1 / wavelength)); + DrawLineV(startPos, endPos, Color.Red); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadAudioStream(stream); // Close raw audio stream and delete buffers from RAM + CloseAudioDevice(); // Close audio device (music streaming is automatically stopped) + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [audio] example - raw stream"); + + SetTargetFPS(30); + //-------------------------------------------------------------------------------------- + + var game = new RawStream(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Audio/SoundMulti.cs b/Examples/Audio/SoundMulti.cs new file mode 100644 index 0000000..40a88bd --- /dev/null +++ b/Examples/Audio/SoundMulti.cs @@ -0,0 +1,114 @@ +/******************************************************************************************* +* +* raylib [audio] example - sound multi +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.0, last time updated with raylib 5.0 +* +* Example contributed by Jeffery Myers (@JeffM2501) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2023-2025 Jeffery Myers (@JeffM2501) +* +********************************************************************************************/ + +using static Raylib_cs.Raylib; + +namespace Examples.Audio; + +public partial class SoundMulti : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MAX_SOUNDS = 10; + + public string Name => "Audio / Sound Multi"; + + public string Title => "raylib [audio] example - sound multi"; + + private Sound[] soundArray = new Sound[MAX_SOUNDS]; + private int currentSound; + + public void Init() + { + InitAudioDevice(); // Initialize audio device + + // Load audio file into the first slot as the 'source' sound, + // this sound owns the sample data + soundArray[0] = LoadSound("resources/audio/sound.wav"); + + // Load an alias of the sound into slots 1-9. These do not own the sound data, but can be played + for (int i = 1; i < MAX_SOUNDS; i++) soundArray[i] = LoadSoundAlias(soundArray[0]); + + currentSound = 0; // Set the sound list to the start + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.Space)) + { + PlaySound(soundArray[currentSound]); // Play the next open sound slot + currentSound++; // Increment the sound slot + + // If the sound slot is out of bounds, go back to 0 + if (currentSound >= MAX_SOUNDS) currentSound = 0; + + // NOTE: Another approach would be to look at the list for the first sound + // that is not playing and use that slot + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + DrawText("Press SPACE to PLAY a WAV sound!", 200, 180, 20, Color.LightGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + for (int i = 1; i < MAX_SOUNDS; i++) UnloadSoundAlias(soundArray[i]); // Unload sound aliases + UnloadSound(soundArray[0]); // Unload source sound data + + CloseAudioDevice(); // Close audio device + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [audio] example - sound multi"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new SoundMulti(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Audio/SoundPositioning.cs b/Examples/Audio/SoundPositioning.cs new file mode 100644 index 0000000..b5a9a50 --- /dev/null +++ b/Examples/Audio/SoundPositioning.cs @@ -0,0 +1,154 @@ +/******************************************************************************************* +* +* raylib [audio] example - sound positioning +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.5 +* +* Example contributed by Le Juez Victor (@Bigfoot71) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Le Juez Victor (@Bigfoot71) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Audio; + +public partial class SoundPositioning : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Audio / Sound Positioning"; + + public string Title => "raylib [audio] example - sound positioning"; + + public bool CursorDisabled => true; + + private Sound sound; + private Camera3D camera; + + public void Init() + { + InitAudioDevice(); + + sound = LoadSound("resources/audio/coin.wav"); + + camera = new Camera3D + { + Position = new Vector3(0, 5, 5), + Target = new Vector3(0, 0, 0), + Up = new Vector3(0, 1, 0), + FovY = 60, + Projection = CameraProjection.Perspective + }; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Free); + + float th = (float)GetTime(); + + Vector3 spherePos = new Vector3( + 5.0f * MathF.Cos(th), + 0.0f, + 5.0f * MathF.Sin(th) + ); + + SetSoundPosition(camera, sound, spherePos, 1.0f); + + if (!IsSoundPlaying(sound)) PlaySound(sound); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + DrawGrid(10, 2); + DrawSphere(spherePos, 0.5f, Color.Red); + EndMode3D(); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadSound(sound); + CloseAudioDevice(); // Close audio device + } + + //------------------------------------------------------------------------------------ + // Module Functions Definition + //------------------------------------------------------------------------------------ + // Set sound 3d position + private static void SetSoundPosition(Camera3D listener, Sound sound, Vector3 position, float maxDist) + { + // Calculate direction vector and distance between listener and sound source + Vector3 direction = Vector3.Subtract(position, listener.Position); + float distance = direction.Length(); + + // Apply logarithmic distance attenuation and clamp between 0-1 + float attenuation = 1.0f / (1.0f + (distance / maxDist)); + attenuation = Math.Clamp(attenuation, 0.0f, 1.0f); + + // Calculate normalized vectors for spatial positioning + Vector3 normalizedDirection = Vector3.Normalize(direction); + Vector3 forward = Vector3.Normalize(Vector3.Subtract(listener.Target, listener.Position)); + Vector3 right = Vector3.Normalize(Vector3.Cross(listener.Up, forward)); + + // Reduce volume for sounds behind the listener + float dotProduct = Vector3.Dot(forward, normalizedDirection); + if (dotProduct < 0.0f) attenuation *= (1.0f + dotProduct * 0.5f); + + // Set stereo panning based on sound position relative to listener + float pan = 0.5f + 0.5f * Vector3.Dot(normalizedDirection, right); + + // Apply final sound properties + SetSoundVolume(sound, attenuation); + SetSoundPan(sound, pan); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [audio] example - sound positioning"); + + DisableCursor(); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new SoundPositioning(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Audio/StreamCallback.cs b/Examples/Audio/StreamCallback.cs new file mode 100644 index 0000000..488e19e --- /dev/null +++ b/Examples/Audio/StreamCallback.cs @@ -0,0 +1,306 @@ +/******************************************************************************************* +* +* raylib [audio] example - stream callback +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example created by Dan Hoang (@dan-hoang) and reviewed by Ramon Santamaria (@raysan5) +* +* NOTE: Example sends a wave to the audio device, +* user gets the choice of four waves: sine, square, triangle, and sawtooth +* A stream is set up to play to the audio device; stream is hooked to a callback that +* generates a wave, that is determined by user choice +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2026 Dan Hoang (@dan-hoang) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static Raylib_cs.Raylib; + +namespace Examples.Audio; + +[ExcludeFromBrowser("SetAudioStreamCallback is unreliable on the wasm audio backend")] +public unsafe partial class StreamCallback : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int BUFFER_SIZE = 4096; + private const int SAMPLE_RATE = 44100; + + // Wave type + private enum WaveType + { + Sine, + Square, + Triangle, + Sawtooth + } + + public string Name => "Audio / Stream Callback"; + + public string Title => "raylib [audio] example - stream callback"; + + public int TargetFps => 30; + + private static int waveFrequency = 440; + private static int newWaveFrequency = 440; + private static int waveIndex = 0; + + // Buffer to keep the last second of uploaded audio, + // part of which will be drawn on the screen + private static readonly float[] buffer = new float[SAMPLE_RATE]; + + private static readonly string[] waveTypesAsString = { "sine", "square", "triangle", "sawtooth" }; + + private AudioStream stream; + private WaveType waveType; + + public void Init() + { + InitAudioDevice(); + + waveFrequency = 440; + newWaveFrequency = 440; + waveIndex = 0; + Array.Clear(buffer, 0, buffer.Length); + + // Set the number of samples the stream will keep in memory at a time to BUFFER_SIZE + SetAudioStreamBufferSizeDefault(BUFFER_SIZE); + + // Init raw audio stream (sample rate: 44100, sample size: 32bit-float, channels: 1-mono) + stream = LoadAudioStream(SAMPLE_RATE, 32, 1); + PlayAudioStream(stream); + + // Configure it so that the callback for waveType is called whenever stream is out of samples + waveType = WaveType.Sine; + SetWaveCallback(); + } + + // Attach the callback matching the current waveType to the stream + private void SetWaveCallback() + { + switch (waveType) + { + case WaveType.Sine: SetAudioStreamCallback(stream, &SineCallback); break; + case WaveType.Square: SetAudioStreamCallback(stream, &SquareCallback); break; + case WaveType.Triangle: SetAudioStreamCallback(stream, &TriangleCallback); break; + case WaveType.Sawtooth: SetAudioStreamCallback(stream, &SawtoothCallback); break; + } + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyDown(KeyboardKey.Up)) + { + newWaveFrequency += 10; + if (newWaveFrequency > 12500) newWaveFrequency = 12500; + } + + if (IsKeyDown(KeyboardKey.Down)) + { + newWaveFrequency -= 10; + if (newWaveFrequency < 20) newWaveFrequency = 20; + } + + if (IsKeyPressed(KeyboardKey.Left)) + { + if (waveType == WaveType.Sine) waveType = WaveType.Sawtooth; + else if (waveType == WaveType.Square) waveType = WaveType.Sine; + else if (waveType == WaveType.Triangle) waveType = WaveType.Square; + else waveType = WaveType.Triangle; + + SetWaveCallback(); + } + + if (IsKeyPressed(KeyboardKey.Right)) + { + if (waveType == WaveType.Sine) waveType = WaveType.Square; + else if (waveType == WaveType.Square) waveType = WaveType.Triangle; + else if (waveType == WaveType.Triangle) waveType = WaveType.Sawtooth; + else waveType = WaveType.Sine; + + SetWaveCallback(); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + DrawText($"frequency: {newWaveFrequency}", screenWidth - 220, 10, 20, Color.Red); + DrawText($"wave type: {waveTypesAsString[(int)waveType]}", screenWidth - 220, 30, 20, Color.Red); + DrawText("Up/down to change frequency", 10, 10, 20, Color.DarkGray); + DrawText("Left/right to change wave type", 10, 30, 20, Color.DarkGray); + + // Draw the last 10 ms of uploaded audio + for (int i = 0; i < screenWidth; i++) + { + Vector2 startPos = new Vector2(i, 250 - 50 * buffer[WaveSampleIndex(i)]); + Vector2 endPos = new Vector2(i + 1, 250 - 50 * buffer[WaveSampleIndex(i + 1)]); + DrawLineV(startPos, endPos, Color.Red); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadAudioStream(stream); // Close raw audio stream and delete buffers from RAM + CloseAudioDevice(); // Close audio device (music streaming is automatically stopped) + } + + // Maps a screen column to a sample index in the last 10 ms of the buffer. The final column + // maps one sample past the end of the buffer; upstream C reads out of bounds there, so we + // clamp to the last valid sample (visually identical, but safe under C# bounds checking). + private static int WaveSampleIndex(int column) + { + int index = SAMPLE_RATE - SAMPLE_RATE / 100 + column * SAMPLE_RATE / 100 / screenWidth; + return index < SAMPLE_RATE ? index : SAMPLE_RATE - 1; + } + + //------------------------------------------------------------------------------------ + // Module Functions Definition + //------------------------------------------------------------------------------------ + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void SineCallback(void* framesOut, uint frameCount) + { + int fc = (int)frameCount; + int wavelength = SAMPLE_RATE / waveFrequency; + float* frames = (float*)framesOut; + + // Synthesize the sine wave + for (int i = 0; i < fc; i++) + { + frames[i] = MathF.Sin(2 * MathF.PI * waveIndex / wavelength); + + waveIndex++; + + if (waveIndex >= wavelength) + { + waveFrequency = newWaveFrequency; + waveIndex = 0; + } + } + + // Save the synthesized samples for later drawing + for (int i = 0; i < SAMPLE_RATE - fc; i++) buffer[i] = buffer[i + fc]; + for (int i = 0; i < fc; i++) buffer[SAMPLE_RATE - fc + i] = frames[i]; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void SquareCallback(void* framesOut, uint frameCount) + { + int fc = (int)frameCount; + int wavelength = SAMPLE_RATE / waveFrequency; + float* frames = (float*)framesOut; + + // Synthesize the square wave + for (int i = 0; i < fc; i++) + { + frames[i] = (waveIndex < wavelength / 2) ? 1 : -1; + waveIndex++; + + if (waveIndex >= wavelength) + { + waveFrequency = newWaveFrequency; + waveIndex = 0; + } + } + + // Save the synthesized samples for later drawing + for (int i = 0; i < SAMPLE_RATE - fc; i++) buffer[i] = buffer[i + fc]; + for (int i = 0; i < fc; i++) buffer[SAMPLE_RATE - fc + i] = frames[i]; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void TriangleCallback(void* framesOut, uint frameCount) + { + int fc = (int)frameCount; + int wavelength = SAMPLE_RATE / waveFrequency; + float* frames = (float*)framesOut; + + // Synthesize the triangle wave + for (int i = 0; i < fc; i++) + { + frames[i] = (waveIndex < wavelength / 2) ? (-1 + 2.0f * waveIndex / (wavelength / 2)) : (1 - 2.0f * (waveIndex - wavelength / 2) / (wavelength / 2)); + waveIndex++; + + if (waveIndex >= wavelength) + { + waveFrequency = newWaveFrequency; + waveIndex = 0; + } + } + + // Save the synthesized samples for later drawing + for (int i = 0; i < SAMPLE_RATE - fc; i++) buffer[i] = buffer[i + fc]; + for (int i = 0; i < fc; i++) buffer[SAMPLE_RATE - fc + i] = frames[i]; + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void SawtoothCallback(void* framesOut, uint frameCount) + { + int fc = (int)frameCount; + int wavelength = SAMPLE_RATE / waveFrequency; + float* frames = (float*)framesOut; + + // Synthesize the sawtooth wave + for (int i = 0; i < fc; i++) + { + frames[i] = -1 + 2.0f * waveIndex / wavelength; + waveIndex++; + + if (waveIndex >= wavelength) + { + waveFrequency = newWaveFrequency; + waveIndex = 0; + } + } + + // Save the synthesized samples for later drawing + for (int i = 0; i < SAMPLE_RATE - fc; i++) buffer[i] = buffer[i + fc]; + for (int i = 0; i < fc; i++) buffer[SAMPLE_RATE - fc + i] = frames[i]; + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [audio] example - stream callback"); + + SetTargetFPS(30); + //-------------------------------------------------------------------------------------- + + var game = new StreamCallback(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Audio/StreamEffects.cs b/Examples/Audio/StreamEffects.cs new file mode 100644 index 0000000..9a9c960 --- /dev/null +++ b/Examples/Audio/StreamEffects.cs @@ -0,0 +1,222 @@ +/******************************************************************************************* +* +* raylib [audio] example - stream effects +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 4.2, last time updated with raylib 5.0 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2022-2025 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static Raylib_cs.Raylib; + +namespace Examples.Audio; + +[ExcludeFromBrowser("AttachAudioStreamProcessor is unreliable on the wasm audio backend")] +public unsafe partial class StreamEffects : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Audio / Stream Effects"; + + public string Title => "raylib [audio] example - stream effects"; + + //---------------------------------------------------------------------------------- + // Global Variables Definition + //---------------------------------------------------------------------------------- + private static float[] delayBuffer = null; + private static uint delayBufferSize = 0; + private static uint delayReadIndex = 2; + private static uint delayWriteIndex = 0; + + // Low-pass filter state (was a function-local static in the C example) + private static readonly float[] low = { 0.0f, 0.0f }; + + private Music music; + private float timePlayed; + private bool pause; + private bool enableEffectLPF; + private bool enableEffectDelay; + + public void Init() + { + InitAudioDevice(); // Initialize audio device + + music = LoadMusicStream("resources/audio/country.mp3"); + + // Allocate buffer for the delay effect + delayBufferSize = 48000 * 2; // 1 second delay (device sampleRate*channels) + delayBuffer = new float[delayBufferSize]; + delayReadIndex = 2; + delayWriteIndex = 0; + low[0] = 0.0f; + low[1] = 0.0f; + + PlayMusicStream(music); + + timePlayed = 0.0f; // Time played normalized [0.0f..1.0f] + pause = false; // Music playing paused + + enableEffectLPF = false; // Enable effect low-pass-filter + enableEffectDelay = false; // Enable effect delay (1 second) + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateMusicStream(music); // Update music buffer with new stream data + + // Restart music playing (stop and play) + if (IsKeyPressed(KeyboardKey.Space)) + { + StopMusicStream(music); + PlayMusicStream(music); + } + + // Pause/Resume music playing + if (IsKeyPressed(KeyboardKey.P)) + { + pause = !pause; + + if (pause) PauseMusicStream(music); + else ResumeMusicStream(music); + } + + // Add/Remove effect: lowpass filter + if (IsKeyPressed(KeyboardKey.F)) + { + enableEffectLPF = !enableEffectLPF; + if (enableEffectLPF) AttachAudioStreamProcessor(music.Stream, &AudioProcessEffectLPF); + else DetachAudioStreamProcessor(music.Stream, &AudioProcessEffectLPF); + } + + // Add/Remove effect: delay + if (IsKeyPressed(KeyboardKey.D)) + { + enableEffectDelay = !enableEffectDelay; + if (enableEffectDelay) AttachAudioStreamProcessor(music.Stream, &AudioProcessEffectDelay); + else DetachAudioStreamProcessor(music.Stream, &AudioProcessEffectDelay); + } + + // Get normalized time played for current music stream + timePlayed = GetMusicTimePlayed(music) / GetMusicTimeLength(music); + + if (timePlayed > 1.0f) timePlayed = 1.0f; // Make sure time played is no longer than music + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + DrawText("MUSIC SHOULD BE PLAYING!", 245, 150, 20, Color.LightGray); + + DrawRectangle(200, 180, 400, 12, Color.LightGray); + DrawRectangle(200, 180, (int)(timePlayed * 400.0f), 12, Color.Maroon); + DrawRectangleLines(200, 180, 400, 12, Color.Gray); + + DrawText("PRESS SPACE TO RESTART MUSIC", 215, 230, 20, Color.LightGray); + DrawText("PRESS P TO PAUSE/RESUME MUSIC", 208, 260, 20, Color.LightGray); + + DrawText($"PRESS F TO TOGGLE LPF EFFECT: {(enableEffectLPF ? "ON" : "OFF")}", 200, 320, 20, Color.Gray); + DrawText($"PRESS D TO TOGGLE DELAY EFFECT: {(enableEffectDelay ? "ON" : "OFF")}", 180, 350, 20, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadMusicStream(music); // Unload music stream buffers from RAM + + CloseAudioDevice(); // Close audio device (music streaming is automatically stopped) + + delayBuffer = null; // Free delay buffer + } + + //------------------------------------------------------------------------------------ + // Module Functions Definition + //------------------------------------------------------------------------------------ + // Audio effect: lowpass filter + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void AudioProcessEffectLPF(void* buffer, uint frames) + { + const float cutoff = 70.0f / 44100.0f; // 70 Hz lowpass filter + const float k = cutoff / (cutoff + 0.1591549431f); // RC filter formula + + // Converts the buffer data before using it + float* bufferData = (float*)buffer; + for (uint i = 0; i < frames * 2; i += 2) + { + float l = bufferData[i]; + float r = bufferData[i + 1]; + + low[0] += k * (l - low[0]); + low[1] += k * (r - low[1]); + bufferData[i] = low[0]; + bufferData[i + 1] = low[1]; + } + } + + // Audio effect: delay + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static void AudioProcessEffectDelay(void* buffer, uint frames) + { + float* bufferData = (float*)buffer; + fixed (float* delay = delayBuffer) + { + for (uint i = 0; i < frames * 2; i += 2) + { + float leftDelay = delay[delayReadIndex++]; // ERROR: Reading buffer -> WHY??? Maybe thread related??? + float rightDelay = delay[delayReadIndex++]; + + if (delayReadIndex == delayBufferSize) delayReadIndex = 0; + + bufferData[i] = 0.5f * bufferData[i] + 0.5f * leftDelay; + bufferData[i + 1] = 0.5f * bufferData[i + 1] + 0.5f * rightDelay; + + delay[delayWriteIndex++] = bufferData[i]; + delay[delayWriteIndex++] = bufferData[i + 1]; + if (delayWriteIndex == delayBufferSize) delayWriteIndex = 0; + } + } + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [audio] example - stream effects"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new StreamEffects(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/AutomationEvents.cs b/Examples/Core/AutomationEvents.cs new file mode 100644 index 0000000..6ba6a5c --- /dev/null +++ b/Examples/Core/AutomationEvents.cs @@ -0,0 +1,379 @@ +/******************************************************************************************* +* +* raylib [core] example - automation events +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 5.0, last time updated with raylib 5.0 +* +* Example based on 2d_camera_platformer example by arvyy (@arvyy) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2023-2025 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +public partial class AutomationEvents : IExample +{ + private const int GRAVITY = 400; + private const float PLAYER_JUMP_SPD = 350.0f; + private const float PLAYER_HOR_SPD = 200.0f; + + private const int MAX_ENVIRONMENT_ELEMENTS = 5; + + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Automation Events"; + + public string Title => "raylib [core] example - automation events"; + + private struct Player + { + public Vector2 Position; + public float Speed; + public bool CanJump; + } + + private struct EnvElement + { + public Rectangle Rect; + public int Blocking; + public Color Color; + } + + private Player player; + private EnvElement[] envElements; + private Camera2D camera; + + private AutomationEventList aelist; + private bool eventRecording; + private bool eventPlaying; + + private uint frameCounter; + private uint playFrameCounter; + private uint currentPlayFrame; + + public unsafe void Init() + { + // Define player + player = new Player(); + player.Position = new Vector2(400, 280); + player.Speed = 0; + player.CanJump = false; + + // Define environment elements (platforms) + envElements = new EnvElement[MAX_ENVIRONMENT_ELEMENTS] + { + new EnvElement { Rect = new Rectangle(0, 0, 1000, 400), Blocking = 0, Color = Color.LightGray }, + new EnvElement { Rect = new Rectangle(0, 400, 1000, 200), Blocking = 1, Color = Color.Gray }, + new EnvElement { Rect = new Rectangle(300, 200, 400, 10), Blocking = 1, Color = Color.Gray }, + new EnvElement { Rect = new Rectangle(250, 300, 100, 10), Blocking = 1, Color = Color.Gray }, + new EnvElement { Rect = new Rectangle(650, 300, 100, 10), Blocking = 1, Color = Color.Gray } + }; + + // Define camera + camera = new Camera2D(); + camera.Target = player.Position; + camera.Offset = new Vector2(screenWidth / 2.0f, screenHeight / 2.0f); + camera.Rotation = 0.0f; + camera.Zoom = 1.0f; + + // Automation events + aelist = LoadAutomationEventList((sbyte*)null); // Initialize list of automation events to record new events + SetAutomationEventList(ref aelist); + eventRecording = false; + eventPlaying = false; + + frameCounter = 0; + playFrameCounter = 0; + currentPlayFrame = 0; + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + float deltaTime = 0.015f;//GetFrameTime(); + + // Dropped files logic + //---------------------------------------------------------------------------------- +#if BROWSER + // NOTE: drag-and-drop event-list loading (.txt/.rae) is not supported in the browser host. +#else + if (IsFileDropped()) + { + FilePathList droppedFiles = LoadDroppedFiles(); + + // Supports loading .rgs style files (text or binary) and .png style palette images + if (IsFileExtension(droppedFiles[0], ".txt;.rae")) + { + UnloadAutomationEventList(aelist); + aelist = LoadAutomationEventList(droppedFiles[0]); + + eventRecording = false; + + // Reset scene state to play + eventPlaying = true; + playFrameCounter = 0; + currentPlayFrame = 0; + + player.Position = new Vector2(400, 280); + player.Speed = 0; + player.CanJump = false; + + camera.Target = player.Position; + camera.Offset = new Vector2(screenWidth / 2.0f, screenHeight / 2.0f); + camera.Rotation = 0.0f; + camera.Zoom = 1.0f; + } + + UnloadDroppedFiles(droppedFiles); // Unload filepaths from memory + } +#endif + //---------------------------------------------------------------------------------- + + // Update player + //---------------------------------------------------------------------------------- + if (IsKeyDown(KeyboardKey.Left)) player.Position.X -= PLAYER_HOR_SPD * deltaTime; + if (IsKeyDown(KeyboardKey.Right)) player.Position.X += PLAYER_HOR_SPD * deltaTime; + if (IsKeyDown(KeyboardKey.Space) && player.CanJump) + { + player.Speed = -PLAYER_JUMP_SPD; + player.CanJump = false; + } + + int hitObstacle = 0; + for (int i = 0; i < MAX_ENVIRONMENT_ELEMENTS; i++) + { + EnvElement element = envElements[i]; + if (element.Blocking != 0 && + element.Rect.X <= player.Position.X && + element.Rect.X + element.Rect.Width >= player.Position.X && + element.Rect.Y >= player.Position.Y && + element.Rect.Y <= player.Position.Y + player.Speed * deltaTime) + { + hitObstacle = 1; + player.Speed = 0.0f; + player.Position.Y = element.Rect.Y; + } + } + + if (hitObstacle == 0) + { + player.Position.Y += player.Speed * deltaTime; + player.Speed += GRAVITY * deltaTime; + player.CanJump = false; + } + else player.CanJump = true; + + if (IsKeyPressed(KeyboardKey.R)) + { + // Reset game state + player.Position = new Vector2(400, 280); + player.Speed = 0; + player.CanJump = false; + + camera.Target = player.Position; + camera.Offset = new Vector2(screenWidth / 2.0f, screenHeight / 2.0f); + camera.Rotation = 0.0f; + camera.Zoom = 1.0f; + } + //---------------------------------------------------------------------------------- + + // Events playing + // NOTE: Logic must be before Camera update because it depends on mouse-wheel value, + // that can be set by the played event... but some other inputs could be affected + //---------------------------------------------------------------------------------- + if (eventPlaying) + { + // NOTE: Multiple events could be executed in a single frame + while (playFrameCounter == aelist.Events[currentPlayFrame].Frame) + { + PlayAutomationEvent(aelist.Events[currentPlayFrame]); + currentPlayFrame++; + + if (currentPlayFrame == aelist.Count) + { + eventPlaying = false; + currentPlayFrame = 0; + playFrameCounter = 0; + + TraceLog(TraceLogLevel.Info, "FINISH PLAYING!"); + break; + } + } + + playFrameCounter++; + } + //---------------------------------------------------------------------------------- + + // Update camera + //---------------------------------------------------------------------------------- + camera.Target = player.Position; + camera.Offset = new Vector2(screenWidth / 2.0f, screenHeight / 2.0f); + float minX = 1000, minY = 1000, maxX = -1000, maxY = -1000; + + // WARNING: On event replay, mouse-wheel internal value is set + camera.Zoom += ((float)GetMouseWheelMove() * 0.05f); + if (camera.Zoom > 3.0f) camera.Zoom = 3.0f; + else if (camera.Zoom < 0.25f) camera.Zoom = 0.25f; + + for (int i = 0; i < MAX_ENVIRONMENT_ELEMENTS; i++) + { + EnvElement element = envElements[i]; + minX = MathF.Min(element.Rect.X, minX); + maxX = MathF.Max(element.Rect.X + element.Rect.Width, maxX); + minY = MathF.Min(element.Rect.Y, minY); + maxY = MathF.Max(element.Rect.Y + element.Rect.Height, maxY); + } + + Vector2 max = GetWorldToScreen2D(new Vector2(maxX, maxY), camera); + Vector2 min = GetWorldToScreen2D(new Vector2(minX, minY), camera); + + if (max.X < screenWidth) camera.Offset.X = screenWidth - (max.X - (float)screenWidth / 2); + if (max.Y < screenHeight) camera.Offset.Y = screenHeight - (max.Y - (float)screenHeight / 2); + if (min.X > 0) camera.Offset.X = (float)screenWidth / 2 - min.X; + if (min.Y > 0) camera.Offset.Y = (float)screenHeight / 2 - min.Y; + //---------------------------------------------------------------------------------- + + // Events management + if (IsKeyPressed(KeyboardKey.S)) // Toggle events recording + { + if (!eventPlaying) + { + if (eventRecording) + { + StopAutomationEventRecording(); + eventRecording = false; + + ExportAutomationEventList(aelist, "automation.rae"); + + TraceLog(TraceLogLevel.Info, $"RECORDED FRAMES: {aelist.Count}"); + } + else + { + SetAutomationEventBaseFrame(180); + StartAutomationEventRecording(); + eventRecording = true; + } + } + } + else if (IsKeyPressed(KeyboardKey.A)) // Toggle events playing (WARNING: Starts next frame) + { + if (!eventRecording && (aelist.Count > 0)) + { + // Reset scene state to play + eventPlaying = true; + playFrameCounter = 0; + currentPlayFrame = 0; + + player.Position = new Vector2(400, 280); + player.Speed = 0; + player.CanJump = false; + + camera.Target = player.Position; + camera.Offset = new Vector2(screenWidth / 2.0f, screenHeight / 2.0f); + camera.Rotation = 0.0f; + camera.Zoom = 1.0f; + } + } + + if (eventRecording || eventPlaying) frameCounter++; + else frameCounter = 0; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.LightGray); + + BeginMode2D(camera); + + // Draw environment elements + for (int i = 0; i < MAX_ENVIRONMENT_ELEMENTS; i++) + { + DrawRectangleRec(envElements[i].Rect, envElements[i].Color); + } + + // Draw player rectangle + DrawRectangleRec(new Rectangle(player.Position.X - 20, player.Position.Y - 40, 40, 40), Color.Red); + + EndMode2D(); + + // Draw game controls + DrawRectangle(10, 10, 290, 145, Fade(Color.SkyBlue, 0.5f)); + DrawRectangleLines(10, 10, 290, 145, Fade(Color.Blue, 0.8f)); + + DrawText("Controls:", 20, 20, 10, Color.Black); + DrawText("- RIGHT | LEFT: Player movement", 30, 40, 10, Color.DarkGray); + DrawText("- SPACE: Player jump", 30, 60, 10, Color.DarkGray); + DrawText("- R: Reset game state", 30, 80, 10, Color.DarkGray); + + DrawText("- S: START/STOP RECORDING INPUT EVENTS", 30, 110, 10, Color.Black); + DrawText("- A: REPLAY LAST RECORDED INPUT EVENTS", 30, 130, 10, Color.Black); + + // Draw automation events recording indicator + if (eventRecording) + { + DrawRectangle(10, 160, 290, 30, Fade(Color.Red, 0.3f)); + DrawRectangleLines(10, 160, 290, 30, Fade(Color.Maroon, 0.8f)); + DrawCircle(30, 175, 10, Color.Maroon); + + if (((frameCounter / 15) % 2) == 1) DrawText($"RECORDING EVENTS... [{aelist.Count}]", 50, 170, 10, Color.Maroon); + } + else if (eventPlaying) + { + DrawRectangle(10, 160, 290, 30, Fade(Color.Lime, 0.3f)); + DrawRectangleLines(10, 160, 290, 30, Fade(Color.DarkGreen, 0.8f)); + DrawTriangle(new Vector2(20, 155 + 10), new Vector2(20, 155 + 30), new Vector2(40, 155 + 20), Color.DarkGreen); + + if (((frameCounter / 15) % 2) == 1) DrawText($"PLAYING RECORDED EVENTS... [{currentPlayFrame}]", 50, 170, 10, Color.DarkGreen); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadAutomationEventList(aelist); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - automation events"); + + var game = new AutomationEvents(); + game.Init(); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/Camera2dMouseZoom.cs b/Examples/Core/Camera2dMouseZoom.cs new file mode 100644 index 0000000..05d4f39 --- /dev/null +++ b/Examples/Core/Camera2dMouseZoom.cs @@ -0,0 +1,184 @@ +/******************************************************************************************* +* +* raylib [core] example - 2d camera mouse zoom +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 4.2, last time updated with raylib 4.2 +* +* Example contributed by Jeffery Myers (@JeffM2501) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2022-2025 Jeffery Myers (@JeffM2501) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; + +namespace Examples.Core; + +public partial class Camera2dMouseZoom : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Camera 2D Mouse Zoom"; + + public string Title => "raylib [core] example - 2d camera mouse zoom"; + + private Camera2D camera; + private int zoomMode; // 0-Mouse Wheel, 1-Mouse Move + + public void Init() + { + camera = new Camera2D(); + camera.Zoom = 1.0f; + + zoomMode = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.One)) + { + zoomMode = 0; + } + else if (IsKeyPressed(KeyboardKey.Two)) + { + zoomMode = 1; + } + + // Translate based on mouse right click + if (IsMouseButtonDown(MouseButton.Left)) + { + Vector2 delta = GetMouseDelta(); + delta = Vector2Scale(delta, -1.0f / camera.Zoom); + camera.Target = Vector2Add(camera.Target, delta); + } + + if (zoomMode == 0) + { + // Zoom based on mouse wheel + float wheel = GetMouseWheelMove(); + if (wheel != 0) + { + // Get the world point that is under the mouse + Vector2 mouseWorldPos = GetScreenToWorld2D(GetMousePosition(), camera); + + // Set the offset to where the mouse is + camera.Offset = GetMousePosition(); + + // Set the target to match, so that the camera maps the world space point + // under the cursor to the screen space point under the cursor at any zoom + camera.Target = mouseWorldPos; + + // Zoom increment + // Uses log scaling to provide consistent zoom speed + float scale = 0.2f * wheel; + camera.Zoom = Clamp(MathF.Exp(MathF.Log(camera.Zoom) + scale), 0.125f, 64.0f); + } + } + else + { + // Zoom based on mouse right click + if (IsMouseButtonPressed(MouseButton.Right)) + { + // Get the world point that is under the mouse + Vector2 mouseWorldPos = GetScreenToWorld2D(GetMousePosition(), camera); + + // Set the offset to where the mouse is + camera.Offset = GetMousePosition(); + + // Set the target to match, so that the camera maps the world space point + // under the cursor to the screen space point under the cursor at any zoom + camera.Target = mouseWorldPos; + } + + if (IsMouseButtonDown(MouseButton.Right)) + { + // Zoom increment + // Uses log scaling to provide consistent zoom speed + float deltaX = GetMouseDelta().X; + float scale = 0.005f * deltaX; + camera.Zoom = Clamp(MathF.Exp(MathF.Log(camera.Zoom) + scale), 0.125f, 64.0f); + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode2D(camera); + // Draw the 3d grid, rotated 90 degrees and centered around 0,0 + // just so we have something in the XY plane + Rlgl.PushMatrix(); + Rlgl.Translatef(0, 25 * 50, 0); + Rlgl.Rotatef(90, 1, 0, 0); + DrawGrid(100, 50); + Rlgl.PopMatrix(); + + // Draw a reference circle + DrawCircle(GetScreenWidth() / 2, GetScreenHeight() / 2, 50, Color.Maroon); + EndMode2D(); + + // Draw mouse reference + //Vector2 mousePos = GetWorldToScreen2D(GetMousePosition(), camera) + DrawCircleV(GetMousePosition(), 4, Color.DarkGray); + DrawTextEx(GetFontDefault(), $"[{GetMouseX()}, {GetMouseY()}]", + Vector2Add(GetMousePosition(), new Vector2(-44, -24)), 20, 2, Color.Black); + + DrawText("[1][2] Select mouse zoom mode (Wheel or Move)", 20, 20, 20, Color.DarkGray); + if (zoomMode == 0) + { + DrawText("Mouse left button drag to move, mouse wheel to zoom", 20, 50, 20, Color.DarkGray); + } + else + { + DrawText("Mouse left button drag to move, mouse press and move to zoom", 20, 50, 20, Color.DarkGray); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - 2d camera mouse zoom"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new Camera2dMouseZoom(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/Camera3dFps.cs b/Examples/Core/Camera3dFps.cs new file mode 100644 index 0000000..bb234d4 --- /dev/null +++ b/Examples/Core/Camera3dFps.cs @@ -0,0 +1,348 @@ +/******************************************************************************************* +* +* raylib [core] example - 3d camera fps +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.5 +* +* Example contributed by Agnis Aldiņš (@nezvers) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Agnis Aldiņš (@nezvers) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; + +namespace Examples.Core; + +public partial class Camera3dFps : IExample +{ + // Movement constants + private const float GRAVITY = 32.0f; + private const float MAX_SPEED = 20.0f; + private const float CROUCH_SPEED = 5.0f; + private const float JUMP_FORCE = 12.0f; + private const float MAX_ACCEL = 150.0f; + // Grounded drag + private const float FRICTION = 0.86f; + // Increasing air drag, increases strafing speed + private const float AIR_DRAG = 0.98f; + // Responsiveness for turning movement direction to looked direction + private const float CONTROL = 15.0f; + private const float CROUCH_HEIGHT = 0.0f; + private const float STAND_HEIGHT = 1.0f; + private const float BOTTOM_HEIGHT = 0.5f; + + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / 3D Camera FPS"; + + public string Title => "raylib [core] example - 3d camera fps"; + + public bool CursorDisabled => true; + + // Body structure + private struct Body + { + public Vector3 Position; + public Vector3 Velocity; + public Vector3 Dir; + public bool IsGrounded; + } + + // State that was global in the C example + private readonly Vector2 sensitivity = new Vector2(0.001f, 0.001f); + + private Body player; + private Vector2 lookRotation; + private float headTimer; + private float walkLerp; + private float headLerp; + private Vector2 lean; + + private Camera3D camera; + + public void Init() + { + player = new Body(); + lookRotation = new Vector2(0, 0); + headTimer = 0.0f; + walkLerp = 0.0f; + headLerp = STAND_HEIGHT; + lean = new Vector2(0, 0); + + // Initialize camera variables + // NOTE: UpdateCameraFPS() takes care of the rest + camera = new Camera3D(); + camera.FovY = 60.0f; + camera.Projection = CameraProjection.Perspective; + camera.Position = new Vector3( + player.Position.X, + player.Position.Y + (BOTTOM_HEIGHT + headLerp), + player.Position.Z); + + UpdateCameraFPS(ref camera); // Update camera parameters + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + Vector2 mouseDelta = GetMouseDelta(); + lookRotation.X -= mouseDelta.X * sensitivity.X; + lookRotation.Y += mouseDelta.Y * sensitivity.Y; + + int sideway = (IsKeyDown(KeyboardKey.D) ? 1 : 0) - (IsKeyDown(KeyboardKey.A) ? 1 : 0); + int forward = (IsKeyDown(KeyboardKey.W) ? 1 : 0) - (IsKeyDown(KeyboardKey.S) ? 1 : 0); + bool crouching = IsKeyDown(KeyboardKey.LeftControl); + UpdateBody(ref player, lookRotation.X, sideway, forward, IsKeyPressed(KeyboardKey.Space), crouching); + + float delta = GetFrameTime(); + headLerp = Lerp(headLerp, (crouching ? CROUCH_HEIGHT : STAND_HEIGHT), 20.0f * delta); + camera.Position = new Vector3( + player.Position.X, + player.Position.Y + (BOTTOM_HEIGHT + headLerp), + player.Position.Z); + + if (player.IsGrounded && ((forward != 0) || (sideway != 0))) + { + headTimer += delta * 3.0f; + walkLerp = Lerp(walkLerp, 1.0f, 10.0f * delta); + camera.FovY = Lerp(camera.FovY, 55.0f, 5.0f * delta); + } + else + { + walkLerp = Lerp(walkLerp, 0.0f, 10.0f * delta); + camera.FovY = Lerp(camera.FovY, 60.0f, 5.0f * delta); + } + + lean.X = Lerp(lean.X, sideway * 0.02f, 10.0f * delta); + lean.Y = Lerp(lean.Y, forward * 0.015f, 10.0f * delta); + + UpdateCameraFPS(ref camera); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + DrawLevel(); + EndMode3D(); + + // Draw info box + DrawRectangle(5, 5, 330, 75, Fade(Color.SkyBlue, 0.5f)); + DrawRectangleLines(5, 5, 330, 75, Color.Blue); + + DrawText("Camera controls:", 15, 15, 10, Color.Black); + DrawText("- Move keys: W, A, S, D, Space, Left-Ctrl", 15, 30, 10, Color.Black); + DrawText("- Look around: arrow keys or mouse", 15, 45, 10, Color.Black); + float velLen = Vector2Length(new Vector2(player.Velocity.X, player.Velocity.Z)); + DrawText($"- Velocity Len: ({velLen:00.000})", 15, 60, 10, Color.Black); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + // Update body considering current world state + private void UpdateBody(ref Body body, float rot, int side, int forward, bool jumpPressed, bool crouchHold) + { + Vector2 input = new Vector2((float)side, (float)-forward); + + // Upstream guards this with `#if defined(NORMALIZE_INPUT)`, which is always true given the + // `#define NORMALIZE_INPUT 0` above it (defined() tests definedness, not the value), so the + // diagonal-movement normalization is active. + // Slow down diagonal movement + if ((side != 0) && (forward != 0)) input = Vector2Normalize(input); + + float delta = GetFrameTime(); + + if (!body.IsGrounded) body.Velocity.Y -= GRAVITY * delta; + + if (body.IsGrounded && jumpPressed) + { + body.Velocity.Y = JUMP_FORCE; + body.IsGrounded = false; + + // Sound can be played at this moment + //SetSoundPitch(fxJump, 1.0f + (GetRandomValue(-100, 100)*0.001)); + //PlaySound(fxJump); + } + + Vector3 front = new Vector3(MathF.Sin(rot), 0.0f, MathF.Cos(rot)); + Vector3 right = new Vector3(MathF.Cos(-rot), 0.0f, MathF.Sin(-rot)); + + Vector3 desiredDir = new Vector3( + input.X * right.X + input.Y * front.X, + 0.0f, + input.X * right.Z + input.Y * front.Z); + body.Dir = Vector3Lerp(body.Dir, desiredDir, CONTROL * delta); + + float decel = (body.IsGrounded ? FRICTION : AIR_DRAG); + Vector3 hvel = new Vector3(body.Velocity.X * decel, 0.0f, body.Velocity.Z * decel); + + float hvelLength = Vector3Length(hvel); // Magnitude + if (hvelLength < (MAX_SPEED * 0.01f)) hvel = new Vector3(0, 0, 0); + + // This is what creates strafing + float speed = Vector3DotProduct(hvel, body.Dir); + + // Whenever the amount of acceleration to add is clamped by the maximum acceleration constant, + // a Player can make the speed faster by bringing the direction closer to horizontal velocity angle + // More info here: https://youtu.be/v3zT3Z5apaM?t=165 + float maxSpeed = (crouchHold ? CROUCH_SPEED : MAX_SPEED); + float accel = Clamp(maxSpeed - speed, 0.0f, MAX_ACCEL * delta); + hvel.X += body.Dir.X * accel; + hvel.Z += body.Dir.Z * accel; + + body.Velocity.X = hvel.X; + body.Velocity.Z = hvel.Z; + + body.Position.X += body.Velocity.X * delta; + body.Position.Y += body.Velocity.Y * delta; + body.Position.Z += body.Velocity.Z * delta; + + // Fancy collision system against the floor + if (body.Position.Y <= 0.0f) + { + body.Position.Y = 0.0f; + body.Velocity.Y = 0.0f; + body.IsGrounded = true; // Enable jumping + } + } + + // Update camera for FPS behaviour + private void UpdateCameraFPS(ref Camera3D camera) + { + Vector3 up = new Vector3(0.0f, 1.0f, 0.0f); + Vector3 targetOffset = new Vector3(0.0f, 0.0f, -1.0f); + + // Left and right + Vector3 yaw = Vector3RotateByAxisAngle(targetOffset, up, lookRotation.X); + + // Clamp view up + float maxAngleUp = Vector3Angle(up, yaw); + maxAngleUp -= 0.001f; // Avoid numerical errors + if (-(lookRotation.Y) > maxAngleUp) { lookRotation.Y = -maxAngleUp; } + + // Clamp view down + float maxAngleDown = Vector3Angle(Vector3Negate(up), yaw); + maxAngleDown *= -1.0f; // Downwards angle is negative + maxAngleDown += 0.001f; // Avoid numerical errors + if (-(lookRotation.Y) < maxAngleDown) { lookRotation.Y = -maxAngleDown; } + + // Up and down + Vector3 right = Vector3Normalize(Vector3CrossProduct(yaw, up)); + + // Rotate view vector around right axis + float pitchAngle = -lookRotation.Y - lean.Y; + pitchAngle = Clamp(pitchAngle, -MathF.PI / 2 + 0.0001f, MathF.PI / 2 - 0.0001f); // Clamp angle so it doesn't go past straight up or straight down + Vector3 pitch = Vector3RotateByAxisAngle(yaw, right, pitchAngle); + + // Head animation + // Rotate up direction around forward axis + float headSin = MathF.Sin(headTimer * MathF.PI); + float headCos = MathF.Cos(headTimer * MathF.PI); + const float stepRotation = 0.01f; + camera.Up = Vector3RotateByAxisAngle(up, pitch, headSin * stepRotation + lean.X); + + // Camera BOB + const float bobSide = 0.1f; + const float bobUp = 0.15f; + Vector3 bobbing = Vector3Scale(right, headSin * bobSide); + bobbing.Y = MathF.Abs(headCos * bobUp); + + camera.Position = Vector3Add(camera.Position, Vector3Scale(bobbing, walkLerp)); + camera.Target = Vector3Add(camera.Position, pitch); + } + + // Draw game level + private void DrawLevel() + { + const int floorExtent = 25; + const float tileSize = 5.0f; + Color tileColor1 = new Color(150, 200, 200, 255); + + // Floor tiles + for (int y = -floorExtent; y < floorExtent; y++) + { + for (int x = -floorExtent; x < floorExtent; x++) + { + if ((y & 1) != 0 && (x & 1) != 0) + { + DrawPlane(new Vector3(x * tileSize, 0.0f, y * tileSize), new Vector2(tileSize, tileSize), tileColor1); + } + else if ((y & 1) == 0 && (x & 1) == 0) + { + DrawPlane(new Vector3(x * tileSize, 0.0f, y * tileSize), new Vector2(tileSize, tileSize), Color.LightGray); + } + } + } + + Vector3 towerSize = new Vector3(16.0f, 32.0f, 16.0f); + Color towerColor = new Color(150, 200, 200, 255); + + Vector3 towerPos = new Vector3(16.0f, 16.0f, 16.0f); + DrawCubeV(towerPos, towerSize, towerColor); + DrawCubeWiresV(towerPos, towerSize, Color.DarkBlue); + + towerPos.X *= -1; + DrawCubeV(towerPos, towerSize, towerColor); + DrawCubeWiresV(towerPos, towerSize, Color.DarkBlue); + + towerPos.Z *= -1; + DrawCubeV(towerPos, towerSize, towerColor); + DrawCubeWiresV(towerPos, towerSize, Color.DarkBlue); + + towerPos.X *= -1; + DrawCubeV(towerPos, towerSize, towerColor); + DrawCubeWiresV(towerPos, towerSize, Color.DarkBlue); + + // Red sun + DrawSphere(new Vector3(300.0f, 300.0f, 0.0f), 100.0f, new Color(255, 0, 0, 255)); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - 3d camera fps"); + + DisableCursor(); // Limit cursor to relative movement inside the window + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new Camera3dFps(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/ClipboardText.cs b/Examples/Core/ClipboardText.cs new file mode 100644 index 0000000..0bcbd74 --- /dev/null +++ b/Examples/Core/ClipboardText.cs @@ -0,0 +1,286 @@ +/******************************************************************************************* +* +* raylib [core] example - clipboard text +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Ananth S (@Ananth1839) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Ananth S (@Ananth1839) +* +********************************************************************************************/ + +// NOTE: The upstream C example uses raygui (GuiTextBox/GuiButton/GuiLabel) for its UI. +// raygui is not bound in raylib-cs, so the widgets below are minimal re-implementations +// using plain raylib drawing/input. Clipboard behaviour (cut/copy/paste and CTRL+X/C/V +// shortcuts) matches the original. + +using System; +using System.Numerics; +using System.Text; +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +public partial class ClipboardText : IExample +{ + private const int MaxTextSamples = 5; + + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Clipboard Text"; + + public string Title => "raylib [core] example - clipboard text"; + + private string[] sampleTexts; + private string clipboardText; + private StringBuilder inputBuffer; + + // UI required variables + private bool textBoxEditMode; + + private bool btnCutPressed; + private bool btnCopyPressed; + private bool btnPastePressed; + private bool btnClearPressed; + private bool btnRandomPressed; + + private int framesCounter; + + public void Init() + { + // Define some sample texts + sampleTexts = new string[] + { + "Hello from raylib!", + "The quick brown fox jumps over the lazy dog", + "Clipboard operations are useful!", + "raylib is a simple and easy-to-use library", + "Copy and paste me!" + }; + + clipboardText = null; + inputBuffer = new StringBuilder("Hello from raylib!"); // Random initial string + + // UI required variables + textBoxEditMode = false; + + btnCutPressed = false; + btnCopyPressed = false; + btnPastePressed = false; + btnClearPressed = false; + btnRandomPressed = false; + + framesCounter = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + framesCounter++; + + // Handle button interactions + if (btnCutPressed) + { + SetClipboardText(inputBuffer.ToString()); + clipboardText = GetClipboardText_(); + inputBuffer.Clear(); // Quick solution to clear text + } + + if (btnCopyPressed) + { + SetClipboardText(inputBuffer.ToString()); // Copy text to clipboard + clipboardText = GetClipboardText_(); // Get text from clipboard + } + + if (btnPastePressed) + { + // Paste text from clipboard + clipboardText = GetClipboardText_(); + if (clipboardText != null) SetInputBuffer(clipboardText); + } + + if (btnClearPressed) + { + inputBuffer.Clear(); // Quick solution to clear text + } + + if (btnRandomPressed) + { + // Get random text from sample list + SetInputBuffer(sampleTexts[GetRandomValue(0, MaxTextSamples - 1)]); + } + + // Quick cut/copy/paste with keyboard shortcuts + if (IsKeyDown(KeyboardKey.LeftControl) || IsKeyDown(KeyboardKey.RightControl)) + { + if (IsKeyPressed(KeyboardKey.X)) + { + SetClipboardText(inputBuffer.ToString()); + inputBuffer.Clear(); // Quick solution to clear text + } + + if (IsKeyPressed(KeyboardKey.C)) SetClipboardText(inputBuffer.ToString()); + + if (IsKeyPressed(KeyboardKey.V)) + { + clipboardText = GetClipboardText_(); + if (clipboardText != null) SetInputBuffer(clipboardText); + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + // Draw instructions + GuiLabel(new Rectangle(50, 20, 700, 36), "Use the BUTTONS or KEY SHORTCUTS:"); + DrawText("[CTRL+X] - CUT | [CTRL+C] COPY | [CTRL+V] | PASTE", 50, 60, 20, Color.Maroon); + + // Draw text box + if (GuiTextBox(new Rectangle(50, 120, 652, 40), inputBuffer, 256, textBoxEditMode)) textBoxEditMode = !textBoxEditMode; + + // Random text button + btnRandomPressed = GuiButton(new Rectangle(50 + 652 + 8, 120, 40, 40), "RND"); + + // Draw buttons + btnCutPressed = GuiButton(new Rectangle(50, 180, 158, 40), "CUT"); + btnCopyPressed = GuiButton(new Rectangle(50 + 165, 180, 158, 40), "COPY"); + btnPastePressed = GuiButton(new Rectangle(50 + 165 * 2, 180, 158, 40), "PASTE"); + btnClearPressed = GuiButton(new Rectangle(50 + 165 * 3, 180, 158, 40), "CLEAR"); + + // Draw clipboard status + GuiLabel(new Rectangle(50, 260, 700, 40), "Clipboard current text data:"); + GuiTextBoxReadOnly(new Rectangle(50, 300, 700, 40), clipboardText); + GuiLabel(new Rectangle(50, 360, 700, 40), "Try copying text from other applications and pasting here!"); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + // Replace input buffer contents (equivalent to raylib TextCopy into a fixed buffer) + private void SetInputBuffer(string text) + { + inputBuffer.Clear(); + if (text != null) + { + if (text.Length > 255) text = text.Substring(0, 255); + inputBuffer.Append(text); + } + } + + //---------------------------------------------------------------------------------- + // Minimal raygui-like widgets (plain raylib re-implementation) + //---------------------------------------------------------------------------------- + private static void GuiLabel(Rectangle bounds, string text) + { + DrawText(text, (int)bounds.X, (int)(bounds.Y + (bounds.Height - 20) / 2), 20, Color.DarkGray); + } + + private static bool GuiButton(Rectangle bounds, string text) + { + bool pressed = false; + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + + Color fill = hover ? (IsMouseButtonDown(MouseButton.Left) ? Color.SkyBlue : Color.LightGray) : Color.RayWhite; + DrawRectangleRec(bounds, fill); + DrawRectangleLinesEx(bounds, 1, hover ? Color.Blue : Color.Gray); + + int textWidth = MeasureText(text, 20); + DrawText(text, (int)(bounds.X + (bounds.Width - textWidth) / 2), (int)(bounds.Y + (bounds.Height - 20) / 2), 20, Color.DarkGray); + + if (hover && IsMouseButtonReleased(MouseButton.Left)) pressed = true; + + return pressed; + } + + private bool GuiTextBox(Rectangle bounds, StringBuilder text, int maxChars, bool editMode) + { + bool pressed = false; + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + + if (editMode) + { + // Get char pressed (unicode character) on the queue + int key = GetCharPressed(); + while (key > 0) + { + if ((key >= 32) && (key <= 125) && (text.Length < maxChars - 1)) + { + text.Append((char)key); + } + + key = GetCharPressed(); + } + + if (IsKeyPressed(KeyboardKey.Backspace) && (text.Length > 0)) text.Remove(text.Length - 1, 1); + } + + DrawRectangleRec(bounds, Color.RayWhite); + DrawRectangleLinesEx(bounds, editMode ? 2 : 1, editMode ? Color.Red : (hover ? Color.Blue : Color.Gray)); + + string content = text.ToString(); + DrawText(content, (int)bounds.X + 4, (int)(bounds.Y + (bounds.Height - 20) / 2), 20, Color.DarkGray); + + // Draw blinking cursor while editing + if (editMode && ((framesCounter / 20) % 2 == 0)) + { + DrawText("_", (int)bounds.X + 4 + MeasureText(content, 20), (int)(bounds.Y + (bounds.Height - 20) / 2), 20, Color.DarkGray); + } + + if (hover && IsMouseButtonPressed(MouseButton.Left)) pressed = true; + + return pressed; + } + + private static void GuiTextBoxReadOnly(Rectangle bounds, string text) + { + DrawRectangleRec(bounds, Color.LightGray); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (text != null) DrawText(text, (int)bounds.X + 4, (int)(bounds.Y + (bounds.Height - 20) / 2), 20, Color.Gray); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - clipboard text"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new ClipboardText(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/ComputeHash.cs b/Examples/Core/ComputeHash.cs new file mode 100644 index 0000000..69a2d9f --- /dev/null +++ b/Examples/Core/ComputeHash.cs @@ -0,0 +1,253 @@ +/******************************************************************************************* +* +* raylib [core] example - compute hash +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +// NOTE: The upstream C example uses raygui (GuiTextBox/GuiButton/GuiLabel) for its UI. +// raygui is not bound in raylib-cs, so the widgets below are minimal re-implementations +// using plain raylib drawing/input. The hashing/Base64 logic is a faithful port. + +using System; +using System.Numerics; +using System.Text; +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +public unsafe partial class ComputeHash : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Compute Hash"; + + public string Title => "raylib [core] example - compute hash"; + + // UI controls variables + private StringBuilder textInput; + private bool textBoxEditMode; + private bool btnComputeHashes; + + // Data hash values + private uint hashCRC32; + private uint[] hashMD5; + private uint[] hashSHA1; + private uint[] hashSHA256; + + // Base64 encoded data + private string base64Text; + + private int framesCounter; + + public void Init() + { + textInput = new StringBuilder("The quick brown fox jumps over the lazy dog."); + textBoxEditMode = false; + btnComputeHashes = false; + + hashCRC32 = 0; + hashMD5 = null; + hashSHA1 = null; + hashSHA256 = null; + + base64Text = null; + + framesCounter = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + framesCounter++; + + if (btnComputeHashes) + { + byte[] bytes = Encoding.UTF8.GetBytes(textInput.ToString()); + int textInputLen = bytes.Length; + + fixed (byte* textInputPtr = bytes) + { + int base64TextSize; + + // Encode data to Base64 string (includes NULL terminator), memory must be MemFree() + sbyte* base64 = EncodeDataBase64(textInputPtr, textInputLen, &base64TextSize); + base64Text = (base64 != null) ? new string(base64) : null; + MemFree(base64); // Free Base64 text data (kept managed above) + + hashCRC32 = ComputeCRC32(textInputPtr, textInputLen); // Compute CRC32 hash code (4 bytes) + hashMD5 = CopyHash(ComputeMD5(textInputPtr, textInputLen), 4); // Compute MD5 hash code, returns static int[4] (16 bytes) + hashSHA1 = CopyHash(ComputeSHA1(textInputPtr, textInputLen), 5); // Compute SHA1 hash code, returns static int[5] (20 bytes) + hashSHA256 = CopyHash(ComputeSHA256(textInputPtr, textInputLen), 8); // Compute SHA256 hash code, returns static int[8] (32 bytes) + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + GuiLabel(new Rectangle(40, 26, 720, 32), "INPUT DATA (TEXT):", 20); + + if (GuiTextBox(new Rectangle(40, 64, 720, 32), textInput, 95, textBoxEditMode, 10)) textBoxEditMode = !textBoxEditMode; + + btnComputeHashes = GuiButton(new Rectangle(40, 64 + 40, 720, 32), "COMPUTE INPUT DATA HASHES", 10); + + GuiLabel(new Rectangle(40, 160, 720, 32), "INPUT DATA HASH VALUES:", 20); + + GuiLabel(new Rectangle(40, 200, 120, 32), "CRC32 [32 bit]:", 10); + GuiTextBoxReadOnly(new Rectangle(40 + 120, 200, 720 - 120, 32), GetDataAsHexText(new uint[] { hashCRC32 }, 1), 10); + GuiLabel(new Rectangle(40, 200 + 36, 120, 32), "MD5 [128 bit]:", 10); + GuiTextBoxReadOnly(new Rectangle(40 + 120, 200 + 36, 720 - 120, 32), GetDataAsHexText(hashMD5, 4), 10); + GuiLabel(new Rectangle(40, 200 + 36 * 2, 120, 32), "SHA1 [160 bit]:", 10); + GuiTextBoxReadOnly(new Rectangle(40 + 120, 200 + 36 * 2, 720 - 120, 32), GetDataAsHexText(hashSHA1, 5), 10); + GuiLabel(new Rectangle(40, 200 + 36 * 3, 120, 32), "SHA256 [256 bit]:", 10); + GuiTextBoxReadOnly(new Rectangle(40 + 120, 200 + 36 * 3, 720 - 120, 32), GetDataAsHexText(hashSHA256, 8), 10); + + GuiLabel(new Rectangle(40, 200 + 36 * 5 - 30, 320, 32), "BONUS - BAS64 ENCODED STRING:", 10); + GuiLabel(new Rectangle(40, 200 + 36 * 5, 120, 32), "BASE64 ENCODING:", 10); + GuiTextBoxReadOnly(new Rectangle(40 + 120, 200 + 36 * 5, 720 - 120, 32), base64Text, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + //---------------------------------------------------------------------------------- + // Module Functions Definition + //---------------------------------------------------------------------------------- + private static uint[] CopyHash(uint* data, int count) + { + if (data == null) return null; + + uint[] result = new uint[count]; + for (int i = 0; i < count; i++) result[i] = data[i]; + return result; + } + + private static string GetDataAsHexText(uint[] data, int dataSize) + { + if ((data != null) && (dataSize > 0) && (dataSize < ((128 / 8) - 1))) + { + StringBuilder text = new StringBuilder(); + for (int i = 0; i < dataSize; i++) text.Append(data[i].ToString("X8")); + return text.ToString(); + } + + return "00000000"; + } + + //---------------------------------------------------------------------------------- + // Minimal raygui-like widgets (plain raylib re-implementation) + //---------------------------------------------------------------------------------- + private static void GuiLabel(Rectangle bounds, string text, int fontSize) + { + DrawText(text, (int)bounds.X, (int)(bounds.Y + (bounds.Height - fontSize) / 2), fontSize, Color.DarkGray); + } + + private static bool GuiButton(Rectangle bounds, string text, int fontSize) + { + bool pressed = false; + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + + Color fill = hover ? (IsMouseButtonDown(MouseButton.Left) ? Color.SkyBlue : Color.LightGray) : Color.RayWhite; + DrawRectangleRec(bounds, fill); + DrawRectangleLinesEx(bounds, 1, hover ? Color.Blue : Color.Gray); + + int textWidth = MeasureText(text, fontSize); + DrawText(text, (int)(bounds.X + (bounds.Width - textWidth) / 2), (int)(bounds.Y + (bounds.Height - fontSize) / 2), fontSize, Color.DarkGray); + + if (hover && IsMouseButtonReleased(MouseButton.Left)) pressed = true; + + return pressed; + } + + private bool GuiTextBox(Rectangle bounds, StringBuilder text, int maxChars, bool editMode, int fontSize) + { + bool pressed = false; + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + + if (editMode) + { + int key = GetCharPressed(); + while (key > 0) + { + if ((key >= 32) && (key <= 125) && (text.Length < maxChars - 1)) + { + text.Append((char)key); + } + + key = GetCharPressed(); + } + + if (IsKeyPressed(KeyboardKey.Backspace) && (text.Length > 0)) text.Remove(text.Length - 1, 1); + } + + DrawRectangleRec(bounds, Color.RayWhite); + DrawRectangleLinesEx(bounds, editMode ? 2 : 1, editMode ? Color.Red : (hover ? Color.Blue : Color.Gray)); + + string content = text.ToString(); + DrawText(content, (int)bounds.X + 4, (int)(bounds.Y + (bounds.Height - fontSize) / 2), fontSize, Color.DarkGray); + + if (editMode && ((framesCounter / 20) % 2 == 0)) + { + DrawText("_", (int)bounds.X + 4 + MeasureText(content, fontSize), (int)(bounds.Y + (bounds.Height - fontSize) / 2), fontSize, Color.DarkGray); + } + + if (hover && IsMouseButtonPressed(MouseButton.Left)) pressed = true; + + return pressed; + } + + private static void GuiTextBoxReadOnly(Rectangle bounds, string text, int fontSize) + { + DrawRectangleRec(bounds, Color.LightGray); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (text != null) DrawText(text, (int)bounds.X + 4, (int)(bounds.Y + (bounds.Height - fontSize) / 2), fontSize, Color.Gray); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - compute hash"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new ComputeHash(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/CustomFrameControl.cs b/Examples/Core/CustomFrameControl.cs new file mode 100644 index 0000000..2526e8a --- /dev/null +++ b/Examples/Core/CustomFrameControl.cs @@ -0,0 +1,188 @@ +/******************************************************************************************* +* +* raylib [core] example - custom frame control +* +* Example complexity rating: [★★★★] 4/4 +* +* NOTE: WARNING: This is an example for advanced users willing to have full control over +* the frame processes. By default, EndDrawing() calls the following processes: +* 1. Draw remaining batch data: rlDrawRenderBatchActive() +* 2. SwapScreenBuffer() +* 3. Frame time control: WaitTime() +* 4. PollInputEvents() +* +* To avoid steps 2, 3 and 4, flag SUPPORT_CUSTOM_FRAME_CONTROL can be enabled in +* config.h (it requires recompiling raylib). This way those steps are up to the user +* +* Note that enabling this flag invalidates some functions: +* - GetFrameTime() +* - SetTargetFPS() +* - GetFPS() +* +* Example originally created with raylib 4.0, last time updated with raylib 4.0 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2021-2025 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +// NOTE: This example is intended to run against a raylib built with SUPPORT_CUSTOM_FRAME_CONTROL, +// where EndDrawing() does NOT swap buffers, wait or poll input, leaving those to the user code +// below. The stock raylib-cs native library is built WITHOUT that flag, so EndDrawing() still +// performs those steps and the manual SwapScreenBuffer()/WaitTime()/PollInputEvents() calls here +// run in addition to them. The port is kept faithful to upstream regardless. +[ExcludeFromBrowser("manual PollInputEvents/SwapScreenBuffer/WaitTime clashes with the emscripten main loop")] +public partial class CustomFrameControl : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Custom Frame Control"; + + public string Title => "raylib [core] example - custom frame control"; + + // Custom timming variables + private double previousTime; // Previous time measure + private double currentTime; // Current time measure + private double updateDrawTime; // Update + Draw time + private double waitTime; // Wait time (if target fps required) + private float deltaTime; // Frame time (Update + Draw + Wait time) + + private float timeCounter; // Accumulative time counter (seconds) + private float position; // Circle position + private bool pause; // Pause control flag + + private int targetFPS; // Our initial target fps + + public void Init() + { + previousTime = GetTime(); + currentTime = 0.0; + updateDrawTime = 0.0; + waitTime = 0.0; + deltaTime = 0.0f; + + timeCounter = 0.0f; + position = 0.0f; + pause = false; + + targetFPS = 60; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- +#if !BROWSER + // NOTE: On non web platforms the PollInputEvents just works before the inputs checks + PollInputEvents(); // Poll input events (SUPPORT_CUSTOM_FRAME_CONTROL) +#endif + + if (IsKeyPressed(KeyboardKey.Space)) pause = !pause; + + if (IsKeyPressed(KeyboardKey.Up)) targetFPS += 20; + else if (IsKeyPressed(KeyboardKey.Down)) targetFPS -= 20; + + if (targetFPS < 0) targetFPS = 0; + + if (!pause) + { + position += 200 * deltaTime; // We move at 200 pixels per second + if (position >= GetScreenWidth()) position = 0; + timeCounter += deltaTime; // We count time (seconds) + } + +#if BROWSER + // NOTE: On web platform for some reason the PollInputEvents only works after the inputs + // check, so just call it after check all your inputs (on web) + PollInputEvents(); // Poll input events (SUPPORT_CUSTOM_FRAME_CONTROL) +#endif + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + for (int i = 0; i < GetScreenWidth() / 200; i++) DrawRectangle(200 * i, 0, 1, GetScreenHeight(), Color.SkyBlue); + + DrawCircle((int)position, GetScreenHeight() / 2 - 25, 50, Color.Red); + + DrawText($"{timeCounter * 1000.0f:000} ms", (int)position - 40, GetScreenHeight() / 2 - 100, 20, Color.Maroon); + DrawText($"PosX: {position:000}", (int)position - 50, GetScreenHeight() / 2 + 40, 20, Color.Black); + + DrawText("Circle is moving at a constant 200 pixels/sec,\nindependently of the frame rate.", 10, 10, 20, Color.DarkGray); + DrawText("PRESS SPACE to PAUSE MOVEMENT", 10, GetScreenHeight() - 60, 20, Color.Gray); + DrawText("PRESS UP | DOWN to CHANGE TARGET FPS", 10, GetScreenHeight() - 30, 20, Color.Gray); + DrawText($"TARGET FPS: {targetFPS}", GetScreenWidth() - 220, 10, 20, Color.Lime); + if (deltaTime != 0) + { + DrawText($"CURRENT FPS: {(int)(1.0f / deltaTime)}", GetScreenWidth() - 220, 40, 20, Color.Green); + } + + EndDrawing(); + + // NOTE: In case raylib is configured to SUPPORT_CUSTOM_FRAME_CONTROL, + // Events polling, screen buffer swap and frame time control must be managed by the user + + SwapScreenBuffer(); // Flip the back buffer to screen (front buffer) + + currentTime = GetTime(); + updateDrawTime = currentTime - previousTime; + + if (targetFPS > 0) // We want a fixed frame rate + { + waitTime = (1.0f / (float)targetFPS) - updateDrawTime; + if (waitTime > 0.0) + { + WaitTime((float)waitTime); + currentTime = GetTime(); + deltaTime = (float)(currentTime - previousTime); + } + } + else deltaTime = (float)updateDrawTime; // Framerate could be variable + + previousTime = currentTime; + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - custom frame control"); + + // NOTE: Not calling SetTargetFPS(): this example manages frame timing manually with + // WaitTime() (SetTargetFPS/GetFrameTime/GetFPS are invalidated by SUPPORT_CUSTOM_FRAME_CONTROL) + //-------------------------------------------------------------------------------------- + + var game = new CustomFrameControl(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/DirectoryFiles.cs b/Examples/Core/DirectoryFiles.cs new file mode 100644 index 0000000..2f7cff3 --- /dev/null +++ b/Examples/Core/DirectoryFiles.cs @@ -0,0 +1,205 @@ +/******************************************************************************************* +* +* raylib [core] example - directory files +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.6 +* +* Example contributed by Hugo ARNAL (@hugoarnal) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Hugo ARNAL (@hugoarnal) +* +********************************************************************************************/ + +// NOTE: The original example relies on raygui (GuiButton, GuiLabel, GuiListViewEx) for its UI. +// raygui is not part of the raylib-cs bindings, so the back button, directory label and file +// list view are reimplemented here with plain raylib primitives. The directory navigation +// behaviour (enter directories, go back) is preserved. + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +public partial class DirectoryFiles : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const string FileFilter = "DIRS*;.png;.c"; + + public string Name => "Core / Directory Files"; + + public string Title => "raylib [core] example - directory files"; + + private string directory; + private FilePathList files; + + private bool btnBackPressed; + + private int listScrollIndex; + private int listItemActive; + private int listItemFocused; + + public void Init() + { + directory = GetWorkingDirectoryAsString(); + + // Load file-paths on current working directory + // NOTE: LoadDirectoryFiles() loads files and directories by default, + // use LoadDirectoryFilesEx() for custom filters and recursive directories loading + //files = LoadDirectoryFiles(directory); + files = LoadDirectoryFilesEx(directory, FileFilter, false); + + btnBackPressed = false; + + listScrollIndex = 0; + listItemActive = -1; + listItemFocused = -1; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (btnBackPressed) + { + directory = GetPrevDirectoryPath(directory); + UnloadDirectoryFiles(files); + files = LoadDirectoryFilesEx(directory, FileFilter, false); + + listScrollIndex = 0; + listItemActive = -1; + listItemFocused = -1; + } + + if ((listItemActive >= 0) && (listItemActive < (int)files.Count)) + { + string selected = files[(uint)listItemActive]; + bool isDirectory = DirectoryExists(selected); + if (isDirectory) + { + directory = selected; + UnloadDirectoryFiles(files); + files = LoadDirectoryFilesEx(directory, FileFilter, false); + + listScrollIndex = 0; + listItemActive = -1; + listItemFocused = -1; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + Vector2 mouse = GetMousePosition(); + + // Back button "<" + Rectangle backBounds = new Rectangle(40.0f, 10.0f, 48, 28); + bool backHover = CheckCollisionPointRec(mouse, backBounds); + DrawRectangleRec(backBounds, backHover ? Color.SkyBlue : Color.LightGray); + DrawRectangleLinesEx(backBounds, 1, Color.Gray); + int backTextWidth = MeasureText("<", 20); + DrawText("<", (int)(backBounds.X + (backBounds.Width - backTextWidth) / 2), (int)(backBounds.Y + 4), 20, Color.DarkGray); + btnBackPressed = backHover && IsMouseButtonReleased(MouseButton.Left); + + // Current directory label + DrawText(directory, 40 + 48 + 10, 16, 20, Color.DarkGray); + + // File list view + Rectangle listBounds = new Rectangle(0, 50, GetScreenWidth(), GetScreenHeight() - 50); + DrawRectangleRec(listBounds, Color.RayWhite); + DrawRectangleLinesEx(listBounds, 1, Color.Gray); + + int count = (int)files.Count; + float rowHeight = 28; + int visibleRows = (int)(listBounds.Height / rowHeight); + + bool mouseInList = CheckCollisionPointRec(mouse, listBounds); + if (mouseInList) + { + listScrollIndex -= (int)GetMouseWheelMove(); + } + int maxScroll = Math.Max(0, count - visibleRows); + listScrollIndex = Math.Clamp(listScrollIndex, 0, maxScroll); + + listItemFocused = -1; + for (int i = 0; i < visibleRows; i++) + { + int itemIndex = listScrollIndex + i; + if (itemIndex >= count) + { + break; + } + + Rectangle rowRec = new Rectangle(listBounds.X + 1, listBounds.Y + 1 + i * rowHeight, listBounds.Width - 2, rowHeight); + bool rowHover = mouseInList && CheckCollisionPointRec(mouse, rowRec); + + if (itemIndex == listItemActive) + { + DrawRectangleRec(rowRec, Fade(Color.SkyBlue, 0.7f)); + } + else if (rowHover) + { + DrawRectangleRec(rowRec, Fade(Color.SkyBlue, 0.3f)); + } + + if (rowHover) + { + listItemFocused = itemIndex; + if (IsMouseButtonReleased(MouseButton.Left)) + { + listItemActive = itemIndex; + } + } + + DrawText(files[(uint)itemIndex], (int)rowRec.X + 40, (int)rowRec.Y + 8, 10, Color.DarkGray); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + // De-Initialization + //-------------------------------------------------------------------------------------- + UnloadDirectoryFiles(files); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - directory files"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new DirectoryFiles(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/DropFiles.cs b/Examples/Core/DropFiles.cs index c0b5a86..c4dcca6 100644 --- a/Examples/Core/DropFiles.cs +++ b/Examples/Core/DropFiles.cs @@ -20,6 +20,7 @@ using static Raylib_cs.Raylib; namespace Examples.Core; +[ExcludeFromBrowser] public partial class DropFiles : IExample { private const int screenWidth = 800; diff --git a/Examples/Core/HighDpiDemo.cs b/Examples/Core/HighDpiDemo.cs new file mode 100644 index 0000000..2b1ee9b --- /dev/null +++ b/Examples/Core/HighDpiDemo.cs @@ -0,0 +1,173 @@ +/******************************************************************************************* +* +* raylib [core] example - highdpi demo +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.0, last time updated with raylib 5.5 +* +* Example contributed by Jonathan Marler (@marler8997) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Jonathan Marler (@marler8997) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +[ExcludeFromBrowser("monitor/DPI APIs are meaningless on the fixed wasm canvas")] +public partial class HighDpiDemo : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / High DPI Demo"; + + public string Title => "raylib [core] example - highdpi demo"; + + public ConfigFlags ConfigFlags => ConfigFlags.HighDpiWindow | ConfigFlags.ResizableWindow; + + private int logicalGridDescY; + private int logicalGridLabelY; + private int logicalGridTop; + private int logicalGridBottom; + private int pixelGridTop; + private int pixelGridBottom; + private int pixelGridLabelY; + private int pixelGridDescY; + private int cellSize; + private float cellSizePx; + + public void Init() + { + SetWindowMinSize(450, 450); + + logicalGridDescY = 120; + logicalGridLabelY = logicalGridDescY + 30; + logicalGridTop = logicalGridLabelY + 30; + logicalGridBottom = logicalGridTop + 80; + pixelGridTop = logicalGridBottom - 20; + pixelGridBottom = pixelGridTop + 80; + pixelGridLabelY = pixelGridBottom + 30; + pixelGridDescY = pixelGridLabelY + 30; + cellSize = 50; + cellSizePx = (float)cellSize; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + int monitorCount = GetMonitorCount(); + + if ((monitorCount > 1) && IsKeyPressed(KeyboardKey.N)) + { + SetWindowMonitor((GetCurrentMonitor() + 1) % monitorCount); + } + + int currentMonitor = GetCurrentMonitor(); + Vector2 dpiScale = GetWindowScaleDPI(); + cellSizePx = ((float)cellSize) / dpiScale.X; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + int windowCenter = GetScreenWidth() / 2; + DrawTextCenter($"Dpi Scale: {dpiScale.X:F6}", windowCenter, 30, 40, Color.DarkGray); + DrawTextCenter($"Monitor: {currentMonitor + 1}/{monitorCount} ([N] next monitor)", windowCenter, 70, 20, Color.LightGray); + DrawTextCenter($"Window is {GetScreenWidth()} \"logical points\" wide", windowCenter, logicalGridDescY, 20, Color.Orange); + + bool odd = true; + for (int i = cellSize; i < GetScreenWidth(); i += cellSize, odd = !odd) + { + if (odd) + { + DrawRectangle(i, logicalGridTop, cellSize, logicalGridBottom - logicalGridTop, Color.Orange); + } + + DrawTextCenter($"{i}", i, logicalGridLabelY, 10, Color.LightGray); + DrawLine(i, logicalGridLabelY + 10, i, logicalGridBottom, Color.Gray); + } + + odd = true; + const int minTextSpace = 30; + int lastTextX = -minTextSpace; + for (int i = cellSize; i < GetRenderWidth(); i += cellSize, odd = !odd) + { + int x = (int)(((float)i) / dpiScale.X); + if (odd) + { + DrawRectangle(x, pixelGridTop, (int)cellSizePx, pixelGridBottom - pixelGridTop, new Color(0, 121, 241, 100)); + } + + DrawLine(x, pixelGridTop, (int)(((float)i) / dpiScale.X), pixelGridLabelY - 10, Color.Gray); + + if ((x - lastTextX) >= minTextSpace) + { + DrawTextCenter($"{i}", x, pixelGridLabelY, 10, Color.LightGray); + lastTextX = x; + } + } + + DrawTextCenter($"Window is {GetRenderWidth()} \"physical pixels\" wide", windowCenter, pixelGridDescY, 20, Color.Blue); + + string text = "Can you see this?"; + Vector2 size = MeasureTextEx(GetFontDefault(), text, 20, 3); + Vector2 pos = new Vector2(GetScreenWidth() - size.X - 5, GetScreenHeight() - size.Y - 5); + DrawTextEx(GetFontDefault(), text, pos, 20, 3, Color.LightGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + // Draw text centered on the given position + private static void DrawTextCenter(string text, int x, int y, int fontSize, Color color) + { + Vector2 size = MeasureTextEx(GetFontDefault(), text, (float)fontSize, 3); + Vector2 pos = new Vector2(x - size.X / 2, y - size.Y / 2); + DrawTextEx(GetFontDefault(), text, pos, (float)fontSize, 3, color); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.HighDpiWindow | ConfigFlags.ResizableWindow); + InitWindow(screenWidth, screenHeight, "raylib [core] example - highdpi demo"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new HighDpiDemo(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/HighDpiTestbed.cs b/Examples/Core/HighDpiTestbed.cs new file mode 100644 index 0000000..43cb649 --- /dev/null +++ b/Examples/Core/HighDpiTestbed.cs @@ -0,0 +1,145 @@ +/******************************************************************************************* +* +* raylib [core] example - highdpi testbed +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Ramon Santamaria (@raysan5) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +[ExcludeFromBrowser("fullscreen/borderless/monitor APIs are meaningless on the fixed wasm canvas")] +public partial class HighDpiTestbed : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / High DPI Testbed"; + + public string Title => "raylib [core] example - highdpi testbed"; + + public ConfigFlags ConfigFlags => ConfigFlags.ResizableWindow | ConfigFlags.HighDpiWindow; + + private Vector2 scaleDpi; + private Vector2 mousePos; + private int currentMonitor; + private Vector2 windowPos; + + private int gridSpacing; // Grid spacing in pixels + + public void Init() + { + scaleDpi = GetWindowScaleDPI(); + mousePos = GetMousePosition(); + currentMonitor = GetCurrentMonitor(); + windowPos = GetWindowPosition(); + + gridSpacing = 40; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + mousePos = GetMousePosition(); + currentMonitor = GetCurrentMonitor(); + scaleDpi = GetWindowScaleDPI(); + windowPos = GetWindowPosition(); + + if (IsKeyPressed(KeyboardKey.Space)) + { + ToggleBorderlessWindowed(); + } + if (IsKeyPressed(KeyboardKey.F)) + { + ToggleFullscreen(); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + // Draw grid + for (int h = 0; h < GetScreenHeight() / gridSpacing + 1; h++) + { + DrawText($"{h * gridSpacing:D2}", 4, h * gridSpacing - 4, 10, Color.Gray); + DrawLine(24, h * gridSpacing, GetScreenWidth(), h * gridSpacing, Color.LightGray); + } + for (int v = 0; v < GetScreenWidth() / gridSpacing + 1; v++) + { + DrawText($"{v * gridSpacing:D2}", v * gridSpacing - 10, 4, 10, Color.Gray); + DrawLine(v * gridSpacing, 20, v * gridSpacing, GetScreenHeight(), Color.LightGray); + } + + // Draw UI info + DrawText($"CURRENT MONITOR: {currentMonitor + 1}/{GetMonitorCount()} ({GetMonitorWidth(currentMonitor)}x{GetMonitorHeight(currentMonitor)})", 50, 50, 20, Color.DarkGray); + DrawText($"WINDOW POSITION: {(int)windowPos.X}x{(int)windowPos.Y}", 50, 90, 20, Color.DarkGray); + DrawText($"SCREEN SIZE: {GetScreenWidth()}x{GetScreenHeight()}", 50, 130, 20, Color.DarkGray); + DrawText($"RENDER SIZE: {GetRenderWidth()}x{GetRenderHeight()}", 50, 170, 20, Color.DarkGray); + DrawText($"SCALE FACTOR: {scaleDpi.X:F2}x{scaleDpi.Y:F2}", 50, 210, 20, Color.Gray); + + // Draw reference rectangles, top-left and bottom-right corners + DrawRectangle(0, 0, 30, 60, Color.Red); + DrawRectangle(GetScreenWidth() - 30, GetScreenHeight() - 60, 30, 60, Color.Blue); + + // Draw mouse position + DrawCircleV(GetMousePosition(), 20, Color.Maroon); + DrawRectangleRec(new Rectangle(mousePos.X - 25, mousePos.Y, 50, 2), Color.Black); + DrawRectangleRec(new Rectangle(mousePos.X, mousePos.Y - 25, 2, 50), Color.Black); + DrawText($"[{GetMouseX()},{GetMouseY()}]", (int)mousePos.X - 44, + (mousePos.Y > GetScreenHeight() - 60) ? (int)mousePos.Y - 46 : (int)mousePos.Y + 30, 20, Color.Black); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + // TODO: Unload all loaded resources at this point + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.ResizableWindow | ConfigFlags.HighDpiWindow); + InitWindow(screenWidth, screenHeight, "raylib [core] example - highdpi testbed"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new HighDpiTestbed(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/InputActions.cs b/Examples/Core/InputActions.cs new file mode 100644 index 0000000..6b30287 --- /dev/null +++ b/Examples/Core/InputActions.cs @@ -0,0 +1,224 @@ +/******************************************************************************************* +* +* raylib [core] example - input actions +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.6 +* +* Example contributed by Jett (@JettMonstersGoBoom) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Jett (@JettMonstersGoBoom) +* +********************************************************************************************/ + +// Simple example for decoding input as actions, allowing remapping of input to different keys or gamepad buttons +// For example instead of using `IsKeyDown(KEY_LEFT)`, you can use `IsActionDown(ACTION_LEFT)` +// which can be reassigned to e.g. KEY_A and also assigned to a gamepad button. the action will trigger with either gamepad or keys + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +public partial class InputActions : IExample +{ + //---------------------------------------------------------------------------------- + // Types and Structures Definition + //---------------------------------------------------------------------------------- + private enum ActionType + { + NoAction = 0, + ActionUp, + ActionDown, + ActionLeft, + ActionRight, + ActionFire, + MaxAction + } + + // Key and button inputs + private struct ActionInput + { + public KeyboardKey Key; + public GamepadButton Button; + } + + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Input Actions"; + + public string Title => "raylib [core] example - input actions"; + + private int gamepadIndex; // Gamepad default index + private ActionInput[] actionInputs; + + private int actionSet; + private bool releaseAction; + private Vector2 position; + private Vector2 size; + + public void Init() + { + gamepadIndex = 0; + actionInputs = new ActionInput[(int)ActionType.MaxAction]; + + // Set default actions + actionSet = 0; + SetActionsDefault(); + releaseAction = false; + + position = new Vector2(400.0f, 200.0f); + size = new Vector2(40.0f, 40.0f); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + gamepadIndex = 0; // Set gamepad being checked + + if (IsActionDown(ActionType.ActionUp)) position.Y -= 2; + if (IsActionDown(ActionType.ActionDown)) position.Y += 2; + if (IsActionDown(ActionType.ActionLeft)) position.X -= 2; + if (IsActionDown(ActionType.ActionRight)) position.X += 2; + if (IsActionPressed(ActionType.ActionFire)) + { + position.X = (screenWidth - size.X) / 2; + position.Y = (screenHeight - size.Y) / 2; + } + + // Register release action for one frame + releaseAction = false; + if (IsActionReleased(ActionType.ActionFire)) releaseAction = true; + + // Switch control scheme by pressing TAB + if (IsKeyPressed(KeyboardKey.Tab)) + { + actionSet = (actionSet == 0) ? 1 : 0; + if (actionSet == 0) SetActionsDefault(); + else SetActionsCursor(); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.Gray); + + DrawRectangleV(position, size, releaseAction ? Color.Blue : Color.Red); + + DrawText((actionSet == 0) ? "Current input set: WASD (default)" : "Current input set: Arrow keys", 10, 10, 20, Color.White); + DrawText("Use TAB key to toggles Actions keyset", 10, 50, 20, Color.Green); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + //---------------------------------------------------------------------------------- + // Module Functions Definition + //---------------------------------------------------------------------------------- + // Check action key/button pressed + // NOTE: Combines key pressed and gamepad button pressed in one action + private bool IsActionPressed(ActionType action) + { + bool result = false; + + if (action < ActionType.MaxAction) result = (IsKeyPressed(actionInputs[(int)action].Key) || IsGamepadButtonPressed(gamepadIndex, actionInputs[(int)action].Button)); + + return result; + } + + // Check action key/button released + // NOTE: Combines key released and gamepad button released in one action + private bool IsActionReleased(ActionType action) + { + bool result = false; + + if (action < ActionType.MaxAction) result = (IsKeyReleased(actionInputs[(int)action].Key) || IsGamepadButtonReleased(gamepadIndex, actionInputs[(int)action].Button)); + + return result; + } + + // Check action key/button down + // NOTE: Combines key down and gamepad button down in one action + private bool IsActionDown(ActionType action) + { + bool result = false; + + if (action < ActionType.MaxAction) result = (IsKeyDown(actionInputs[(int)action].Key) || IsGamepadButtonDown(gamepadIndex, actionInputs[(int)action].Button)); + + return result; + } + + // Set the "default" keyset + // NOTE: Here WASD and gamepad buttons on the left side for movement + private void SetActionsDefault() + { + actionInputs[(int)ActionType.ActionUp].Key = KeyboardKey.W; + actionInputs[(int)ActionType.ActionDown].Key = KeyboardKey.S; + actionInputs[(int)ActionType.ActionLeft].Key = KeyboardKey.A; + actionInputs[(int)ActionType.ActionRight].Key = KeyboardKey.D; + actionInputs[(int)ActionType.ActionFire].Key = KeyboardKey.Space; + + actionInputs[(int)ActionType.ActionUp].Button = GamepadButton.LeftFaceUp; + actionInputs[(int)ActionType.ActionDown].Button = GamepadButton.LeftFaceDown; + actionInputs[(int)ActionType.ActionLeft].Button = GamepadButton.LeftFaceLeft; + actionInputs[(int)ActionType.ActionRight].Button = GamepadButton.LeftFaceRight; + actionInputs[(int)ActionType.ActionFire].Button = GamepadButton.RightFaceDown; + } + + // Set the "alternate" keyset + // NOTE: Here cursor keys and gamepad buttons on the right side for movement + private void SetActionsCursor() + { + actionInputs[(int)ActionType.ActionUp].Key = KeyboardKey.Up; + actionInputs[(int)ActionType.ActionDown].Key = KeyboardKey.Down; + actionInputs[(int)ActionType.ActionLeft].Key = KeyboardKey.Left; + actionInputs[(int)ActionType.ActionRight].Key = KeyboardKey.Right; + actionInputs[(int)ActionType.ActionFire].Key = KeyboardKey.Space; + + actionInputs[(int)ActionType.ActionUp].Button = GamepadButton.RightFaceUp; + actionInputs[(int)ActionType.ActionDown].Button = GamepadButton.RightFaceDown; + actionInputs[(int)ActionType.ActionLeft].Button = GamepadButton.RightFaceLeft; + actionInputs[(int)ActionType.ActionRight].Button = GamepadButton.RightFaceRight; + actionInputs[(int)ActionType.ActionFire].Button = GamepadButton.LeftFaceDown; + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - input actions"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new InputActions(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/KeyboardTestbed.cs b/Examples/Core/KeyboardTestbed.cs new file mode 100644 index 0000000..ad413da --- /dev/null +++ b/Examples/Core/KeyboardTestbed.cs @@ -0,0 +1,374 @@ +/******************************************************************************************* +* +* raylib [core] example - keyboard testbed +* +* Example complexity rating: [★★☆☆] 2/4 +* +* NOTE: raylib defined keys refer to ENG-US Keyboard layout, +* mapping to other layouts is up to the user +* +* Example originally created with raylib 5.6, last time updated with raylib 5.6 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2026 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +public partial class KeyboardTestbed : IExample +{ + private const int KeyRecSpacing = 4; // Space in pixels between key rectangles + + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Keyboard Testbed"; + + public string Title => "raylib [core] example - keyboard testbed"; + + private int[] line01KeyWidths; + private int[] line01Keys; + private int[] line02KeyWidths; + private int[] line02Keys; + private int[] line03KeyWidths; + private int[] line03Keys; + private int[] line04KeyWidths; + private int[] line04Keys; + private int[] line05KeyWidths; + private int[] line05Keys; + private int[] line06KeyWidths; + private int[] line06Keys; + + private Vector2 keyboardOffset; + + public void Init() + { + SetExitKey(KeyboardKey.Null); // Avoid exit on KEY_ESCAPE + + // Keyboard line 01 + line01KeyWidths = new int[15]; + for (int i = 0; i < 15; i++) line01KeyWidths[i] = 45; + line01KeyWidths[13] = 62; // PRINTSCREEN + line01Keys = new int[] + { + (int)KeyboardKey.Escape, (int)KeyboardKey.F1, (int)KeyboardKey.F2, (int)KeyboardKey.F3, (int)KeyboardKey.F4, (int)KeyboardKey.F5, + (int)KeyboardKey.F6, (int)KeyboardKey.F7, (int)KeyboardKey.F8, (int)KeyboardKey.F9, (int)KeyboardKey.F10, (int)KeyboardKey.F11, + (int)KeyboardKey.F12, (int)KeyboardKey.PrintScreen, (int)KeyboardKey.Pause + }; + + // Keyboard line 02 + line02KeyWidths = new int[15]; + for (int i = 0; i < 15; i++) line02KeyWidths[i] = 45; + line02KeyWidths[0] = 25; // GRAVE + line02KeyWidths[13] = 82; // BACKSPACE + line02Keys = new int[] + { + (int)KeyboardKey.Grave, (int)KeyboardKey.One, (int)KeyboardKey.Two, (int)KeyboardKey.Three, (int)KeyboardKey.Four, + (int)KeyboardKey.Five, (int)KeyboardKey.Six, (int)KeyboardKey.Seven, (int)KeyboardKey.Eight, (int)KeyboardKey.Nine, + (int)KeyboardKey.Zero, (int)KeyboardKey.Minus, (int)KeyboardKey.Equal, (int)KeyboardKey.Backspace, (int)KeyboardKey.Delete + }; + + // Keyboard line 03 + line03KeyWidths = new int[15]; + for (int i = 0; i < 15; i++) line03KeyWidths[i] = 45; + line03KeyWidths[0] = 50; // TAB + line03KeyWidths[13] = 57; // BACKSLASH + line03Keys = new int[] + { + (int)KeyboardKey.Tab, (int)KeyboardKey.Q, (int)KeyboardKey.W, (int)KeyboardKey.E, (int)KeyboardKey.R, (int)KeyboardKey.T, (int)KeyboardKey.Y, + (int)KeyboardKey.U, (int)KeyboardKey.I, (int)KeyboardKey.O, (int)KeyboardKey.P, (int)KeyboardKey.LeftBracket, + (int)KeyboardKey.RightBracket, (int)KeyboardKey.Backslash, (int)KeyboardKey.Insert + }; + + // Keyboard line 04 + line04KeyWidths = new int[14]; + for (int i = 0; i < 14; i++) line04KeyWidths[i] = 45; + line04KeyWidths[0] = 68; // CAPS + line04KeyWidths[12] = 88; // ENTER + line04Keys = new int[] + { + (int)KeyboardKey.CapsLock, (int)KeyboardKey.A, (int)KeyboardKey.S, (int)KeyboardKey.D, (int)KeyboardKey.F, (int)KeyboardKey.G, + (int)KeyboardKey.H, (int)KeyboardKey.J, (int)KeyboardKey.K, (int)KeyboardKey.L, (int)KeyboardKey.Semicolon, + (int)KeyboardKey.Apostrophe, (int)KeyboardKey.Enter, (int)KeyboardKey.PageUp + }; + + // Keyboard line 05 + line05KeyWidths = new int[14]; + for (int i = 0; i < 14; i++) line05KeyWidths[i] = 45; + line05KeyWidths[0] = 80; // LSHIFT + line05KeyWidths[11] = 76; // RSHIFT + line05Keys = new int[] + { + (int)KeyboardKey.LeftShift, (int)KeyboardKey.Z, (int)KeyboardKey.X, (int)KeyboardKey.C, (int)KeyboardKey.V, (int)KeyboardKey.B, + (int)KeyboardKey.N, (int)KeyboardKey.M, (int)KeyboardKey.Comma, (int)KeyboardKey.Period, /*KEY_MINUS*/ + (int)KeyboardKey.Slash, (int)KeyboardKey.RightShift, (int)KeyboardKey.Up, (int)KeyboardKey.PageDown + }; + + // Keyboard line 06 + line06KeyWidths = new int[11]; + for (int i = 0; i < 11; i++) line06KeyWidths[i] = 45; + line06KeyWidths[0] = 80; // LCTRL + line06KeyWidths[3] = 208; // SPACE + line06KeyWidths[7] = 60; // RCTRL + line06Keys = new int[] + { + (int)KeyboardKey.LeftControl, (int)KeyboardKey.LeftSuper, (int)KeyboardKey.LeftAlt, + (int)KeyboardKey.Space, (int)KeyboardKey.RightAlt, 162, (int)KeyboardKey.Null, + (int)KeyboardKey.RightControl, (int)KeyboardKey.Left, (int)KeyboardKey.Down, (int)KeyboardKey.Right + }; + + keyboardOffset = new Vector2(26, 80); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + int key = GetKeyPressed(); // Get pressed keycode + if (key > 0) TraceLog(TraceLogLevel.Info, $"KEYBOARD TESTBED: KEY PRESSED: {key}"); + + int ch = GetCharPressed(); // Get pressed char for text input, using OS mapping + if (ch > 0) TraceLog(TraceLogLevel.Info, $"KEYBOARD TESTBED: CHAR PRESSED: {(char)ch} ({ch})"); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + DrawText("KEYBOARD LAYOUT: ENG-US", 26, 38, 20, Color.LightGray); + + // Keyboard line 01 - 15 keys + // ESC, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, IMP, CLOSE + for (int i = 0, recOffsetX = 0; i < 15; i++) + { + GuiKeyboardKey(new Rectangle(keyboardOffset.X + recOffsetX, keyboardOffset.Y, (float)line01KeyWidths[i], 30.0f), line01Keys[i]); + recOffsetX += line01KeyWidths[i] + KeyRecSpacing; + } + + // Keyboard line 02 - 15 keys + // `, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, -, =, BACKSPACE, DEL + for (int i = 0, recOffsetX = 0; i < 15; i++) + { + GuiKeyboardKey(new Rectangle(keyboardOffset.X + recOffsetX, keyboardOffset.Y + 30 + KeyRecSpacing, (float)line02KeyWidths[i], 38.0f), line02Keys[i]); + recOffsetX += line02KeyWidths[i] + KeyRecSpacing; + } + + // Keyboard line 03 - 15 keys + // TAB, Q, W, E, R, T, Y, U, I, O, P, [, ], \, INS + for (int i = 0, recOffsetX = 0; i < 15; i++) + { + GuiKeyboardKey(new Rectangle(keyboardOffset.X + recOffsetX, keyboardOffset.Y + 30 + 38 + KeyRecSpacing * 2, (float)line03KeyWidths[i], 38.0f), line03Keys[i]); + recOffsetX += line03KeyWidths[i] + KeyRecSpacing; + } + + // Keyboard line 04 - 14 keys + // MAYUS, A, S, D, F, G, H, J, K, L, ;, ', ENTER, REPAG + for (int i = 0, recOffsetX = 0; i < 14; i++) + { + GuiKeyboardKey(new Rectangle(keyboardOffset.X + recOffsetX, keyboardOffset.Y + 30 + 38 * 2 + KeyRecSpacing * 3, (float)line04KeyWidths[i], 38.0f), line04Keys[i]); + recOffsetX += line04KeyWidths[i] + KeyRecSpacing; + } + + // Keyboard line 05 - 14 keys + // LSHIFT, Z, X, C, V, B, N, M, ,, ., /, RSHIFT, UP, AVPAG + for (int i = 0, recOffsetX = 0; i < 14; i++) + { + GuiKeyboardKey(new Rectangle(keyboardOffset.X + recOffsetX, keyboardOffset.Y + 30 + 38 * 3 + KeyRecSpacing * 4, (float)line05KeyWidths[i], 38.0f), line05Keys[i]); + recOffsetX += line05KeyWidths[i] + KeyRecSpacing; + } + + // Keyboard line 06 - 11 keys + // LCTRL, WIN, LALT, SPACE, ALTGR, \, FN, RCTRL, LEFT, DOWN, RIGHT + for (int i = 0, recOffsetX = 0; i < 11; i++) + { + GuiKeyboardKey(new Rectangle(keyboardOffset.X + recOffsetX, keyboardOffset.Y + 30 + 38 * 4 + KeyRecSpacing * 5, (float)line06KeyWidths[i], 38.0f), line06Keys[i]); + recOffsetX += line06KeyWidths[i] + KeyRecSpacing; + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + //------------------------------------------------------------------------------------ + // Module Functions Definition + //------------------------------------------------------------------------------------ + // Get keyboard keycode as text (US keyboard) + // NOTE: Mapping for other keyboard layouts can be done here + private static string GetKeyText(int key) + { + switch ((KeyboardKey)key) + { + case KeyboardKey.Apostrophe: return "'"; // Key: ' + case KeyboardKey.Comma: return ","; // Key: , + case KeyboardKey.Minus: return "-"; // Key: - + case KeyboardKey.Period: return "."; // Key: . + case KeyboardKey.Slash: return "/"; // Key: / + case KeyboardKey.Zero: return "0"; // Key: 0 + case KeyboardKey.One: return "1"; // Key: 1 + case KeyboardKey.Two: return "2"; // Key: 2 + case KeyboardKey.Three: return "3"; // Key: 3 + case KeyboardKey.Four: return "4"; // Key: 4 + case KeyboardKey.Five: return "5"; // Key: 5 + case KeyboardKey.Six: return "6"; // Key: 6 + case KeyboardKey.Seven: return "7"; // Key: 7 + case KeyboardKey.Eight: return "8"; // Key: 8 + case KeyboardKey.Nine: return "9"; // Key: 9 + case KeyboardKey.Semicolon: return ";"; // Key: ; + case KeyboardKey.Equal: return "="; // Key: = + case KeyboardKey.A: return "A"; // Key: A | a + case KeyboardKey.B: return "B"; // Key: B | b + case KeyboardKey.C: return "C"; // Key: C | c + case KeyboardKey.D: return "D"; // Key: D | d + case KeyboardKey.E: return "E"; // Key: E | e + case KeyboardKey.F: return "F"; // Key: F | f + case KeyboardKey.G: return "G"; // Key: G | g + case KeyboardKey.H: return "H"; // Key: H | h + case KeyboardKey.I: return "I"; // Key: I | i + case KeyboardKey.J: return "J"; // Key: J | j + case KeyboardKey.K: return "K"; // Key: K | k + case KeyboardKey.L: return "L"; // Key: L | l + case KeyboardKey.M: return "M"; // Key: M | m + case KeyboardKey.N: return "N"; // Key: N | n + case KeyboardKey.O: return "O"; // Key: O | o + case KeyboardKey.P: return "P"; // Key: P | p + case KeyboardKey.Q: return "Q"; // Key: Q | q + case KeyboardKey.R: return "R"; // Key: R | r + case KeyboardKey.S: return "S"; // Key: S | s + case KeyboardKey.T: return "T"; // Key: T | t + case KeyboardKey.U: return "U"; // Key: U | u + case KeyboardKey.V: return "V"; // Key: V | v + case KeyboardKey.W: return "W"; // Key: W | w + case KeyboardKey.X: return "X"; // Key: X | x + case KeyboardKey.Y: return "Y"; // Key: Y | y + case KeyboardKey.Z: return "Z"; // Key: Z | z + case KeyboardKey.LeftBracket: return "["; // Key: [ + case KeyboardKey.Backslash: return "\\"; // Key: '\' + case KeyboardKey.RightBracket: return "]"; // Key: ] + case KeyboardKey.Grave: return "`"; // Key: ` + case KeyboardKey.Space: return "SPACE"; // Key: Space + case KeyboardKey.Escape: return "ESC"; // Key: Esc + case KeyboardKey.Enter: return "ENTER"; // Key: Enter + case KeyboardKey.Tab: return "TAB"; // Key: Tab + case KeyboardKey.Backspace: return "BACK"; // Key: Backspace + case KeyboardKey.Insert: return "INS"; // Key: Ins + case KeyboardKey.Delete: return "DEL"; // Key: Del + case KeyboardKey.Right: return "RIGHT"; // Key: Cursor right + case KeyboardKey.Left: return "LEFT"; // Key: Cursor left + case KeyboardKey.Down: return "DOWN"; // Key: Cursor down + case KeyboardKey.Up: return "UP"; // Key: Cursor up + case KeyboardKey.PageUp: return "PGUP"; // Key: Page up + case KeyboardKey.PageDown: return "PGDOWN"; // Key: Page down + case KeyboardKey.Home: return "HOME"; // Key: Home + case KeyboardKey.End: return "END"; // Key: End + case KeyboardKey.CapsLock: return "CAPS"; // Key: Caps lock + case KeyboardKey.ScrollLock: return "LOCK"; // Key: Scroll down + case KeyboardKey.NumLock: return "NUMLOCK"; // Key: Num lock + case KeyboardKey.PrintScreen: return "PRINTSCR"; // Key: Print screen + case KeyboardKey.Pause: return "PAUSE"; // Key: Pause + case KeyboardKey.F1: return "F1"; // Key: F1 + case KeyboardKey.F2: return "F2"; // Key: F2 + case KeyboardKey.F3: return "F3"; // Key: F3 + case KeyboardKey.F4: return "F4"; // Key: F4 + case KeyboardKey.F5: return "F5"; // Key: F5 + case KeyboardKey.F6: return "F6"; // Key: F6 + case KeyboardKey.F7: return "F7"; // Key: F7 + case KeyboardKey.F8: return "F8"; // Key: F8 + case KeyboardKey.F9: return "F9"; // Key: F9 + case KeyboardKey.F10: return "F10"; // Key: F10 + case KeyboardKey.F11: return "F11"; // Key: F11 + case KeyboardKey.F12: return "F12"; // Key: F12 + case KeyboardKey.LeftShift: return "LSHIFT"; // Key: Shift left + case KeyboardKey.LeftControl: return "LCTRL"; // Key: Control left + case KeyboardKey.LeftAlt: return "LALT"; // Key: Alt left + case KeyboardKey.LeftSuper: return "WIN"; // Key: Super left + case KeyboardKey.RightShift: return "RSHIFT"; // Key: Shift right + case KeyboardKey.RightControl: return "RCTRL"; // Key: Control right + case KeyboardKey.RightAlt: return "ALTGR"; // Key: Alt right + case KeyboardKey.RightSuper: return "RSUPER"; // Key: Super right + case KeyboardKey.KeyboardMenu: return "KBMENU"; // Key: KB menu + case KeyboardKey.Kp0: return "KP0"; // Key: Keypad 0 + case KeyboardKey.Kp1: return "KP1"; // Key: Keypad 1 + case KeyboardKey.Kp2: return "KP2"; // Key: Keypad 2 + case KeyboardKey.Kp3: return "KP3"; // Key: Keypad 3 + case KeyboardKey.Kp4: return "KP4"; // Key: Keypad 4 + case KeyboardKey.Kp5: return "KP5"; // Key: Keypad 5 + case KeyboardKey.Kp6: return "KP6"; // Key: Keypad 6 + case KeyboardKey.Kp7: return "KP7"; // Key: Keypad 7 + case KeyboardKey.Kp8: return "KP8"; // Key: Keypad 8 + case KeyboardKey.Kp9: return "KP9"; // Key: Keypad 9 + case KeyboardKey.KpDecimal: return "KPDEC"; // Key: Keypad . + case KeyboardKey.KpDivide: return "KPDIV"; // Key: Keypad / + case KeyboardKey.KpMultiply: return "KPMUL"; // Key: Keypad * + case KeyboardKey.KpSubtract: return "KPSUB"; // Key: Keypad - + case KeyboardKey.KpAdd: return "KPADD"; // Key: Keypad + + case KeyboardKey.KpEnter: return "KPENTER"; // Key: Keypad Enter + case KeyboardKey.KpEqual: return "KPEQU"; // Key: Keypad = + default: return ""; + } + } + + // Draw keyboard key + private static void GuiKeyboardKey(Rectangle bounds, int key) + { + if (key == (int)KeyboardKey.Null) DrawRectangleLinesEx(bounds, 2.0f, Color.LightGray); + else + { + if (IsKeyDown((KeyboardKey)key)) + { + DrawRectangleLinesEx(bounds, 2.0f, Color.Maroon); + DrawText(GetKeyText(key), (int)(bounds.X + 4), (int)(bounds.Y + 4), 10, Color.Maroon); + } + else + { + DrawRectangleLinesEx(bounds, 2.0f, Color.DarkGray); + DrawText(GetKeyText(key), (int)(bounds.X + 4), (int)(bounds.Y + 4), 10, Color.DarkGray); + } + } + + if (CheckCollisionPointRec(GetMousePosition(), bounds)) + { + DrawRectangleRec(bounds, Fade(Color.Red, 0.2f)); + DrawRectangleLinesEx(bounds, 3.0f, Color.Red); + } + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - keyboard testbed"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new KeyboardTestbed(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/LoadingThread.cs b/Examples/Core/LoadingThread.cs index affdbb0..0371097 100644 --- a/Examples/Core/LoadingThread.cs +++ b/Examples/Core/LoadingThread.cs @@ -18,6 +18,7 @@ using static Raylib_cs.Raylib; namespace Examples.Core; +[ExcludeFromBrowser("System.Threading.Thread is unsupported on single-threaded wasm")] public partial class LoadingThread : IExample { const int screenWidth = 800; diff --git a/Examples/Core/MonitorDetector.cs b/Examples/Core/MonitorDetector.cs new file mode 100644 index 0000000..9ecc0ec --- /dev/null +++ b/Examples/Core/MonitorDetector.cs @@ -0,0 +1,209 @@ +/******************************************************************************************* +* +* raylib [core] example - monitor detector +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.6 +* +* Example contributed by Maicon Santana (@maiconpintoabreu) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Maicon Santana (@maiconpintoabreu) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +[ExcludeFromBrowser("GetMonitorCount() is not implemented on the wasm target")] +public partial class MonitorDetector : IExample +{ + private const int MaxMonitors = 10; + + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Monitor Detector"; + + public string Title => "raylib [core] example - monitor detector"; + + // Monitor info + private struct MonitorInfo + { + public Vector2 Position; + public string Name; + public int Width; + public int Height; + public int PhysicalWidth; + public int PhysicalHeight; + public int RefreshRate; + } + + private MonitorInfo[] monitors; + private int currentMonitorIndex; + private int monitorCount; + + public void Init() + { + monitors = new MonitorInfo[MaxMonitors]; + currentMonitorIndex = GetCurrentMonitor(); + monitorCount = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Variables to find the max x and Y to calculate the scale + int maxWidth = 1; + int maxHeight = 1; + + // Monitor offset is to fix when monitor position x is negative + int monitorOffsetX = 0; + + // Rebuild monitors array every frame + monitorCount = GetMonitorCount(); + for (int i = 0; i < monitorCount; i++) + { + monitors[i] = new MonitorInfo + { + Position = GetMonitorPosition(i), + Name = GetMonitorName_(i), + Width = GetMonitorWidth(i), + Height = GetMonitorHeight(i), + PhysicalWidth = GetMonitorPhysicalWidth(i), + PhysicalHeight = GetMonitorPhysicalHeight(i), + RefreshRate = GetMonitorRefreshRate(i) + }; + + if (monitors[i].Position.X < monitorOffsetX) + { + monitorOffsetX = -(int)monitors[i].Position.X; + } + + int width = (int)monitors[i].Position.X + monitors[i].Width; + int height = (int)monitors[i].Position.Y + monitors[i].Height; + + if (maxWidth < width) + { + maxWidth = width; + } + if (maxHeight < height) + { + maxHeight = height; + } + } + + if (IsKeyPressed(KeyboardKey.Enter) && (monitorCount > 1)) + { + currentMonitorIndex += 1; + + // Set index to 0 if the last one + if (currentMonitorIndex == monitorCount) + { + currentMonitorIndex = 0; + } + + SetWindowMonitor(currentMonitorIndex); // Move window to currentMonitorIndex + } + else + { + currentMonitorIndex = GetCurrentMonitor(); // Get currentMonitorIndex if manually moved + } + + float monitorScale = 0.6f; + + if (maxHeight > (maxWidth + monitorOffsetX)) + { + monitorScale *= ((float)screenHeight / (float)maxHeight); + } + else + { + monitorScale *= ((float)screenWidth / (float)(maxWidth + monitorOffsetX)); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + DrawText("Press [Enter] to move window to next monitor available", 20, 20, 20, Color.DarkGray); + + DrawRectangleLines(20, 60, screenWidth - 40, screenHeight - 100, Color.DarkGray); + + // Draw Monitor Rectangles with information inside + for (int i = 0; i < monitorCount; i++) + { + // Calculate retangle position and size using monitorScale + Rectangle rec = new Rectangle( + (monitors[i].Position.X + monitorOffsetX) * monitorScale + 140, + monitors[i].Position.Y * monitorScale + 80, + monitors[i].Width * monitorScale, + monitors[i].Height * monitorScale + ); + + // Draw monitor name and information inside the rectangle + DrawText($"[{i}] {monitors[i].Name}", (int)rec.X + 10, (int)rec.Y + (int)(100 * monitorScale), (int)(120 * monitorScale), Color.Blue); + DrawText( + $"Resolution: [{monitors[i].Width}px x {monitors[i].Height}px]\nRefreshRate: [{monitors[i].RefreshRate}hz]\nPhysical Size: [{monitors[i].PhysicalWidth}mm x {monitors[i].PhysicalHeight}mm]\nPosition: {monitors[i].Position.X,3:F0} x {monitors[i].Position.Y,3:F0}", + (int)rec.X + 10, (int)rec.Y + (int)(200 * monitorScale), (int)(120 * monitorScale), Color.DarkGray); + + // Highlight current monitor + if (i == currentMonitorIndex) + { + DrawRectangleLinesEx(rec, 5, Color.Red); + Vector2 windowPosition = new Vector2((GetWindowPosition().X + monitorOffsetX) * monitorScale + 140, GetWindowPosition().Y * monitorScale + 80); + + // Draw window position based on monitors + DrawRectangleV(windowPosition, new Vector2(screenWidth * monitorScale, screenHeight * monitorScale), Fade(Color.Green, 0.5f)); + } + else + { + DrawRectangleLinesEx(rec, 5, Color.Gray); + } + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - monitor detector"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new MonitorDetector(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/Picking3D.cs b/Examples/Core/Picking3D.cs index 9c7ecc3..d76948c 100644 --- a/Examples/Core/Picking3D.cs +++ b/Examples/Core/Picking3D.cs @@ -27,8 +27,6 @@ public partial class Picking3d : IExample public string Title => "raylib [core] example - 3d picking"; - public bool CursorDisabled => true; - private Camera3D camera; private Vector3 cubePosition; private Vector3 cubeSize; diff --git a/Examples/Core/RandomSequence.cs b/Examples/Core/RandomSequence.cs new file mode 100644 index 0000000..ccd1dd6 --- /dev/null +++ b/Examples/Core/RandomSequence.cs @@ -0,0 +1,183 @@ +/******************************************************************************************* +* +* raylib [core] example - random sequence +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 5.0, last time updated with raylib 5.0 +* +* Example contributed by Dalton Overmyer (@REDl3east) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2023-2025 Dalton Overmyer (@REDl3east) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; + +namespace Examples.Core; + +public partial class RandomSequence : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Random Sequence"; + + public string Title => "raylib [core] example - random sequence"; + + private struct ColorRect + { + public Color Color; + public Rectangle Rect; + } + + private int rectCount; + private float rectSize; + private ColorRect[] rectangles; + + public void Init() + { + rectCount = 20; + rectSize = (float)screenWidth / rectCount; + rectangles = GenerateRandomColorRectSequence(rectCount, rectSize, screenWidth, 0.75f * screenHeight); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.Space)) + { + ShuffleColorRectSequence(rectangles, rectCount); + } + + if (IsKeyPressed(KeyboardKey.Up)) + { + rectCount++; + rectSize = (float)screenWidth / rectCount; + + // Re-generate random sequence with new count + rectangles = GenerateRandomColorRectSequence(rectCount, rectSize, screenWidth, 0.75f * screenHeight); + } + + if (IsKeyPressed(KeyboardKey.Down)) + { + if (rectCount >= 4) + { + rectCount--; + rectSize = (float)screenWidth / rectCount; + + // Re-generate random sequence with new count + rectangles = GenerateRandomColorRectSequence(rectCount, rectSize, screenWidth, 0.75f * screenHeight); + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + for (int i = 0; i < rectCount; i++) + { + DrawRectangleRec(rectangles[i].Rect, rectangles[i].Color); + + DrawText("Press SPACE to shuffle the current sequence", 10, screenHeight - 96, 20, Color.Black); + DrawText("Press UP to add a rectangle and generate a new sequence", 10, screenHeight - 64, 20, Color.Black); + DrawText("Press DOWN to remove a rectangle and generate a new sequence", 10, screenHeight - 32, 20, Color.Black); + } + + DrawText($"Count: {rectCount} rectangles", 10, 10, 20, Color.Maroon); + + DrawFPS(screenWidth - 80, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + private static Color GenerateRandomColor() + { + return new Color( + GetRandomValue(0, 255), + GetRandomValue(0, 255), + GetRandomValue(0, 255), + 255 + ); + } + + private static ColorRect[] GenerateRandomColorRectSequence(float rectCount, float rectWidth, float screenWidth, float screenHeight) + { + ColorRect[] rectangles = new ColorRect[(int)rectCount]; + + int[] seq = GetRandomSequence((uint)rectCount, 0, (int)rectCount - 1); + float rectSeqWidth = rectCount * rectWidth; + float startX = (screenWidth - rectSeqWidth) * 0.5f; + + for (int i = 0; i < rectCount; i++) + { + int rectHeight = (int)Remap(seq[i], 0, rectCount - 1, 0, screenHeight); + + rectangles[i].Color = GenerateRandomColor(); + rectangles[i].Rect = new Rectangle(startX + i * rectWidth, screenHeight - rectHeight, rectWidth, rectHeight); + } + + return rectangles; + } + + private static void ShuffleColorRectSequence(ColorRect[] rectangles, int rectCount) + { + int[] seq = GetRandomSequence((uint)rectCount, 0, rectCount - 1); + + for (int i1 = 0; i1 < rectCount; i1++) + { + int i2 = seq[i1]; + + // Swap only the color and height + ColorRect tmp = rectangles[i1]; + rectangles[i1].Color = rectangles[i2].Color; + rectangles[i1].Rect.Height = rectangles[i2].Rect.Height; + rectangles[i1].Rect.Y = rectangles[i2].Rect.Y; + rectangles[i2].Color = tmp.Color; + rectangles[i2].Rect.Height = tmp.Rect.Height; + rectangles[i2].Rect.Y = tmp.Rect.Y; + } + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - random sequence"); + + SetTargetFPS(60); + + var game = new RandomSequence(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/RenderTexture.cs b/Examples/Core/RenderTexture.cs new file mode 100644 index 0000000..358a9da --- /dev/null +++ b/Examples/Core/RenderTexture.cs @@ -0,0 +1,139 @@ +/******************************************************************************************* +* +* raylib [core] example - render texture +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +public partial class RenderTextureDemo : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + // Define a render texture to render + private const int renderTextureWidth = 300; + private const int renderTextureHeight = 300; + + public string Name => "Core / Render Texture"; + + public string Title => "raylib [core] example - render texture"; + + private RenderTexture2D target; + private Vector2 ballPosition; + private Vector2 ballSpeed; + private int ballRadius; + private float rotation; + + public void Init() + { + target = LoadRenderTexture(renderTextureWidth, renderTextureHeight); + + ballPosition = new Vector2(renderTextureWidth / 2.0f, renderTextureHeight / 2.0f); + ballSpeed = new Vector2(5.0f, 4.0f); + ballRadius = 20; + + rotation = 0.0f; + } + + public void Update() + { + // Update + //----------------------------------------------------- + // Ball movement logic + ballPosition.X += ballSpeed.X; + ballPosition.Y += ballSpeed.Y; + + // Check walls collision for bouncing + if ((ballPosition.X >= (renderTextureWidth - ballRadius)) || (ballPosition.X <= ballRadius)) + { + ballSpeed.X *= -1.0f; + } + if ((ballPosition.Y >= (renderTextureHeight - ballRadius)) || (ballPosition.Y <= ballRadius)) + { + ballSpeed.Y *= -1.0f; + } + + // Render texture rotation + rotation += 0.5f; + //----------------------------------------------------- + + // Draw + //----------------------------------------------------- + // Draw our scene to the render texture + BeginTextureMode(target); + + ClearBackground(Color.SkyBlue); + + DrawRectangle(0, 0, 20, 20, Color.Red); + DrawCircleV(ballPosition, ballRadius, Color.Maroon); + + EndTextureMode(); + + // Draw render texture to main framebuffer + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + // Draw our render texture with rotation applied + // NOTE 1: We set the origin of the texture to the center of the render texture + // NOTE 2: We flip vertically the texture setting negative source rectangle height + DrawTexturePro(target.Texture, + new Rectangle(0, 0, target.Texture.Width, -target.Texture.Height), + new Rectangle(screenWidth / 2.0f, screenHeight / 2.0f, target.Texture.Width, target.Texture.Height), + new Vector2(target.Texture.Width / 2.0f, target.Texture.Height / 2.0f), rotation, Color.White); + + DrawText("DRAWING BOUNCING BALL INSIDE RENDER TEXTURE!", 10, screenHeight - 40, 20, Color.Black); + + DrawFPS(10, 10); + + EndDrawing(); + //----------------------------------------------------- + } + + public void Unload() + { + UnloadRenderTexture(target); + } + + public static int Main() + { + // Initialization + //--------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - render texture"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //---------------------------------------------------------- + + var game = new RenderTextureDemo(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //--------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //---------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/ScreenRecording.cs b/Examples/Core/ScreenRecording.cs new file mode 100644 index 0000000..9652c5f --- /dev/null +++ b/Examples/Core/ScreenRecording.cs @@ -0,0 +1,382 @@ +/******************************************************************************************* +* +* raylib [core] example - screen recording +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +// NOTE: The upstream C example records frames into an animated GIF using the bundled msf_gif.h +// single-header library. raylib-cs does not bind msf_gif, so this port replaces it with a small, +// self-contained GIF89a encoder (GifRecorder, below) that uses a fixed 3-3-2 RGB palette. The +// rest of the example (rendering, CTRL+R toggle, saving to /screenrecording.gif) mirrors +// upstream. Frame capture via LoadImageFromScreen() is slow and can cause stuttering, as noted +// upstream. +[ExcludeFromBrowser("desktop screen capture + gif file export, no web equivalent")] +public partial class ScreenRecording : IExample +{ + private const int GIF_RECORD_FRAMERATE = 5; // Record framerate, we get a frame every N frames + + private const int MAX_SINEWAVE_POINTS = 256; + + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Screen Recording"; + + public string Title => "raylib [core] example - screen recording"; + + private bool gifRecording; // GIF recording state + private uint gifFrameCounter; // GIF frames counter + private GifRecorder gifState; // GIF context state + + private Vector2 circlePosition; + private float timeCounter; + + private Vector2[] sinePoints; + + public void Init() + { + gifRecording = false; + gifFrameCounter = 0; + gifState = new GifRecorder(); + + circlePosition = new Vector2(0.0f, screenHeight / 2.0f); + timeCounter = 0.0f; + + // Get sine wave points for line drawing + sinePoints = new Vector2[MAX_SINEWAVE_POINTS]; + for (int i = 0; i < MAX_SINEWAVE_POINTS; i++) + { + sinePoints[i].X = i * GetScreenWidth() / 180.0f; + sinePoints[i].Y = screenHeight / 2.0f + 150 * MathF.Sin((2 * MathF.PI / 1.5f) * (1.0f / 60.0f) * (float)i); // Calculate for 60 fps + } + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Update circle sinusoidal movement + timeCounter += GetFrameTime(); + circlePosition.X += GetScreenWidth() / 180.0f; + circlePosition.Y = screenHeight / 2.0f + 150 * MathF.Sin((2 * MathF.PI / 1.5f) * timeCounter); + if (circlePosition.X > screenWidth) + { + circlePosition.X = 0.0f; + circlePosition.Y = screenHeight / 2.0f; + timeCounter = 0.0f; + } + + // Start-Stop GIF recording on CTRL+R + if (IsKeyDown(KeyboardKey.LeftControl) && IsKeyPressed(KeyboardKey.R)) + { + if (gifRecording) + { + // Stop current recording and save file + gifRecording = false; + byte[] result = gifState.End(); + SaveFileData(result, $"{GetApplicationDirectoryString()}/screenrecording.gif"); + + TraceLog(TraceLogLevel.Info, "Finish animated GIF recording"); + } + else + { + // Start a new recording + gifRecording = true; + gifFrameCounter = 0; + gifState.Begin(GetRenderWidth(), GetRenderHeight()); + + TraceLog(TraceLogLevel.Info, "Start animated GIF recording"); + } + } + + if (gifRecording) + { + gifFrameCounter++; + + // NOTE: We record one gif frame depending on the desired gif framerate + if (gifFrameCounter > GIF_RECORD_FRAMERATE) + { + // Get image data for the current frame (from backbuffer) + // WARNING: This process is quite slow, it can generate stuttering + Image imScreen = LoadImageFromScreen(); + + // Add the frame to the gif recording, providing and "estimated" time for display in centiseconds + int delayCs = (int)((1.0f / 60.0f) * GIF_RECORD_FRAMERATE) / 10; + gifState.AddFrame((byte*)imScreen.Data, imScreen.Width, imScreen.Height, imScreen.Width * 4, delayCs); + gifFrameCounter = 0; + + UnloadImage(imScreen); // Free image data + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + for (int i = 0; i < (MAX_SINEWAVE_POINTS - 1); i++) + { + DrawLineV(sinePoints[i], sinePoints[i + 1], Color.Maroon); + DrawCircleV(sinePoints[i], 3, Color.Maroon); + } + + DrawCircleV(circlePosition, 30, Color.Red); + + DrawFPS(10, 10); + + /* + // Draw record indicator + // WARNING: If drawn here, it will appear in the recorded image, + // use a render texture instead for the recording and LoadImageFromTexture(rt.texture) + if (gifRecording) + { + // Display the recording indicator every half-second + if ((int)(GetTime()/0.5)%2 == 1) + { + DrawCircle(30, GetScreenHeight() - 20, 10, Color.Maroon); + DrawText("GIF RECORDING", 50, GetScreenHeight() - 25, 10, Color.Red); + } + } + */ + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + // If still recording a GIF on close window, just finish + if (gifRecording) + { + gifState.End(); + gifRecording = false; + } + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - screen recording"); + + var game = new ScreenRecording(); + game.Init(); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } + + // Minimal self-contained animated GIF89a encoder (replacement for msf_gif.h) + // Uses a fixed 3-3-2 RGB global palette and standard GIF-variant LZW compression. + private class GifRecorder + { + private List output; + private int width; + private int height; + + // LZW bit-packing state (per-frame) + private int bitBuffer; + private int bitCount; + private List subBlock; + + public void Begin(int w, int h) + { + width = w; + height = h; + output = new List(); + + // Header + output.AddRange(new byte[] { (byte)'G', (byte)'I', (byte)'F', (byte)'8', (byte)'9', (byte)'a' }); + + // Logical Screen Descriptor + WriteU16(width); + WriteU16(height); + output.Add(0xF7); // Global color table present, 8-bit color res, 256-entry table + output.Add(0x00); // Background color index + output.Add(0x00); // Pixel aspect ratio + + // Global Color Table: 256 entries, 3-3-2 RGB + for (int k = 0; k < 256; k++) + { + int r3 = (k >> 5) & 0x7; + int g3 = (k >> 2) & 0x7; + int b2 = k & 0x3; + output.Add((byte)((r3 << 5) | (r3 << 2) | (r3 >> 1))); + output.Add((byte)((g3 << 5) | (g3 << 2) | (g3 >> 1))); + output.Add((byte)((b2 << 6) | (b2 << 4) | (b2 << 2) | b2)); + } + + // NETSCAPE2.0 application extension (loop forever) + output.Add(0x21); + output.Add(0xFF); + output.Add(0x0B); + output.AddRange(new byte[] { (byte)'N', (byte)'E', (byte)'T', (byte)'S', (byte)'C', (byte)'A', (byte)'P', (byte)'E', (byte)'2', (byte)'.', (byte)'0' }); + output.Add(0x03); + output.Add(0x01); + WriteU16(0); // Loop count (0 = forever) + output.Add(0x00); + } + + public unsafe void AddFrame(byte* data, int w, int h, int stride, int delayCs) + { + if (output == null) return; + + // Graphic Control Extension + output.Add(0x21); + output.Add(0xF9); + output.Add(0x04); + output.Add(0x00); // No transparency, disposal method 0 + WriteU16(delayCs); + output.Add(0x00); // Transparent color index + output.Add(0x00); // Block terminator + + // Image Descriptor + output.Add(0x2C); + WriteU16(0); // Left + WriteU16(0); // Top + WriteU16(w); + WriteU16(h); + output.Add(0x00); // No local color table, not interlaced + + // Map pixels to palette indices (3-3-2) + byte[] indices = new byte[w * h]; + for (int y = 0; y < h; y++) + { + int row = y * stride; + int dst = y * w; + for (int x = 0; x < w; x++) + { + byte r = data[row + x * 4 + 0]; + byte g = data[row + x * 4 + 1]; + byte b = data[row + x * 4 + 2]; + indices[dst + x] = (byte)((r & 0xE0) | ((g & 0xE0) >> 3) | (b >> 6)); + } + } + + // LZW image data + const int minCodeSize = 8; + output.Add((byte)minCodeSize); + + bitBuffer = 0; + bitCount = 0; + subBlock = new List(); + + int clearCode = 1 << minCodeSize; // 256 + int stopCode = clearCode + 1; // 257 + int keySize = minCodeSize + 1; // 9 + int nkeys = clearCode + 2; // 258 + var dict = new Dictionary(); + + WriteBits(clearCode, keySize); + + int key = indices[0]; + for (int i = 1; i < indices.Length; i++) + { + int p = indices[i]; + int combined = (key << 8) | p; + if (dict.TryGetValue(combined, out int existing)) + { + key = existing; + } + else + { + WriteBits(key, keySize); + dict[combined] = nkeys; + nkeys++; + if (nkeys == (1 << keySize)) + { + if (keySize < 12) keySize++; + } + if (nkeys == 0x1000) + { + WriteBits(clearCode, keySize); + dict.Clear(); + keySize = minCodeSize + 1; + nkeys = clearCode + 2; + } + key = p; + } + } + + WriteBits(key, keySize); + WriteBits(stopCode, keySize); + + // Flush remaining bits + if (bitCount > 0) + { + subBlock.Add((byte)(bitBuffer & 0xFF)); + bitBuffer = 0; + bitCount = 0; + } + if (subBlock.Count > 0) FlushSubBlock(); + output.Add(0x00); // Image data block terminator + } + + public byte[] End() + { + if (output == null) return Array.Empty(); + output.Add(0x3B); // Trailer + byte[] result = output.ToArray(); + output = null; + return result; + } + + private void WriteBits(int code, int len) + { + bitBuffer |= code << bitCount; + bitCount += len; + while (bitCount >= 8) + { + subBlock.Add((byte)(bitBuffer & 0xFF)); + bitBuffer >>= 8; + bitCount -= 8; + if (subBlock.Count == 255) FlushSubBlock(); + } + } + + private void FlushSubBlock() + { + output.Add((byte)subBlock.Count); + output.AddRange(subBlock); + subBlock.Clear(); + } + + private void WriteU16(int value) + { + output.Add((byte)(value & 0xFF)); + output.Add((byte)((value >> 8) & 0xFF)); + } + } +} diff --git a/Examples/Core/SplitScreen2D.cs b/Examples/Core/SplitScreen2D.cs new file mode 100644 index 0000000..d09039e --- /dev/null +++ b/Examples/Core/SplitScreen2D.cs @@ -0,0 +1,204 @@ +/******************************************************************************************* +* +* raylib [core] example - 2d camera split screen +* +* Example complexity rating: [★★★★] 4/4 +* +* Addapted from the core_3d_camera_split_screen example: +* https://github.com/raysan5/raylib/blob/master/examples/core/core_3d_camera_split_screen.c +* +* Example originally created with raylib 4.5, last time updated with raylib 4.5 +* +* Example contributed by Gabriel dos Santos Sanches (@gabrielssanches) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2023-2025 Gabriel dos Santos Sanches (@gabrielssanches) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +public partial class SplitScreen2D : IExample +{ + private const int PLAYER_SIZE = 40; + + private const int screenWidth = 800; + private const int screenHeight = 440; + + public string Name => "Core / 2D Camera Split Screen"; + + public string Title => "raylib [core] example - 2d camera split screen"; + + public int Width => screenWidth; + + public int Height => screenHeight; + + private Rectangle player1; + private Rectangle player2; + private Camera2D camera1; + private Camera2D camera2; + private RenderTexture2D screenCamera1; + private RenderTexture2D screenCamera2; + private Rectangle splitScreenRect; + + public void Init() + { + player1 = new Rectangle(200, 200, PLAYER_SIZE, PLAYER_SIZE); + player2 = new Rectangle(250, 200, PLAYER_SIZE, PLAYER_SIZE); + + camera1 = new Camera2D(); + camera1.Target = new Vector2(player1.X, player1.Y); + camera1.Offset = new Vector2(200.0f, 200.0f); + camera1.Rotation = 0.0f; + camera1.Zoom = 1.0f; + + camera2 = new Camera2D(); + camera2.Target = new Vector2(player2.X, player2.Y); + camera2.Offset = new Vector2(200.0f, 200.0f); + camera2.Rotation = 0.0f; + camera2.Zoom = 1.0f; + + screenCamera1 = LoadRenderTexture(screenWidth / 2, screenHeight); + screenCamera2 = LoadRenderTexture(screenWidth / 2, screenHeight); + + // Build a flipped rectangle the size of the split view to use for drawing later + splitScreenRect = new Rectangle(0.0f, 0.0f, (float)screenCamera1.Texture.Width, (float)-screenCamera1.Texture.Height); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyDown(KeyboardKey.S)) player1.Y += 3.0f; + else if (IsKeyDown(KeyboardKey.W)) player1.Y -= 3.0f; + if (IsKeyDown(KeyboardKey.D)) player1.X += 3.0f; + else if (IsKeyDown(KeyboardKey.A)) player1.X -= 3.0f; + + if (IsKeyDown(KeyboardKey.Up)) player2.Y -= 3.0f; + else if (IsKeyDown(KeyboardKey.Down)) player2.Y += 3.0f; + if (IsKeyDown(KeyboardKey.Right)) player2.X += 3.0f; + else if (IsKeyDown(KeyboardKey.Left)) player2.X -= 3.0f; + + camera1.Target = new Vector2(player1.X, player1.Y); + camera2.Target = new Vector2(player2.X, player2.Y); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginTextureMode(screenCamera1); + ClearBackground(Color.RayWhite); + + BeginMode2D(camera1); + + // Draw full scene with first camera + for (int i = 0; i < screenWidth / PLAYER_SIZE + 1; i++) + { + DrawLineV(new Vector2((float)PLAYER_SIZE * i, 0), new Vector2((float)PLAYER_SIZE * i, (float)screenHeight), Color.LightGray); + } + + for (int i = 0; i < screenHeight / PLAYER_SIZE + 1; i++) + { + DrawLineV(new Vector2(0, (float)PLAYER_SIZE * i), new Vector2((float)screenWidth, (float)PLAYER_SIZE * i), Color.LightGray); + } + + for (int i = 0; i < screenWidth / PLAYER_SIZE; i++) + { + for (int j = 0; j < screenHeight / PLAYER_SIZE; j++) + { + DrawText($"[{i},{j}]", 10 + PLAYER_SIZE * i, 15 + PLAYER_SIZE * j, 10, Color.LightGray); + } + } + + DrawRectangleRec(player1, Color.Red); + DrawRectangleRec(player2, Color.Blue); + EndMode2D(); + + DrawRectangle(0, 0, GetScreenWidth() / 2, 30, Fade(Color.RayWhite, 0.6f)); + DrawText("PLAYER1: W/S/A/D to move", 10, 10, 10, Color.Maroon); + + EndTextureMode(); + + BeginTextureMode(screenCamera2); + ClearBackground(Color.RayWhite); + + BeginMode2D(camera2); + + // Draw full scene with second camera + for (int i = 0; i < screenWidth / PLAYER_SIZE + 1; i++) + { + DrawLineV(new Vector2((float)PLAYER_SIZE * i, 0), new Vector2((float)PLAYER_SIZE * i, (float)screenHeight), Color.LightGray); + } + + for (int i = 0; i < screenHeight / PLAYER_SIZE + 1; i++) + { + DrawLineV(new Vector2(0, (float)PLAYER_SIZE * i), new Vector2((float)screenWidth, (float)PLAYER_SIZE * i), Color.LightGray); + } + + for (int i = 0; i < screenWidth / PLAYER_SIZE; i++) + { + for (int j = 0; j < screenHeight / PLAYER_SIZE; j++) + { + DrawText($"[{i},{j}]", 10 + PLAYER_SIZE * i, 15 + PLAYER_SIZE * j, 10, Color.LightGray); + } + } + + DrawRectangleRec(player1, Color.Red); + DrawRectangleRec(player2, Color.Blue); + + EndMode2D(); + + DrawRectangle(0, 0, GetScreenWidth() / 2, 30, Fade(Color.RayWhite, 0.6f)); + DrawText("PLAYER2: UP/DOWN/LEFT/RIGHT to move", 10, 10, 10, Color.DarkBlue); + + EndTextureMode(); + + // Draw both views render textures to the screen side by side + BeginDrawing(); + ClearBackground(Color.Black); + + DrawTextureRec(screenCamera1.Texture, splitScreenRect, new Vector2(0, 0), Color.White); + DrawTextureRec(screenCamera2.Texture, splitScreenRect, new Vector2(screenWidth / 2.0f, 0), Color.White); + + DrawRectangle(GetScreenWidth() / 2 - 2, 0, 4, GetScreenHeight(), Color.LightGray); + EndDrawing(); + } + + public void Unload() + { + UnloadRenderTexture(screenCamera1); // Unload render texture + UnloadRenderTexture(screenCamera2); // Unload render texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - 2d camera split screen"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new SplitScreen2D(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/TextFileLoading.cs b/Examples/Core/TextFileLoading.cs new file mode 100644 index 0000000..deb0efa --- /dev/null +++ b/Examples/Core/TextFileLoading.cs @@ -0,0 +1,209 @@ +/******************************************************************************************* +* +* raylib [core] example - text file loading +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.6 +* +* Example contributed by Aanjishnu Bhattacharyya (@NimComPoo-04) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 0 Aanjishnu Bhattacharyya (@NimComPoo-04) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; + +namespace Examples.Core; + +// NOTE: The upstream C code mutates the raw char buffers returned by LoadTextLines() in place, +// temporarily null-terminating to reuse MeasureText for word wrapping. This C# port keeps the +// same algorithm but works on managed char arrays: LoadFileText + split on '\n' reproduces +// raylib's LoadTextLines('\n') behaviour, and '\n' characters are inserted where lines wrap. +public partial class TextFileLoading : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Text File Loading"; + + public string Title => "raylib [core] example - text file loading"; + + private Camera2D cam; + private string fileName; + private string[] lines; + private int lineCount; + private int fontSize; + private int textTop; + private int wrapWidth; + private int textHeight; + private Rectangle scrollBar; + + public void Init() + { + // Setting up the camera + cam = new Camera2D(); + cam.Offset = new Vector2(0, 0); + cam.Target = new Vector2(0, 0); + cam.Rotation = 0; + cam.Zoom = 1; + + // Loading text file from resources/text_file.txt + fileName = "resources/text_file.txt"; + string text = LoadFileText(fileName); + + // Loading all the text lines (raylib's LoadTextLines splits on '\n') + lines = text.Split('\n'); + lineCount = lines.Length; + + // Stylistic choises + fontSize = 20; + textTop = 25 + fontSize; // Top of the screen from where the text is rendered + wrapWidth = screenWidth - 20; + + // Wrap the lines as needed + for (int i = 0; i < lineCount; i++) + { + char[] chars = lines[i].ToCharArray(); + int len = chars.Length; + int j = 0; + int lastSpace = 0; // Keeping track of last valid space to insert '\n' + int lastWrapStart = 0; // Keeping track of the start of this wrapped line. + + while (j <= len) + { + char cur = (j < len) ? chars[j] : '\0'; + if (cur == ' ' || cur == '\0') + { + // Making a C style string by "cutting" at the required location so that we can use MeasureText + string sub = new string(chars, lastWrapStart, j - lastWrapStart); + + // Checking if the text has crossed the wrapWidth, then going back and inserting a newline + if (MeasureText(sub, fontSize) > wrapWidth) + { + chars[lastSpace] = '\n'; + + // Since we added a newline the place of wrap changed so we update our lastWrapStart + lastWrapStart = lastSpace + 1; + } + + lastSpace = j; // Since we encountered a new space we update our last encountered space location + } + + j++; + } + + lines[i] = new string(chars); + } + + // Calculating the total height so that we can show a scrollbar + textHeight = 0; + + for (int i = 0; i < lineCount; i++) + { + Vector2 size = MeasureTextEx(GetFontDefault(), lines[i], (float)fontSize, 2); + textHeight += (int)size.Y + 10; + } + + // A simple scrollbar on the side to show how far we have read into the file + scrollBar = new Rectangle( + (float)screenWidth - 5, + 0, + 5, + screenHeight * 100.0f / (textHeight - screenHeight)); // Scrollbar height is just a percentage + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + float scroll = GetMouseWheelMove(); + cam.Target.Y -= scroll * fontSize * 1.5f; // Choosing an arbitrary speed for scroll + + if (cam.Target.Y < 0) cam.Target.Y = 0; // Snapping to 0 if we go too far back + + // Ensuring that the camera does not scroll past all text + if (cam.Target.Y > textHeight - screenHeight + textTop) + cam.Target.Y = (float)textHeight - screenHeight + textTop; + + // Computing the position of the scrollBar depending on the percentage of text covered + scrollBar.Y = Lerp((float)textTop, (float)screenHeight - scrollBar.Height, (float)(cam.Target.Y - textTop) / (textHeight - screenHeight)); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode2D(cam); + // Going through all the read lines + for (int i = 0, t = textTop; i < lineCount; i++) + { + // Each time we go through and calculate the height of the text to move the cursor appropriately + Vector2 size; + if (lines[i] != "") + { + size = MeasureTextEx(GetFontDefault(), lines[i], (float)fontSize, 2); + } + else + { + // Fix for empty line in the text file + size = MeasureTextEx(GetFontDefault(), " ", (float)fontSize, 2); + } + + DrawText(lines[i], 10, t, fontSize, Color.Red); + + // Inserting extra space for real newlines, + // wrapped lines are rendered closer together + t += (int)size.Y + 10; + } + EndMode2D(); + + // Header displaying which file is being read currently + DrawRectangle(0, 0, screenWidth, textTop - 10, Color.Beige); + DrawText($"File: {fileName}", 10, 10, fontSize, Color.Maroon); + + DrawRectangleRec(scrollBar, Color.Maroon); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - text file loading"); + + var game = new TextFileLoading(); + game.Init(); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/UndoRedo.cs b/Examples/Core/UndoRedo.cs new file mode 100644 index 0000000..4428f21 --- /dev/null +++ b/Examples/Core/UndoRedo.cs @@ -0,0 +1,333 @@ +/******************************************************************************************* +* +* raylib [core] example - undo redo +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.6 +* +* Example contributed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +public partial class UndoRedo : IExample +{ + private const int MAX_UNDO_STATES = 26; // Maximum undo states supported for the ring buffer + + private const int GRID_CELL_SIZE = 24; + private const int MAX_GRID_CELLS_X = 30; + private const int MAX_GRID_CELLS_Y = 13; + + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Undo Redo"; + + public string Title => "raylib [core] example - undo redo"; + + // Point struct, like Vector2 but using int + private struct Point + { + public int X; + public int Y; + } + + // Player state struct + // NOTE: Contains all player data that needs to be affected by undo/redo + private struct PlayerState + { + public Point Cell; + public Color Color; + } + + // Undo/redo system variables + private int currentUndoIndex; + private int firstUndoIndex; + private int lastUndoIndex; + private int undoFrameCounter; + private Vector2 undoInfoPos; + + private PlayerState player; + private PlayerState[] states; + + // Grid variables + private Vector2 gridPosition; + + // Compare two player states (replaces memcmp) + private static bool SameState(in PlayerState a, in PlayerState b) + { + return (a.Cell.X == b.Cell.X) && (a.Cell.Y == b.Cell.Y) && + (a.Color.R == b.Color.R) && (a.Color.G == b.Color.G) && + (a.Color.B == b.Color.B) && (a.Color.A == b.Color.A); + } + + public void Init() + { + currentUndoIndex = 0; + firstUndoIndex = 0; + lastUndoIndex = 0; + undoFrameCounter = 0; + undoInfoPos = new Vector2(110, 400); + + // Init current player state and undo/redo recorded states array + player = new PlayerState(); + player.Cell = new Point { X = 10, Y = 10 }; + player.Color = Color.Red; + + // Init undo buffer to store MAX_UNDO_STATES states + states = new PlayerState[MAX_UNDO_STATES]; + // Init all undo states to current state + for (int i = 0; i < MAX_UNDO_STATES; i++) states[i] = player; + + // Grid variables + gridPosition = new Vector2(40, 60); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Player movement logic + if (IsKeyPressed(KeyboardKey.Right)) player.Cell.X++; + else if (IsKeyPressed(KeyboardKey.Left)) player.Cell.X--; + else if (IsKeyPressed(KeyboardKey.Up)) player.Cell.Y--; + else if (IsKeyPressed(KeyboardKey.Down)) player.Cell.Y++; + + // Make sure player does not go out of bounds + if (player.Cell.X < 0) player.Cell.X = 0; + else if (player.Cell.X >= MAX_GRID_CELLS_X) player.Cell.X = MAX_GRID_CELLS_X - 1; + if (player.Cell.Y < 0) player.Cell.Y = 0; + else if (player.Cell.Y >= MAX_GRID_CELLS_Y) player.Cell.Y = MAX_GRID_CELLS_Y - 1; + + // Player color change logic + if (IsKeyPressed(KeyboardKey.Space)) + { + player.Color.R = (byte)GetRandomValue(20, 255); + player.Color.G = (byte)GetRandomValue(20, 220); + player.Color.B = (byte)GetRandomValue(20, 240); + } + + // Undo state change logic + undoFrameCounter++; + + // Waiting a number of frames before checking if we should store a new state snapshot + if (undoFrameCounter >= 2) // Checking every 2 frames + { + if (!SameState(states[currentUndoIndex], player)) + { + // Move cursor to next available position of the undo ring buffer to record state + currentUndoIndex++; + if (currentUndoIndex >= MAX_UNDO_STATES) currentUndoIndex = 0; + if (currentUndoIndex == firstUndoIndex) firstUndoIndex++; + if (firstUndoIndex >= MAX_UNDO_STATES) firstUndoIndex = 0; + + states[currentUndoIndex] = player; + lastUndoIndex = currentUndoIndex; + } + + undoFrameCounter = 0; + } + + // Recover previous state from buffer: CTRL+Z + if (IsKeyDown(KeyboardKey.LeftControl) && IsKeyPressed(KeyboardKey.Z)) + { + if (currentUndoIndex != firstUndoIndex) + { + currentUndoIndex--; + if (currentUndoIndex < 0) currentUndoIndex = MAX_UNDO_STATES - 1; + + if (!SameState(states[currentUndoIndex], player)) + { + player = states[currentUndoIndex]; + } + } + } + + // Recover next state from buffer: CTRL+Y + if (IsKeyDown(KeyboardKey.LeftControl) && IsKeyPressed(KeyboardKey.Y)) + { + if (currentUndoIndex != lastUndoIndex) + { + int nextUndoIndex = currentUndoIndex + 1; + if (nextUndoIndex >= MAX_UNDO_STATES) nextUndoIndex = 0; + + if (nextUndoIndex != firstUndoIndex) + { + currentUndoIndex = nextUndoIndex; + + if (!SameState(states[currentUndoIndex], player)) + { + player = states[currentUndoIndex]; + } + } + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // Draw controls info + DrawText("[ARROWS] MOVE PLAYER - [SPACE] CHANGE PLAYER COLOR", 40, 20, 20, Color.DarkGray); + + // Draw player visited cells recorded by undo + // NOTE: Remember we are using a ring buffer approach so, + // some cells info could start at the end of the array and end at the beginning + if (lastUndoIndex > firstUndoIndex) + { + for (int i = firstUndoIndex; i < currentUndoIndex; i++) + DrawRectangleRec(new Rectangle(gridPosition.X + states[i].Cell.X * GRID_CELL_SIZE, gridPosition.Y + states[i].Cell.Y * GRID_CELL_SIZE, + GRID_CELL_SIZE, GRID_CELL_SIZE), Color.LightGray); + } + else if (firstUndoIndex > lastUndoIndex) + { + if ((currentUndoIndex < MAX_UNDO_STATES) && (currentUndoIndex > lastUndoIndex)) + { + for (int i = firstUndoIndex; i < currentUndoIndex; i++) + DrawRectangleRec(new Rectangle(gridPosition.X + states[i].Cell.X * GRID_CELL_SIZE, gridPosition.Y + states[i].Cell.Y * GRID_CELL_SIZE, + GRID_CELL_SIZE, GRID_CELL_SIZE), Color.LightGray); + } + else + { + for (int i = firstUndoIndex; i < MAX_UNDO_STATES; i++) + DrawRectangle((int)gridPosition.X + states[i].Cell.X * GRID_CELL_SIZE, (int)gridPosition.Y + states[i].Cell.Y * GRID_CELL_SIZE, + GRID_CELL_SIZE, GRID_CELL_SIZE, Color.LightGray); + for (int i = 0; i < currentUndoIndex; i++) + DrawRectangle((int)gridPosition.X + states[i].Cell.X * GRID_CELL_SIZE, (int)gridPosition.Y + states[i].Cell.Y * GRID_CELL_SIZE, + GRID_CELL_SIZE, GRID_CELL_SIZE, Color.LightGray); + } + } + + // Draw game grid + for (int y = 0; y <= MAX_GRID_CELLS_Y; y++) + DrawLine((int)gridPosition.X, (int)gridPosition.Y + y * GRID_CELL_SIZE, + (int)gridPosition.X + MAX_GRID_CELLS_X * GRID_CELL_SIZE, (int)gridPosition.Y + y * GRID_CELL_SIZE, Color.Gray); + for (int x = 0; x <= MAX_GRID_CELLS_X; x++) + DrawLine((int)gridPosition.X + x * GRID_CELL_SIZE, (int)gridPosition.Y, + (int)gridPosition.X + x * GRID_CELL_SIZE, (int)gridPosition.Y + MAX_GRID_CELLS_Y * GRID_CELL_SIZE, Color.Gray); + + // Draw player + DrawRectangle((int)gridPosition.X + player.Cell.X * GRID_CELL_SIZE, (int)gridPosition.Y + player.Cell.Y * GRID_CELL_SIZE, + GRID_CELL_SIZE + 1, GRID_CELL_SIZE + 1, player.Color); + + // Draw undo system buffer info + DrawText("UNDO STATES:", (int)undoInfoPos.X - 85, (int)undoInfoPos.Y + 9, 10, Color.DarkGray); + DrawUndoBuffer(undoInfoPos, firstUndoIndex, lastUndoIndex, currentUndoIndex, 24); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + // Draw undo system visualization logic + // NOTE: Visualizing the ring buffer array, every square can store a player state + private static void DrawUndoBuffer(Vector2 position, int firstUndoIndex, int lastUndoIndex, int currentUndoIndex, int slotSize) + { + // Draw index marks + DrawRectangle((int)position.X + 8 + slotSize * currentUndoIndex, (int)position.Y - 10, 8, 8, Color.Red); + DrawRectangleLines((int)position.X + 2 + slotSize * firstUndoIndex, (int)position.Y + 27, 8, 8, Color.Black); + DrawRectangle((int)position.X + 14 + slotSize * lastUndoIndex, (int)position.Y + 27, 8, 8, Color.Black); + + // Draw background gray slots + for (int i = 0; i < MAX_UNDO_STATES; i++) + { + DrawRectangle((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.LightGray); + DrawRectangleLines((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Gray); + } + + // Draw occupied slots: firstUndoIndex --> lastUndoIndex + if (firstUndoIndex <= lastUndoIndex) + { + for (int i = firstUndoIndex; i < lastUndoIndex + 1; i++) + { + DrawRectangle((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.SkyBlue); + DrawRectangleLines((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Blue); + } + } + else if (lastUndoIndex < firstUndoIndex) + { + for (int i = firstUndoIndex; i < MAX_UNDO_STATES; i++) + { + DrawRectangle((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.SkyBlue); + DrawRectangleLines((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Blue); + } + + for (int i = 0; i < lastUndoIndex + 1; i++) + { + DrawRectangle((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.SkyBlue); + DrawRectangleLines((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Blue); + } + } + + // Draw occupied slots: firstUndoIndex --> currentUndoIndex + if (firstUndoIndex < currentUndoIndex) + { + for (int i = firstUndoIndex; i < currentUndoIndex; i++) + { + DrawRectangle((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Green); + DrawRectangleLines((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Lime); + } + } + else if (currentUndoIndex < firstUndoIndex) + { + for (int i = firstUndoIndex; i < MAX_UNDO_STATES; i++) + { + DrawRectangle((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Green); + DrawRectangleLines((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Lime); + } + + for (int i = 0; i < currentUndoIndex; i++) + { + DrawRectangle((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Green); + DrawRectangleLines((int)position.X + slotSize * i, (int)position.Y, slotSize, slotSize, Color.Lime); + } + } + + // Draw current selected UNDO slot + DrawRectangle((int)position.X + slotSize * currentUndoIndex, (int)position.Y, slotSize, slotSize, Color.Gold); + DrawRectangleLines((int)position.X + slotSize * currentUndoIndex, (int)position.Y, slotSize, slotSize, Color.Orange); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - undo redo"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new UndoRedo(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/ViewportScaling.cs b/Examples/Core/ViewportScaling.cs new file mode 100644 index 0000000..b6324f0 --- /dev/null +++ b/Examples/Core/ViewportScaling.cs @@ -0,0 +1,361 @@ +/******************************************************************************************* +* +* raylib [core] example - viewport scaling +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.5 +* +* Example contributed by Agnis Aldiņš (@nezvers) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Agnis Aldiņš (@nezvers) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +public partial class ViewportScaling : IExample +{ + private const int ResolutionCount = 4; // For iteration purposes and teaching example + + private enum ViewportType + { + // Only upscale, useful for pixel art + KeepAspectInteger, + KeepHeightInteger, + KeepWidthInteger, + // Can also downscale + KeepAspect, + KeepHeight, + KeepWidth, + // For itteration purposes and as a teaching example + ViewportTypeCount, + } + + // For displaying on GUI + private static readonly string[] ViewportTypeNames = new string[] + { + "KEEP_ASPECT_INTEGER", + "KEEP_HEIGHT_INTEGER", + "KEEP_WIDTH_INTEGER", + "KEEP_ASPECT", + "KEEP_HEIGHT", + "KEEP_WIDTH", + }; + + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Viewport Scaling"; + + public string Title => "raylib [core] example - viewport scaling"; + + public ConfigFlags ConfigFlags => ConfigFlags.ResizableWindow; + + // Mutable window size (tracked from GetScreenWidth/GetScreenHeight) + private int curScreenWidth; + private int curScreenHeight; + + private Vector2[] resolutionList; + private int resolutionIndex; + private int gameWidth; + private int gameHeight; + + private RenderTexture2D target; + private Rectangle sourceRect; + private Rectangle destRect; + + private ViewportType viewportType; + + // Button rectangles + private Rectangle decreaseResolutionButton; + private Rectangle increaseResolutionButton; + private Rectangle decreaseTypeButton; + private Rectangle increaseTypeButton; + + public void Init() + { + curScreenWidth = screenWidth; + curScreenHeight = screenHeight; + + // Preset resolutions that could be created by subdividing screen resolution + resolutionList = new Vector2[] + { + new Vector2(64, 64), + new Vector2(256, 240), + new Vector2(320, 180), + // 4K doesn't work with integer scaling but included for example purposes with non-integer scaling + new Vector2(3840, 2160), + }; + + resolutionIndex = 0; + gameWidth = 64; + gameHeight = 64; + + target = default; + sourceRect = default; + destRect = default; + + viewportType = ViewportType.KeepAspectInteger; + ResizeRenderSize(viewportType, ref curScreenWidth, ref curScreenHeight, gameWidth, gameHeight, ref sourceRect, ref destRect, ref target); + + // Button rectangles + decreaseResolutionButton = new Rectangle(200, 30, 10, 10); + increaseResolutionButton = new Rectangle(215, 30, 10, 10); + decreaseTypeButton = new Rectangle(200, 45, 10, 10); + increaseTypeButton = new Rectangle(215, 45, 10, 10); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsWindowResized()) ResizeRenderSize(viewportType, ref curScreenWidth, ref curScreenHeight, gameWidth, gameHeight, ref sourceRect, ref destRect, ref target); + + Vector2 mousePosition = GetMousePosition(); + bool mousePressed = IsMouseButtonPressed(MouseButton.Left); + + // Check buttons and rescale + if (CheckCollisionPointRec(mousePosition, decreaseResolutionButton) && mousePressed) + { + resolutionIndex = (resolutionIndex + ResolutionCount - 1) % ResolutionCount; + gameWidth = (int)resolutionList[resolutionIndex].X; + gameHeight = (int)resolutionList[resolutionIndex].Y; + ResizeRenderSize(viewportType, ref curScreenWidth, ref curScreenHeight, gameWidth, gameHeight, ref sourceRect, ref destRect, ref target); + } + + if (CheckCollisionPointRec(mousePosition, increaseResolutionButton) && mousePressed) + { + resolutionIndex = (resolutionIndex + 1) % ResolutionCount; + gameWidth = (int)resolutionList[resolutionIndex].X; + gameHeight = (int)resolutionList[resolutionIndex].Y; + ResizeRenderSize(viewportType, ref curScreenWidth, ref curScreenHeight, gameWidth, gameHeight, ref sourceRect, ref destRect, ref target); + } + + if (CheckCollisionPointRec(mousePosition, decreaseTypeButton) && mousePressed) + { + viewportType = (ViewportType)(((int)viewportType + (int)ViewportType.ViewportTypeCount - 1) % (int)ViewportType.ViewportTypeCount); + ResizeRenderSize(viewportType, ref curScreenWidth, ref curScreenHeight, gameWidth, gameHeight, ref sourceRect, ref destRect, ref target); + } + + if (CheckCollisionPointRec(mousePosition, increaseTypeButton) && mousePressed) + { + viewportType = (ViewportType)(((int)viewportType + 1) % (int)ViewportType.ViewportTypeCount); + ResizeRenderSize(viewportType, ref curScreenWidth, ref curScreenHeight, gameWidth, gameHeight, ref sourceRect, ref destRect, ref target); + } + + Vector2 textureMousePosition = Screen2RenderTexturePosition(mousePosition, sourceRect, destRect); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + // Draw our scene to the render texture + BeginTextureMode(target); + ClearBackground(Color.White); + DrawCircleV(textureMousePosition, 20.0f, Color.Lime); + EndTextureMode(); + + // Draw render texture to main framebuffer + BeginDrawing(); + ClearBackground(Color.Black); + + // Draw our render texture with rotation applied + DrawTexturePro(target.Texture, sourceRect, destRect, new Vector2(0.0f, 0.0f), 0.0f, Color.White); + + // Draw Native resolution (GUI or anything) + // Draw info box + Rectangle infoRect = new Rectangle(5, 5, 330, 105); + DrawRectangleRec(infoRect, Fade(Color.LightGray, 0.7f)); + DrawRectangleLinesEx(infoRect, 1, Color.Blue); + + DrawText($"Window Resolution: {curScreenWidth} x {curScreenHeight}", 15, 15, 10, Color.Black); + DrawText($"Game Resolution: {gameWidth} x {gameHeight}", 15, 30, 10, Color.Black); + + DrawText($"Type: {ViewportTypeNames[(int)viewportType]}", 15, 45, 10, Color.Black); + Vector2 scaleRatio = new Vector2(destRect.Width / sourceRect.Width, -destRect.Height / sourceRect.Height); + if (scaleRatio.X < 0.001f || scaleRatio.Y < 0.001f) DrawText("Scale ratio: INVALID", 15, 60, 10, Color.Black); + else DrawText($"Scale ratio: {scaleRatio.X:F2} x {scaleRatio.Y:F2}", 15, 60, 10, Color.Black); + + DrawText($"Source size: {sourceRect.Width:F2} x {-sourceRect.Height:F2}", 15, 75, 10, Color.Black); + DrawText($"Destination size: {destRect.Width:F2} x {destRect.Height:F2}", 15, 90, 10, Color.Black); + + // Draw buttons + DrawRectangleRec(decreaseTypeButton, Color.SkyBlue); + DrawRectangleRec(increaseTypeButton, Color.SkyBlue); + DrawRectangleRec(decreaseResolutionButton, Color.SkyBlue); + DrawRectangleRec(increaseResolutionButton, Color.SkyBlue); + DrawText("<", (int)decreaseTypeButton.X + 3, (int)decreaseTypeButton.Y + 1, 10, Color.Black); + DrawText(">", (int)increaseTypeButton.X + 3, (int)increaseTypeButton.Y + 1, 10, Color.Black); + DrawText("<", (int)decreaseResolutionButton.X + 3, (int)decreaseResolutionButton.Y + 1, 10, Color.Black); + DrawText(">", (int)increaseResolutionButton.X + 3, (int)increaseResolutionButton.Y + 1, 10, Color.Black); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadRenderTexture(target); + } + + //-------------------------------------------------------------------------------------- + // Module Functions Definition + //-------------------------------------------------------------------------------------- + private static void KeepAspectCenteredInteger(int screenWidth, int screenHeight, int gameWidth, int gameHeight, ref Rectangle sourceRect, ref Rectangle destRect) + { + sourceRect.X = 0.0f; + sourceRect.Y = (float)gameHeight; + sourceRect.Width = (float)gameWidth; + sourceRect.Height = (float)-gameHeight; + + int ratioX = (screenWidth / gameWidth); + int ratioY = (screenHeight / gameHeight); + float resizeRatio = (float)((ratioX < ratioY) ? ratioX : ratioY); + + destRect.X = (float)(int)((screenWidth - (gameWidth * resizeRatio)) * 0.5f); + destRect.Y = (float)(int)((screenHeight - (gameHeight * resizeRatio)) * 0.5f); + destRect.Width = (float)(int)(gameWidth * resizeRatio); + destRect.Height = (float)(int)(gameHeight * resizeRatio); + } + + private static void KeepHeightCenteredInteger(int screenWidth, int screenHeight, int gameWidth, int gameHeight, ref Rectangle sourceRect, ref Rectangle destRect) + { + float resizeRatio = (float)screenHeight / gameHeight; + sourceRect.X = 0.0f; + sourceRect.Y = 0.0f; + sourceRect.Width = (float)(int)(screenWidth / resizeRatio); + sourceRect.Height = (float)-gameHeight; + + destRect.X = (float)(int)((screenWidth - (sourceRect.Width * resizeRatio)) * 0.5f); + destRect.Y = (float)(int)((screenHeight - (gameHeight * resizeRatio)) * 0.5f); + destRect.Width = (float)(int)(sourceRect.Width * resizeRatio); + destRect.Height = (float)(int)(gameHeight * resizeRatio); + } + + private static void KeepWidthCenteredInteger(int screenWidth, int screenHeight, int gameWidth, int gameHeight, ref Rectangle sourceRect, ref Rectangle destRect) + { + float resizeRatio = (float)screenWidth / gameWidth; + sourceRect.X = 0.0f; + sourceRect.Y = 0.0f; + sourceRect.Width = (float)gameWidth; + sourceRect.Height = (float)(int)(screenHeight / resizeRatio); + + destRect.X = (float)(int)((screenWidth - (gameWidth * resizeRatio)) * 0.5f); + destRect.Y = (float)(int)((screenHeight - (sourceRect.Height * resizeRatio)) * 0.5f); + destRect.Width = (float)(int)(gameWidth * resizeRatio); + destRect.Height = (float)(int)(sourceRect.Height * resizeRatio); + + sourceRect.Height *= -1.0f; + } + + private static void KeepAspectCentered(int screenWidth, int screenHeight, int gameWidth, int gameHeight, ref Rectangle sourceRect, ref Rectangle destRect) + { + sourceRect.X = 0.0f; + sourceRect.Y = (float)gameHeight; + sourceRect.Width = (float)gameWidth; + sourceRect.Height = (float)-gameHeight; + + float ratioX = ((float)screenWidth / (float)gameWidth); + float ratioY = ((float)screenHeight / (float)gameHeight); + float resizeRatio = (ratioX < ratioY ? ratioX : ratioY); + + destRect.X = (float)(int)((screenWidth - (gameWidth * resizeRatio)) * 0.5f); + destRect.Y = (float)(int)((screenHeight - (gameHeight * resizeRatio)) * 0.5f); + destRect.Width = (float)(int)(gameWidth * resizeRatio); + destRect.Height = (float)(int)(gameHeight * resizeRatio); + } + + private static void KeepHeightCentered(int screenWidth, int screenHeight, int gameWidth, int gameHeight, ref Rectangle sourceRect, ref Rectangle destRect) + { + float resizeRatio = ((float)screenHeight / (float)gameHeight); + sourceRect.X = 0.0f; + sourceRect.Y = 0.0f; + sourceRect.Width = (float)(int)((float)screenWidth / resizeRatio); + sourceRect.Height = (float)-gameHeight; + + destRect.X = (float)(int)((screenWidth - (sourceRect.Width * resizeRatio)) * 0.5f); + destRect.Y = (float)(int)((screenHeight - (gameHeight * resizeRatio)) * 0.5f); + destRect.Width = (float)(int)(sourceRect.Width * resizeRatio); + destRect.Height = (float)(int)(gameHeight * resizeRatio); + } + + private static void KeepWidthCentered(int screenWidth, int screenHeight, int gameWidth, int gameHeight, ref Rectangle sourceRect, ref Rectangle destRect) + { + float resizeRatio = ((float)screenWidth / (float)gameWidth); + sourceRect.X = 0.0f; + sourceRect.Y = 0.0f; + sourceRect.Width = (float)gameWidth; + sourceRect.Height = (float)(int)((float)screenHeight / resizeRatio); + + destRect.X = (float)(int)((screenWidth - (gameWidth * resizeRatio)) * 0.5f); + destRect.Y = (float)(int)((screenHeight - (sourceRect.Height * resizeRatio)) * 0.5f); + destRect.Width = (float)(int)(gameWidth * resizeRatio); + destRect.Height = (float)(int)(sourceRect.Height * resizeRatio); + + sourceRect.Height *= -1.0f; + } + + private static void ResizeRenderSize(ViewportType viewportType, ref int screenWidth, ref int screenHeight, int gameWidth, int gameHeight, ref Rectangle sourceRect, ref Rectangle destRect, ref RenderTexture2D target) + { + screenWidth = GetScreenWidth(); + screenHeight = GetScreenHeight(); + + switch (viewportType) + { + case ViewportType.KeepAspectInteger: KeepAspectCenteredInteger(screenWidth, screenHeight, gameWidth, gameHeight, ref sourceRect, ref destRect); break; + case ViewportType.KeepHeightInteger: KeepHeightCenteredInteger(screenWidth, screenHeight, gameWidth, gameHeight, ref sourceRect, ref destRect); break; + case ViewportType.KeepWidthInteger: KeepWidthCenteredInteger(screenWidth, screenHeight, gameWidth, gameHeight, ref sourceRect, ref destRect); break; + case ViewportType.KeepAspect: KeepAspectCentered(screenWidth, screenHeight, gameWidth, gameHeight, ref sourceRect, ref destRect); break; + case ViewportType.KeepHeight: KeepHeightCentered(screenWidth, screenHeight, gameWidth, gameHeight, ref sourceRect, ref destRect); break; + case ViewportType.KeepWidth: KeepWidthCentered(screenWidth, screenHeight, gameWidth, gameHeight, ref sourceRect, ref destRect); break; + default: break; + } + + UnloadRenderTexture(target); + target = LoadRenderTexture((int)sourceRect.Width, -(int)sourceRect.Height); + } + + // Example how to calculate position on RenderTexture + private static Vector2 Screen2RenderTexturePosition(Vector2 point, Rectangle textureRect, Rectangle scaledRect) + { + Vector2 relativePosition = new Vector2(point.X - scaledRect.X, point.Y - scaledRect.Y); + Vector2 ratio = new Vector2(textureRect.Width / scaledRect.Width, -textureRect.Height / scaledRect.Height); + + return new Vector2(relativePosition.X * ratio.X, relativePosition.Y * ratio.X); + } + + public static int Main() + { + // Initialization + //--------------------------------------------------------- + SetConfigFlags(ConfigFlags.ResizableWindow); + InitWindow(screenWidth, screenHeight, "raylib [core] example - viewport scaling"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //---------------------------------------------------------- + + var game = new ViewportScaling(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //---------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //---------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Core/WindowFlags.cs b/Examples/Core/WindowFlags.cs index 172ef69..2f82111 100644 --- a/Examples/Core/WindowFlags.cs +++ b/Examples/Core/WindowFlags.cs @@ -19,6 +19,7 @@ using static Raylib_cs.ConfigFlags; namespace Examples.Core; +[ExcludeFromBrowser("runtime window-state flags don't apply to the emscripten canvas")] public partial class WindowFlags : IExample { private const int screenWidth = 800; diff --git a/Examples/Core/WindowShouldClose.cs b/Examples/Core/WindowShouldClose.cs new file mode 100644 index 0000000..b1d4ed5 --- /dev/null +++ b/Examples/Core/WindowShouldClose.cs @@ -0,0 +1,130 @@ +/******************************************************************************************* +* +* raylib [core] example - window should close +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 4.2, last time updated with raylib 4.2 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2013-2025 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Core; + +public partial class WindowShouldClose : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Window Should Close"; + + public string Title => "raylib [core] example - window should close"; + + // The runner drives its loop off this instead of WindowShouldClose(), so the exit + // confirmation (polled in Update) can intercept both the X-button and KEY_ESCAPE. + public bool ShouldClose => exitWindow; + + private bool exitWindowRequested; // Flag to request window to exit + private bool exitWindow; // Flag to set window to exit + + public void Init() + { + SetExitKey(KeyboardKey.Null); // Disable KEY_ESCAPE to close window, X-button still works + + exitWindowRequested = false; + exitWindow = false; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Detect if X-button or KEY_ESCAPE have been pressed to close window + bool closeRequested = IsKeyPressed(KeyboardKey.Escape); +#if !BROWSER + // WindowShouldClose() polls the OS window close button, but on the wasm target it calls + // emscripten_sleep (ASYNCIFY is not enabled) and the canvas has no window chrome anyway, + // so on web the confirmation is triggered by KEY_ESCAPE only. + closeRequested |= Raylib.WindowShouldClose(); +#endif + if (closeRequested) + { + exitWindowRequested = true; + } + + if (exitWindowRequested) + { + // A request for close window has been issued, we can save data before closing + // or just show a message asking for confirmation + + if (IsKeyPressed(KeyboardKey.Y)) + { + exitWindow = true; + } + else if (IsKeyPressed(KeyboardKey.N)) + { + exitWindowRequested = false; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + if (exitWindowRequested) + { + DrawRectangle(0, 100, screenWidth, 200, Color.Black); + DrawText("Are you sure you want to exit program? [Y/N]", 40, 180, 30, Color.White); + } + else + { + DrawText("Try to close the window to get confirmation message!", 120, 200, 20, Color.LightGray); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - window should close"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new WindowShouldClose(); + game.Init(); + + // Main game loop + while (!game.exitWindow) + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/ExampleRegistry.cs b/Examples/ExampleRegistry.cs index 12ee307..175a443 100644 --- a/Examples/ExampleRegistry.cs +++ b/Examples/ExampleRegistry.cs @@ -1,15 +1,12 @@ using System.Diagnostics.CodeAnalysis; -using Examples.Core; -using Examples.Models; -using Examples.Shapes; namespace Examples; /// -/// Discovers every implementation in this assembly via reflection and -/// derives the per-platform lists: (Program.cs) and -/// (Web/Host.cs). Ordering is by category (browser dropdown -/// grouping), then display name. +/// Discovers every implementation in this assembly via reflection: +/// (Program.cs) is all of them, +/// (Web/Host.cs) is everything not marked . +/// Ordering is by category (browser dropdown grouping), then display name. /// public static class ExampleRegistry { @@ -25,29 +22,12 @@ public static class ExampleRegistry "Shaders", ]; - /// Desktop examples omitted from the browser host (platform limitations). - private static readonly Type[] DesktopExcludedFromBrowser = - [ - typeof(DropFiles), - typeof(LoadingThread), // System.Threading.Thread is unsupported on single-threaded wasm - typeof(SkyboxDemo), - ]; - - /// Browser-only shape examples not registered for desktop CLI runs. - private static readonly Type[] BrowserOnly = - [ - typeof(DrawCircleSector), - typeof(DrawRectangleRounded), - typeof(DrawRing), - ]; - private static readonly IExample[] AllExamples = DiscoverAll(); - public static readonly IExample[] DesktopExamples = - Array.FindAll(AllExamples, e => Array.IndexOf(BrowserOnly, e.GetType()) < 0); + public static readonly IExample[] DesktopExamples = AllExamples; public static readonly IExample[] BrowserExamples = - Array.FindAll(AllExamples, e => Array.IndexOf(DesktopExcludedFromBrowser, e.GetType()) < 0); + Array.FindAll(AllExamples, e => !e.GetType().IsDefined(typeof(ExcludeFromBrowserAttribute), inherit: false)); [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The Examples assembly is rooted via TrimmerRootAssembly in Examples.csproj.")] diff --git a/Examples/Examples.csproj b/Examples/Examples.csproj index 4fd62aa..43d205b 100644 --- a/Examples/Examples.csproj +++ b/Examples/Examples.csproj @@ -2,6 +2,9 @@ Exe net8.0;net10.0 + + net10.0 true Examples.Program Examples.Web.Host @@ -31,10 +34,6 @@ false - - - - diff --git a/Examples/ExcludeFromBrowserAttribute.cs b/Examples/ExcludeFromBrowserAttribute.cs new file mode 100644 index 0000000..e2224c7 --- /dev/null +++ b/Examples/ExcludeFromBrowserAttribute.cs @@ -0,0 +1,17 @@ +namespace Examples; + +/// +/// Marks an that is omitted from the browser host (Web/Host.cs) +/// because of a wasm/WebGL platform limitation. Desktop runs are unaffected. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public sealed class ExcludeFromBrowserAttribute : Attribute +{ + /// Why the example cannot run in the browser. + public string Reason { get; } + + public ExcludeFromBrowserAttribute(string reason = null) + { + Reason = reason; + } +} diff --git a/Examples/IExample.cs b/Examples/IExample.cs index 7c51b49..f2100d9 100644 --- a/Examples/IExample.cs +++ b/Examples/IExample.cs @@ -1,18 +1,14 @@ namespace Examples; /// -/// A runnable raylib example. Each example class implements this interface directly: loop-spanning -/// state from the original example's Main lives in instance fields, (re)initialized in +/// A runnable raylib example. Loop-spanning state lives in instance fields, (re)initialized in /// so re-selecting an example resets it. /// /// -/// Desktop runs the example via its static Main(), a thin driver that owns the window -/// (InitWindow/CloseWindow, SetTargetFPS and friends) and drives , -/// , and around a blocking -/// while (!WindowShouldClose()) loop. In the browser, Web/Host.cs owns the single -/// window and calls one frame at a time from JavaScript, so examples never -/// block. Platform divergences (e.g. GLSL 100 vs 330 shaders) are guarded with #if BROWSER, -/// preferably around a single constant so both platforms share one code path. +/// Desktop drives examples via Program.cs (each also keeps a thin standalone +/// static Main()); in the browser, Web/Host.cs owns the single window and calls +/// one frame at a time from JavaScript. Platform divergences are guarded +/// with #if BROWSER, preferably around a single constant. /// /// public interface IExample @@ -23,19 +19,32 @@ public interface IExample /// Window title, matching the example's standalone Main(). string Title { get; } - /// Config flags the desktop runner applies before window creation. + /// Desktop window size, matching the standalone Main(). The browser canvas is fixed at 800x450. + int Width => 800; + + /// + int Height => 450; + + /// Config flags the desktop runner applies before window creation. Ignored in the browser. ConfigFlags ConfigFlags => 0; - /// Target FPS the desktop runner sets after window creation. + /// Target FPS. The desktop runner always applies it; the browser host paces its frame loop with it. int TargetFps => 60; - /// Whether the desktop runner disables the cursor (relative mouse movement). + /// Whether the desktop runner disables the cursor. Ignored in the browser (the pointer stays visible). bool CursorDisabled => false; - /// Whether the desktop runner hides the cursor. + /// Whether the desktop runner hides the cursor. Ignored in the browser. bool CursorHidden => false; - /// One-time setup. + /// + /// Whether the desktop runner should exit its frame loop; defaults to + /// . Examples that intercept the close request + /// override this and poll WindowShouldClose in themselves. + /// + bool ShouldClose => WindowShouldClose(); + + /// Set up the Example and preload necessary resources. void Init(); /// Render one frame, including BeginDrawing/EndDrawing. diff --git a/Examples/Models/AnimationBlendCustom.cs b/Examples/Models/AnimationBlendCustom.cs new file mode 100644 index 0000000..2bacd9e --- /dev/null +++ b/Examples/Models/AnimationBlendCustom.cs @@ -0,0 +1,390 @@ +/******************************************************************************************* +* +* raylib [models] example - animation blend custom +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 6.0 +* +* Example contributed by dmitrii-brand (@dmitrii-brand) and reviewed by Ramon Santamaria (@raysan5) +* +* DETAILS: Example demonstrates per-bone animation blending, allowing smooth transitions +* between two animations by interpolating bone transforms. This is useful for: +* - Blending movement animations (walk/run) with action animations (jump/attack) +* - Creating smooth animation transitions +* - Layering animations (e.g., upper body attack while lower body walks) +* +* WARNING: GPU skinning must be enabled in raylib with a compilation flag, +* if not enabled, CPU skinning will be used instead +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2026 dmitrii-brand (@dmitrii-brand) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; + +namespace Examples.Models; + +public partial class AnimationBlendCustom : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + +#if BROWSER + private const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Models / Animation Blend Custom"; + + public string Title => "raylib [models] example - animation blend custom"; + + private Camera3D camera; + private Model model; + private Vector3 position; + private Shader skinningShader; + private unsafe ModelAnimation* anims; + private int animCount; + private int animIndex0; + private int animIndex1; + private int animCurrentFrame0; + private int animCurrentFrame1; + private bool upperBodyBlend; + + public unsafe void Init() + { + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(4.0f, 4.0f, 4.0f); // Camera position + camera.Target = new Vector3(0.0f, 1.0f, 0.0f); // Camera looking at point + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target) + camera.FovY = 45.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + + // Load gltf model + model = LoadModel("resources/models/gltf/greenman.glb"); + position = new Vector3(0.0f, 0.0f, 0.0f); // Set model position + + // Load skinning shader + // WARNING: GPU skinning must be enabled in raylib with a compilation flag, + // if not enabled, CPU skinning will be used instead + skinningShader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/skinning.vs", + $"resources/shaders/glsl{GlslVersion}/skinning.fs" + ); + model.Materials[1].Shader = skinningShader; + + // Load gltf model animations + animCount = 0; + anims = LoadModelAnimations("resources/models/gltf/greenman.glb", ref animCount); + + // Use specific animation indices: 2-walk/move, 3-attack + animIndex0 = 2; // Walk/Move animation (index 2) + animIndex1 = 3; // Attack animation (index 3) + animCurrentFrame0 = 0; + animCurrentFrame1 = 0; + + // Validate indices + if (animIndex0 >= animCount) + { + animIndex0 = 0; + } + if (animIndex1 >= animCount) + { + animIndex1 = (animCount > 1) ? 1 : 0; + } + + upperBodyBlend = true; // Toggle: true = upper/lower body blending, false = uniform blending (50/50) + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + + // Toggle upper/lower body blending mode (SPACE key) + if (IsKeyPressed(KeyboardKey.Space)) + { + upperBodyBlend = !upperBodyBlend; + } + + // Update animation frames + var anim0 = anims[animIndex0]; + var anim1 = anims[animIndex1]; + + animCurrentFrame0 = (animCurrentFrame0 + 1) % anim0.KeyFrameCount; + animCurrentFrame1 = (animCurrentFrame1 + 1) % anim1.KeyFrameCount; + + // Blend the two animations + // When upperBodyBlend is ON: upper body = attack (1.0), lower body = walk (0.0) + // When upperBodyBlend is OFF: uniform blend at 0.5 (50% walk, 50% attack) + var blendFactor = upperBodyBlend ? 1.0f : 0.5f; + UpdateModelAnimationBones(anim0, animCurrentFrame0, anim1, animCurrentFrame1, blendFactor, upperBodyBlend); + + // raylib provided animation blending function + //UpdateModelAnimationEx(model, anim0, (float)animCurrentFrame0, + // anim1, (float)animCurrentFrame1, blendFactor); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawModel(model, position, 1.0f, Color.White); + + DrawGrid(10, 1.0f); + + EndMode3D(); + + // Draw UI + DrawText($"ANIM 0: {anim0.NameToString()}", 10, 10, 20, Color.Gray); + DrawText($"ANIM 1: {anim1.NameToString()}", 10, 40, 20, Color.Gray); + DrawText($"[SPACE] Toggle blending mode: {(upperBodyBlend ? "Upper/Lower Body Blending" : "Uniform Blending")}", + 10, GetScreenHeight() - 30, 20, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public unsafe void Unload() + { + UnloadModelAnimations(anims, animCount); // Unload model animation + UnloadModel(model); // Unload model and meshes/material + UnloadShader(skinningShader); // Unload GPU skinning shader + } + + // Check if a bone is part of upper body (for selective blending) + private static bool IsUpperBodyBone(string boneName) + { + // Common upper body bone names (adjust based on your model) + if (boneName is "spine" or "spine1" or "spine2" or + "chest" or "upperChest" or + "neck" or "head" or + "shoulder" or "shoulder_L" or "shoulder_R" or + "upperArm" or "upperArm_L" or "upperArm_R" or + "lowerArm" or "lowerArm_L" or "lowerArm_R" or + "hand" or "hand_L" or "hand_R" or + "clavicle" or "clavicle_L" or "clavicle_R") + { + return true; + } + + // Check if bone name contains upper body keywords + if (boneName.Contains("spine") || boneName.Contains("chest") || + boneName.Contains("neck") || boneName.Contains("head") || + boneName.Contains("shoulder") || boneName.Contains("arm") || + boneName.Contains("hand") || boneName.Contains("clavicle")) + { + return true; + } + + return false; + } + + // Blend two animations per-bone with selective upper/lower body blending + private unsafe void UpdateModelAnimationBones(ModelAnimation anim0, int frame0, + ModelAnimation anim1, int frame1, float blend, bool upperBodyBlend) + { + // Validate inputs + if ((anim0.BoneCount != 0) && (anim0.KeyframePoses != null) && + (anim1.BoneCount != 0) && (anim1.KeyframePoses != null) && + (model.Skeleton.BoneCount != 0) && (model.Skeleton.BindPose != null)) + { + // Clamp blend factor to [0, 1] + blend = MathF.Min(1.0f, MathF.Max(0.0f, blend)); + + // Ensure frame indices are valid + if (frame0 >= anim0.KeyFrameCount) + { + frame0 = anim0.KeyFrameCount - 1; + } + if (frame1 >= anim1.KeyFrameCount) + { + frame1 = anim1.KeyFrameCount - 1; + } + if (frame0 < 0) + { + frame0 = 0; + } + if (frame1 < 0) + { + frame1 = 0; + } + + // Get bone count (use minimum of all to be safe) + var boneCount = model.Skeleton.BoneCount; + if (anim0.BoneCount < boneCount) + { + boneCount = anim0.BoneCount; + } + if (anim1.BoneCount < boneCount) + { + boneCount = anim1.BoneCount; + } + + // Blend each bone + for (var boneIndex = 0; boneIndex < boneCount; boneIndex++) + { + // Determine blend factor for this bone + var boneBlendFactor = blend; + + // If upper body blending is enabled, use different blend factors for upper vs lower body + if (upperBodyBlend) + { + var boneName = model.Skeleton.Bones[boneIndex].NameToString(); + var isUpperBody = IsUpperBodyBone(boneName); + + // Upper body: use anim1 (attack), Lower body: use anim0 (walk) + // blend = 0.0 means full anim0 (walk), 1.0 means full anim1 (attack) + if (isUpperBody) + { + boneBlendFactor = blend; // Upper body: blend towards anim1 (attack) + } + else + { + boneBlendFactor = 1.0f - blend; // Lower body: blend towards anim0 (walk) - invert the blend + } + } + + // Get transforms from both animations + var bindTransform = model.Skeleton.BindPose[boneIndex]; + var animTransform0 = anim0.KeyframePoses[frame0][boneIndex]; + var animTransform1 = anim1.KeyframePoses[frame1][boneIndex]; + + // Blend the transforms + Transform blended = new(); + blended.Translation = Vector3Lerp(animTransform0.Translation, animTransform1.Translation, boneBlendFactor); + blended.Rotation = QuaternionSlerp(animTransform0.Rotation, animTransform1.Rotation, boneBlendFactor); + blended.Scale = Vector3Lerp(animTransform0.Scale, animTransform1.Scale, boneBlendFactor); + + // Convert bind pose to matrix + var bindMatrix = MatrixMultiply(MatrixMultiply( + MatrixScale(bindTransform.Scale.X, bindTransform.Scale.Y, bindTransform.Scale.Z), + QuaternionToMatrix(bindTransform.Rotation)), + MatrixTranslate(bindTransform.Translation.X, bindTransform.Translation.Y, bindTransform.Translation.Z)); + + // Convert blended transform to matrix + var blendedMatrix = MatrixMultiply(MatrixMultiply( + MatrixScale(blended.Scale.X, blended.Scale.Y, blended.Scale.Z), + QuaternionToMatrix(blended.Rotation)), + MatrixTranslate(blended.Translation.X, blended.Translation.Y, blended.Translation.Z)); + + // Calculate final bone matrix (similar to UpdateModelAnimationBones) + model.BoneMatrices[boneIndex] = MatrixMultiply(MatrixInvert(bindMatrix), blendedMatrix); + } + + // CPU skinning, updates CPU buffers and uploads them to GPU (if available) + // NOTE: Fallback in case GPU skinning is not supported or enabled + for (var m = 0; m < model.MeshCount; m++) + { + var mesh = model.Meshes[m]; + Vector3 animVertex; + Vector3 animNormal; + var vertexValuesCount = mesh.VertexCount * 3; + + var boneCounter = 0; + var bufferUpdateRequired = false; // Flag to check when anim vertex information is updated + + // Skip if missing bone data or missing anim buffers initialization + if ((mesh.BoneWeights == null) || (mesh.BoneIndices == null) || + (mesh.AnimVertices == null) || (mesh.AnimNormals == null)) + { + continue; + } + + for (var vCounter = 0; vCounter < vertexValuesCount; vCounter += 3) + { + mesh.AnimVertices[vCounter] = 0; + mesh.AnimVertices[vCounter + 1] = 0; + mesh.AnimVertices[vCounter + 2] = 0; + if (mesh.AnimNormals != null) + { + mesh.AnimNormals[vCounter] = 0; + mesh.AnimNormals[vCounter + 1] = 0; + mesh.AnimNormals[vCounter + 2] = 0; + } + + // Iterates over 4 bones per vertex + for (var j = 0; j < 4; j++, boneCounter++) + { + var boneWeight = mesh.BoneWeights[boneCounter]; + var boneIndex = mesh.BoneIndices[boneCounter]; + + // Early stop when no transformation will be applied + if (boneWeight == 0.0f) + { + continue; + } + animVertex = new Vector3(mesh.Vertices[vCounter], mesh.Vertices[vCounter + 1], mesh.Vertices[vCounter + 2]); + animVertex = Vector3Transform(animVertex, model.BoneMatrices[boneIndex]); + mesh.AnimVertices[vCounter] += animVertex.X * boneWeight; + mesh.AnimVertices[vCounter + 1] += animVertex.Y * boneWeight; + mesh.AnimVertices[vCounter + 2] += animVertex.Z * boneWeight; + bufferUpdateRequired = true; + + // Normals processing + // NOTE: We use meshes.baseNormals (default normal) to calculate meshes.normals (animated normals) + if ((mesh.Normals != null) && (mesh.AnimNormals != null)) + { + animNormal = new Vector3(mesh.Normals[vCounter], mesh.Normals[vCounter + 1], mesh.Normals[vCounter + 2]); + animNormal = Vector3Transform(animNormal, MatrixTranspose(MatrixInvert(model.BoneMatrices[boneIndex]))); + mesh.AnimNormals[vCounter] += animNormal.X * boneWeight; + mesh.AnimNormals[vCounter + 1] += animNormal.Y * boneWeight; + mesh.AnimNormals[vCounter + 2] += animNormal.Z * boneWeight; + } + } + } + + if (bufferUpdateRequired) + { + // Update GPU vertex buffers with updated data (position + normals) + Rlgl.UpdateVertexBuffer(mesh.VboId[(int)ShaderLocationIndex.VertexPosition], mesh.AnimVertices, mesh.VertexCount * 3 * sizeof(float), 0); + if (mesh.Normals != null) + { + Rlgl.UpdateVertexBuffer(mesh.VboId[(int)ShaderLocationIndex.VertexNormal], mesh.AnimNormals, mesh.VertexCount * 3 * sizeof(float), 0); + } + } + } + } + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - animation blend custom"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new AnimationBlendCustom(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Models/AnimationBlending.cs b/Examples/Models/AnimationBlending.cs new file mode 100644 index 0000000..eba664b --- /dev/null +++ b/Examples/Models/AnimationBlending.cs @@ -0,0 +1,474 @@ +/******************************************************************************************* +* +* raylib [models] example - animation blending +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 6.0 +* +* Example contributed by Kirandeep (@Kirandeep-Singh-Khehra) and reviewed by Ramon Santamaria (@raysan5) +* +* WARNING: GPU skinning must be enabled in raylib with a compilation flag, +* if not enabled, CPU skinning will be used instead +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2024-2026 Kirandeep (@Kirandeep-Singh-Khehra) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +// NOTE: The upstream example uses raygui for its UI controls (dropdowns, sliders, progress bars). +// raygui is not part of raylib-cs, so the required controls are reimplemented here with +// basic raylib drawing primitives, preserving the original behaviour. + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Models; + +public partial class AnimationBlending : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + +#if BROWSER + private const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Models / Animation Blending"; + + public string Title => "raylib [models] example - animation blending"; + + private Camera3D camera; + private Model model; + private Vector3 position; + private Shader skinningShader; + private unsafe ModelAnimation* anims; + private int animCount; + + private int currentAnimPlaying; + private int nextAnimToPlay; + private bool animTransition; + + private int animIndex0; + private float animCurrentFrame0; + private float animFrameSpeed0; + private int animIndex1; + private float animCurrentFrame1; + private float animFrameSpeed1; + + private float animBlendFactor; + private float animBlendTime; + private float animBlendTimeCounter; + + private bool animPause; + + private string[] animNames; + private bool dropdownEditMode0; + private bool dropdownEditMode1; + private float animFrameProgress0; + private float animFrameProgress1; + private float animBlendProgress; + + public unsafe void Init() + { + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(6.0f, 6.0f, 6.0f); // Camera position + camera.Target = new Vector3(0.0f, 2.0f, 0.0f); // Camera looking at point + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target) + camera.FovY = 45.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + + // Load model + model = LoadModel("resources/models/gltf/robot.glb"); // Load character model + position = new Vector3(0.0f, 0.0f, 0.0f); // Set model world position + + // Load skinning shader + // NOTE: It must be a valid shader, following raylib attribs/uniform conventions for GPU skinning + skinningShader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/skinning.vs", + $"resources/shaders/glsl{GlslVersion}/skinning.fs" + ); + + // Skinning shader could be required to be assigned to all materials shaders, just to make + // sure required uniforms are being updated for the mesh using that material (and shader) + for (var i = 0; i < model.MaterialCount; i++) + { + model.Materials[i].Shader = skinningShader; + } + + // Load model animations + animCount = 0; + anims = LoadModelAnimations("resources/models/gltf/robot.glb", ref animCount); + + // Animation playing variables + // NOTE: Two animations are played with a smooth transition between them + currentAnimPlaying = 0; // Current animation playing (0 o 1) + nextAnimToPlay = 1; // Next animation to play (to transition) + animTransition = false; // Flag to register anim transition state + + animIndex0 = 10; // Current animation playing (walking) + animCurrentFrame0 = 0.0f; // Current animation frame (supporting interpolated frames) + animFrameSpeed0 = 0.5f; // Current animation play speed + animIndex1 = 6; // Next animation to play (running) + animCurrentFrame1 = 0.0f; // Next animation frame (supporting interpolated frames) + animFrameSpeed1 = 0.5f; // Next animation play speed + + animBlendFactor = 0.0f; // Blend factor from anim0[frame0] --> anim1[frame1], [0.0f..1.0f] + + animBlendTime = 2.0f; // Time to blend from one playing animation to another (in seconds) + animBlendTimeCounter = 0.0f; // Time counter (delta time) + + animPause = false; // Pause animation + + // UI required variables + animNames = new string[animCount]; + for (var i = 0; i < animCount; i++) + { + animNames[i] = anims[i].NameToString(); + } + + dropdownEditMode0 = false; + dropdownEditMode1 = false; + animFrameProgress0 = 0.0f; + animFrameProgress1 = 0.0f; + animBlendProgress = 0.0f; + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + + if (IsKeyPressed(KeyboardKey.P)) + { + animPause = !animPause; + } + + if (!animPause) + { + // Start transition from anim0[] to anim1[] + if (IsKeyPressed(KeyboardKey.Space) && !animTransition) + { + if (currentAnimPlaying == 0) + { + // Transition anim0 --> anim1 + nextAnimToPlay = 1; + animCurrentFrame1 = 0.0f; + } + else + { + // Transition anim1 --> anim0 + nextAnimToPlay = 0; + animCurrentFrame0 = 0.0f; + } + + // Set animation transition + animTransition = true; + animBlendTimeCounter = 0.0f; + animBlendFactor = 0.0f; + } + + if (animTransition) + { + // Playing anim0 and anim1 at the same time + animCurrentFrame0 += animFrameSpeed0; + if (animCurrentFrame0 >= anims[animIndex0].KeyFrameCount) + { + animCurrentFrame0 = 0.0f; + } + animCurrentFrame1 += animFrameSpeed1; + if (animCurrentFrame1 >= anims[animIndex1].KeyFrameCount) + { + animCurrentFrame1 = 0.0f; + } + + // Increment blend factor over time to transition from anim0 --> anim1 over time + // NOTE: Time blending could be other than linear, using some easing + animBlendFactor = animBlendTimeCounter / animBlendTime; + animBlendTimeCounter += GetFrameTime(); + animBlendProgress = animBlendFactor; + + // Update model with animations blending + if (nextAnimToPlay == 1) + { + // Blend anim0 --> anim1 + UpdateModelAnimationEx(model, anims[animIndex0], animCurrentFrame0, + anims[animIndex1], animCurrentFrame1, animBlendFactor); + } + else + { + // Blend anim1 --> anim0 + UpdateModelAnimationEx(model, anims[animIndex1], animCurrentFrame1, + anims[animIndex0], animCurrentFrame0, animBlendFactor); + } + + // Check if transition completed + if (animBlendFactor > 1.0f) + { + // Reset frame states + if (currentAnimPlaying == 0) + { + animCurrentFrame0 = 0.0f; + } + else if (currentAnimPlaying == 1) + { + animCurrentFrame1 = 0.0f; + } + currentAnimPlaying = nextAnimToPlay; // Update current animation playing + + animBlendFactor = 0.0f; // Reset blend factor + animTransition = false; // Exit transition mode + animBlendTimeCounter = 0.0f; + } + } + else + { + // Play only one anim, the current one + if (currentAnimPlaying == 0) + { + // Playing anim0 at defined speed + animCurrentFrame0 += animFrameSpeed0; + if (animCurrentFrame0 >= anims[animIndex0].KeyFrameCount) + { + animCurrentFrame0 = 0.0f; + } + UpdateModelAnimation(model, anims[animIndex0], animCurrentFrame0); + } + else if (currentAnimPlaying == 1) + { + // Playing anim1 at defined speed + animCurrentFrame1 += animFrameSpeed1; + if (animCurrentFrame1 >= anims[animIndex1].KeyFrameCount) + { + animCurrentFrame1 = 0.0f; + } + UpdateModelAnimation(model, anims[animIndex1], animCurrentFrame1); + } + } + } + + // Update progress bars values with current frame for each animation + animFrameProgress0 = animCurrentFrame0; + animFrameProgress1 = animCurrentFrame1; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawModel(model, position, 1.0f, Color.White); // Draw animated model + + DrawGrid(10, 1.0f); + + EndMode3D(); + + if (animTransition) + { + DrawText("ANIM TRANSITION BLENDING!", 170, 50, 30, Color.Blue); + } + + // Draw UI elements + //--------------------------------------------------------------------------------------------- + GuiSlider(new Rectangle(10, 38, 160, 12), null, $"x{animFrameSpeed0:0.0}", ref animFrameSpeed0, 0.1f, 2.0f); + GuiSlider(new Rectangle(GetScreenWidth() - 170.0f, 38, 160, 12), $"{animFrameSpeed1:0.0}x", null, ref animFrameSpeed1, 0.1f, 2.0f); + + // Blending process progress bar + GuiProgressBar(new Rectangle(180, 14, 440, 16), null, null, animBlendProgress, 0.0f, 1.0f); + + // Centered "PRESS SPACE" label + const string spaceLabel = "PRESS SPACE to START BLENDING"; + var spaceLabelWidth = MeasureText(spaceLabel, 20); + DrawText(spaceLabel, (GetScreenWidth() - spaceLabelWidth) / 2, (int)(GetScreenHeight() - 100.0f + 10), 20, Color.DarkGray); + + // Draw playing timeline with keyframes for anim0[] + GuiProgressBar(new Rectangle(60, GetScreenHeight() - 60.0f, GetScreenWidth() - 180.0f, 20), "ANIM 0", + $"FRAME: {animFrameProgress0:0.00} / {anims[animIndex0].KeyFrameCount}", + animFrameProgress0, 0.0f, anims[animIndex0].KeyFrameCount); + for (var i = 0; i < anims[animIndex0].KeyFrameCount; i++) + { + DrawRectangle(60 + (int)(((float)(GetScreenWidth() - 180) / anims[animIndex0].KeyFrameCount) * i), + GetScreenHeight() - 60, 1, 20, Color.Blue); + } + + // Draw playing timeline with keyframes for anim1[] + GuiProgressBar(new Rectangle(60, GetScreenHeight() - 30.0f, GetScreenWidth() - 180.0f, 20), "ANIM 1", + $"FRAME: {animFrameProgress1:0.00} / {anims[animIndex1].KeyFrameCount}", + animFrameProgress1, 0.0f, anims[animIndex1].KeyFrameCount); + for (var i = 0; i < anims[animIndex1].KeyFrameCount; i++) + { + DrawRectangle(60 + (int)(((float)(GetScreenWidth() - 180) / anims[animIndex1].KeyFrameCount) * i), + GetScreenHeight() - 30, 1, 20, Color.Blue); + } + + // Draw animation selectors for blending transition (drawn last so open lists render on top) + // NOTE: Transition does not start until requested + if (GuiDropdownBox(new Rectangle(10, 10, 160, 24), animNames, ref animIndex0, dropdownEditMode0)) + { + dropdownEditMode0 = !dropdownEditMode0; + } + if (GuiDropdownBox(new Rectangle(GetScreenWidth() - 170.0f, 10, 160, 24), animNames, ref animIndex1, dropdownEditMode1)) + { + dropdownEditMode1 = !dropdownEditMode1; + } + //--------------------------------------------------------------------------------------------- + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public unsafe void Unload() + { + UnloadModelAnimations(anims, animCount); // Unload model animation + UnloadModel(model); // Unload model and meshes/material + UnloadShader(skinningShader); // Unload GPU skinning shader + } + + // Minimal immediate-mode slider (raygui replacement) + private static bool GuiSlider(Rectangle bounds, string textLeft, string textRight, ref float value, float minValue, float maxValue) + { + var mouse = GetMousePosition(); + var dragging = false; + if (CheckCollisionPointRec(mouse, bounds) && IsMouseButtonDown(MouseButton.Left)) + { + value = minValue + ((mouse.X - bounds.X) / bounds.Width) * (maxValue - minValue); + if (value < minValue) + { + value = minValue; + } + if (value > maxValue) + { + value = maxValue; + } + dragging = true; + } + + DrawRectangleRec(bounds, Color.LightGray); + var pct = (value - minValue) / (maxValue - minValue); + DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), Color.SkyBlue); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (!string.IsNullOrEmpty(textLeft)) + { + DrawText(textLeft, (int)(bounds.X - MeasureText(textLeft, 10) - 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + if (!string.IsNullOrEmpty(textRight)) + { + DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + return dragging; + } + + // Minimal immediate-mode progress bar (raygui replacement) + private static void GuiProgressBar(Rectangle bounds, string textLeft, string textRight, float value, float minValue, float maxValue) + { + DrawRectangleRec(bounds, Color.LightGray); + var pct = maxValue > minValue ? (value - minValue) / (maxValue - minValue) : 0.0f; + if (pct < 0) + { + pct = 0; + } + if (pct > 1) + { + pct = 1; + } + DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), Color.SkyBlue); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (!string.IsNullOrEmpty(textLeft)) + { + DrawText(textLeft, (int)(bounds.X - MeasureText(textLeft, 10) - 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + if (!string.IsNullOrEmpty(textRight)) + { + DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + } + + // Minimal immediate-mode dropdown box (raygui replacement) + private static bool GuiDropdownBox(Rectangle bounds, string[] items, ref int active, bool editMode) + { + var result = false; + var mouse = GetMousePosition(); + + // Draw main box + DrawRectangleRec(bounds, Color.LightGray); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (active >= 0 && active < items.Length) + { + DrawText(items[active], (int)bounds.X + 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + DrawText(editMode ? "^" : "v", (int)(bounds.X + bounds.Width - 12), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + + // Draw items when open + if (editMode) + { + for (var i = 0; i < items.Length; i++) + { + Rectangle item = new(bounds.X, bounds.Y + bounds.Height * (i + 1), bounds.Width, bounds.Height); + var hover = CheckCollisionPointRec(mouse, item); + DrawRectangleRec(item, hover ? Color.SkyBlue : Color.LightGray); + DrawRectangleLinesEx(item, 1, Color.Gray); + DrawText(items[i], (int)item.X + 5, (int)(item.Y + item.Height / 2 - 5), 10, Color.DarkGray); + } + } + + if (IsMouseButtonPressed(MouseButton.Left)) + { + if (editMode) + { + for (var i = 0; i < items.Length; i++) + { + Rectangle item = new(bounds.X, bounds.Y + bounds.Height * (i + 1), bounds.Width, bounds.Height); + if (CheckCollisionPointRec(mouse, item)) + { + active = i; + break; + } + } + result = true; // any click closes the dropdown + } + else if (CheckCollisionPointRec(mouse, bounds)) + { + result = true; // open the dropdown + } + } + + return result; + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - animation blending"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new AnimationBlending(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Models/AnimationGpuSkinning.cs b/Examples/Models/AnimationGpuSkinning.cs new file mode 100644 index 0000000..f4e67bc --- /dev/null +++ b/Examples/Models/AnimationGpuSkinning.cs @@ -0,0 +1,160 @@ +/******************************************************************************************* +* +* raylib [models] example - animation gpu skinning +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 4.5, last time updated with raylib 4.5 +* +* Example contributed by Daniel Holden (@orangeduck) and reviewed by Ramon Santamaria (@raysan5) +* +* WARNING: GPU skinning must be enabled in raylib with a compilation flag, +* if not enabled, CPU skinning will be used instead +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2024-2025 Daniel Holden (@orangeduck) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Models; + +public partial class AnimationGpuSkinning : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + +#if BROWSER + private const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Models / Animation GPU Skinning"; + + public string Title => "raylib [models] example - animation gpu skinning"; + + private Camera3D camera; + private Model model; + private Vector3 position; + private Shader skinningShader; + private unsafe ModelAnimation* anims; + private int animCount; + private int animIndex; + private int animCurrentFrame; + + public unsafe void Init() + { + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(5.0f, 5.0f, 5.0f); // Camera position + camera.Target = new Vector3(0.0f, 1.0f, 0.0f); // Camera looking at point + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target) + camera.FovY = 45.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + + // Load gltf model + model = LoadModel("resources/models/gltf/greenman.glb"); // Load character model + position = new Vector3(0.0f, 0.0f, 0.0f); // Set model position + + // Load skinning shader + // NOTE: It must be a valid shader, following raylib attribs/uniform conventions for GPU skinning + skinningShader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/skinning.vs", + $"resources/shaders/glsl{GlslVersion}/skinning.fs" + ); + + // Skinning shader could be required to be assigned to all materials shaders, just to make + // sure required uniforms are being updated for the mesh using that material (and shader) + model.Materials[1].Shader = skinningShader; // Just assigning to materials[1] for this model + + // Load gltf model animations + animCount = 0; + anims = LoadModelAnimations("resources/models/gltf/greenman.glb", ref animCount); + + // Animation playing variables + animIndex = 0; // Current animation playing + animCurrentFrame = 0; // Current animation frame + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + + // Select current animation + if (IsKeyPressed(KeyboardKey.Right)) + { + animIndex = (animIndex + 1) % animCount; + } + else if (IsKeyPressed(KeyboardKey.Left)) + { + animIndex = (animIndex + animCount - 1) % animCount; + } + + // Update model animation + animCurrentFrame = (animCurrentFrame + 1) % anims[animIndex].KeyFrameCount; + UpdateModelAnimation(model, anims[animIndex], animCurrentFrame); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawModel(model, position, 1.0f, Color.White); + + DrawGrid(10, 1.0f); + + EndMode3D(); + + DrawText($"Current animation: {anims[animIndex].NameToString()}", 10, 40, 20, Color.Maroon); + DrawText("Use the LEFT/RIGHT keys to switch animation", 10, 10, 20, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public unsafe void Unload() + { + UnloadModelAnimations(anims, animCount); // Unload model animation + UnloadModel(model); // Unload model and meshes/material + UnloadShader(skinningShader); // Unload GPU skinning shader + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - animation gpu skinning"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new AnimationGpuSkinning(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Models/AnimationTiming.cs b/Examples/Models/AnimationTiming.cs new file mode 100644 index 0000000..8fff3c5 --- /dev/null +++ b/Examples/Models/AnimationTiming.cs @@ -0,0 +1,292 @@ +/******************************************************************************************* +* +* raylib [models] example - animation timing +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2026 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +// NOTE: The upstream example uses raygui for its UI controls (dropdown, slider, progress bar). +// raygui is not part of raylib-cs, so the required controls are reimplemented here with +// basic raylib drawing primitives, preserving the original behaviour. + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Models; + +public partial class AnimationTiming : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Animation Timing"; + + public string Title => "raylib [models] example - animation timing"; + + private Camera3D camera; + private Model model; + private Vector3 position; + private unsafe ModelAnimation* anims; + private int animCount; + private int animIndex; + private float animCurrentFrame; + private float animFrameSpeed; + private bool animPause; + private string[] animNames; + private bool dropdownEditMode; + private float animFrameProgress; + + public unsafe void Init() + { + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(6.0f, 6.0f, 6.0f); // Camera position + camera.Target = new Vector3(0.0f, 2.0f, 0.0f); // Camera looking at point + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target) + camera.FovY = 45.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + + // Load model + model = LoadModel("resources/models/gltf/robot.glb"); + position = new Vector3(0.0f, 0.0f, 0.0f); // Set model world position + + // Load model animations + animCount = 0; + anims = LoadModelAnimations("resources/models/gltf/robot.glb", ref animCount); + + // Animation playing variables + animIndex = 10; // Current animation playing + animCurrentFrame = 0.0f; // Current animation frame (supporting interpolated frames) + animFrameSpeed = 0.5f; // Animation play speed + animPause = false; // Pause animation + + // UI required variables + animNames = new string[animCount]; + for (var i = 0; i < animCount; i++) + { + animNames[i] = anims[i].NameToString(); + } + + dropdownEditMode = false; + animFrameProgress = 0.0f; + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + + if (IsKeyPressed(KeyboardKey.P)) + { + animPause = !animPause; + } + + if (!animPause && (animIndex < animCount)) + { + // Update model animation + animCurrentFrame += animFrameSpeed; + if (animCurrentFrame >= anims[animIndex].KeyFrameCount) + { + animCurrentFrame = 0.0f; + } + UpdateModelAnimation(model, anims[animIndex], animCurrentFrame); + } + + // NOTE: Animation and playing speed selected through UI + + // Update progressbar value with current frame + animFrameProgress = animCurrentFrame; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawModel(model, position, 1.0f, Color.White); + + DrawGrid(10, 1.0f); + + EndMode3D(); + + // Draw UI, select anim and playing speed + GuiSlider(new Rectangle(260, 10, 500, 24), "FRAME SPEED: ", $"x{animFrameSpeed:0.0}", ref animFrameSpeed, 0.1f, 2.0f); + + // Draw playing timeline with keyframes + DrawText($"CURRENT FRAME: {animFrameProgress:0.00} / {anims[animIndex].KeyFrameCount}", + 10, (int)(GetScreenHeight() - 64.0f), 10, Color.DarkGray); + GuiProgressBar(new Rectangle(10, GetScreenHeight() - 40.0f, GetScreenWidth() - 20.0f, 24), null, null, + animFrameProgress, 0.0f, anims[animIndex].KeyFrameCount); + for (var i = 0; i < anims[animIndex].KeyFrameCount; i++) + { + DrawRectangle(10 + (int)(((float)(GetScreenWidth() - 20) / anims[animIndex].KeyFrameCount) * i), + GetScreenHeight() - 40, 1, 24, Color.Blue); + } + + // NOTE: Dropdown drawn last so its open item list renders on top + if (GuiDropdownBox(new Rectangle(10, 10, 140, 24), animNames, ref animIndex, dropdownEditMode)) + { + dropdownEditMode = !dropdownEditMode; + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public unsafe void Unload() + { + UnloadModelAnimations(anims, animCount); // Unload model animation + UnloadModel(model); // Unload model and meshes/material + } + + // Minimal immediate-mode slider (raygui replacement) + private static bool GuiSlider(Rectangle bounds, string textLeft, string textRight, ref float value, float minValue, float maxValue) + { + var mouse = GetMousePosition(); + var dragging = false; + if (CheckCollisionPointRec(mouse, bounds) && IsMouseButtonDown(MouseButton.Left)) + { + value = minValue + ((mouse.X - bounds.X) / bounds.Width) * (maxValue - minValue); + if (value < minValue) + { + value = minValue; + } + if (value > maxValue) + { + value = maxValue; + } + dragging = true; + } + + DrawRectangleRec(bounds, Color.LightGray); + var pct = (value - minValue) / (maxValue - minValue); + DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), Color.SkyBlue); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (!string.IsNullOrEmpty(textLeft)) + { + DrawText(textLeft, (int)(bounds.X - MeasureText(textLeft, 10) - 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + if (!string.IsNullOrEmpty(textRight)) + { + DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + return dragging; + } + + // Minimal immediate-mode progress bar (raygui replacement) + private static void GuiProgressBar(Rectangle bounds, string textLeft, string textRight, float value, float minValue, float maxValue) + { + DrawRectangleRec(bounds, Color.LightGray); + var pct = maxValue > minValue ? (value - minValue) / (maxValue - minValue) : 0.0f; + if (pct < 0) + { + pct = 0; + } + if (pct > 1) + { + pct = 1; + } + DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), Color.SkyBlue); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (!string.IsNullOrEmpty(textLeft)) + { + DrawText(textLeft, (int)(bounds.X - MeasureText(textLeft, 10) - 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + if (!string.IsNullOrEmpty(textRight)) + { + DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + } + + // Minimal immediate-mode dropdown box (raygui replacement) + private static bool GuiDropdownBox(Rectangle bounds, string[] items, ref int active, bool editMode) + { + var result = false; + var mouse = GetMousePosition(); + + // Draw main box + DrawRectangleRec(bounds, Color.LightGray); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (active >= 0 && active < items.Length) + { + DrawText(items[active], (int)bounds.X + 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + DrawText(editMode ? "^" : "v", (int)(bounds.X + bounds.Width - 12), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + + // Draw items when open + if (editMode) + { + for (var i = 0; i < items.Length; i++) + { + Rectangle item = new(bounds.X, bounds.Y + bounds.Height * (i + 1), bounds.Width, bounds.Height); + var hover = CheckCollisionPointRec(mouse, item); + DrawRectangleRec(item, hover ? Color.SkyBlue : Color.LightGray); + DrawRectangleLinesEx(item, 1, Color.Gray); + DrawText(items[i], (int)item.X + 5, (int)(item.Y + item.Height / 2 - 5), 10, Color.DarkGray); + } + } + + if (IsMouseButtonPressed(MouseButton.Left)) + { + if (editMode) + { + for (var i = 0; i < items.Length; i++) + { + Rectangle item = new(bounds.X, bounds.Y + bounds.Height * (i + 1), bounds.Width, bounds.Height); + if (CheckCollisionPointRec(mouse, item)) + { + active = i; + break; + } + } + result = true; // any click closes the dropdown + } + else if (CheckCollisionPointRec(mouse, bounds)) + { + result = true; // open the dropdown + } + } + + return result; + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - animation timing"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new AnimationTiming(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Models/BasicVoxel.cs b/Examples/Models/BasicVoxel.cs new file mode 100644 index 0000000..ae7b66d --- /dev/null +++ b/Examples/Models/BasicVoxel.cs @@ -0,0 +1,202 @@ +/******************************************************************************************* +* +* raylib [models] example - basic voxel +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.5 +* +* Example contributed by Tim Little (@timlittle) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Tim Little (@timlittle) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Models; + +public partial class BasicVoxel : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int WorldSize = 8; // Size of our voxel world (8x8x8 cubes) + + public string Name => "Models / Basic Voxel"; + + public string Title => "raylib [models] example - basic voxel"; + + public bool CursorDisabled => true; + + private Camera3D camera; + private Model cubeModel; + private bool[,,] voxels; + + public unsafe void Init() + { + // Define the camera to look into our 3d world (first person) + camera = new(); + camera.Position = new Vector3(-2.0f, 0.0f, -2.0f); // Camera position at ground level + camera.Target = new Vector3(0.0f, 0.0f, 0.0f); // Camera looking at point + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector + camera.FovY = 45.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + + // Create a cube model + var cubeMesh = GenMeshCube(1.0f, 1.0f, 1.0f); // Create a unit cube mesh + cubeModel = LoadModelFromMesh(cubeMesh); // Convert mesh to a model + cubeModel.Materials[0].Maps[(int)MaterialMapIndex.Diffuse].Color = Color.Beige; + + // Initialize voxel world - fill with voxels + voxels = new bool[WorldSize, WorldSize, WorldSize]; + for (var x = 0; x < WorldSize; x++) + { + for (var y = 0; y < WorldSize; y++) + { + for (var z = 0; z < WorldSize; z++) + { + voxels[x, y, z] = true; + } + } + } + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.FirstPerson); + + // Handle voxel removal with mouse click + // This method is quite inefficient. Ray marching through the voxel grid using DDA would be faster, but more complex. + if (IsMouseButtonPressed(MouseButton.Left)) + { + // Cast a ray from the screen center (where crosshair would be) + Vector2 screenCenter = new(GetScreenWidth() / 2.0f, GetScreenHeight() / 2.0f); + var ray = GetScreenToWorldRay(screenCenter, camera); + + // Check ray collision with all voxels + var closestDistance = 99999.0f; + Vector3 closestVoxelPosition = new(-1, -1, -1); + var voxelFound = false; + for (var x = 0; x < WorldSize; x++) + { + for (var y = 0; y < WorldSize; y++) + { + for (var z = 0; z < WorldSize; z++) + { + if (!voxels[x, y, z]) + { + continue; // Skip empty voxels + } + + // Build a bounding box for this voxel + Vector3 position = new(x, y, z); + BoundingBox box = new( + new Vector3(position.X - 0.5f, position.Y - 0.5f, position.Z - 0.5f), + new Vector3(position.X + 0.5f, position.Y + 0.5f, position.Z + 0.5f) + ); + + // Check ray-box collision + var collision = GetRayCollisionBox(ray, box); + if (collision.Hit && (collision.Distance < closestDistance)) + { + closestDistance = collision.Distance; + closestVoxelPosition = new Vector3(x, y, z); + voxelFound = true; + } + } + } + } + + // Remove the closest voxel if one was hit + if (voxelFound) + { + voxels[(int)closestVoxelPosition.X, + (int)closestVoxelPosition.Y, + (int)closestVoxelPosition.Z] = false; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawGrid(10, 1.0f); + + // Draw all voxels + for (var x = 0; x < WorldSize; x++) + { + for (var y = 0; y < WorldSize; y++) + { + for (var z = 0; z < WorldSize; z++) + { + if (!voxels[x, y, z]) + { + continue; + } + + Vector3 position = new(x, y, z); + DrawModel(cubeModel, position, 1.0f, Color.Beige); + DrawCubeWires(position, 1.0f, 1.0f, 1.0f, Color.Black); + } + } + } + + EndMode3D(); + + // Draw reference point for raycasting to delete blocks + DrawCircle(GetScreenWidth() / 2, GetScreenHeight() / 2, 4, Color.Red); + + DrawText("Left-click a voxel to remove it!", 10, 10, 20, Color.DarkGray); + DrawText("WASD to move, mouse to look around", 10, 35, 10, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadModel(cubeModel); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - basic voxel"); + + DisableCursor(); // Lock mouse to window center + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new BasicVoxel(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Models/BoneSocket.cs b/Examples/Models/BoneSocket.cs new file mode 100644 index 0000000..95dd488 --- /dev/null +++ b/Examples/Models/BoneSocket.cs @@ -0,0 +1,246 @@ +/******************************************************************************************* +* +* raylib [models] example - bone socket +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 4.5, last time updated with raylib 4.5 +* +* Example contributed by iP (@ipzaur) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2024-2025 iP (@ipzaur) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; + +namespace Examples.Models; + +public partial class BoneSocket : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int BoneSockets = 3; + private const int BoneSocketHat = 0; + private const int BoneSocketHandR = 1; + private const int BoneSocketHandL = 2; + + public string Name => "Models / Bone Socket"; + + public string Title => "raylib [models] example - bone socket"; + + public bool CursorDisabled => true; + + private Camera3D camera; + private Model characterModel; + private Model[] equipModel; + private bool[] showEquip; + private int animsCount; + private int animIndex; + private int animCurrentFrame; + private unsafe ModelAnimation* modelAnimations; + private int[] boneSocketIndex; + private Vector3 position; + private int angle; + + public unsafe void Init() + { + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(5.0f, 5.0f, 5.0f); // Camera position + camera.Target = new Vector3(0.0f, 2.0f, 0.0f); // Camera looking at point + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target) + camera.FovY = 45.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + + // Load gltf model + characterModel = LoadModel("resources/models/gltf/greenman.glb"); // Load character model + equipModel = new Model[BoneSockets] + { + LoadModel("resources/models/gltf/greenman_hat.glb"), // Index for the hat model is the same as BONE_SOCKET_HAT + LoadModel("resources/models/gltf/greenman_sword.glb"), // Index for the sword model is the same as BONE_SOCKET_HAND_R + LoadModel("resources/models/gltf/greenman_shield.glb") // Index for the shield model is the same as BONE_SOCKET_HAND_L + }; + + showEquip = new bool[3] { true, true, true }; // Toggle on/off equip + + // Load gltf model animations + animsCount = 0; + animIndex = 0; + animCurrentFrame = 0; + modelAnimations = LoadModelAnimations("resources/models/gltf/greenman.glb", ref animsCount); + + // Indices of bones for sockets + boneSocketIndex = new int[BoneSockets] { -1, -1, -1 }; + + // Search bones for sockets + for (var i = 0; i < characterModel.Skeleton.BoneCount; i++) + { + var boneName = characterModel.Skeleton.Bones[i].NameToString(); + + if (boneName == "socket_hat") + { + boneSocketIndex[BoneSocketHat] = i; + continue; + } + + if (boneName == "socket_hand_R") + { + boneSocketIndex[BoneSocketHandR] = i; + continue; + } + + if (boneName == "socket_hand_L") + { + boneSocketIndex[BoneSocketHandL] = i; + continue; + } + } + + position = new Vector3(0.0f, 0.0f, 0.0f); // Set model position + angle = 0; // Set angle for rotate character + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.ThirdPerson); + + // Rotate character + if (IsKeyDown(KeyboardKey.F)) + { + angle = (angle + 1) % 360; + } + else if (IsKeyDown(KeyboardKey.H)) + { + angle = (360 + angle - 1) % 360; + } + + // Select current animation + if (IsKeyPressed(KeyboardKey.T)) + { + animIndex = (animIndex + 1) % animsCount; + } + else if (IsKeyPressed(KeyboardKey.G)) + { + animIndex = (animIndex + animsCount - 1) % animsCount; + } + + // Toggle shown of equip + if (IsKeyPressed(KeyboardKey.One)) + { + showEquip[BoneSocketHat] = !showEquip[BoneSocketHat]; + } + if (IsKeyPressed(KeyboardKey.Two)) + { + showEquip[BoneSocketHandR] = !showEquip[BoneSocketHandR]; + } + if (IsKeyPressed(KeyboardKey.Three)) + { + showEquip[BoneSocketHandL] = !showEquip[BoneSocketHandL]; + } + + // Update model animation + var anim = modelAnimations[animIndex]; + animCurrentFrame = (animCurrentFrame + 1) % anim.KeyFrameCount; + UpdateModelAnimation(characterModel, anim, animCurrentFrame); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + // Draw character + var characterRotate = QuaternionFromAxisAngle(new Vector3(0.0f, 1.0f, 0.0f), angle * DEG2RAD); + characterModel.Transform = MatrixMultiply(QuaternionToMatrix(characterRotate), MatrixTranslate(position.X, position.Y, position.Z)); + UpdateModelAnimation(characterModel, anim, animCurrentFrame); + DrawMesh(characterModel.Meshes[0], characterModel.Materials[1], characterModel.Transform); + + // Draw equipments (hat, sword, shield) + for (var i = 0; i < BoneSockets; i++) + { + if (!showEquip[i]) + { + continue; + } + + var transform = &anim.KeyframePoses[animCurrentFrame][boneSocketIndex[i]]; + var inRotation = characterModel.Skeleton.BindPose[boneSocketIndex[i]].Rotation; + var outRotation = transform->Rotation; + + // Calculate socket rotation (angle between bone in initial pose and same bone in current animation frame) + var rotate = QuaternionMultiply(outRotation, QuaternionInvert(inRotation)); + var matrixTransform = QuaternionToMatrix(rotate); + // Translate socket to its position in the current animation + matrixTransform = MatrixMultiply(matrixTransform, MatrixTranslate(transform->Translation.X, transform->Translation.Y, transform->Translation.Z)); + // Transform the socket using the transform of the character (angle and translate) + matrixTransform = MatrixMultiply(matrixTransform, characterModel.Transform); + + // Draw mesh at socket position with socket angle rotation + DrawMesh(equipModel[i].Meshes[0], equipModel[i].Materials[1], matrixTransform); + } + + DrawGrid(10, 1.0f); + EndMode3D(); + + DrawText("Use the T/G to switch animation", 10, 10, 20, Color.Gray); + DrawText("Use the F/H to rotate character left/right", 10, 35, 20, Color.Gray); + DrawText("Use the 1,2,3 to toggle shown of hat, sword and shield", 10, 60, 20, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public unsafe void Unload() + { + UnloadModelAnimations(modelAnimations, animsCount); + UnloadModel(characterModel); // Unload character model and meshes/material + + // Unload equipment model and meshes/material + for (var i = 0; i < BoneSockets; i++) + { + UnloadModel(equipModel[i]); + } + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - bone socket"); + + DisableCursor(); // Limit cursor to relative movement inside the window + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new BoneSocket(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Models/Decals.cs b/Examples/Models/Decals.cs new file mode 100644 index 0000000..1e655ee --- /dev/null +++ b/Examples/Models/Decals.cs @@ -0,0 +1,592 @@ +/******************************************************************************************* +* +* raylib [models] example - decals +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by JP Mortiboys (@themushroompirates) and reviewed by Ramon Santamaria (@raysan5) +* Based on previous work by @mrdoob +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 JP Mortiboys (@themushroompirates) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; + +namespace Examples.Models; + +public partial class Decals : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MaxDecals = 256; + + public string Name => "Models / Decals"; + + public string Title => "raylib [models] example - decals"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + private Camera3D camera; + private Model model; + private Texture2D modelTexture; + private BoundingBox modelBBox; + private float decalSize; + private float decalOffset; + private Model placementCube; + private Material decalMaterial; + private Texture2D decalTexture; + private bool showModel; + private readonly List decalModels = new(); + + public unsafe void Init() + { + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(5.0f, 5.0f, 5.0f); // Camera position + camera.Target = new Vector3(0.0f, 1.0f, 0.0f); // Camera looking at point + camera.Up = new Vector3(0.0f, 1.6f, 0.0f); // Camera up vector (rotation towards target) + camera.FovY = 45.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + + // Load character model + model = LoadModel("resources/models/obj/character.obj"); + + // Apply character skin + modelTexture = LoadTexture("resources/models/obj/character_diffuse.png"); + SetTextureFilter(modelTexture, TextureFilter.Bilinear); + model.Materials[0].Maps[(int)MaterialMapIndex.Diffuse].Texture = modelTexture; + + modelBBox = GetMeshBoundingBox(model.Meshes[0]); // Get mesh bounding box + + camera.Target = Vector3Lerp(modelBBox.Min, modelBBox.Max, 0.5f); + camera.Position = modelBBox.Max * 1.0f; + camera.Position.X *= 0.1f; + + var modelSize = MathF.Min( + MathF.Min(MathF.Abs(modelBBox.Max.X - modelBBox.Min.X), MathF.Abs(modelBBox.Max.Y - modelBBox.Min.Y)), + MathF.Abs(modelBBox.Max.Z - modelBBox.Min.Z)); + + camera.Position = new Vector3(0.0f, modelBBox.Max.Y * 1.2f, modelSize * 3.0f); + + decalSize = modelSize * 0.25f; + decalOffset = 0.01f; + + placementCube = LoadModelFromMesh(GenMeshCube(decalSize, decalSize, decalSize)); + placementCube.Materials[0].Maps[0].Color = Color.Lime; + + decalMaterial = LoadMaterialDefault(); + decalMaterial.Maps[0].Color = Color.Yellow; + + var decalImage = LoadImage("resources/raylib_logo.png"); + ImageResizeNN(ref decalImage, decalImage.Width / 4, decalImage.Height / 4); + decalTexture = LoadTextureFromImage(decalImage); + UnloadImage(decalImage); + + SetTextureFilter(decalTexture, TextureFilter.Bilinear); + decalMaterial.Maps[(int)MaterialMapIndex.Diffuse].Texture = decalTexture; + decalMaterial.Maps[(int)MaterialMapIndex.Diffuse].Color = Color.RayWhite; + + showModel = true; + decalModels.Clear(); + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsMouseButtonDown(MouseButton.Right)) + { + UpdateCamera(ref camera, CameraMode.ThirdPerson); + } + + // Display information about closest hit + RayCollision collision = new(); + collision.Distance = float.MaxValue; + collision.Hit = false; + + // Get mouse ray + var ray = GetScreenToWorldRay(GetMousePosition(), camera); + + // Check ray collision against bounding box first, before trying the full ray-mesh test + var boxHitInfo = GetRayCollisionBox(ray, modelBBox); + + RayCollision meshHitInfo = new(); + if (boxHitInfo.Hit && (decalModels.Count < MaxDecals)) + { + // Check ray collision against model meshes + for (var m = 0; m < model.MeshCount; m++) + { + // NOTE: We consider the model.transform for the collision check but + // it can be checked against any transform Matrix, used when checking against same + // model drawn multiple times with multiple transforms + meshHitInfo = GetRayCollisionMesh(ray, model.Meshes[m], model.Transform); + if (meshHitInfo.Hit) + { + // Save the closest hit mesh + if (!collision.Hit || (collision.Distance > meshHitInfo.Distance)) + { + collision = meshHitInfo; + } + } + } + + if (meshHitInfo.Hit) + { + collision = meshHitInfo; + } + } + + // Add decal to mesh on hit point + if (collision.Hit && IsMouseButtonPressed(MouseButton.Left) && (decalModels.Count < MaxDecals)) + { + // Create the transformation to project the decal + var origin = collision.Point + (collision.Normal * 1.0f); + var splat = MatrixLookAt(collision.Point, origin, new Vector3(0.0f, 1.0f, 0.0f)); + + // Spin the placement around a bit + splat = MatrixMultiply(splat, MatrixRotateZ(DEG2RAD * GetRandomValue(-180, 180))); + + var decalMesh = GenMeshDecal(model, splat, decalSize, decalOffset); + + if (decalMesh.VertexCount > 0) + { + var decalModel = LoadModelFromMesh(decalMesh); + decalModel.Materials[0].Maps[0] = decalMaterial.Maps[0]; + decalModels.Add(decalModel); + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + // Draw the model at the origin and default scale + if (showModel) + { + DrawModel(model, new Vector3(0.0f, 0.0f, 0.0f), 1.0f, Color.White); + } + + // Draw the decal models + for (var i = 0; i < decalModels.Count; i++) + { + DrawModel(decalModels[i], Vector3.Zero, 1.0f, Color.White); + } + + // If we hit the mesh, draw the box for the decal + if (collision.Hit) + { + var origin = collision.Point + (collision.Normal * 1.0f); + var splat = MatrixLookAt(collision.Point, origin, new Vector3(0, 1, 0)); + placementCube.Transform = MatrixInvert(splat); + DrawModel(placementCube, Vector3.Zero, 1.0f, Fade(Color.White, 0.5f)); + } + + DrawGrid(10, 10.0f); + EndMode3D(); + + float yPos = 10; + var x0 = GetScreenWidth() - 300.0f; + var x1 = x0 + 100; + var x2 = x1 + 100; + + DrawText("Vertices", (int)x1, (int)yPos, 10, Color.Lime); + DrawText("Triangles", (int)x2, (int)yPos, 10, Color.Lime); + yPos += 15; + + var vertexCount = 0; + var triangleCount = 0; + + for (var i = 0; i < model.MeshCount; i++) + { + vertexCount += model.Meshes[i].VertexCount; + triangleCount += model.Meshes[i].TriangleCount; + } + + DrawText("Main model", (int)x0, (int)yPos, 10, Color.Lime); + DrawText($"{vertexCount}", (int)x1, (int)yPos, 10, Color.Lime); + DrawText($"{triangleCount}", (int)x2, (int)yPos, 10, Color.Lime); + yPos += 15; + + for (var i = 0; i < decalModels.Count; i++) + { + if (i == 20) + { + DrawText("...", (int)x0, (int)yPos, 10, Color.Lime); + yPos += 15; + } + + if (i < 20) + { + DrawText($"Decal #{i + 1}", (int)x0, (int)yPos, 10, Color.Lime); + DrawText($"{decalModels[i].Meshes[0].VertexCount}", (int)x1, (int)yPos, 10, Color.Lime); + DrawText($"{decalModels[i].Meshes[0].TriangleCount}", (int)x2, (int)yPos, 10, Color.Lime); + yPos += 15; + } + + vertexCount += decalModels[i].Meshes[0].VertexCount; + triangleCount += decalModels[i].Meshes[0].TriangleCount; + } + + DrawText("TOTAL", (int)x0, (int)yPos, 10, Color.Lime); + DrawText($"{vertexCount}", (int)x1, (int)yPos, 10, Color.Lime); + DrawText($"{triangleCount}", (int)x2, (int)yPos, 10, Color.Lime); + yPos += 15; + + DrawText("Hold RMB to move camera", 10, 430, 10, Color.Gray); + DrawText("(c) Character model and texture from kenney.nl", screenWidth - 260, screenHeight - 20, 10, Color.Gray); + + // UI elements + if (GuiButton(new Rectangle(10, screenHeight - 1000.0f, 100, 60), showModel ? "Hide Model" : "Show Model")) + { + showModel = !showModel; + } + + if (GuiButton(new Rectangle(10 + 110, screenHeight - 100.0f, 100, 60), "Clear Decals")) + { + // Clear decals, unload all decal models + for (var i = 0; i < decalModels.Count; i++) + { + UnloadModel(decalModels[i]); + } + decalModels.Clear(); + } + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadModel(model); + UnloadTexture(modelTexture); + + // Unload decal models + for (var i = 0; i < decalModels.Count; i++) + { + UnloadModel(decalModels[i]); + } + decalModels.Clear(); + + UnloadTexture(decalTexture); + UnloadModel(placementCube); + } + + // Clip segment + private static Vector3 ClipSegment(Vector3 v0, Vector3 v1, Vector3 p, float s) + { + var d0 = Vector3.Dot(v0, p) - s; + var d1 = Vector3.Dot(v1, p) - s; + var s0 = d0 / (d0 - d1); + + return Vector3.Lerp(v0, v1, s0); + } + + // Generate mesh decals for provided model + private static unsafe Mesh GenMeshDecal(Model target, Matrix4x4 projection, float decalSize, float decalOffset) + { + // We're going to use these to build up our decal meshes + var meshBuilders = new List[2] { new(), new() }; + + // We're going to need the inverse matrix + var invProj = MatrixInvert(projection); + + // We'll be flip-flopping between the two mesh builders + // Reading from one and writing to the other, then swapping + var mbIndex = 0; + + // First pass, just get any triangle inside the bounding box (for each mesh of the model) + for (var meshIndex = 0; meshIndex < target.MeshCount; meshIndex++) + { + var mesh = target.Meshes[meshIndex]; + for (var tri = 0; tri < mesh.TriangleCount; tri++) + { + var vertices = new Vector3[3]; + + // The way we calculate the vertices of the mesh triangle + // depend on whether the mesh vertices are indexed or not + if (mesh.Indices == null) + { + for (var v = 0; v < 3; v++) + { + vertices[v] = new Vector3( + mesh.Vertices[3 * 3 * tri + 3 * v + 0], + mesh.Vertices[3 * 3 * tri + 3 * v + 1], + mesh.Vertices[3 * 3 * tri + 3 * v + 2] + ); + } + } + else + { + for (var v = 0; v < 3; v++) + { + vertices[v] = new Vector3( + mesh.Vertices[3 * mesh.Indices[3 * tri + 0] + v], + mesh.Vertices[3 * mesh.Indices[3 * tri + 1] + v], + mesh.Vertices[3 * mesh.Indices[3 * tri + 2] + v] + ); + } + } + + // Transform all 3 vertices of the triangle + // and check if they are inside our decal box + var insideCount = 0; + for (var i = 0; i < 3; i++) + { + // To projection space + var v = Vector3Transform(vertices[i], projection); + + if ((MathF.Abs(v.X) < decalSize) || (MathF.Abs(v.Y) <= decalSize) || (MathF.Abs(v.Z) <= decalSize)) + { + insideCount++; + } + + // We need to keep the transformed vertex + vertices[i] = v; + } + + // If any of them are inside, we add the triangle - we'll clip it later + if (insideCount > 0) + { + meshBuilders[mbIndex].Add(vertices[0]); + meshBuilders[mbIndex].Add(vertices[1]); + meshBuilders[mbIndex].Add(vertices[2]); + } + } + } + + // Clipping time! We need to clip against all 6 directions + Vector3[] planes = + { + new(1, 0, 0), + new(-1, 0, 0), + new(0, 1, 0), + new(0, -1, 0), + new(0, 0, 1), + new(0, 0, -1) + }; + + for (var face = 0; face < 6; face++) + { + // Swap current model builder (so we read from the one we just wrote to) + mbIndex = 1 - mbIndex; + + var inMesh = meshBuilders[1 - mbIndex]; + var outMesh = meshBuilders[mbIndex]; + + // Reset write builder + outMesh.Clear(); + + var s = 0.5f * decalSize; + + for (var i = 0; i < inMesh.Count; i += 3) + { + Vector3 nV1, nV2, nV3, nV4; + + var d1 = Vector3.Dot(inMesh[i + 0], planes[face]) - s; + var d2 = Vector3.Dot(inMesh[i + 1], planes[face]) - s; + var d3 = Vector3.Dot(inMesh[i + 2], planes[face]) - s; + + var v1Out = d1 > 0; + var v2Out = d2 > 0; + var v3Out = d3 > 0; + + // Calculate, how many vertices of the face lie outside of the clipping plane + var total = (v1Out ? 1 : 0) + (v2Out ? 1 : 0) + (v3Out ? 1 : 0); + + switch (total) + { + case 0: + // The entire face lies inside of the plane, no clipping needed + outMesh.Add(inMesh[i]); + outMesh.Add(inMesh[i + 1]); + outMesh.Add(inMesh[i + 2]); + break; + case 1: + // One vertex lies outside of the plane, perform clipping + if (v2Out) + { + nV1 = inMesh[i]; + nV2 = inMesh[i + 2]; + nV3 = ClipSegment(inMesh[i + 1], nV1, planes[face], s); + nV4 = ClipSegment(inMesh[i + 1], nV2, planes[face], s); + + outMesh.Add(nV3); outMesh.Add(nV2); outMesh.Add(nV1); + outMesh.Add(nV2); outMesh.Add(nV3); outMesh.Add(nV4); + } + else + { + if (v1Out) + { + nV1 = inMesh[i + 1]; + nV2 = inMesh[i + 2]; + nV3 = ClipSegment(inMesh[i], nV1, planes[face], s); + nV4 = ClipSegment(inMesh[i], nV2, planes[face], s); + } + else // v3Out + { + nV1 = inMesh[i]; + nV2 = inMesh[i + 1]; + nV3 = ClipSegment(inMesh[i + 2], nV1, planes[face], s); + nV4 = ClipSegment(inMesh[i + 2], nV2, planes[face], s); + } + + outMesh.Add(nV1); outMesh.Add(nV2); outMesh.Add(nV3); + outMesh.Add(nV4); outMesh.Add(nV3); outMesh.Add(nV2); + } + break; + case 2: + // Two vertices lies outside of the plane, perform clipping + if (!v1Out) + { + nV1 = inMesh[i]; + nV2 = ClipSegment(nV1, inMesh[i + 1], planes[face], s); + nV3 = ClipSegment(nV1, inMesh[i + 2], planes[face], s); + outMesh.Add(nV1); outMesh.Add(nV2); outMesh.Add(nV3); + } + + if (!v2Out) + { + nV1 = inMesh[i + 1]; + nV2 = ClipSegment(nV1, inMesh[i + 2], planes[face], s); + nV3 = ClipSegment(nV1, inMesh[i], planes[face], s); + outMesh.Add(nV1); outMesh.Add(nV2); outMesh.Add(nV3); + } + + if (!v3Out) + { + nV1 = inMesh[i + 2]; + nV2 = ClipSegment(nV1, inMesh[i], planes[face], s); + nV3 = ClipSegment(nV1, inMesh[i + 1], planes[face], s); + outMesh.Add(nV1); outMesh.Add(nV2); outMesh.Add(nV3); + } + break; + default: // The entire face lies outside of the plane, so let's discard the corresponding vertices + break; + } + } + } + + // Now we just need to re-transform the vertices + var theMesh = meshBuilders[mbIndex]; + + // Allocate room for UVs + if (theMesh.Count > 0) + { + var uvs = new Vector2[theMesh.Count]; + + for (var i = 0; i < theMesh.Count; i++) + { + var vert = theMesh[i]; + + // Calculate the UVs based on the projected coords + // They are clipped to (-decalSize .. decalSize) and we want them (0..1) + uvs[i] = new Vector2(vert.X / decalSize + 0.5f, vert.Y / decalSize + 0.5f); + + // Tiny nudge in the normal direction so it renders properly over the mesh + vert.Z -= decalOffset; + + // From projection space to world space + theMesh[i] = Vector3Transform(vert, invProj); + } + + // Decal model data ready, create the mesh and return it + return BuildMesh(theMesh, uvs); + } + + // Return a blank mesh as there's nothing to add + return new Mesh(); + } + + // Build a Mesh from builder data + private static unsafe Mesh BuildMesh(List builderVertices, Vector2[] uvs) + { + Mesh outMesh = new(builderVertices.Count, builderVertices.Count / 3); + outMesh.AllocVertices(); + outMesh.AllocTexCoords(); + + var vertices = outMesh.VerticesAs(); + var texcoords = outMesh.TexCoordsAs(); + + for (var i = 0; i < builderVertices.Count; i++) + { + vertices[i] = builderVertices[i]; + texcoords[i] = uvs[i]; + } + + UploadMesh(ref outMesh, false); + + return outMesh; + } + + // Button UI element + private static bool GuiButton(Rectangle rec, string label) + { + var bgColor = Color.Gray; + var pressed = false; + + if (CheckCollisionPointRec(GetMousePosition(), rec)) + { + bgColor = Color.LightGray; + if (IsMouseButtonPressed(MouseButton.Left)) + { + pressed = true; + } + } + + DrawRectangleRec(rec, bgColor); + DrawRectangleLinesEx(rec, 2.0f, Color.DarkGray); + + var fontSize = 10; + var textWidth = MeasureText(label, fontSize); + + DrawText(label, (int)(rec.X + rec.Width * 0.5f - textWidth * 0.5f), (int)(rec.Y + rec.Height * 0.5f - fontSize * 0.5f), fontSize, Color.DarkGray); + + return pressed; + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); + InitWindow(screenWidth, screenHeight, "raylib [models] example - decals"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new Decals(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Models/DirectionalBillboard.cs b/Examples/Models/DirectionalBillboard.cs new file mode 100644 index 0000000..f5439db --- /dev/null +++ b/Examples/Models/DirectionalBillboard.cs @@ -0,0 +1,148 @@ +/******************************************************************************************* +* +* raylib [models] example - directional billboard +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Robin (@RobinsAviary) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Robin (@RobinsAviary) +* Killbot art by patvanmackelberg https://opengameart.org/content/killbot-8-directional under CC0 +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; + +namespace Examples.Models; + +public partial class DirectionalBillboard : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Directional Billboard"; + + public string Title => "raylib [models] example - directional billboard"; + + private Camera3D camera; + private Texture2D skillbot; + private float animTimer; + private uint anim; + + public void Init() + { + // Set up the camera + camera = new(); + camera.Position = new Vector3(2.0f, 1.0f, 2.0f); // Starting position + camera.Target = new Vector3(0.0f, 0.5f, 0.0f); // Target position + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Up vector + camera.FovY = 45.0f; // FOV + camera.Projection = CameraProjection.Perspective; // Projection type (Standard 3D perspective) + + // Load billboard texture + skillbot = LoadTexture("resources/skillbot.png"); + + // Timer to update animation + animTimer = 0.0f; + // Animation frame + anim = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + + // Update timer with delta time + animTimer += GetFrameTime(); + + // Update frame index after a certain amount of time (half a second) + if (animTimer > 0.5f) + { + animTimer = 0.0f; + anim += 1; + } + + // Reset frame index to zero on overflow + if (anim >= 4) + { + anim = 0; + } + + // Find the current direction frame based on the camera position to the billboard object + var dir = (float)Math.Floor(((Vector2Angle(new Vector2(2.0f, 0.0f), new Vector2(camera.Position.X, camera.Position.Z)) / MathF.PI) * 4.0f) + 0.25f); + + // Correct frame index if angle is negative + if (dir < 0.0f) + { + dir = 8.0f - Math.Abs((int)dir); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawGrid(10, 1.0f); + + // Draw billboard pointing straight up to the sky, rotated relative to the camera and offset from the bottom + DrawBillboardPro(camera, skillbot, new Rectangle(0.0f + (anim * 24.0f), 0.0f + (dir * 24.0f), 24.0f, 24.0f), + Vector3.Zero, new Vector3(0.0f, 1.0f, 0.0f), Vector2.One, new Vector2(0.5f, 0.0f), 0, Color.White); + + EndMode3D(); + + // Render various variables for reference + DrawText($"animation: {anim}", 10, 10, 20, Color.DarkGray); + DrawText($"direction frame: {dir:0}", 10, 40, 20, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + // Unload billboard texture + UnloadTexture(skillbot); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - directional billboard"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new DirectionalBillboard(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Models/LoadingM3d.cs b/Examples/Models/LoadingM3d.cs new file mode 100644 index 0000000..343d24f --- /dev/null +++ b/Examples/Models/LoadingM3d.cs @@ -0,0 +1,173 @@ +/******************************************************************************************* +* +* raylib [models] example - loading m3d +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 4.5, last time updated with raylib 4.5 +* +* Example contributed by bzt (@bztsrc) and reviewed by Ramon Santamaria (@raysan5) +* +* NOTES: +* - Model3D (M3D) fileformat specs: https://gitlab.com/bztsrc/model3d +* - Bender M3D exported: https://gitlab.com/bztsrc/model3d/-/tree/master/blender +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2022-2025 bzt (@bztsrc) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Models; + +public partial class LoadingM3d : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Loading M3D"; + + public string Title => "raylib [models] example - loading m3d"; + + private Camera3D camera; + private Model model; + private Vector3 position; + private unsafe ModelAnimation* anims; + private int animCount; + private int animIndex; + private float animCurrentFrame; + + public unsafe void Init() + { + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(1.5f, 1.5f, 1.5f); // Camera position + camera.Target = new Vector3(0.0f, 0.4f, 0.0f); // Camera looking at point + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target) + camera.FovY = 45.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + + // Load model + model = LoadModel("resources/models/m3d/cesium_man.m3d"); // Load the animated model mesh and basic data + position = new Vector3(0.0f, 0.0f, 0.0f); // Set model position + + // Load animation data + animCount = 0; + anims = LoadModelAnimations("resources/models/m3d/cesium_man.m3d", ref animCount); + + // Animation playing variables + animIndex = 0; // Current animation playing + animCurrentFrame = 0.0f; // Current animation frame (supporting interpolated frames) + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + + // Select current animation + if (IsKeyPressed(KeyboardKey.Right)) + { + animIndex = (animIndex + 1) % animCount; + } + else if (IsKeyPressed(KeyboardKey.Left)) + { + animIndex = (animIndex + animCount - 1) % animCount; + } + + // Update model animation + animCurrentFrame += 1.0f; + if (animCurrentFrame >= anims[animIndex].KeyFrameCount) + { + animCurrentFrame = 0.0f; + } + UpdateModelAnimation(model, anims[animIndex], animCurrentFrame); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + // Draw 3d model with texture + if (!IsKeyDown(KeyboardKey.Space)) + { + DrawModel(model, position, 1.0f, Color.White); + } + else + { + // Draw the animated skeleton + DrawModelSkeleton(model.Skeleton, anims[animIndex].KeyframePoses[(int)animCurrentFrame], 1.0f, Color.Red); + } + + DrawGrid(10, 1.0f); + + EndMode3D(); + + DrawText($"Current animation: {anims[animIndex].NameToString()}", 10, 10, 20, Color.LightGray); + DrawText("Press SPACE to draw skeleton", 10, 40, 20, Color.Maroon); + DrawText("(c) CesiumMan model by KhronosGroup", GetScreenWidth() - 210, GetScreenHeight() - 20, 10, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public unsafe void Unload() + { + UnloadModelAnimations(anims, animCount); // Unload model animations data + UnloadModel(model); // Unload model + } + + // Draw model skeleton + private static unsafe void DrawModelSkeleton(ModelSkeleton skeleton, Transform* pose, float scale, Color color) + { + // Loop to (boneCount - 1) because the last one is a special "no bone" bone, + // needed to workaround buggy models without a -1, a cube is always drawn at the origin + for (var i = 0; i < skeleton.BoneCount - 1; i++) + { + // Display the frame-pose skeleton + DrawCube(pose[i].Translation, scale * 0.05f, scale * 0.05f, scale * 0.05f, color); + + if (skeleton.Bones[i].Parent >= 0) + { + DrawLine3D(pose[i].Translation, pose[skeleton.Bones[i].Parent].Translation, color); + } + } + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - loading m3d"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new LoadingM3d(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Models/LoadingVox.cs b/Examples/Models/LoadingVox.cs new file mode 100644 index 0000000..7b267ac --- /dev/null +++ b/Examples/Models/LoadingVox.cs @@ -0,0 +1,246 @@ +/******************************************************************************************* +* +* raylib [models] example - loading vox +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 4.0, last time updated with raylib 4.0 +* +* Example contributed by Johann Nadalutti (@procfxgen) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2021-2025 Johann Nadalutti (@procfxgen) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; +using Examples.Shared; + +namespace Examples.Models; + +public partial class LoadingVox : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MaxVoxFiles = 4; + private const int MaxLights = 4; + +#if BROWSER + private const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Models / Loading VOX"; + + public string Title => "raylib [models] example - loading vox"; + + private static readonly string[] VoxFileNames = + { + "resources/models/vox/chr_knight.vox", + "resources/models/vox/chr_sword.vox", + "resources/models/vox/monu9.vox", + "resources/models/vox/fez.vox" + }; + + private Camera3D camera; + private Model[] models; + private int currentModel; + private Vector3 modelpos; + private Vector3 camerarot; + private Shader shader; + private Light[] lights; + + public unsafe void Init() + { + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(10.0f, 10.0f, 10.0f); // Camera position + camera.Target = new Vector3(0.0f, 0.0f, 0.0f); // Camera looking at point + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target) + camera.FovY = 45.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + + // Load MagicaVoxel files + models = new Model[MaxVoxFiles]; + + for (var i = 0; i < MaxVoxFiles; i++) + { + // Load VOX file and measure time + var t0 = GetTime() * 1000.0; + models[i] = LoadModel(VoxFileNames[i]); + var t1 = GetTime() * 1000.0; + + TraceLog(TraceLogLevel.Info, $"[{VoxFileNames[i]}] Model file loaded in {t1 - t0:0.000} ms"); + + // Compute model translation matrix to center model on draw position (0, 0 , 0) + var bb = GetModelBoundingBox(models[i]); + Vector3 center = new(); + center.X = bb.Min.X + ((bb.Max.X - bb.Min.X) / 2); + center.Z = bb.Min.Z + ((bb.Max.Z - bb.Min.Z) / 2); + + var matTranslate = MatrixTranslate(-center.X, 0, -center.Z); + models[i].Transform = matTranslate; + } + + currentModel = 0; + modelpos = new Vector3(0, 0, 0); + camerarot = new Vector3(0, 0, 0); + + // Load voxel shader + shader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/voxel_lighting.vs", + $"resources/shaders/glsl{GlslVersion}/voxel_lighting.fs" + ); + + // Get some required shader locations + shader.Locs[(int)ShaderLocationIndex.VectorView] = GetShaderLocation(shader, "viewPos"); + // NOTE: "matModel" location name is automatically assigned on shader loading, + // no need to get the location again if using that uniform name + //shader.Locs[(int)ShaderLocationIndex.MatrixModel] = GetShaderLocation(shader, "matModel"); + + // Ambient light level (some basic lighting) + var ambientLoc = GetShaderLocation(shader, "ambient"); + Raylib.SetShaderValue(shader, ambientLoc, new[] { 0.1f, 0.1f, 0.1f, 1.0f }, ShaderUniformDataType.Vec4); + + // Assign out lighting shader to model + for (var i = 0; i < MaxVoxFiles; i++) + { + for (var j = 0; j < models[i].MaterialCount; j++) + { + models[i].Materials[j].Shader = shader; + } + } + + // Create lights + lights = new Light[MaxLights]; + lights[0] = Rlights.CreateLight(0, LightType.Point, new Vector3(-20, 20, -20), Vector3.Zero, Color.Gray, shader); + lights[1] = Rlights.CreateLight(1, LightType.Point, new Vector3(20, -20, 20), Vector3.Zero, Color.Gray, shader); + lights[2] = Rlights.CreateLight(2, LightType.Point, new Vector3(-20, 20, 20), Vector3.Zero, Color.Gray, shader); + lights[3] = Rlights.CreateLight(3, LightType.Point, new Vector3(20, -20, -20), Vector3.Zero, Color.Gray, shader); + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsMouseButtonDown(MouseButton.Middle)) + { + var mouseDelta = GetMouseDelta(); + camerarot.X = mouseDelta.X * 0.05f; + camerarot.Y = mouseDelta.Y * 0.05f; + } + else + { + camerarot.X = 0; + camerarot.Y = 0; + } + + // Update camere movement, custom controls + UpdateCameraPro(ref camera, + new Vector3( + (IsKeyDown(KeyboardKey.W) || IsKeyDown(KeyboardKey.Up) ? 0.1f : 0.0f) - (IsKeyDown(KeyboardKey.S) || IsKeyDown(KeyboardKey.Down) ? 0.1f : 0.0f), // Move forward-backward + (IsKeyDown(KeyboardKey.D) || IsKeyDown(KeyboardKey.Right) ? 0.1f : 0.0f) - (IsKeyDown(KeyboardKey.A) || IsKeyDown(KeyboardKey.Left) ? 0.1f : 0.0f), // Move right-left + 0.0f), // Move up-down + camerarot, // Camera rotation + GetMouseWheelMove() * -2.0f); // Move to target (zoom) + + // Cycle between models on mouse click + if (IsMouseButtonPressed(MouseButton.Left)) + { + currentModel = (currentModel + 1) % MaxVoxFiles; + } + + // Update the shader with the camera view vector (points towards { 0.0f, 0.0f, 0.0f }) + Raylib.SetShaderValue(shader, shader.Locs[(int)ShaderLocationIndex.VectorView], camera.Position, ShaderUniformDataType.Vec3); + + // Update light values (actually, only enable/disable them) + for (var i = 0; i < MaxLights; i++) + { + Rlights.UpdateLightValues(shader, lights[i]); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + // Draw 3D model + BeginMode3D(camera); + + DrawModel(models[currentModel], modelpos, 1.0f, Color.White); + DrawGrid(10, 1.0f); + + // Draw spheres to show where the lights are + for (var i = 0; i < MaxLights; i++) + { + if (lights[i].Enabled) + { + DrawSphereEx(lights[i].Position, 0.2f, 8, 8, lights[i].Color); + } + else + { + DrawSphereWires(lights[i].Position, 0.2f, 8, 8, ColorAlpha(lights[i].Color, 0.3f)); + } + } + + EndMode3D(); + + // Display info + DrawRectangle(10, 40, 340, 70, Fade(Color.SkyBlue, 0.5f)); + DrawRectangleLines(10, 40, 340, 70, Fade(Color.DarkBlue, 0.5f)); + DrawText("- MOUSE LEFT BUTTON: CYCLE VOX MODELS", 20, 50, 10, Color.Blue); + DrawText("- MOUSE MIDDLE BUTTON: ZOOM OR ROTATE CAMERA", 20, 70, 10, Color.Blue); + DrawText("- UP-DOWN-LEFT-RIGHT KEYS: MOVE CAMERA", 20, 90, 10, Color.Blue); + DrawText($"VOX model file: {GetFileName(VoxFileNames[currentModel])}", 10, 10, 20, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + // Unload models data (GPU VRAM) + for (var i = 0; i < MaxVoxFiles; i++) + { + UnloadModel(models[i]); + } + + UnloadShader(shader); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - loading vox"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new LoadingVox(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Models/MeshPicking.cs b/Examples/Models/MeshPicking.cs index 7aaa549..3f62595 100644 --- a/Examples/Models/MeshPicking.cs +++ b/Examples/Models/MeshPicking.cs @@ -30,8 +30,6 @@ public partial class MeshPicking : IExample public string Title => "raylib [models] example - mesh picking"; - public bool CursorDisabled => true; - private Camera3D camera; private Ray ray; private Model tower; diff --git a/Examples/Models/ModelLoading.cs b/Examples/Models/ModelLoading.cs index 21f7862..9198ecc 100644 --- a/Examples/Models/ModelLoading.cs +++ b/Examples/Models/ModelLoading.cs @@ -79,8 +79,7 @@ public partial class ModelLoading : IExample UpdateCamera(ref camera, CameraMode.Orbital); #if BROWSER - // NOTE: Drag & drop file loading (IsFileDropped) is not available in the browser - // host, so it is skipped here. The default model stays loaded. + // NOTE: drag-and-drop model loading is not supported in the browser host; default loaded model is kept. #else // Load new models/textures on drag&drop if (IsFileDropped()) diff --git a/Examples/Models/PointRendering.cs b/Examples/Models/PointRendering.cs new file mode 100644 index 0000000..15cd2bf --- /dev/null +++ b/Examples/Models/PointRendering.cs @@ -0,0 +1,235 @@ +/******************************************************************************************* +* +* raylib [models] example - point rendering +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 5.0, last time updated with raylib 5.0 +* +* Example contributed by Reese Gallagher (@satchelfrost) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2024-2025 Reese Gallagher (@satchelfrost) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Models; + +[ExcludeFromBrowser("rlEnablePointMode needs glPolygonMode, which OpenGL ES/WebGL lacks (renders as triangles)")] +public partial class PointRendering : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MaxPoints = 10000000; // 10 million + private const int MinPoints = 1000; // 1 thousand + + public string Name => "Models / Point Rendering"; + + public string Title => "raylib [models] example - point rendering"; + + private static readonly Random Rand = new(); + + private Camera3D camera; + private Vector3 position; + private bool useDrawModelPoints; + private bool numPointsChanged; + private int numPoints; + private Mesh mesh; + private Model model; + + public void Init() + { + camera = new() + { + Position = new Vector3(3.0f, 3.0f, 3.0f), + Target = new Vector3(0.0f, 0.0f, 0.0f), + Up = new Vector3(0.0f, 1.0f, 0.0f), + FovY = 45.0f, + Projection = CameraProjection.Perspective + }; + + position = new Vector3(0.0f, 0.0f, 0.0f); + useDrawModelPoints = true; + numPointsChanged = false; + numPoints = 1000; + + mesh = GenMeshPoints(numPoints); + model = LoadModelFromMesh(mesh); + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + + if (IsKeyPressed(KeyboardKey.Space)) + { + useDrawModelPoints = !useDrawModelPoints; + } + if (IsKeyPressed(KeyboardKey.Up)) + { + numPoints = (numPoints * 10 > MaxPoints) ? MaxPoints : numPoints * 10; + numPointsChanged = true; + } + if (IsKeyPressed(KeyboardKey.Down)) + { + numPoints = (numPoints / 10 < MinPoints) ? MinPoints : numPoints / 10; + numPointsChanged = true; + } + + // Upload a different point cloud size + if (numPointsChanged) + { + UnloadModel(model); + mesh = GenMeshPoints(numPoints); + model = LoadModelFromMesh(mesh); + numPointsChanged = false; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.Black); + + BeginMode3D(camera); + + // The new method only uploads the points once to the GPU + if (useDrawModelPoints) + { + DrawModelPoints(model, position, 1.0f, Color.White); + } + else + { + // The old method must continually draw the "points" (lines) + for (var i = 0; i < numPoints; i++) + { + Vector3 pos = new( + mesh.Vertices[i * 3 + 0], + mesh.Vertices[i * 3 + 1], + mesh.Vertices[i * 3 + 2] + ); + Color color = new( + mesh.Colors[i * 4 + 0], + mesh.Colors[i * 4 + 1], + mesh.Colors[i * 4 + 2], + mesh.Colors[i * 4 + 3] + ); + + DrawPoint3D(pos, color); + } + } + + // Draw a unit sphere for reference + DrawSphereWires(position, 1.0f, 10, 10, Color.Yellow); + + EndMode3D(); + + // Draw UI text + DrawText($"Point Count: {numPoints}", 10, screenHeight - 50, 40, Color.White); + DrawText("UP - Increase points", 10, 40, 20, Color.White); + DrawText("DOWN - Decrease points", 10, 70, 20, Color.White); + DrawText("SPACE - Drawing function", 10, 100, 20, Color.White); + + if (useDrawModelPoints) + { + DrawText("Using: DrawModelPoints()", 10, 130, 20, Color.Green); + } + else + { + DrawText("Using: DrawPoint3D()", 10, 130, 20, Color.Red); + } + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadModel(model); + } + + // Generate a spherical point cloud + private static unsafe Mesh GenMeshPoints(int numPoints) + { + Mesh mesh = new(numPoints, 1); + mesh.AllocVertices(); + mesh.AllocColors(); + + // REF: https://en.wikipedia.org/wiki/Spherical_coordinate_system + for (var i = 0; i < numPoints; i++) + { + var theta = MathF.PI * (float)Rand.NextDouble(); + var phi = 2.0f * MathF.PI * (float)Rand.NextDouble(); + var r = 10.0f * (float)Rand.NextDouble(); + + mesh.Vertices[i * 3 + 0] = r * MathF.Sin(theta) * MathF.Cos(phi); + mesh.Vertices[i * 3 + 1] = r * MathF.Sin(theta) * MathF.Sin(phi); + mesh.Vertices[i * 3 + 2] = r * MathF.Cos(theta); + + var color = ColorFromHSV(r * 360.0f, 1.0f, 1.0f); + + mesh.Colors[i * 4 + 0] = color.R; + mesh.Colors[i * 4 + 1] = color.G; + mesh.Colors[i * 4 + 2] = color.B; + mesh.Colors[i * 4 + 3] = color.A; + } + + // Upload mesh data from CPU (RAM) to GPU (VRAM) memory + UploadMesh(ref mesh, false); + + return mesh; + } + + // Draw a model points + // WARNING: OpenGL ES 2.0 does not support point mode drawing + private static void DrawModelPoints(Model model, Vector3 position, float scale, Color tint) + { + Rlgl.EnablePointMode(); + Rlgl.DisableBackfaceCulling(); + + DrawModel(model, position, scale, tint); + + Rlgl.EnableBackfaceCulling(); + Rlgl.DisablePointMode(); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - point rendering"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new PointRendering(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Models/RotatingCube.cs b/Examples/Models/RotatingCube.cs new file mode 100644 index 0000000..c903d96 --- /dev/null +++ b/Examples/Models/RotatingCube.cs @@ -0,0 +1,122 @@ +/******************************************************************************************* +* +* raylib [models] example - rotating cube +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Jopestpe (@jopestpe) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Jopestpe (@jopestpe) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Models; + +public partial class RotatingCube : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Rotating Cube"; + + public string Title => "raylib [models] example - rotating cube"; + + private Camera3D camera; + private Model model; + private Texture2D texture; + private float rotation; + + public unsafe void Init() + { + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(0.0f, 3.0f, 3.0f); + camera.Target = new Vector3(0.0f, 0.0f, 0.0f); + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); + camera.FovY = 45.0f; + camera.Projection = CameraProjection.Perspective; + + // Load image to create texture for the cube + model = LoadModelFromMesh(GenMeshCube(1.0f, 1.0f, 1.0f)); + var img = LoadImage("resources/cubicmap_atlas.png"); + var crop = ImageFromImage(img, new Rectangle(0, img.Height / 2.0f, img.Width / 2.0f, img.Height / 2.0f)); + texture = LoadTextureFromImage(crop); + UnloadImage(img); + UnloadImage(crop); + + model.Materials[0].Maps[(int)MaterialMapIndex.Diffuse].Texture = texture; + + rotation = 0.0f; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + rotation += 1.0f; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + // Draw model defining: position, size, rotation-axis, rotation (degrees), size, and tint-color + DrawModelEx(model, new Vector3(0.0f, 0.0f, 0.0f), new Vector3(0.5f, 1.0f, 0.0f), + rotation, new Vector3(1.0f, 1.0f, 1.0f), Color.White); + + DrawGrid(10, 1.0f); + + EndMode3D(); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texture); // Unload texture + UnloadModel(model); // Unload model + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - rotating cube"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new RotatingCube(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Models/SkyboxDemo.cs b/Examples/Models/SkyboxDemo.cs index 69761dc..8238eae 100644 --- a/Examples/Models/SkyboxDemo.cs +++ b/Examples/Models/SkyboxDemo.cs @@ -19,13 +19,18 @@ using static Raylib_cs.Raylib; namespace Examples.Models; +[ExcludeFromBrowser("cubemap generation is too memory-heavy on web (upstream note)")] public partial class SkyboxDemo : IExample { private const int screenWidth = 800; private const int screenHeight = 450; // GLSL version used for shaders (330 desktop, 100 web/GLES) +#if BROWSER + public const int GlslVersion = 100; +#else public const int GlslVersion = 330; +#endif public string Name => "Models / Skybox Demo"; diff --git a/Examples/Models/TesseractView.cs b/Examples/Models/TesseractView.cs new file mode 100644 index 0000000..b5a1964 --- /dev/null +++ b/Examples/Models/TesseractView.cs @@ -0,0 +1,163 @@ +/******************************************************************************************* +* +* raylib [models] example - tesseract view +* +* NOTE: This example only works on platforms that support drag & drop (Windows, Linux, OSX, Html5?) +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Timothy van der Valk (@arceryz) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2024-2025 Timothy van der Valk (@arceryz) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; + +namespace Examples.Models; + +public partial class TesseractView : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Tesseract View"; + + public string Title => "raylib [models] example - tesseract view"; + + // Define the camera to look into our 3d world + private Camera3D camera; + + // Find the coordinates by setting XYZW to +-1 + private Vector4[] tesseract; + + private float rotation; + private Vector3[] transformed; + private float[] wValues; + + public void Init() + { + // Define the camera to look into our 3d world + camera = new Camera3D(); + camera.Position = new Vector3(4.0f, 4.0f, 4.0f); // Camera position + camera.Target = new Vector3(0.0f, 0.0f, 0.0f); // Camera looking at point + camera.Up = new Vector3(0.0f, 0.0f, 1.0f); // Camera up vector (rotation towards target) + camera.FovY = 50.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera mode type + + // Find the coordinates by setting XYZW to +-1 + tesseract = new Vector4[16] + { + new( 1, 1, 1, 1 ), new( 1, 1, 1, -1 ), + new( 1, 1, -1, 1 ), new( 1, 1, -1, -1 ), + new( 1, -1, 1, 1 ), new( 1, -1, 1, -1 ), + new( 1, -1, -1, 1 ), new( 1, -1, -1, -1 ), + new(-1, 1, 1, 1 ), new(-1, 1, 1, -1 ), + new(-1, 1, -1, 1 ), new(-1, 1, -1, -1 ), + new(-1, -1, 1, 1 ), new(-1, -1, 1, -1 ), + new(-1, -1, -1, 1 ), new(-1, -1, -1, -1 ), + }; + + rotation = 0.0f; + transformed = new Vector3[16]; + wValues = new float[16]; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + rotation = DEG2RAD * 45.0f * (float)GetTime(); + + for (int i = 0; i < 16; i++) + { + Vector4 p = tesseract[i]; + + // Rotate the XW part of the vector + Vector2 rotXW = Vector2Rotate(new Vector2(p.X, p.W), rotation); + p.X = rotXW.X; + p.W = rotXW.Y; + + // Projection from XYZW to XYZ from perspective point (0, 0, 0, 3) + // NOTE: Trace a ray from (0, 0, 0, 3) > p and continue until W = 0 + float c = 3.0f / (3.0f - p.W); + p.X = c * p.X; + p.Y = c * p.Y; + p.Z = c * p.Z; + + // Split XYZ coordinate and W values later for drawing + transformed[i] = new Vector3(p.X, p.Y, p.Z); + wValues[i] = p.W; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + for (int i = 0; i < 16; i++) + { + // Draw spheres to indicate the W value + DrawSphere(transformed[i], MathF.Abs(wValues[i] * 0.1f), Color.Red); + + for (int j = 0; j < 16; j++) + { + // Two lines are connected if they differ by 1 coordinate + // This way we dont have to keep an edge list + Vector4 v1 = tesseract[i]; + Vector4 v2 = tesseract[j]; + int diff = (v1.X == v2.X ? 1 : 0) + (v1.Y == v2.Y ? 1 : 0) + (v1.Z == v2.Z ? 1 : 0) + (v1.W == v2.W ? 1 : 0); + + // Draw only differing by 1 coordinate and the lower index only (duplicate lines) + if (diff == 3 && i < j) DrawLine3D(transformed[i], transformed[j], Color.Maroon); + } + } + EndMode3D(); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - tesseract view"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new TesseractView(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Program.cs b/Examples/Program.cs index f68b81e..a24b557 100644 --- a/Examples/Program.cs +++ b/Examples/Program.cs @@ -5,9 +5,6 @@ namespace Examples; internal static class Program { - private const int screenWidth = 800; - private const int screenHeight = 450; - private static unsafe void Main(string[] args) { Raylib.SetTraceLogCallback(&Logging.LogConsole); @@ -50,7 +47,7 @@ internal static class Program SetConfigFlags(example.ConfigFlags); } - InitWindow(screenWidth, screenHeight, example.Title); + InitWindow(example.Width, example.Height, example.Title); if (example.CursorDisabled) { @@ -63,16 +60,26 @@ internal static class Program SetTargetFPS(example.TargetFps); - example.Init(); - - while (!WindowShouldClose()) + try { - example.Update(); + example.Init(); + + while (!example.ShouldClose) + { + example.Update(); + } + } + finally + { + try + { + example.Unload(); + } + finally + { + CloseWindow(); + } } - - example.Unload(); - - CloseWindow(); } private static void RunExampleProcess( diff --git a/Examples/Shaders/AsciiRendering.cs b/Examples/Shaders/AsciiRendering.cs new file mode 100644 index 0000000..7924c79 --- /dev/null +++ b/Examples/Shaders/AsciiRendering.cs @@ -0,0 +1,166 @@ +/******************************************************************************************* +* +* raylib [shaders] example - ascii rendering +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 6.0 +* +* Example contributed by Maicon Santana (@maiconpintoabreu) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Maicon Santana (@maiconpintoabreu) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shaders; + +public partial class AsciiRendering : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Ascii Rendering"; + + public string Title => "raylib [shaders] example - ascii rendering"; + + private Texture2D fudesumi; + private Texture2D raysan; + private Shader shader; + private int resolutionLoc; + private int fontSizeLoc; + private float fontSize; + private Vector2 circlePos; + private float circleSpeed; + private RenderTexture2D target; + + public void Init() + { + // Texture to test static drawing + fudesumi = LoadTexture("resources/fudesumi.png"); + // Texture to test moving drawing + raysan = LoadTexture("resources/raysan.png"); + + // Load shader to be used on postprocessing + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/ascii.fs"); + + // These locations are used to send data to the GPU + resolutionLoc = GetShaderLocation(shader, "resolution"); + fontSizeLoc = GetShaderLocation(shader, "fontSize"); + + // Set the character size for the ASCII effect + // Fontsize should be 9 or more + fontSize = 9.0f; + + // Send the updated values to the shader + var resolution = new[] { (float)screenWidth, (float)screenHeight }; + Raylib.SetShaderValue(shader, resolutionLoc, resolution, ShaderUniformDataType.Vec2); + + circlePos = new Vector2(40.0f, screenHeight * 0.5f); + circleSpeed = 1.0f; + + // RenderTexture to apply the postprocessing later + target = LoadRenderTexture(screenWidth, screenHeight); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + circlePos.X += circleSpeed; + if ((circlePos.X > 200.0f) || (circlePos.X < 40.0f)) + { + circleSpeed *= -1; // Revert speed + } + + if (IsKeyPressed(KeyboardKey.Left) && (fontSize > 9.0)) + { + fontSize -= 1; // Reduce fontSize + } + if (IsKeyPressed(KeyboardKey.Right) && (fontSize < 15.0)) + { + fontSize += 1; // Increase fontSize + } + + // Set fontsize for the shader + Raylib.SetShaderValue(shader, fontSizeLoc, fontSize, ShaderUniformDataType.Float); + + // Draw + //---------------------------------------------------------------------------------- + BeginTextureMode(target); + ClearBackground(Color.White); + + // Draw scene in our render texture + DrawTexture(fudesumi, 500, -30, Color.White); + DrawTextureV(raysan, circlePos, Color.White); + EndTextureMode(); + + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginShaderMode(shader); + // Draw the scene texture (that we rendered earlier) to the screen + // The shader will process every pixel of this texture + DrawTextureRec( + target.Texture, + new Rectangle(0, 0, target.Texture.Width, -target.Texture.Height), + new Vector2(0, 0), + Color.White + ); + EndShaderMode(); + + DrawRectangle(0, 0, screenWidth, 40, Color.Black); + DrawText($"Ascii effect - FontSize:{fontSize,2:F0} - [Left] -1 [Right] +1 ", 120, 10, 20, Color.LightGray); + DrawFPS(10, 10); + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadRenderTexture(target); // Unload render texture + + UnloadShader(shader); // Unload shader + UnloadTexture(fudesumi); // Unload texture + UnloadTexture(raysan); // Unload texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - ascii rendering"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new AsciiRendering(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shaders/CelShading.cs b/Examples/Shaders/CelShading.cs new file mode 100644 index 0000000..c65153f --- /dev/null +++ b/Examples/Shaders/CelShading.cs @@ -0,0 +1,252 @@ +/******************************************************************************************* +* +* raylib [shaders] example - cel shading +* +* Example complexity rating: [★★★☆] 3/4 +* +* NOTE: This example requires raylib OpenGL 3.3 or ES2 versions for shaders support, +* OpenGL 1.1 does not support shaders, recompile raylib to OpenGL 3.3 version +* +* NOTE: Shaders used in this example are #version 330 (OpenGL 3.3) +* +* Example contributed by Gleb A (@ggrizzly) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2026 Gleb A (@ggrizzly) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; +using static Raylib_cs.Rlgl; +using Examples.Shared; + +namespace Examples.Shaders; + +public partial class CelShading : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MaxLights = 4; + + // rlgl cull face modes (rlgl.h: RL_CULL_FACE_FRONT = 0, RL_CULL_FACE_BACK = 1) + private const int CullFaceFront = 0; + private const int CullFaceBack = 1; + +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Cel Shading"; + + public string Title => "raylib [shaders] example - cel shading"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + private Camera3D camera; + private Model model; + private Shader celShader; + private Shader defaultShader; + private Shader outlineShader; + private float numBands; + private int numBandsLoc; + private int outlineThicknessLoc; + private Light[] lights; + private bool celEnabled; + private bool outlineEnabled; + + public unsafe void Init() + { + camera = new(); + camera.Position = new Vector3(9.0f, 6.0f, 9.0f); + camera.Target = new Vector3(0.0f, 1.0f, 0.0f); + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); + camera.FovY = 45.0f; + camera.Projection = CameraProjection.Perspective; + + // Load model + model = LoadModel("resources/models/old_car_new.glb"); + + // Load cel shader + celShader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/cel.vs", + $"resources/shaders/glsl{GlslVersion}/cel.fs" + ); + celShader.Locs[(int)ShaderLocationIndex.VectorView] = GetShaderLocation(celShader, "viewPos"); + + // Apply cel shader to model, keep copy of default shader + defaultShader = model.Materials[0].Shader; + model.Materials[0].Shader = celShader; + + // numBands: controls toon quantization steps (2 = hard binary, 20 = near-smooth) + numBands = 10.0f; + numBandsLoc = GetShaderLocation(celShader, "numBands"); + Raylib.SetShaderValue(celShader, numBandsLoc, numBands, ShaderUniformDataType.Float); + + // Inverted-hull outline shader: draws back faces extruded along normals + outlineShader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/outline_hull.vs", + $"resources/shaders/glsl{GlslVersion}/outline_hull.fs" + ); + outlineThicknessLoc = GetShaderLocation(outlineShader, "outlineThickness"); + + // Single directional white light, angled so toon bands are visible on the model sides. + // Spins opposite to CAMERA_ORBITAL (0.5 rad/s) so lighting changes as you watch. + lights = new Light[MaxLights]; + lights[0] = Rlights.CreateLight( + 0, + LightType.Directorional, + new Vector3(50.0f, 50.0f, 50.0f), + Vector3.Zero, + Color.White, + celShader + ); + + celEnabled = true; + outlineEnabled = true; + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + + Raylib.SetShaderValue( + celShader, + celShader.Locs[(int)ShaderLocationIndex.VectorView], + camera.Position, + ShaderUniformDataType.Vec3 + ); + + // [Z] Toggle cel shading on/off + if (IsKeyPressed(KeyboardKey.Z)) + { + celEnabled = !celEnabled; + if (celEnabled) + { + model.Materials[0].Shader = celShader; // Apply cel shader to model + } + else + { + model.Materials[0].Shader = defaultShader; // Apply default shader to model + } + } + + // [C] Toggle outline on/off + if (IsKeyPressed(KeyboardKey.C)) + { + outlineEnabled = !outlineEnabled; + } + + // [Q/E] Decrease/increase toon band count (press or hold to repeat) + if (IsKeyPressed(KeyboardKey.E) || IsKeyPressedRepeat(KeyboardKey.E)) + { + numBands = Clamp(numBands + 1.0f, 2.0f, 20.0f); + } + if (IsKeyPressed(KeyboardKey.Q) || IsKeyPressedRepeat(KeyboardKey.Q)) + { + numBands = Clamp(numBands - 1.0f, 2.0f, 20.0f); + } + Raylib.SetShaderValue(celShader, numBandsLoc, numBands, ShaderUniformDataType.Float); + + // Spin light opposite to CAMERA_ORBITAL (0.5 rad/s), angled 45 degrees off vertical + float t = (float)GetTime(); + lights[0].Position = new Vector3(MathF.Sin(-t * 0.3f) * 5.0f, 5.0f, MathF.Cos(-t * 0.3f) * 5.0f); + + for (var i = 0; i < MaxLights; i++) + { + Rlights.UpdateLightValues(celShader, lights[i]); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + if (outlineEnabled) + { + // Outline pass: cull front faces, draw extruded back faces as silhouette + float thickness = 0.005f; + Raylib.SetShaderValue(outlineShader, outlineThicknessLoc, thickness, ShaderUniformDataType.Float); + + SetCullFace(CullFaceFront); + + model.Materials[0].Shader = outlineShader; + + DrawModel(model, Vector3.Zero, 0.75f, Color.White); + + if (celEnabled) + { + model.Materials[0].Shader = celShader; // Apply cel shader to model + } + else + { + model.Materials[0].Shader = defaultShader; // Apply default shader to model + } + + SetCullFace(CullFaceBack); + } + + DrawModel(model, Vector3.Zero, 0.75f, Color.White); + DrawSphereEx(lights[0].Position, 0.2f, 50, 50, Color.Yellow); // Light position indicator + DrawGrid(10, 10.0f); + + EndMode3D(); + + DrawFPS(10, 10); + DrawText($"Cel: {(celEnabled ? "ON" : "OFF")} [Z]", 10, 65, 20, celEnabled ? Color.DarkGreen : Color.DarkGray); + DrawText($"Outline: {(outlineEnabled ? "ON" : "OFF")} [C]", 10, 90, 20, outlineEnabled ? Color.DarkGreen : Color.DarkGray); + DrawText($"Bands: {numBands:0} [Q/E]", 10, 115, 20, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadModel(model); + UnloadShader(celShader); + UnloadShader(outlineShader); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - cel shading"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new CelShading(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shaders/ColorCorrection.cs b/Examples/Shaders/ColorCorrection.cs new file mode 100644 index 0000000..c70dce5 --- /dev/null +++ b/Examples/Shaders/ColorCorrection.cs @@ -0,0 +1,184 @@ +/******************************************************************************************* +* +* raylib [shaders] example - color correction +* +* Example complexity rating: [★★☆☆] 2/4 +* +* NOTE: This example requires raylib OpenGL 3.3 or ES2 versions for shaders support, +* OpenGL 1.1 does not support shaders, recompile raylib to OpenGL 3.3 version +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Jordi Santonja (@JordSant) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Jordi Santonja (@JordSant) +* +********************************************************************************************/ + +using static Raylib_cs.Raylib; + +namespace Examples.Shaders; + +public partial class ColorCorrection : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + private const int MaxTextures = 4; + + public string Name => "Shaders / Color Correction"; + + public string Title => "raylib [shaders] example - color correction"; + + private Texture2D[] texture; + private Shader shdrColorCorrection; + private int imageIndex; + private int resetButtonClicked; + private float contrast; + private float saturation; + private float brightness; + private int contrastLoc; + private int saturationLoc; + private int brightnessLoc; + + public void Init() + { + texture = new[] + { + LoadTexture("resources/parrots.png"), + LoadTexture("resources/cat.png"), + LoadTexture("resources/mandrill.png"), + LoadTexture("resources/fudesumi.png") + }; + + shdrColorCorrection = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/color_correction.fs"); + + imageIndex = 0; + resetButtonClicked = 0; + + contrast = 0.0f; + saturation = 0.0f; + brightness = 0.0f; + + // Get shader locations + contrastLoc = GetShaderLocation(shdrColorCorrection, "contrast"); + saturationLoc = GetShaderLocation(shdrColorCorrection, "saturation"); + brightnessLoc = GetShaderLocation(shdrColorCorrection, "brightness"); + + // Set shader values (they can be changed later) + Raylib.SetShaderValue(shdrColorCorrection, contrastLoc, contrast, ShaderUniformDataType.Float); + Raylib.SetShaderValue(shdrColorCorrection, saturationLoc, saturation, ShaderUniformDataType.Float); + Raylib.SetShaderValue(shdrColorCorrection, brightnessLoc, brightness, ShaderUniformDataType.Float); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Select texture to draw + if (IsKeyPressed(KeyboardKey.One)) imageIndex = 0; + else if (IsKeyPressed(KeyboardKey.Two)) imageIndex = 1; + else if (IsKeyPressed(KeyboardKey.Three)) imageIndex = 2; + else if (IsKeyPressed(KeyboardKey.Four)) imageIndex = 3; + + // Reset values to 0 + if (IsKeyPressed(KeyboardKey.R) || resetButtonClicked != 0) + { + contrast = 0.0f; + saturation = 0.0f; + brightness = 0.0f; + } + + // Send the values to the shader + Raylib.SetShaderValue(shdrColorCorrection, contrastLoc, contrast, ShaderUniformDataType.Float); + Raylib.SetShaderValue(shdrColorCorrection, saturationLoc, saturation, ShaderUniformDataType.Float); + Raylib.SetShaderValue(shdrColorCorrection, brightnessLoc, brightness, ShaderUniformDataType.Float); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginShaderMode(shdrColorCorrection); + + DrawTexture(texture[imageIndex], 580 / 2 - texture[imageIndex].Width / 2, GetScreenHeight() / 2 - texture[imageIndex].Height / 2, Color.White); + + EndShaderMode(); + + DrawLine(580, 0, 580, GetScreenHeight(), new Color(218, 218, 218, 255)); + DrawRectangle(580, 0, GetScreenWidth(), GetScreenHeight(), new Color(232, 232, 232, 255)); + + // Draw UI info text + DrawText("Color Correction", 585, 40, 20, Color.Gray); + + DrawText("Picture", 602, 75, 10, Color.Gray); + DrawText("Press [1] - [4] to Change Picture", 600, 230, 8, Color.Gray); + DrawText("Press [R] to Reset Values", 600, 250, 8, Color.Gray); + + // Draw GUI controls + //------------------------------------------------------------------------------ + // NOTE: raygui is not bound in raylib-cs; controls are kept for reference. Use + // keyboard shortcuts ([1]-[4], [R]) to interact with the example. + /*GuiToggleGroup(new Rectangle( 645, 70, 20, 20 ), "1;2;3;4", ref imageIndex); + + GuiSliderBar(new Rectangle( 645, 100, 120, 20 ), "Contrast", TextFormat("%.0f", contrast), ref contrast, -100.0f, 100.0f); + GuiSliderBar(new Rectangle( 645, 130, 120, 20 ), "Saturation", TextFormat("%.0f", saturation), ref saturation, -100.0f, 100.0f); + GuiSliderBar(new Rectangle( 645, 160, 120, 20 ), "Brightness", TextFormat("%.0f", brightness), ref brightness, -100.0f, 100.0f); + + resetButtonClicked = GuiButton(new Rectangle( 645, 190, 40, 20 ), "Reset");*/ + //------------------------------------------------------------------------------ + + DrawFPS(710, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + for (var i = 0; i < MaxTextures; i++) + { + UnloadTexture(texture[i]); + } + UnloadShader(shdrColorCorrection); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - color correction"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new ColorCorrection(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shaders/DeferredRendering.cs b/Examples/Shaders/DeferredRendering.cs new file mode 100644 index 0000000..f6af204 --- /dev/null +++ b/Examples/Shaders/DeferredRendering.cs @@ -0,0 +1,400 @@ +/******************************************************************************************* +* +* raylib [shaders] example - deferred rendering +* +* Example complexity rating: [★★★★] 4/4 +* +* NOTE: This example requires raylib OpenGL 3.3 or OpenGL ES 3.0 +* +* Example originally created with raylib 4.5, last time updated with raylib 4.5 +* +* Example contributed by Justin Andreas Lacoste (@27justin) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2023-2025 Justin Andreas Lacoste (@27justin) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; +using Examples.Shared; + +namespace Examples.Shaders; + +[ExcludeFromBrowser("multiple-render-target G-buffer, unsupported on WebGL1/GLSL100")] +public partial class DeferredRendering : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + private const int MaxCubes = 30; + private const int MaxLights = 4; + private const float CubeScale = 0.25f; + + // GL_READ_FRAMEBUFFER / GL_DRAW_FRAMEBUFFER / GL_DEPTH_BUFFER_BIT + private const uint RlReadFramebuffer = 0x8CA8; + private const uint RlDrawFramebuffer = 0x8CA9; + private const int GlDepthBufferBit = 0x00000100; + + public string Name => "Shaders / Deferred Rendering"; + + public string Title => "raylib [shaders] example - deferred rendering"; + + // GBuffer data + private struct GBuffer + { + public uint FramebufferId; + + public uint PositionTextureId; + public uint NormalTextureId; + public uint AlbedoSpecTextureId; + + public uint DepthRenderbufferId; + } + + // Deferred mode passes + private enum DeferredMode + { + Position, + Normal, + Albedo, + Shading + } + + private Camera3D camera; + private Model model; + private Model cube; + private Shader gbufferShader; + private Shader deferredShader; + private GBuffer gBuffer; + private Light[] lights; + private Vector3[] cubePositions; + private float[] cubeRotations; + private DeferredMode mode; + + // Texture units our g-buffer textures are bound to + private const int TexUnitPosition = 0; + private const int TexUnitNormal = 1; + private const int TexUnitAlbedoSpec = 2; + + public unsafe void Init() + { + camera = new(); + camera.Position = new Vector3(5.0f, 4.0f, 5.0f); // Camera position + camera.Target = new Vector3(0.0f, 1.0f, 0.0f); // Camera looking at point + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target) + camera.FovY = 60.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + + // Load plane model from a generated mesh + model = LoadModelFromMesh(GenMeshPlane(10.0f, 10.0f, 3, 3)); + cube = LoadModelFromMesh(GenMeshCube(2.0f, 2.0f, 2.0f)); + + // Load geometry buffer (G-buffer) shader and deferred shader + gbufferShader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/gbuffer.vs", + $"resources/shaders/glsl{GlslVersion}/gbuffer.fs" + ); + + deferredShader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/deferred_shading.vs", + $"resources/shaders/glsl{GlslVersion}/deferred_shading.fs" + ); + deferredShader.Locs[(int)ShaderLocationIndex.VectorView] = GetShaderLocation(deferredShader, "viewPosition"); + + // Initialize the G-buffer + gBuffer = new(); + gBuffer.FramebufferId = Rlgl.LoadFramebuffer(); + if (gBuffer.FramebufferId == 0) + { + TraceLog(TraceLogLevel.Warning, "Failed to create framebufferId"); + } + + Rlgl.EnableFramebuffer(gBuffer.FramebufferId); + + // NOTE: Vertex positions are stored in a texture for simplicity. A better approach would use a depth texture + // (instead of a detph renderbuffer) to reconstruct world positions in the final render shader via clip-space position, + // depth, and the inverse view/projection matrices + + // 16-bit precision ensures OpenGL ES 3 compatibility, though it may lack precision for real scenarios + gBuffer.PositionTextureId = Rlgl.LoadTexture(null, screenWidth, screenHeight, PixelFormat.UncompressedR16G16B16, 1); + + // Similarly, 16-bit precision is used for normals ensures OpenGL ES 3 compatibility + gBuffer.NormalTextureId = Rlgl.LoadTexture(null, screenWidth, screenHeight, PixelFormat.UncompressedR16G16B16, 1); + + // Albedo (diffuse color) and specular strength can be combined into one texture + // The color in RGB, and the specular strength in the alpha channel + gBuffer.AlbedoSpecTextureId = Rlgl.LoadTexture(null, screenWidth, screenHeight, PixelFormat.UncompressedR8G8B8A8, 1); + + // Activate the draw buffers for our framebufferId + Rlgl.ActiveDrawBuffers(3); + + // Now we attach our textures to the framebufferId + Rlgl.FramebufferAttach(gBuffer.FramebufferId, gBuffer.PositionTextureId, FramebufferAttachType.ColorChannel0, FramebufferAttachTextureType.Texture2D, 0); + Rlgl.FramebufferAttach(gBuffer.FramebufferId, gBuffer.NormalTextureId, FramebufferAttachType.ColorChannel1, FramebufferAttachTextureType.Texture2D, 0); + Rlgl.FramebufferAttach(gBuffer.FramebufferId, gBuffer.AlbedoSpecTextureId, FramebufferAttachType.ColorChannel2, FramebufferAttachTextureType.Texture2D, 0); + + // Finally we attach the depth buffer + gBuffer.DepthRenderbufferId = Rlgl.LoadTextureDepth(screenWidth, screenHeight, true); + Rlgl.FramebufferAttach(gBuffer.FramebufferId, gBuffer.DepthRenderbufferId, FramebufferAttachType.Depth, FramebufferAttachTextureType.Renderbuffer, 0); + + // Make sure our framebufferId is complete + // NOTE: rlFramebufferComplete() automatically unbinds the framebufferId, so we don't have to rlDisableFramebuffer() here + if (Rlgl.FramebufferComplete(gBuffer.FramebufferId) == 0) + { + TraceLog(TraceLogLevel.Warning, "Framebuffer is not complete"); + } + + // Now we initialize the sampler2D uniform's in the deferred shader + // We do this by setting the uniform's values to the texture units that + // we later bind our g-buffer textures to + Rlgl.EnableShader(deferredShader.Id); + int texUnitPosition = TexUnitPosition; + int texUnitNormal = TexUnitNormal; + int texUnitAlbedoSpec = TexUnitAlbedoSpec; + Raylib.SetShaderValue(deferredShader, GetShaderLocation(deferredShader, "gPosition"), texUnitPosition, ShaderUniformDataType.Sampler2D); + Raylib.SetShaderValue(deferredShader, GetShaderLocation(deferredShader, "gNormal"), texUnitNormal, ShaderUniformDataType.Sampler2D); + Raylib.SetShaderValue(deferredShader, GetShaderLocation(deferredShader, "gAlbedoSpec"), texUnitAlbedoSpec, ShaderUniformDataType.Sampler2D); + Rlgl.DisableShader(); + + // Assign out lighting shader to model + model.Materials[0].Shader = gbufferShader; + cube.Materials[0].Shader = gbufferShader; + + // Create lights + lights = new Light[MaxLights]; + lights[0] = Rlights.CreateLight(0, LightType.Point, new Vector3(-2, 1, -2), Vector3.Zero, Color.Yellow, deferredShader); + lights[1] = Rlights.CreateLight(1, LightType.Point, new Vector3(2, 1, 2), Vector3.Zero, Color.Red, deferredShader); + lights[2] = Rlights.CreateLight(2, LightType.Point, new Vector3(-2, 1, 2), Vector3.Zero, Color.Green, deferredShader); + lights[3] = Rlights.CreateLight(3, LightType.Point, new Vector3(2, 1, -2), Vector3.Zero, Color.Blue, deferredShader); + + var rand = new Random(); + cubePositions = new Vector3[MaxCubes]; + cubeRotations = new float[MaxCubes]; + + for (var i = 0; i < MaxCubes; i++) + { + cubePositions[i] = new Vector3( + (float)(rand.Next() % 10) - 5, + (float)(rand.Next() % 5), + (float)(rand.Next() % 10) - 5 + ); + + cubeRotations[i] = (float)(rand.Next() % 360); + } + + mode = DeferredMode.Shading; + + Rlgl.EnableDepthTest(); + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + + // Update the shader with the camera view vector (points towards { 0.0f, 0.0f, 0.0f }) + Raylib.SetShaderValue( + deferredShader, + deferredShader.Locs[(int)ShaderLocationIndex.VectorView], + camera.Position, + ShaderUniformDataType.Vec3 + ); + + // Check key inputs to enable/disable lights + if (IsKeyPressed(KeyboardKey.Y)) { lights[0].Enabled = !lights[0].Enabled; } + if (IsKeyPressed(KeyboardKey.R)) { lights[1].Enabled = !lights[1].Enabled; } + if (IsKeyPressed(KeyboardKey.G)) { lights[2].Enabled = !lights[2].Enabled; } + if (IsKeyPressed(KeyboardKey.B)) { lights[3].Enabled = !lights[3].Enabled; } + + // Check key inputs to switch between G-buffer textures + if (IsKeyPressed(KeyboardKey.One)) mode = DeferredMode.Position; + if (IsKeyPressed(KeyboardKey.Two)) mode = DeferredMode.Normal; + if (IsKeyPressed(KeyboardKey.Three)) mode = DeferredMode.Albedo; + if (IsKeyPressed(KeyboardKey.Four)) mode = DeferredMode.Shading; + + // Update light values (actually, only enable/disable them) + for (var i = 0; i < MaxLights; i++) Rlights.UpdateLightValues(deferredShader, lights[i]); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + // Draw to the geometry buffer by first activating it + Rlgl.EnableFramebuffer(gBuffer.FramebufferId); + Rlgl.ClearColor(0, 0, 0, 0); + Rlgl.ClearScreenBuffers(); // Clear color and depth buffer + Rlgl.DisableColorBlend(); + + BeginMode3D(camera); + // NOTE: We have to use rlEnableShader here. `BeginShaderMode` or thus `rlSetShader` + // will not work, as they won't immediately load the shader program + Rlgl.EnableShader(gbufferShader.Id); + // When drawing a model here, make sure that the material's shaders are set to the gbuffer shader! + DrawModel(model, Vector3.Zero, 1.0f, Color.White); + DrawModel(cube, new Vector3(0.0f, 1.0f, 0.0f), 1.0f, Color.White); + + for (var i = 0; i < MaxCubes; i++) + { + var position = cubePositions[i]; + DrawModelEx(cube, position, new Vector3(1, 1, 1), cubeRotations[i], new Vector3(CubeScale, CubeScale, CubeScale), Color.White); + } + Rlgl.DisableShader(); + EndMode3D(); + + Rlgl.EnableColorBlend(); + + // Go back to the default framebufferId (0) and draw our deferred shading + Rlgl.DisableFramebuffer(); + Rlgl.ClearScreenBuffers(); // Clear color & depth buffer + + switch (mode) + { + case DeferredMode.Shading: + { + BeginMode3D(camera); + Rlgl.DisableColorBlend(); + Rlgl.EnableShader(deferredShader.Id); + // Bind our g-buffer textures + // We are binding them to locations that we earlier set in sampler2D uniforms `gPosition`, `gNormal`, + // and `gAlbedoSpec` + Rlgl.ActiveTextureSlot(TexUnitPosition); + Rlgl.EnableTexture(gBuffer.PositionTextureId); + Rlgl.ActiveTextureSlot(TexUnitNormal); + Rlgl.EnableTexture(gBuffer.NormalTextureId); + Rlgl.ActiveTextureSlot(TexUnitAlbedoSpec); + Rlgl.EnableTexture(gBuffer.AlbedoSpecTextureId); + + // Finally, we draw a fullscreen quad to our default framebufferId + // This will now be shaded using our deferred shader + Rlgl.LoadDrawQuad(); + Rlgl.DisableShader(); + Rlgl.EnableColorBlend(); + EndMode3D(); + + // As a last step, we now copy over the depth buffer from our g-buffer to the default framebufferId + Rlgl.BindFramebuffer(RlReadFramebuffer, gBuffer.FramebufferId); + Rlgl.BindFramebuffer(RlDrawFramebuffer, 0); + Rlgl.BlitFramebuffer(0, 0, screenWidth, screenHeight, 0, 0, screenWidth, screenHeight, GlDepthBufferBit); + Rlgl.DisableFramebuffer(); + + // Since our shader is now done and disabled, we can draw spheres + // that represent light positions in default forward rendering + BeginMode3D(camera); + Rlgl.EnableShader(Rlgl.GetShaderIdDefault()); + for (var i = 0; i < MaxLights; i++) + { + if (lights[i].Enabled) DrawSphereEx(lights[i].Position, 0.2f, 8, 8, lights[i].Color); + else DrawSphereWires(lights[i].Position, 0.2f, 8, 8, ColorAlpha(lights[i].Color, 0.3f)); + } + Rlgl.DisableShader(); + EndMode3D(); + + DrawText("FINAL RESULT", 10, screenHeight - 30, 20, Color.DarkGreen); + } + break; + case DeferredMode.Position: + { + DrawTextureRec( + new Texture2D { Id = gBuffer.PositionTextureId, Width = screenWidth, Height = screenHeight }, + new Rectangle(0, 0, screenWidth, -screenHeight), + Vector2.Zero, + Color.RayWhite + ); + + DrawText("POSITION TEXTURE", 10, screenHeight - 30, 20, Color.DarkGreen); + } + break; + case DeferredMode.Normal: + { + DrawTextureRec( + new Texture2D { Id = gBuffer.NormalTextureId, Width = screenWidth, Height = screenHeight }, + new Rectangle(0, 0, screenWidth, -screenHeight), + Vector2.Zero, + Color.RayWhite + ); + + DrawText("NORMAL TEXTURE", 10, screenHeight - 30, 20, Color.DarkGreen); + } + break; + case DeferredMode.Albedo: + { + DrawTextureRec( + new Texture2D { Id = gBuffer.AlbedoSpecTextureId, Width = screenWidth, Height = screenHeight }, + new Rectangle(0, 0, screenWidth, -screenHeight), + Vector2.Zero, + Color.RayWhite + ); + + DrawText("ALBEDO TEXTURE", 10, screenHeight - 30, 20, Color.DarkGreen); + } + break; + default: break; + } + + DrawText("Toggle lights keys: [Y][R][G][B]", 10, 40, 20, Color.DarkGray); + DrawText("Switch G-buffer textures: [1][2][3][4]", 10, 70, 20, Color.DarkGray); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + // Unload the models + UnloadModel(model); + UnloadModel(cube); + + // Unload shaders + UnloadShader(deferredShader); + UnloadShader(gbufferShader); + + // Unload geometry buffer and all attached textures + Rlgl.UnloadFramebuffer(gBuffer.FramebufferId); + Rlgl.UnloadTexture(gBuffer.PositionTextureId); + Rlgl.UnloadTexture(gBuffer.NormalTextureId); + Rlgl.UnloadTexture(gBuffer.AlbedoSpecTextureId); + Rlgl.UnloadTexture(gBuffer.DepthRenderbufferId); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - deferred rendering"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new DeferredRendering(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shaders/DepthRendering.cs b/Examples/Shaders/DepthRendering.cs new file mode 100644 index 0000000..51bb937 --- /dev/null +++ b/Examples/Shaders/DepthRendering.cs @@ -0,0 +1,226 @@ +/******************************************************************************************* +* +* raylib [shaders] example - depth rendering +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Luís Almeida (@luis605) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Luís Almeida (@luis605) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shaders; + +public class DepthRendering : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Depth Rendering"; + + public string Title => "raylib [shaders] example - depth rendering"; + + public bool CursorDisabled => true; + + private Camera3D camera; + private RenderTexture2D target; + private Shader depthShader; + private int depthLoc; + private Model cube; + private Model floor; + + public void Init() + { + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(4.0f, 1.0f, 5.0f); + camera.Target = new Vector3(0.0f, 0.0f, 0.0f); + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); + camera.FovY = 45.0f; + camera.Projection = CameraProjection.Perspective; + + // Load render texture with a depth texture attached + target = LoadRenderTextureDepthTex(screenWidth, screenHeight); + + // Load depth shader and get depth texture shader location + depthShader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/depth_render.fs"); + depthLoc = GetShaderLocation(depthShader, "depthTexture"); + var flipTextureLoc = GetShaderLocation(depthShader, "flipY"); + Raylib.SetShaderValue(depthShader, flipTextureLoc, 1, ShaderUniformDataType.Int); // Flip Y texture + + // Load scene models + cube = LoadModelFromMesh(GenMeshCube(1.0f, 1.0f, 1.0f)); + floor = LoadModelFromMesh(GenMeshPlane(20.0f, 20.0f, 1, 1)); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Free); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginTextureMode(target); + ClearBackground(Color.White); + + BeginMode3D(camera); + DrawModel(cube, new Vector3(0.0f, 0.0f, 0.0f), 3.0f, Color.Yellow); + DrawModel(floor, new Vector3(10.0f, 0.0f, 2.0f), 2.0f, Color.Red); + EndMode3D(); + EndTextureMode(); + + // Draw into screen (main framebuffer) + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginShaderMode(depthShader); + SetShaderValueTexture(depthShader, depthLoc, target.Depth); + DrawTexture(target.Depth, 0, 0, Color.White); + EndShaderMode(); + + DrawRectangle(10, 10, 320, 93, Fade(Color.SkyBlue, 0.5f)); + DrawRectangleLines(10, 10, 320, 93, Color.Blue); + + DrawText("Camera Controls:", 20, 20, 10, Color.Black); + DrawText("- WASD to move", 40, 40, 10, Color.DarkGray); + DrawText("- Mouse Wheel Pressed to Pan", 40, 60, 10, Color.DarkGray); + DrawText("- Z to zoom to (0, 0, 0)", 40, 80, 10, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadModel(cube); // Unload model + UnloadModel(floor); // Unload model + UnloadRenderTextureDepthTex(target); + UnloadShader(depthShader); // Unload shader + } + + // Load custom render texture, create a writable depth texture buffer + private static unsafe RenderTexture2D LoadRenderTextureDepthTex(int width, int height) + { + RenderTexture2D target = new(); + + // Load an empty framebuffer + target.Id = Rlgl.LoadFramebuffer(); + + if (target.Id > 0) + { + Rlgl.EnableFramebuffer(target.Id); + + // Create color texture (default to RGBA) + target.Texture.Id = Rlgl.LoadTexture( + null, + width, + height, + PixelFormat.UncompressedR8G8B8A8, + 1 + ); + target.Texture.Width = width; + target.Texture.Height = height; + target.Texture.Format = PixelFormat.UncompressedR8G8B8A8; + target.Texture.Mipmaps = 1; + + // Create depth texture buffer (instead of raylib default renderbuffer) + target.Depth.Id = Rlgl.LoadTextureDepth(width, height, false); + target.Depth.Width = width; + target.Depth.Height = height; + target.Depth.Format = PixelFormat.CompressedPvrtRgba; // DEPTH_COMPONENT_24BIT: Not defined in raylib + target.Depth.Mipmaps = 1; + + // Attach color texture and depth texture to FBO + Rlgl.FramebufferAttach( + target.Id, + target.Texture.Id, + FramebufferAttachType.ColorChannel0, + FramebufferAttachTextureType.Texture2D, + 0 + ); + Rlgl.FramebufferAttach( + target.Id, + target.Depth.Id, + FramebufferAttachType.Depth, + FramebufferAttachTextureType.Texture2D, + 0 + ); + + // Check if fbo is complete with attachments (valid) + if (Rlgl.FramebufferComplete(target.Id) != 0) + { + TraceLog(TraceLogLevel.Info, $"FBO: [ID {target.Id}] Framebuffer object created successfully"); + } + + Rlgl.DisableFramebuffer(); + } + else + { + TraceLog(TraceLogLevel.Warning, "FBO: Framebuffer object can not be created"); + } + + return target; + } + + // Unload render texture from GPU memory (VRAM) + private static void UnloadRenderTextureDepthTex(RenderTexture2D target) + { + if (target.Id > 0) + { + // Color texture attached to FBO is deleted + Rlgl.UnloadTexture(target.Texture.Id); + Rlgl.UnloadTexture(target.Depth.Id); + + // NOTE: Depth texture is automatically + // queried and deleted before deleting framebuffer + Rlgl.UnloadFramebuffer(target.Id); + } + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - depth rendering"); + + DisableCursor(); // Limit cursor to relative movement inside the window + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new DepthRendering(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shaders/GameOfLife.cs b/Examples/Shaders/GameOfLife.cs new file mode 100644 index 0000000..6f62950 --- /dev/null +++ b/Examples/Shaders/GameOfLife.cs @@ -0,0 +1,433 @@ +/******************************************************************************************* +* +* raylib [shaders] example - game of life +* +* Example complexity rating: [★★★☆] 3/4 +* +* NOTE: This example requires raylib OpenGL 3.3 or ES2 versions for shaders support, +* OpenGL 1.1 does not support shaders, recompile raylib to OpenGL 3.3 version +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Jordi Santonja (@JordSant) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Jordi Santonja (@JordSant) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shaders; + +public partial class GameOfLife : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Game of Life"; + + public string Title => "raylib [shaders] example - game of life"; + + // Interaction mode + private enum InteractionMode + { + Run = 0, + Pause, + Draw, + } + + // Struct to store example preset patterns + private struct PresetPattern + { + public string Name; + public Vector2 Position; + + public PresetPattern(string name, Vector2 position) + { + Name = name; + Position = position; + } + } + + private const int menuWidth = 100; + private const int windowWidth = screenWidth - menuWidth; + private const int windowHeight = screenHeight; + + private const int worldWidth = 2048; + private const int worldHeight = 2048; + + private const int randomTiles = 8; // Random preset: divide the world to compute random points in each tile + + private static readonly PresetPattern[] presetPatterns = + { + new("Glider", new Vector2(0.5f, 0.5f)), new("R-pentomino", new Vector2(0.5f, 0.5f)), new("Acorn", new Vector2(0.5f, 0.5f)), + new("Spaceships", new Vector2(0.1f, 0.5f)), new("Still lifes", new Vector2(0.5f, 0.5f)), new("Oscillators", new Vector2(0.5f, 0.5f)), + new("Puffer train", new Vector2(0.1f, 0.5f)), new("Glider Gun", new Vector2(0.2f, 0.2f)), new("Breeder", new Vector2(0.1f, 0.5f)), + new("Random", new Vector2(0.5f, 0.5f)) + }; + + private static readonly int numberOfPresets = presetPatterns.Length; + + private Rectangle worldRectSource; + private Rectangle worldRectDest; + private Rectangle textureOnScreen; + + private int zoom; + private float offsetX; + private float offsetY; + private int framesPerStep; + private int frame; + + private int preset; + private InteractionMode mode; + private bool buttonZoomIn; + private bool buttonZomOut; + private bool buttonFaster; + private bool buttonSlower; + + private Shader shdrGameOfLife; + private int resolutionLoc; + + private RenderTexture2D world1; + private RenderTexture2D world2; + private RenderTexture2D currentWorld; + private RenderTexture2D previousWorld; + + // Image to be used in DRAW mode, to be changed with mouse input + private Image imageToDraw; + private bool imageToDrawValid; + + // Static locals in the original loop, promoted to fields + private Vector2 previousMousePosition; + private int firstColor; + + private void FreeImageToDraw() + { + if (imageToDrawValid) + { + UnloadImage(imageToDraw); + imageToDrawValid = false; + } + } + + public unsafe void Init() + { + worldRectSource = new Rectangle(0, 0, worldWidth, -worldHeight); + worldRectDest = new Rectangle(0, 0, worldWidth, worldHeight); + textureOnScreen = new Rectangle(0, 0, windowWidth, windowHeight); + + zoom = 1; + offsetX = (worldWidth - windowWidth) / 2.0f; // Centered on window + offsetY = (worldHeight - windowHeight) / 2.0f; // Centered on window + framesPerStep = 1; + frame = 0; + + preset = -1; // No button pressed for preset + mode = InteractionMode.Run; // Starting mode: running + buttonZoomIn = false; // Button states: false not pressed + buttonZomOut = false; + buttonFaster = false; + buttonSlower = false; + + previousMousePosition = new Vector2(0.0f, 0.0f); + firstColor = -1; + imageToDrawValid = false; + + // Load shader + shdrGameOfLife = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/game_of_life.fs"); + + // Set shader uniform size of the world + resolutionLoc = GetShaderLocation(shdrGameOfLife, "resolution"); + var resolution = new[] { (float)worldWidth, (float)worldHeight }; + Raylib.SetShaderValue(shdrGameOfLife, resolutionLoc, resolution, ShaderUniformDataType.Vec2); + + // Define two textures: the current world and the previous world + world1 = LoadRenderTexture(worldWidth, worldHeight); + world2 = LoadRenderTexture(worldWidth, worldHeight); + BeginTextureMode(world2); + ClearBackground(Color.RayWhite); + EndTextureMode(); + + var startPattern = LoadImage("resources/game_of_life/r_pentomino.png"); + UpdateTextureRec( + world2.Texture, + new Rectangle(worldWidth / 2.0f, worldHeight / 2.0f, startPattern.Width, startPattern.Height), + startPattern.Data + ); + UnloadImage(startPattern); + + // References to the two textures, to be swapped + currentWorld = world2; + previousWorld = world1; + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + frame++; + + // Change zoom: both by buttons or by mouse wheel + var mouseWheelMove = GetMouseWheelMove(); + if (buttonZoomIn || (buttonZomOut && (zoom > 1)) || (mouseWheelMove != 0.0f)) + { + FreeImageToDraw(); // Zoom change: free the image to draw to be recreated again + + var centerX = offsetX + (windowWidth / 2.0f) / zoom; + var centerY = offsetY + (windowHeight / 2.0f) / zoom; + if (buttonZoomIn || (mouseWheelMove > 0.0f)) zoom *= 2; + if ((buttonZomOut || (mouseWheelMove < 0.0f)) && (zoom > 1)) zoom /= 2; + offsetX = centerX - (windowWidth / 2.0f) / zoom; + offsetY = centerY - (windowHeight / 2.0f) / zoom; + } + + // Change speed: number of frames per step + if (buttonFaster && framesPerStep > 1) framesPerStep--; + if (buttonSlower) framesPerStep++; + + // Mouse management + if ((mode == InteractionMode.Run) || (mode == InteractionMode.Pause)) + { + FreeImageToDraw(); // Free the image to draw: no longer needed in these modes + + // Pan with mouse left button + var mousePosition = GetMousePosition(); + if (IsMouseButtonDown(MouseButton.Left) && (mousePosition.X < windowWidth)) + { + offsetX -= (mousePosition.X - previousMousePosition.X) / zoom; + offsetY -= (mousePosition.Y - previousMousePosition.Y) / zoom; + } + previousMousePosition = mousePosition; + } + else // MODE_DRAW + { + var offsetDecimalX = offsetX - MathF.Floor(offsetX); + var offsetDecimalY = offsetY - MathF.Floor(offsetY); + var sizeInWorldX = (int)(MathF.Ceiling((windowWidth + offsetDecimalX * zoom) / zoom)); + var sizeInWorldY = (int)(MathF.Ceiling((windowHeight + offsetDecimalY * zoom) / zoom)); + if (offsetX + sizeInWorldX >= worldWidth) sizeInWorldX = worldWidth - (int)MathF.Floor(offsetX); + if (offsetY + sizeInWorldY >= worldHeight) sizeInWorldY = worldHeight - (int)MathF.Floor(offsetY); + + // Create image to draw if not created yet + if (!imageToDrawValid) + { + var worldOnScreen = LoadRenderTexture(sizeInWorldX, sizeInWorldY); + BeginTextureMode(worldOnScreen); + DrawTexturePro( + currentWorld.Texture, + new Rectangle(MathF.Floor(offsetX), MathF.Floor(offsetY), sizeInWorldX, -sizeInWorldY), + new Rectangle(0, 0, sizeInWorldX, sizeInWorldY), + new Vector2(0, 0), 0.0f, Color.White + ); + EndTextureMode(); + imageToDraw = LoadImageFromTexture(worldOnScreen.Texture); + imageToDrawValid = true; + + UnloadRenderTexture(worldOnScreen); + } + + var mousePosition = GetMousePosition(); + if (IsMouseButtonDown(MouseButton.Left) && (mousePosition.X < windowWidth)) + { + var mouseX = (int)(mousePosition.X + offsetDecimalX * zoom) / zoom; + var mouseY = (int)(mousePosition.Y + offsetDecimalY * zoom) / zoom; + if (mouseX >= sizeInWorldX) mouseX = sizeInWorldX - 1; + if (mouseY >= sizeInWorldY) mouseY = sizeInWorldY - 1; + if (firstColor == -1) firstColor = (GetImageColor(imageToDraw, mouseX, mouseY).R < 5) ? 0 : 1; + var prevColor = (GetImageColor(imageToDraw, mouseX, mouseY).R < 5) ? 0 : 1; + + ImageDrawPixel(ref imageToDraw, mouseX, mouseY, (firstColor != 0) ? Color.Black : Color.RayWhite); + + if (prevColor != firstColor) + { + UpdateTextureRec( + currentWorld.Texture, + new Rectangle(MathF.Floor(offsetX), MathF.Floor(offsetY), sizeInWorldX, sizeInWorldY), + imageToDraw.Data + ); + } + } + else firstColor = -1; + } + + // Load selected preset + if (preset >= 0) + { + Image pattern; + if (preset < numberOfPresets - 1) // Preset with pattern image to load + { + pattern = preset switch + { + 0 => LoadImage("resources/game_of_life/glider.png"), + 1 => LoadImage("resources/game_of_life/r_pentomino.png"), + 2 => LoadImage("resources/game_of_life/acorn.png"), + 3 => LoadImage("resources/game_of_life/spaceships.png"), + 4 => LoadImage("resources/game_of_life/still_lifes.png"), + 5 => LoadImage("resources/game_of_life/oscillators.png"), + 6 => LoadImage("resources/game_of_life/puffer_train.png"), + 7 => LoadImage("resources/game_of_life/glider_gun.png"), + 8 => LoadImage("resources/game_of_life/breeder.png"), + _ => default, + }; + BeginTextureMode(currentWorld); + ClearBackground(Color.RayWhite); + EndTextureMode(); + + UpdateTextureRec( + currentWorld.Texture, + new Rectangle( + worldWidth * presetPatterns[preset].Position.X - pattern.Width / 2.0f, + worldHeight * presetPatterns[preset].Position.Y - pattern.Height / 2.0f, + pattern.Width, pattern.Height + ), + pattern.Data + ); + } + else // Last preset: Random values + { + pattern = GenImageColor(worldWidth / randomTiles, worldHeight / randomTiles, Color.RayWhite); + for (var i = 0; i < randomTiles; i++) + { + for (var j = 0; j < randomTiles; j++) + { + ImageClearBackground(ref pattern, Color.RayWhite); + for (var x = 0; x < pattern.Width; x++) + { + for (var y = 0; y < pattern.Height; y++) + { + if (GetRandomValue(0, 100) < 15) ImageDrawPixel(ref pattern, x, y, Color.Black); + } + } + UpdateTextureRec( + currentWorld.Texture, + new Rectangle(pattern.Width * i, pattern.Height * j, pattern.Width, pattern.Height), + pattern.Data + ); + } + } + } + + UnloadImage(pattern); + + mode = InteractionMode.Pause; + offsetX = worldWidth * presetPatterns[preset].Position.X - (float)windowWidth / zoom / 2.0f; + offsetY = worldHeight * presetPatterns[preset].Position.Y - (float)windowHeight / zoom / 2.0f; + } + + // Check window draw inside world limits + if (offsetX < 0) offsetX = 0; + if (offsetY < 0) offsetY = 0; + if (offsetX > worldWidth - (float)windowWidth / zoom) offsetX = worldWidth - (float)windowWidth / zoom; + if (offsetY > worldHeight - (float)windowHeight / zoom) offsetY = worldHeight - (float)windowHeight / zoom; + + // Rectangles for drawing texture portion to screen + var textureSourceToScreen = new Rectangle(offsetX, offsetY, (float)windowWidth / zoom, (float)windowHeight / zoom); + //---------------------------------------------------------------------------------- + + // Draw to texture + //---------------------------------------------------------------------------------- + if ((mode == InteractionMode.Run) && ((frame % framesPerStep) == 0)) + { + // Swap worlds + var tempWorld = currentWorld; + currentWorld = previousWorld; + previousWorld = tempWorld; + + // Draw to texture + BeginTextureMode(currentWorld); + BeginShaderMode(shdrGameOfLife); + DrawTexturePro(previousWorld.Texture, worldRectSource, worldRectDest, new Vector2(0, 0), 0.0f, Color.RayWhite); + EndShaderMode(); + EndTextureMode(); + } + //---------------------------------------------------------------------------------- + + // Draw to screen + //---------------------------------------------------------------------------------- + BeginDrawing(); + + DrawTexturePro(currentWorld.Texture, textureSourceToScreen, textureOnScreen, new Vector2(0, 0), 0.0f, Color.White); + + DrawLine(windowWidth, 0, windowWidth, screenHeight, new Color(218, 218, 218, 255)); + DrawRectangle(windowWidth, 0, screenWidth - windowWidth, screenHeight, new Color(232, 232, 232, 255)); + + DrawText("Conway's", 704, 4, 20, Color.DarkBlue); + DrawText(" game of", 704, 19, 20, Color.DarkBlue); + DrawText(" life", 708, 34, 20, Color.DarkBlue); + DrawText("in raylib", 757, 42, 6, Color.Black); + + DrawText("Presets", 710, 58, 8, Color.Gray); + preset = -1; + + // Draw GUI controls + //------------------------------------------------------------------------------ + // NOTE: raygui is not bound in raylib-cs; controls are kept for reference. The + // simulation still runs automatically and mouse wheel zoom / left-drag pan work. + /*for (int i = 0; i < numberOfPresets; i++) + if (GuiButton(new Rectangle( 710.0f, 70.0f + 18*i, 80.0f, 16.0f ), presetPatterns[i].Name)) preset = i; + + GuiToggleGroup(new Rectangle( 710, 258, 80, 16 ), "Run\nPause\nDraw", ref mode);*/ + + DrawText($"Zoom: {zoom}x", 710, 316, 8, Color.Gray); + /*buttonZoomIn = GuiButton(new Rectangle( 710, 328, 80, 16 ), "Zoom in"); + buttonZomOut = GuiButton(new Rectangle( 710, 346, 80, 16 ), "Zoom out");*/ + + DrawText($"Speed: {framesPerStep} frame{((framesPerStep > 1) ? "s" : "")}", 710, 370, 8, Color.Gray); + /*buttonFaster = GuiButton(new Rectangle( 710, 382, 80, 16 ), "Faster"); + buttonSlower = GuiButton(new Rectangle( 710, 400, 80, 16 ), "Slower");*/ + //------------------------------------------------------------------------------ + + DrawFPS(712, 426); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shdrGameOfLife); + UnloadRenderTexture(world1); + UnloadRenderTexture(world2); + + FreeImageToDraw(); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - game of life"); + + SetTargetFPS(60); // Set at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new GameOfLife(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shaders/LightmapRendering.cs b/Examples/Shaders/LightmapRendering.cs new file mode 100644 index 0000000..140f013 --- /dev/null +++ b/Examples/Shaders/LightmapRendering.cs @@ -0,0 +1,213 @@ +/******************************************************************************************* +* +* raylib [shaders] example - lightmap rendering +* +* Example complexity rating: [★★★☆] 3/4 +* +* NOTE: This example requires raylib OpenGL 3.3 or ES2 versions for shaders support, +* OpenGL 1.1 does not support shaders, recompile raylib to OpenGL 3.3 version +* +* NOTE: Shaders used in this example are #version 330 (OpenGL 3.3) +* +* Example originally created with raylib 4.5, last time updated with raylib 4.5 +* +* Example contributed by Jussi Viitala (@nullstare) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2019-2025 Jussi Viitala (@nullstare) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Rlgl; + +namespace Examples.Shaders; + +public partial class LightmapRendering : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MapSize = 16; + +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Lightmap Rendering"; + + public string Title => "raylib [shaders] example - lightmap rendering"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + private Camera3D camera; + private Mesh mesh; + private Shader shader; + private Texture2D texture; + private Texture2D light; + private RenderTexture2D lightmap; + private Material material; + + public unsafe void Init() + { + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(4.0f, 6.0f, 8.0f); // Camera position + camera.Target = new Vector3(0.0f, 0.0f, 0.0f); // Camera looking at point + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target) + camera.FovY = 45.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + + mesh = GenMeshPlane((float)MapSize, (float)MapSize, 1, 1); + + // GenMeshPlane doesn't generate texcoords2 so we will upload them separately + mesh.AllocTexCoords2(); + + // X // Y + mesh.TexCoords2[0] = 0.0f; mesh.TexCoords2[1] = 0.0f; + mesh.TexCoords2[2] = 1.0f; mesh.TexCoords2[3] = 0.0f; + mesh.TexCoords2[4] = 0.0f; mesh.TexCoords2[5] = 1.0f; + mesh.TexCoords2[6] = 1.0f; mesh.TexCoords2[7] = 1.0f; + + // Load a new texcoords2 attributes buffer + mesh.VboId[(int)ShaderLocationIndex.VertexTexcoord02] = + LoadVertexBuffer(mesh.TexCoords2, mesh.VertexCount * 2 * sizeof(float), false); + EnableVertexArray(mesh.VaoId); + + // Index 5 is for texcoords2 + SetVertexAttribute(5, 2, Rlgl.FLOAT, false, 0, 0); + EnableVertexAttribute(5); + DisableVertexArray(); + + // Load lightmap shader + shader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/lightmap.vs", + $"resources/shaders/glsl{GlslVersion}/lightmap.fs" + ); + + texture = LoadTexture("resources/cubicmap_atlas.png"); + light = LoadTexture("resources/spark_flame.png"); + + GenTextureMipmaps(ref texture); + SetTextureFilter(texture, TextureFilter.Trilinear); + + lightmap = LoadRenderTexture(MapSize, MapSize); + + material = LoadMaterialDefault(); + material.Shader = shader; + material.Maps[(int)MaterialMapIndex.Albedo].Texture = texture; + material.Maps[(int)MaterialMapIndex.Metalness].Texture = lightmap.Texture; + + // Drawing to lightmap + BeginTextureMode(lightmap); + ClearBackground(Color.Black); + + BeginBlendMode(BlendMode.Additive); + DrawTexturePro( + light, + new Rectangle(0, 0, (float)light.Width, (float)light.Height), + new Rectangle(0, 0, 2.0f * MapSize, 2.0f * MapSize), + new Vector2((float)MapSize, (float)MapSize), + 0.0f, + Color.Red + ); + DrawTexturePro( + light, + new Rectangle(0, 0, (float)light.Width, (float)light.Height), + new Rectangle((float)MapSize * 0.8f, (float)MapSize / 2.0f, 2.0f * MapSize, 2.0f * MapSize), + new Vector2((float)MapSize, (float)MapSize), + 0.0f, + Color.Blue + ); + DrawTexturePro( + light, + new Rectangle(0, 0, (float)light.Width, (float)light.Height), + new Rectangle((float)MapSize * 0.8f, (float)MapSize * 0.8f, (float)MapSize, (float)MapSize), + new Vector2((float)MapSize / 2.0f, (float)MapSize / 2.0f), + 0.0f, + Color.Green + ); + BeginBlendMode(BlendMode.Alpha); + EndTextureMode(); + + // NOTE: To enable trilinear filtering we need mipmaps available for texture + GenTextureMipmaps(ref lightmap.Texture); + SetTextureFilter(lightmap.Texture, TextureFilter.Trilinear); + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + DrawMesh(mesh, material, Matrix4x4.Identity); + EndMode3D(); + + DrawTexturePro( + lightmap.Texture, + new Rectangle(0, 0, -MapSize, -MapSize), + new Rectangle((float)GetRenderWidth() - MapSize * 8 - 10, 10, (float)MapSize * 8, (float)MapSize * 8), + new Vector2(0.0f, 0.0f), + 0.0f, + Color.White + ); + + DrawText($"LIGHTMAP: {MapSize}x{MapSize} pixels", GetRenderWidth() - 130, 20 + MapSize * 8, 10, Color.Green); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadMesh(mesh); // Unload the mesh + UnloadShader(shader); // Unload shader + UnloadTexture(texture); // Unload texture + UnloadTexture(light); // Unload texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); // Enable Multi Sampling Anti Aliasing 4x (if available) + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - lightmap rendering"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new LightmapRendering(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shaders/MandelbrotSet.cs b/Examples/Shaders/MandelbrotSet.cs new file mode 100644 index 0000000..66b73a8 --- /dev/null +++ b/Examples/Shaders/MandelbrotSet.cs @@ -0,0 +1,265 @@ +/******************************************************************************************* +* +* raylib [shaders] example - mandelbrot set +* +* Example complexity rating: [★★★☆] 3/4 +* +* NOTE: This example requires raylib OpenGL 3.3 or ES2 versions for shaders support, +* OpenGL 1.1 does not support shaders, recompile raylib to OpenGL 3.3 version +* +* NOTE: Shaders used in this example are #version 330 (OpenGL 3.3) +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Jordi Santonja (@JordSant) +* Based on previous work by Josh Colclough (@joshcol9232) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Jordi Santonja (@JordSant) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shaders; + +public partial class MandelbrotSet : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Mandelbrot Set"; + + public string Title => "raylib [shaders] example - mandelbrot set"; + + // A few good interesting places + private static readonly float[][] pointsOfInterest = new[] + { + new[] { -1.76826775f, -0.00422996283f, 28435.9238f }, + new[] { 0.322004497f, -0.0357099883f, 56499.7266f }, + new[] { -0.748880744f, -0.0562955774f, 9237.59082f }, + new[] { -1.78385007f, -0.0156200649f, 14599.5283f }, + new[] { -0.0985441282f, -0.924688697f, 26259.8535f }, + new[] { 0.317785531f, -0.0322612226f, 29297.9258f }, + }; + + private const float zoomSpeed = 1.01f; + private const float offsetSpeedMul = 2.0f; + + private const float startingZoom = 0.6f; + private static readonly float[] startingOffset = { -0.5f, 0.0f }; + + private Shader shader; + private RenderTexture2D target; + private float[] offset; + private float zoom; + private int maxIterations; + private float maxIterationsMultiplier; + private int zoomLoc; + private int offsetLoc; + private int maxIterationsLoc; + private bool showControls; + + public void Init() + { + // Load mandelbrot set shader + // NOTE: Defining null (NULL) for vertex shader forces usage of internal default vertex shader + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/mandelbrot_set.fs"); + + // Create a RenderTexture2D to be used for render to texture + target = LoadRenderTexture(GetScreenWidth(), GetScreenHeight()); + + // Offset and zoom to draw the mandelbrot set at. (centered on screen and default size) + offset = new[] { startingOffset[0], startingOffset[1] }; + zoom = startingZoom; + + // Depending on the zoom the maximum number of iterations must be adapted to get more detail as we zoom in + // The solution is not perfect, so a control has been added to increase/decrease the number of iterations with UP/DOWN keys +#if BROWSER + maxIterations = 43; + maxIterationsMultiplier = 22.0f; +#else + maxIterations = 333; + maxIterationsMultiplier = 166.5f; +#endif + + // Get variable (uniform) locations on the shader to connect with the program + // NOTE: If uniform variable could not be found in the shader, function returns -1 + zoomLoc = GetShaderLocation(shader, "zoom"); + offsetLoc = GetShaderLocation(shader, "offset"); + maxIterationsLoc = GetShaderLocation(shader, "maxIterations"); + + // Upload the shader uniform values! + Raylib.SetShaderValue(shader, zoomLoc, zoom, ShaderUniformDataType.Float); + Raylib.SetShaderValue(shader, offsetLoc, offset, ShaderUniformDataType.Vec2); + Raylib.SetShaderValue(shader, maxIterationsLoc, maxIterations, ShaderUniformDataType.Int); + + showControls = true; // Show controls + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + var updateShader = false; + + // Press [1 - 6] to reset c to a point of interest + if (IsKeyPressed(KeyboardKey.One) || + IsKeyPressed(KeyboardKey.Two) || + IsKeyPressed(KeyboardKey.Three) || + IsKeyPressed(KeyboardKey.Four) || + IsKeyPressed(KeyboardKey.Five) || + IsKeyPressed(KeyboardKey.Six)) + { + var interestIndex = 0; + if (IsKeyPressed(KeyboardKey.One)) interestIndex = 0; + else if (IsKeyPressed(KeyboardKey.Two)) interestIndex = 1; + else if (IsKeyPressed(KeyboardKey.Three)) interestIndex = 2; + else if (IsKeyPressed(KeyboardKey.Four)) interestIndex = 3; + else if (IsKeyPressed(KeyboardKey.Five)) interestIndex = 4; + else if (IsKeyPressed(KeyboardKey.Six)) interestIndex = 5; + + offset[0] = pointsOfInterest[interestIndex][0]; + offset[1] = pointsOfInterest[interestIndex][1]; + zoom = pointsOfInterest[interestIndex][2]; + updateShader = true; + } + + // If "R" is pressed, reset zoom and offset + if (IsKeyPressed(KeyboardKey.R)) + { + offset[0] = startingOffset[0]; + offset[1] = startingOffset[1]; + zoom = startingZoom; + updateShader = true; + } + + if (IsKeyPressed(KeyboardKey.F1)) showControls = !showControls; // Toggle whether or not to show controls + + // Change number of max iterations with UP and DOWN keys + // WARNING: Increasing the number of max iterations greatly impacts performance + if (IsKeyPressed(KeyboardKey.Up)) + { + maxIterationsMultiplier *= 1.4f; + updateShader = true; + } + else if (IsKeyPressed(KeyboardKey.Down)) + { + maxIterationsMultiplier /= 1.4f; + updateShader = true; + } + + // If either left or right button is pressed, zoom in/out + if (IsMouseButtonDown(MouseButton.Left) || IsMouseButtonDown(MouseButton.Right)) + { + // Change zoom. If Mouse left -> zoom in. Mouse right -> zoom out + zoom *= IsMouseButtonDown(MouseButton.Left) ? zoomSpeed : (1.0f / zoomSpeed); + + var mousePos = GetMousePosition(); + Vector2 offsetVelocity; + // Find the velocity at which to change the camera. Take the distance of the mouse + // From the center of the screen as the direction, and adjust magnitude based on the current zoom + offsetVelocity.X = (mousePos.X / (float)screenWidth - 0.5f) * offsetSpeedMul / zoom; + offsetVelocity.Y = (mousePos.Y / (float)screenHeight - 0.5f) * offsetSpeedMul / zoom; + + // Apply move velocity to camera + offset[0] += GetFrameTime() * offsetVelocity.X; + offset[1] += GetFrameTime() * offsetVelocity.Y; + + updateShader = true; + } + + // In case a parameter has been changed, update the shader values + if (updateShader) + { + // As we zoom in, increase the number of max iterations to get more detail + // Aproximate formula, but it works-ish + maxIterations = (int)(MathF.Sqrt(2.0f * MathF.Sqrt(MathF.Abs(1.0f - MathF.Sqrt(37.5f * zoom)))) * maxIterationsMultiplier); + + // Update the shader uniform values! + Raylib.SetShaderValue(shader, zoomLoc, zoom, ShaderUniformDataType.Float); + Raylib.SetShaderValue(shader, offsetLoc, offset, ShaderUniformDataType.Vec2); + Raylib.SetShaderValue(shader, maxIterationsLoc, maxIterations, ShaderUniformDataType.Int); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + // Using a render texture to draw Mandelbrot set + BeginTextureMode(target); // Enable drawing to texture + ClearBackground(Color.Black); // Clear the render texture + + // Draw a rectangle in shader mode to be used as shader canvas + // NOTE: Rectangle uses font white character texture coordinates, + // So shader can not be applied here directly because input vertexTexCoord + // Do not represent full screen coordinates (space where want to apply shader) + DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color.Black); + EndTextureMode(); + + BeginDrawing(); + ClearBackground(Color.Black); // Clear screen background + + // Draw the saved texture and rendered mandelbrot set with shader + // NOTE: We do not invert texture on Y, already considered inside shader + BeginShaderMode(shader); + // WARNING: If FLAG_WINDOW_HIGHDPI is enabled, HighDPI monitor scaling should be considered + // When rendering the RenderTexture2D to fit in the HighDPI scaled Window + DrawTextureEx(target.Texture, new Vector2(0.0f, 0.0f), 0.0f, 1.0f, Color.White); + EndShaderMode(); + + if (showControls) + { + DrawText("Press Mouse buttons right/left to zoom in/out and move", 10, 15, 10, Color.RayWhite); + DrawText("Press F1 to toggle these controls", 10, 30, 10, Color.RayWhite); + DrawText("Press [1 - 6] to change point of interest", 10, 45, 10, Color.RayWhite); + DrawText("Press UP | DOWN to change number of iterations", 10, 60, 10, Color.RayWhite); + DrawText("Press R to recenter the camera", 10, 75, 10, Color.RayWhite); + } + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); // Unload shader + UnloadRenderTexture(target); // Unload render texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - mandelbrot set"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new MandelbrotSet(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shaders/NormalmapRendering.cs b/Examples/Shaders/NormalmapRendering.cs new file mode 100644 index 0000000..0db18d9 --- /dev/null +++ b/Examples/Shaders/NormalmapRendering.cs @@ -0,0 +1,233 @@ +/******************************************************************************************* +* +* raylib [shaders] example - normalmap rendering +* +* Example complexity rating: [★★★★] 4/4 +* +* NOTE: This example requires raylib OpenGL 3.3 or ES2 versions for shaders support, +* OpenGL 1.1 does not support shaders, recompile raylib to OpenGL 3.3 version +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Jeremy Montgomery (@Sir_Irk) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Jeremy Montgomery (@Sir_Irk) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; + +namespace Examples.Shaders; + +public partial class NormalmapRendering : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Normalmap Rendering"; + + public string Title => "raylib [shaders] example - normalmap rendering"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + private Camera3D camera; + private Shader shader; + private Model plane; + private Vector3 lightPosition; + private int lightPosLoc; + private float specularExponent; + private int specularExponentLoc; + private int useNormalMap; + private int useNormalMapLoc; + + public unsafe void Init() + { + camera = new(); + camera.Position = new Vector3(0.0f, 2.0f, -4.0f); + camera.Target = new Vector3(0.0f, 0.0f, 0.0f); + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); + camera.FovY = 45.0f; + camera.Projection = CameraProjection.Perspective; + + // Load basic normal map lighting shader + shader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/normalmap.vs", + $"resources/shaders/glsl{GlslVersion}/normalmap.fs" + ); + + // Get some required shader locations + shader.Locs[(int)ShaderLocationIndex.MapNormal] = GetShaderLocation(shader, "normalMap"); + shader.Locs[(int)ShaderLocationIndex.VectorView] = GetShaderLocation(shader, "viewPos"); + + // NOTE: "matModel" location name is automatically assigned on shader loading, + // no need to get the location again if using that uniform name + // shader.Locs[(int)ShaderLocationIndex.MatrixModel] = GetShaderLocation(shader, "matModel"); + + // This example uses just 1 point light + lightPosition = new Vector3(0.0f, 1.0f, 0.0f); + lightPosLoc = GetShaderLocation(shader, "lightPos"); + + // Load a plane model that has proper normals and tangents + plane = LoadModel("resources/models/plane.glb"); + + // Set the plane model's shader and texture maps + plane.Materials[0].Shader = shader; + plane.Materials[0].Maps[(int)MaterialMapIndex.Diffuse].Texture = LoadTexture("resources/tiles_diffuse.png"); + plane.Materials[0].Maps[(int)MaterialMapIndex.Normal].Texture = LoadTexture("resources/tiles_normal.png"); + + // Generate Mipmaps and use TRILINEAR filtering to help with texture aliasing + GenTextureMipmaps(ref plane.Materials[0].Maps[(int)MaterialMapIndex.Diffuse].Texture); + GenTextureMipmaps(ref plane.Materials[0].Maps[(int)MaterialMapIndex.Normal].Texture); + + SetTextureFilter(plane.Materials[0].Maps[(int)MaterialMapIndex.Diffuse].Texture, TextureFilter.Trilinear); + SetTextureFilter(plane.Materials[0].Maps[(int)MaterialMapIndex.Normal].Texture, TextureFilter.Trilinear); + + // Specular exponent AKA shininess of the material + specularExponent = 8.0f; + specularExponentLoc = GetShaderLocation(shader, "specularExponent"); + + // Allow toggling the normal map on and off for comparison purposes + useNormalMap = 1; + useNormalMapLoc = GetShaderLocation(shader, "useNormalMap"); + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Move the light around on the X and Z axis using WASD keys + Vector3 direction = new(0.0f, 0.0f, 0.0f); + if (IsKeyDown(KeyboardKey.W)) + { + direction = Vector3Add(direction, new Vector3(0.0f, 0.0f, 1.0f)); + } + if (IsKeyDown(KeyboardKey.S)) + { + direction = Vector3Add(direction, new Vector3(0.0f, 0.0f, -1.0f)); + } + if (IsKeyDown(KeyboardKey.D)) + { + direction = Vector3Add(direction, new Vector3(-1.0f, 0.0f, 0.0f)); + } + if (IsKeyDown(KeyboardKey.A)) + { + direction = Vector3Add(direction, new Vector3(1.0f, 0.0f, 0.0f)); + } + + direction = Vector3Normalize(direction); + lightPosition = Vector3Add(lightPosition, Vector3Scale(direction, GetFrameTime() * 3.0f)); + + // Increase/Decrease the specular exponent(shininess) + if (IsKeyDown(KeyboardKey.Up)) + { + specularExponent = Clamp(specularExponent + 40.0f * GetFrameTime(), 2.0f, 128.0f); + } + if (IsKeyDown(KeyboardKey.Down)) + { + specularExponent = Clamp(specularExponent - 40.0f * GetFrameTime(), 2.0f, 128.0f); + } + + // Toggle normal map on and off + if (IsKeyPressed(KeyboardKey.N)) + { + useNormalMap = (useNormalMap != 0) ? 0 : 1; + } + + // Spin plane model at a constant rate + plane.Transform = MatrixRotateY((float)GetTime() * 0.5f); + + // Update shader values + Raylib.SetShaderValue(shader, lightPosLoc, lightPosition, ShaderUniformDataType.Vec3); + + Raylib.SetShaderValue( + shader, + shader.Locs[(int)ShaderLocationIndex.VectorView], + camera.Position, + ShaderUniformDataType.Vec3 + ); + + Raylib.SetShaderValue(shader, specularExponentLoc, specularExponent, ShaderUniformDataType.Float); + + Raylib.SetShaderValue(shader, useNormalMapLoc, useNormalMap, ShaderUniformDataType.Int); + //-------------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + BeginShaderMode(shader); + + DrawModel(plane, Vector3.Zero, 2.0f, Color.White); + + EndShaderMode(); + + // Draw sphere to show light position + DrawSphereWires(lightPosition, 0.2f, 8, 8, Color.Orange); + + EndMode3D(); + + Color textColor = (useNormalMap != 0) ? Color.DarkGreen : Color.Red; + string toggleStr = (useNormalMap != 0) ? "On" : "Off"; + DrawText($"Use key [N] to toggle normal map: {toggleStr}", 10, 10, 10, textColor); + + int yOffset = 24; + DrawText("Use keys [W][A][S][D] to move the light", 10, 10 + yOffset * 1, 10, Color.Black); + DrawText("Use keys [Up][Down] to change specular exponent", 10, 10 + yOffset * 2, 10, Color.Black); + DrawText($"Specular Exponent: {specularExponent:F2}", 10, 10 + yOffset * 3, 10, Color.Blue); + + DrawFPS(screenWidth - 90, 10); + + EndDrawing(); + //-------------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); + UnloadModel(plane); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - normalmap rendering"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new NormalmapRendering(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shaders/RlglCompute.cs b/Examples/Shaders/RlglCompute.cs new file mode 100644 index 0000000..1f67537 --- /dev/null +++ b/Examples/Shaders/RlglCompute.cs @@ -0,0 +1,242 @@ +/******************************************************************************************* +* +* raylib [shaders] example - rlgl compute +* +* WARNING: This example requires raylib compiled with OpenGL 4.3 version for +* compute shaders support, shaders used in this example are #version 430 +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 4.0, last time updated with raylib 4.0 +* +* Example contributed by Teddy Astie (@tsnake41) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2021-2025 Teddy Astie (@tsnake41) +* +********************************************************************************************/ + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using static Raylib_cs.Raylib; + +namespace Examples.Shaders; + +[ExcludeFromBrowser("compute shaders are not available on WebGL")] +public partial class RlglCompute : IExample +{ + // IMPORTANT: This must match gol*.glsl GOL_WIDTH constant + // This must be a multiple of 16 (check golLogic compute dispatch) + private const int GolWidth = 768; + + // Maximum amount of queued draw commands (squares draw from mouse down events) + private const int MaxBufferedTransferts = 48; + + private const int screenWidth = GolWidth; + private const int screenHeight = GolWidth; + + public string Name => "Shaders / Rlgl Compute"; + + public string Title => "raylib [shaders] example - rlgl compute"; + + //---------------------------------------------------------------------------------- + // Types and Structures Definition + //---------------------------------------------------------------------------------- + // Game Of Life Update Command + [StructLayout(LayoutKind.Sequential)] + private struct GolUpdateCmd + { + public uint X; // x coordinate of the gol command + public uint Y; // y coordinate of the gol command + public uint W; // width of the filled zone + public uint Enabled; // whether to enable or disable zone + } + + // Inline fixed-size array of GolUpdateCmd (MAX_BUFFERED_TRANSFERTS entries) + [InlineArray(MaxBufferedTransferts)] + private struct GolUpdateCmdBuffer + { + private GolUpdateCmd _element0; + } + + // Game Of Life Update Commands SSBO + [StructLayout(LayoutKind.Sequential)] + private struct GolUpdateSSBO + { + public uint Count; + public GolUpdateCmdBuffer Commands; + } + + private Vector2 resolution; + private uint brushSize; + private uint golLogicShader; + private uint golLogicProgram; + private uint golTransfertShader; + private uint golTransfertProgram; + private Shader golRenderShader; + private int resUniformLoc; + private uint ssboA; + private uint ssboB; + private uint ssboTransfert; + private GolUpdateSSBO transfertBuffer; + private Texture2D whiteTex; + + public unsafe void Init() + { + resolution = new Vector2(screenWidth, screenHeight); + brushSize = 8; + + // Game of Life logic compute shader + var golLogicCode = LoadFileText("resources/shaders/glsl430/gol.glsl"); + var golLogicBytes = Encoding.UTF8.GetBytes(golLogicCode + "\0"); + fixed (byte* p = golLogicBytes) + { + golLogicShader = Rlgl.LoadShader((sbyte*)p, (int)ShaderType.Compute); + } + golLogicProgram = Rlgl.LoadShaderProgramCompute(golLogicShader); + + // Game of Life logic render shader + golRenderShader = LoadShader(null, "resources/shaders/glsl430/gol_render.glsl"); + resUniformLoc = GetShaderLocation(golRenderShader, "resolution"); + + // Game of Life transfert shader (CPU<->GPU download and upload) + var golTransfertCode = LoadFileText("resources/shaders/glsl430/gol_transfert.glsl"); + var golTransfertBytes = Encoding.UTF8.GetBytes(golTransfertCode + "\0"); + fixed (byte* p = golTransfertBytes) + { + golTransfertShader = Rlgl.LoadShader((sbyte*)p, (int)ShaderType.Compute); + } + golTransfertProgram = Rlgl.LoadShaderProgramCompute(golTransfertShader); + + // Load shader storage buffer object (SSBO), id returned + ssboA = Rlgl.LoadShaderBuffer((uint)(GolWidth * GolWidth * sizeof(uint)), null, Rlgl.DYNAMIC_COPY); + ssboB = Rlgl.LoadShaderBuffer((uint)(GolWidth * GolWidth * sizeof(uint)), null, Rlgl.DYNAMIC_COPY); + ssboTransfert = Rlgl.LoadShaderBuffer((uint)sizeof(GolUpdateSSBO), null, Rlgl.DYNAMIC_COPY); + + transfertBuffer = new(); + + // Create a white texture of the size of the window to update + // each pixel of the window using the fragment shader: golRenderShader + var whiteImage = GenImageColor(GolWidth, GolWidth, Color.White); + whiteTex = LoadTextureFromImage(whiteImage); + UnloadImage(whiteImage); + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + brushSize += (uint)(int)GetMouseWheelMove(); + + if ((IsMouseButtonDown(MouseButton.Left) || IsMouseButtonDown(MouseButton.Right)) + && (transfertBuffer.Count < MaxBufferedTransferts)) + { + // Buffer a new command + transfertBuffer.Commands[(int)transfertBuffer.Count].X = (uint)GetMouseX() - brushSize / 2; + transfertBuffer.Commands[(int)transfertBuffer.Count].Y = (uint)GetMouseY() - brushSize / 2; + transfertBuffer.Commands[(int)transfertBuffer.Count].W = brushSize; + transfertBuffer.Commands[(int)transfertBuffer.Count].Enabled = IsMouseButtonDown(MouseButton.Left) ? 1u : 0u; + transfertBuffer.Count++; + } + else if (transfertBuffer.Count > 0) // Process transfert buffer + { + // Send SSBO buffer to GPU + fixed (GolUpdateSSBO* ptr = &transfertBuffer) + { + Rlgl.UpdateShaderBuffer(ssboTransfert, ptr, (uint)sizeof(GolUpdateSSBO), 0); + } + + // Process SSBO commands on GPU + Rlgl.EnableShader(golTransfertProgram); + Rlgl.BindShaderBuffer(ssboA, 1); + Rlgl.BindShaderBuffer(ssboTransfert, 3); + Rlgl.ComputeShaderDispatch(transfertBuffer.Count, 1, 1); // Each GPU unit will process a command! + Rlgl.DisableShader(); + + transfertBuffer.Count = 0; + } + else + { + // Process game of life logic + Rlgl.EnableShader(golLogicProgram); + Rlgl.BindShaderBuffer(ssboA, 1); + Rlgl.BindShaderBuffer(ssboB, 2); + Rlgl.ComputeShaderDispatch(GolWidth / 16, GolWidth / 16, 1); + Rlgl.DisableShader(); + + // ssboA <-> ssboB + var temp = ssboA; + ssboA = ssboB; + ssboB = temp; + } + + Rlgl.BindShaderBuffer(ssboA, 1); + Raylib.SetShaderValue(golRenderShader, resUniformLoc, resolution, ShaderUniformDataType.Vec2); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.Blank); + + BeginShaderMode(golRenderShader); + DrawTexture(whiteTex, 0, 0, Color.White); + EndShaderMode(); + + DrawRectangleLines(GetMouseX() - (int)(brushSize / 2), GetMouseY() - (int)(brushSize / 2), (int)brushSize, (int)brushSize, Color.Red); + + DrawText("Use Mouse wheel to increase/decrease brush size", 10, 10, 20, Color.White); + DrawFPS(GetScreenWidth() - 100, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + // Unload shader buffers objects + Rlgl.UnloadShaderBuffer(ssboA); + Rlgl.UnloadShaderBuffer(ssboB); + Rlgl.UnloadShaderBuffer(ssboTransfert); + + // Unload compute shader + Rlgl.UnloadShader(golLogicShader); + Rlgl.UnloadShader(golTransfertShader); + Rlgl.UnloadShaderProgram(golTransfertProgram); + Rlgl.UnloadShaderProgram(golLogicProgram); + + UnloadTexture(whiteTex); // Unload white texture + UnloadShader(golRenderShader); // Unload rendering fragment shader + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - rlgl compute"); + //-------------------------------------------------------------------------------------- + + var game = new RlglCompute(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shaders/RoundedRectangle.cs b/Examples/Shaders/RoundedRectangle.cs new file mode 100644 index 0000000..8888b59 --- /dev/null +++ b/Examples/Shaders/RoundedRectangle.cs @@ -0,0 +1,243 @@ +/******************************************************************************************* +* +* raylib [shaders] example - rounded rectangle +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.5 +* +* Example contributed by Anstro Pleuton (@anstropleuton) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Anstro Pleuton (@anstropleuton) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shaders; + +public class RoundedRectangle : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Rounded Rectangle"; + + public string Title => "raylib [shaders] example - rounded rectangle"; + + // Rounded rectangle data + private struct RoundedRect + { + public Vector4 CornerRadius; // Individual corner radius (top-left, top-right, bottom-left, bottom-right) + + // Shadow variables + public float ShadowRadius; + public Vector2 ShadowOffset; + public float ShadowScale; + + // Border variables + public float BorderThickness; // Inner-border thickness + + // Shader locations + public int RectangleLoc; + public int RadiusLoc; + public int ColorLoc; + public int ShadowRadiusLoc; + public int ShadowOffsetLoc; + public int ShadowScaleLoc; + public int ShadowColorLoc; + public int BorderThicknessLoc; + public int BorderColorLoc; + } + + private Shader shader; + private RoundedRect roundedRectangle; + + private readonly Color rectangleColor = Color.Blue; + private readonly Color shadowColor = Color.DarkBlue; + private readonly Color borderColor = Color.SkyBlue; + + public void Init() + { + // Load the shader + shader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/base.vs", + $"resources/shaders/glsl{GlslVersion}/rounded_rectangle.fs" + ); + + // Create a rounded rectangle + roundedRectangle = CreateRoundedRectangle( + new Vector4(5.0f, 10.0f, 15.0f, 20.0f), // Corner radius + 20.0f, // Shadow radius + new Vector2(0.0f, -5.0f), // Shadow offset + 0.95f, // Shadow scale + 5.0f, // Border thickness + shader // Shader + ); + + // Update shader uniforms + UpdateRoundedRectangle(roundedRectangle, shader); + } + + public void Update() + { + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + // Draw rectangle box with rounded corners using shader + Rectangle rec = new(50, 70, 110, 60); + DrawRectangleLines((int)rec.X - 20, (int)rec.Y - 20, (int)rec.Width + 40, (int)rec.Height + 40, Color.DarkGray); + DrawText("Rounded rectangle", (int)rec.X - 20, (int)rec.Y - 35, 10, Color.DarkGray); + + // Flip Y axis to match shader coordinate system + rec.Y = screenHeight - rec.Y - rec.Height; + Raylib.SetShaderValue(shader, roundedRectangle.RectangleLoc, new[] { rec.X, rec.Y, rec.Width, rec.Height }, ShaderUniformDataType.Vec4); + + // Only rectangle color + Raylib.SetShaderValue(shader, roundedRectangle.ColorLoc, new[] { rectangleColor.R / 255.0f, rectangleColor.G / 255.0f, rectangleColor.B / 255.0f, rectangleColor.A / 255.0f }, ShaderUniformDataType.Vec4); + Raylib.SetShaderValue(shader, roundedRectangle.ShadowColorLoc, new[] { 0.0f, 0.0f, 0.0f, 0.0f }, ShaderUniformDataType.Vec4); + Raylib.SetShaderValue(shader, roundedRectangle.BorderColorLoc, new[] { 0.0f, 0.0f, 0.0f, 0.0f }, ShaderUniformDataType.Vec4); + + BeginShaderMode(shader); + DrawRectangle(0, 0, screenWidth, screenHeight, Color.White); + EndShaderMode(); + + // Draw rectangle shadow using shader + rec = new Rectangle(50, 200, 110, 60); + DrawRectangleLines((int)rec.X - 20, (int)rec.Y - 20, (int)rec.Width + 40, (int)rec.Height + 40, Color.DarkGray); + DrawText("Rounded rectangle shadow", (int)rec.X - 20, (int)rec.Y - 35, 10, Color.DarkGray); + + rec.Y = screenHeight - rec.Y - rec.Height; + Raylib.SetShaderValue(shader, roundedRectangle.RectangleLoc, new[] { rec.X, rec.Y, rec.Width, rec.Height }, ShaderUniformDataType.Vec4); + + // Only shadow color + Raylib.SetShaderValue(shader, roundedRectangle.ColorLoc, new[] { 0.0f, 0.0f, 0.0f, 0.0f }, ShaderUniformDataType.Vec4); + Raylib.SetShaderValue(shader, roundedRectangle.ShadowColorLoc, new[] { shadowColor.R / 255.0f, shadowColor.G / 255.0f, shadowColor.B / 255.0f, shadowColor.A / 255.0f }, ShaderUniformDataType.Vec4); + Raylib.SetShaderValue(shader, roundedRectangle.BorderColorLoc, new[] { 0.0f, 0.0f, 0.0f, 0.0f }, ShaderUniformDataType.Vec4); + + BeginShaderMode(shader); + DrawRectangle(0, 0, screenWidth, screenHeight, Color.White); + EndShaderMode(); + + // Draw rectangle's border using shader + rec = new Rectangle(50, 330, 110, 60); + DrawRectangleLines((int)rec.X - 20, (int)rec.Y - 20, (int)rec.Width + 40, (int)rec.Height + 40, Color.DarkGray); + DrawText("Rounded rectangle border", (int)rec.X - 20, (int)rec.Y - 35, 10, Color.DarkGray); + + rec.Y = screenHeight - rec.Y - rec.Height; + Raylib.SetShaderValue(shader, roundedRectangle.RectangleLoc, new[] { rec.X, rec.Y, rec.Width, rec.Height }, ShaderUniformDataType.Vec4); + + // Only border color + Raylib.SetShaderValue(shader, roundedRectangle.ColorLoc, new[] { 0.0f, 0.0f, 0.0f, 0.0f }, ShaderUniformDataType.Vec4); + Raylib.SetShaderValue(shader, roundedRectangle.ShadowColorLoc, new[] { 0.0f, 0.0f, 0.0f, 0.0f }, ShaderUniformDataType.Vec4); + Raylib.SetShaderValue(shader, roundedRectangle.BorderColorLoc, new[] { borderColor.R / 255.0f, borderColor.G / 255.0f, borderColor.B / 255.0f, borderColor.A / 255.0f }, ShaderUniformDataType.Vec4); + + BeginShaderMode(shader); + DrawRectangle(0, 0, screenWidth, screenHeight, Color.White); + EndShaderMode(); + + // Draw one more rectangle with all three colors + rec = new Rectangle(240, 80, 500, 300); + DrawRectangleLines((int)rec.X - 30, (int)rec.Y - 30, (int)rec.Width + 60, (int)rec.Height + 60, Color.DarkGray); + DrawText("Rectangle with all three combined", (int)rec.X - 30, (int)rec.Y - 45, 10, Color.DarkGray); + + rec.Y = screenHeight - rec.Y - rec.Height; + Raylib.SetShaderValue(shader, roundedRectangle.RectangleLoc, new[] { rec.X, rec.Y, rec.Width, rec.Height }, ShaderUniformDataType.Vec4); + + // All three colors + Raylib.SetShaderValue(shader, roundedRectangle.ColorLoc, new[] { rectangleColor.R / 255.0f, rectangleColor.G / 255.0f, rectangleColor.B / 255.0f, rectangleColor.A / 255.0f }, ShaderUniformDataType.Vec4); + Raylib.SetShaderValue(shader, roundedRectangle.ShadowColorLoc, new[] { shadowColor.R / 255.0f, shadowColor.G / 255.0f, shadowColor.B / 255.0f, shadowColor.A / 255.0f }, ShaderUniformDataType.Vec4); + Raylib.SetShaderValue(shader, roundedRectangle.BorderColorLoc, new[] { borderColor.R / 255.0f, borderColor.G / 255.0f, borderColor.B / 255.0f, borderColor.A / 255.0f }, ShaderUniformDataType.Vec4); + + BeginShaderMode(shader); + DrawRectangle(0, 0, screenWidth, screenHeight, Color.White); + EndShaderMode(); + + DrawText("(c) Rounded rectangle SDF by Iñigo Quilez. MIT License.", screenWidth - 300, screenHeight - 20, 10, Color.Black); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); // Unload shader + } + + // Create a rounded rectangle and set uniform locations + private static RoundedRect CreateRoundedRectangle(Vector4 cornerRadius, float shadowRadius, Vector2 shadowOffset, float shadowScale, float borderThickness, Shader shader) + { + RoundedRect rec; + rec.CornerRadius = cornerRadius; + rec.ShadowRadius = shadowRadius; + rec.ShadowOffset = shadowOffset; + rec.ShadowScale = shadowScale; + rec.BorderThickness = borderThickness; + + // Get shader uniform locations + rec.RectangleLoc = GetShaderLocation(shader, "rectangle"); + rec.RadiusLoc = GetShaderLocation(shader, "radius"); + rec.ColorLoc = GetShaderLocation(shader, "color"); + rec.ShadowRadiusLoc = GetShaderLocation(shader, "shadowRadius"); + rec.ShadowOffsetLoc = GetShaderLocation(shader, "shadowOffset"); + rec.ShadowScaleLoc = GetShaderLocation(shader, "shadowScale"); + rec.ShadowColorLoc = GetShaderLocation(shader, "shadowColor"); + rec.BorderThicknessLoc = GetShaderLocation(shader, "borderThickness"); + rec.BorderColorLoc = GetShaderLocation(shader, "borderColor"); + + UpdateRoundedRectangle(rec, shader); + + return rec; + } + + // Update rounded rectangle uniforms + private static void UpdateRoundedRectangle(RoundedRect rec, Shader shader) + { + Raylib.SetShaderValue(shader, rec.RadiusLoc, new[] { rec.CornerRadius.X, rec.CornerRadius.Y, rec.CornerRadius.Z, rec.CornerRadius.W }, ShaderUniformDataType.Vec4); + Raylib.SetShaderValue(shader, rec.ShadowRadiusLoc, rec.ShadowRadius, ShaderUniformDataType.Float); + Raylib.SetShaderValue(shader, rec.ShadowOffsetLoc, new[] { rec.ShadowOffset.X, rec.ShadowOffset.Y }, ShaderUniformDataType.Vec2); + Raylib.SetShaderValue(shader, rec.ShadowScaleLoc, rec.ShadowScale, ShaderUniformDataType.Float); + Raylib.SetShaderValue(shader, rec.BorderThicknessLoc, rec.BorderThickness, ShaderUniformDataType.Float); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - rounded rectangle"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new RoundedRectangle(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shaders/ShadowmapRendering.cs b/Examples/Shaders/ShadowmapRendering.cs new file mode 100644 index 0000000..266c0c6 --- /dev/null +++ b/Examples/Shaders/ShadowmapRendering.cs @@ -0,0 +1,316 @@ +/******************************************************************************************* +* +* raylib [shaders] example - shadowmap rendering +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 5.0, last time updated with raylib 5.0 +* +* Example contributed by TheManTheMythTheGameDev (@TheManTheMythTheGameDev) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2023-2025 TheManTheMythTheGameDev (@TheManTheMythTheGameDev) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; +using static Raylib_cs.Rlgl; + +namespace Examples.Shaders; + +public partial class ShadowmapRendering : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + private const int ShadowmapResolution = 1024; + + public string Name => "Shaders / Shadowmap Rendering"; + + public string Title => "raylib [shaders] example - shadowmap rendering"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + private Camera3D camera; + private Shader shadowShader; + private Vector3 lightDir; + private int lightDirLoc; + private int lightVPLoc; + private int shadowMapLoc; + private Model cube; + private Model robot; + private unsafe ModelAnimation* anims; + private int animCount; + private RenderTexture2D shadowMap; + private Camera3D lightCamera; + private int frameCounter; + private int textureActiveSlot; + + public unsafe void Init() + { + // Shadows are a HUGE topic, and this example shows an extremely simple implementation of the shadowmapping algorithm, + // which is the industry standard for shadows. This algorithm can be extended in a ridiculous number of ways to improve + // realism and also adapt it for different scenes. This is pretty much the simplest possible implementation + + camera = new(); + camera.Position = new Vector3(10.0f, 10.0f, 10.0f); + camera.Target = Vector3.Zero; + camera.Projection = CameraProjection.Perspective; + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); + camera.FovY = 45.0f; + + shadowShader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/shadowmap.vs", + $"resources/shaders/glsl{GlslVersion}/shadowmap.fs" + ); + shadowShader.Locs[(int)ShaderLocationIndex.VectorView] = GetShaderLocation(shadowShader, "viewPos"); + + lightDir = Vector3Normalize(new Vector3(0.35f, -1.0f, -0.35f)); + var lightColor = Color.White; + var lightColorNormalized = ColorNormalize(lightColor); + lightDirLoc = GetShaderLocation(shadowShader, "lightDir"); + var lightColLoc = GetShaderLocation(shadowShader, "lightColor"); + Raylib.SetShaderValue(shadowShader, lightDirLoc, lightDir, ShaderUniformDataType.Vec3); + Raylib.SetShaderValue(shadowShader, lightColLoc, lightColorNormalized, ShaderUniformDataType.Vec4); + var ambientLoc = GetShaderLocation(shadowShader, "ambient"); + var ambient = new[] { 0.1f, 0.1f, 0.1f, 1.0f }; + Raylib.SetShaderValue(shadowShader, ambientLoc, ambient, ShaderUniformDataType.Vec4); + lightVPLoc = GetShaderLocation(shadowShader, "lightVP"); + shadowMapLoc = GetShaderLocation(shadowShader, "shadowMap"); + var shadowMapResolution = ShadowmapResolution; + Raylib.SetShaderValue(shadowShader, GetShaderLocation(shadowShader, "shadowMapResolution"), shadowMapResolution, ShaderUniformDataType.Int); + + cube = LoadModelFromMesh(GenMeshCube(1.0f, 1.0f, 1.0f)); + cube.Materials[0].Shader = shadowShader; + robot = LoadModel("resources/models/robot.glb"); + for (var i = 0; i < robot.MaterialCount; i++) + { + robot.Materials[i].Shader = shadowShader; + } + + animCount = 0; + anims = LoadModelAnimations("resources/models/robot.glb", ref animCount); + + shadowMap = LoadShadowmapRenderTexture(ShadowmapResolution, ShadowmapResolution); + + // For the shadowmapping algorithm, we will be rendering everything from the light's point of view + lightCamera = new(); + lightCamera.Position = Vector3Scale(lightDir, -15.0f); + lightCamera.Target = Vector3.Zero; + lightCamera.Projection = CameraProjection.Orthographic; // Use an orthographic projection for directional lights + lightCamera.Up = new Vector3(0.0f, 1.0f, 0.0f); + lightCamera.FovY = 20.0f; + + frameCounter = 0; + textureActiveSlot = 10; // Can be anything 0 to 15, but 0 will probably be taken up + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + var deltaTime = GetFrameTime(); + + var cameraPos = camera.Position; + Raylib.SetShaderValue(shadowShader, shadowShader.Locs[(int)ShaderLocationIndex.VectorView], cameraPos, ShaderUniformDataType.Vec3); + UpdateCamera(ref camera, CameraMode.Orbital); + + frameCounter++; + frameCounter %= anims[0].KeyFrameCount; + UpdateModelAnimation(robot, anims[0], (float)frameCounter); + + // Move light with arrow keys + const float cameraSpeed = 0.05f; + if (IsKeyDown(KeyboardKey.Left)) + { + if (lightDir.X < 0.6f) + { + lightDir.X += cameraSpeed * 60.0f * deltaTime; + } + } + if (IsKeyDown(KeyboardKey.Right)) + { + if (lightDir.X > -0.6f) + { + lightDir.X -= cameraSpeed * 60.0f * deltaTime; + } + } + if (IsKeyDown(KeyboardKey.Up)) + { + if (lightDir.Z < 0.6f) + { + lightDir.Z += cameraSpeed * 60.0f * deltaTime; + } + } + if (IsKeyDown(KeyboardKey.Down)) + { + if (lightDir.Z > -0.6f) + { + lightDir.Z -= cameraSpeed * 60.0f * deltaTime; + } + } + + lightDir = Vector3Normalize(lightDir); + lightCamera.Position = Vector3Scale(lightDir, -15.0f); + Raylib.SetShaderValue(shadowShader, lightDirLoc, lightDir, ShaderUniformDataType.Vec3); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + // PASS 01: Render all objects into the shadowmap render texture + // We record all the objects' depths (as rendered from the light source's point of view) in a buffer + // Anything that is "visible" to the light is in light, anything that isn't is in shadow + // We can later use the depth buffer when rendering everything from the player's point of view + // to determine whether a given point is "visible" to the light + Matrix4x4 lightView; + Matrix4x4 lightProj; + BeginTextureMode(shadowMap); + ClearBackground(Color.White); + + BeginMode3D(lightCamera); + lightView = GetMatrixModelview(); + lightProj = GetMatrixProjection(); + DrawScene(cube, robot); + EndMode3D(); + + EndTextureMode(); + var lightViewProj = MatrixMultiply(lightView, lightProj); + + // PASS 02: Draw the scene into main framebuffer, using the generated shadowmap + BeginDrawing(); + ClearBackground(Color.RayWhite); + + SetShaderValueMatrix(shadowShader, lightVPLoc, lightViewProj); + EnableShader(shadowShader.Id); + + ActiveTextureSlot(textureActiveSlot); + EnableTexture(shadowMap.Depth.Id); + var slot = textureActiveSlot; + SetUniform(shadowMapLoc, &slot, (int)ShaderUniformDataType.Int, 1); + + BeginMode3D(camera); + DrawScene(cube, robot); // Draw the same exact things as we drew in the shadowmap! + EndMode3D(); + + DrawText("Use the arrow keys to rotate the light!", 10, 10, 30, Color.Red); + DrawText("Shadows in raylib using the shadowmapping algorithm!", screenWidth - 280, screenHeight - 20, 10, Color.Gray); + + EndDrawing(); + + if (IsKeyPressed(KeyboardKey.F)) + { + TakeScreenshot("shaders_shadowmap.png"); + } + //---------------------------------------------------------------------------------- + } + + public unsafe void Unload() + { + UnloadShader(shadowShader); + UnloadModel(cube); + UnloadModel(robot); + UnloadModelAnimations(anims, animCount); + UnloadShadowmapRenderTexture(shadowMap); + } + + // Load render texture for shadowmap projection + // NOTE: Load framebuffer with only a texture depth attachment, + // no color attachment required for shadowmap + private static unsafe RenderTexture2D LoadShadowmapRenderTexture(int width, int height) + { + RenderTexture2D target = new(); + + target.Id = LoadFramebuffer(); // Load an empty framebuffer + target.Texture.Width = width; + target.Texture.Height = height; + + if (target.Id > 0) + { + EnableFramebuffer(target.Id); + + // Create depth texture + // NOTE: No need a color texture attachment for the shadowmap + target.Depth.Id = LoadTextureDepth(width, height, false); + target.Depth.Width = width; + target.Depth.Height = height; + target.Depth.Format = (PixelFormat)19; // DEPTH_COMPONENT_24BIT? + target.Depth.Mipmaps = 1; + + // Attach depth texture to FBO + FramebufferAttach(target.Id, target.Depth.Id, FramebufferAttachType.Depth, FramebufferAttachTextureType.Texture2D, 0); + + // Check if fbo is complete with attachments (valid) + if (FramebufferComplete(target.Id) != 0) + { + TraceLog(TraceLogLevel.Info, $"FBO: [ID {target.Id}] Framebuffer object created successfully"); + } + + DisableFramebuffer(); + } + else + { + TraceLog(TraceLogLevel.Warning, "FBO: Framebuffer object can not be created"); + } + + return target; + } + + // Unload shadowmap render texture from GPU memory (VRAM) + private static void UnloadShadowmapRenderTexture(RenderTexture2D target) + { + if (target.Id > 0) + { + // NOTE: Depth texture/renderbuffer is automatically + // queried and deleted before deleting framebuffer + UnloadFramebuffer(target.Id); + } + } + + // Draw full scene projecting shadows + // NOTE: Required to be called several time to generate shadowmap + private static void DrawScene(Model cube, Model robot) + { + DrawModelEx(cube, Vector3.Zero, new Vector3(0.0f, 1.0f, 0.0f), 0.0f, new Vector3(10.0f, 1.0f, 10.0f), Color.Blue); + DrawModelEx(cube, new Vector3(1.5f, 1.0f, -1.5f), new Vector3(0.0f, 1.0f, 0.0f), 0.0f, Vector3.One, Color.White); + DrawModelEx(robot, new Vector3(0.0f, 0.5f, 0.0f), new Vector3(0.0f, 1.0f, 0.0f), 0.0f, new Vector3(1.0f, 1.0f, 1.0f), Color.Red); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - shadowmap rendering"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new ShadowmapRendering(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shaders/TextureTiling.cs b/Examples/Shaders/TextureTiling.cs new file mode 100644 index 0000000..8b636d3 --- /dev/null +++ b/Examples/Shaders/TextureTiling.cs @@ -0,0 +1,143 @@ +/******************************************************************************************* +* +* raylib [shaders] example - texture tiling +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example demonstrates how to tile a texture on a 3D model using raylib +* +* Example originally created with raylib 4.5, last time updated with raylib 4.5 +* +* Example contributed by Luis Almeida (@luis605) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2023-2025 Luis Almeida (@luis605) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shaders; + +public class TextureTiling : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Texture Tiling"; + + public string Title => "raylib [shaders] example - texture tiling"; + + public bool CursorDisabled => true; + + private Camera3D camera; + private Model model; + private Texture2D texture; + private Shader shader; + + public unsafe void Init() + { + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(4.0f, 4.0f, 4.0f); // Camera position + camera.Target = new Vector3(0.0f, 0.5f, 0.0f); // Camera looking at point + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target) + camera.FovY = 45.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + + // Load a cube model + var cube = GenMeshCube(1.0f, 1.0f, 1.0f); + model = LoadModelFromMesh(cube); + + // Load a texture and assign to cube model + texture = LoadTexture("resources/cubicmap_atlas.png"); + model.Materials[0].Maps[(int)MaterialMapIndex.Diffuse].Texture = texture; + + // Set the texture tiling using a shader + var tiling = new[] { 3.0f, 3.0f }; + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/tiling.fs"); + SetTextureWrap(texture, TextureWrap.Repeat); + Raylib.SetShaderValue(shader, GetShaderLocation(shader, "tiling"), tiling, ShaderUniformDataType.Vec2); + model.Materials[0].Shader = shader; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Free); + + if (IsKeyPressed(KeyboardKey.Z)) + { + camera.Target = new Vector3(0.0f, 0.5f, 0.0f); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + BeginShaderMode(shader); + DrawModel(model, new Vector3(0.0f, 0.0f, 0.0f), 2.0f, Color.White); + EndShaderMode(); + + DrawGrid(10, 1.0f); + + EndMode3D(); + + DrawText("Use mouse to rotate the camera", 10, 10, 20, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadModel(model); // Unload model + UnloadShader(shader); // Unload shader + UnloadTexture(texture); // Unload texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - texture tiling"); + + DisableCursor(); // Limit cursor to relative movement inside the window + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new TextureTiling(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shaders/VertexDisplacement.cs b/Examples/Shaders/VertexDisplacement.cs new file mode 100644 index 0000000..4d78c62 --- /dev/null +++ b/Examples/Shaders/VertexDisplacement.cs @@ -0,0 +1,148 @@ +/******************************************************************************************* +* +* raylib [shaders] example - vertex displacement +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 5.0, last time updated with raylib 4.5 +* +* Example contributed by Alex ZH (@ZzzhHe) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2023-2025 Alex ZH (@ZzzhHe) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Rlgl; + +namespace Examples.Shaders; + +public partial class VertexDisplacement : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Vertex Displacement"; + + public string Title => "raylib [shaders] example - vertex displacement"; + + private Camera3D camera; + private Shader shader; + private Texture2D perlinNoiseMap; + private Model planeModel; + private float time; + + public unsafe void Init() + { + // set up camera + camera = new(); + camera.Position = new Vector3(20.0f, 5.0f, -20.0f); + camera.Target = new Vector3(0.0f, 0.0f, 0.0f); + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); + camera.FovY = 60.0f; + camera.Projection = CameraProjection.Perspective; + + // Load vertex and fragment shaders + shader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/vertex_displacement.vs", + $"resources/shaders/glsl{GlslVersion}/vertex_displacement.fs" + ); + + // Load perlin noise texture + var perlinNoiseImage = GenImagePerlinNoise(512, 512, 0, 0, 1.0f); + perlinNoiseMap = LoadTextureFromImage(perlinNoiseImage); + UnloadImage(perlinNoiseImage); + + // Set shader uniform location + var perlinNoiseMapLoc = GetShaderLocation(shader, "perlinNoiseMap"); + EnableShader(shader.Id); + ActiveTextureSlot(1); + EnableTexture(perlinNoiseMap.Id); + SetUniformSampler(perlinNoiseMapLoc, 1); + + // Create a plane mesh and model + var planeMesh = GenMeshPlane(50, 50, 50, 50); + planeModel = LoadModelFromMesh(planeMesh); + // Set plane model material + var materials = planeModel.Materials; + materials[0].Shader = shader; + + time = 0.0f; + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Free); // Update camera + + time += GetFrameTime(); // Update time variable + Raylib.SetShaderValue(shader, GetShaderLocation(shader, "time"), time, ShaderUniformDataType.Float); // Send time value to shader + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + BeginShaderMode(shader); + // Draw plane model + DrawModel(planeModel, new Vector3(0.0f, 0.0f, 0.0f), 1.0f, new Color(255, 255, 255, 255)); + EndShaderMode(); + + EndMode3D(); + + DrawText("Vertex displacement", 10, 10, 20, Color.DarkGray); + DrawFPS(10, 40); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); + UnloadModel(planeModel); + UnloadTexture(perlinNoiseMap); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - vertex displacement"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new VertexDisplacement(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/BallPhysics.cs b/Examples/Shapes/BallPhysics.cs new file mode 100644 index 0000000..4a7739c --- /dev/null +++ b/Examples/Shapes/BallPhysics.cs @@ -0,0 +1,275 @@ +/******************************************************************************************* +* +* raylib [shapes] example - ball physics +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by David Buzatto (@davidbuzatto) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 David Buzatto (@davidbuzatto) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; + +namespace Examples.Shapes; + +public partial class BallPhysics : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MAX_BALLS = 5000; // Maximum quantity of balls + + public string Name => "Shapes / Ball Physics"; + + public string Title => "raylib [shapes] example - ball physics"; + + //---------------------------------------------------------------------------------- + // Types and Structures Definition + //---------------------------------------------------------------------------------- + // Ball data type + private struct Ball + { + public Vector2 position; + public Vector2 speed; + public Vector2 prevPosition; + public float radius; + public float friction; + public float elasticity; + public Color color; + public bool grabbed; + } + + private Ball[] balls; + private int ballCount; + private int grabbedBallIndex; // Index of the current ball that is grabbed (-1 if none) + private Vector2 pressOffset; // Mouse press offset relative to the ball that grabbedd + + private float gravity; // World gravity + + private Vector2 windowPosition; + + public void Init() + { + balls = new Ball[MAX_BALLS]; + + // Init first ball in the array + balls[0] = new Ball + { + position = new Vector2(GetScreenWidth()/2.0f, GetScreenHeight()/2.0f), + speed = new Vector2(200, 200), + prevPosition = new Vector2(0, 0), + radius = 40, + friction = 0.99f, + elasticity = 0.9f, + color = Color.Blue, + grabbed = false + }; + + ballCount = 1; + grabbedBallIndex = -1; // A reference to the current ball that is grabbed + pressOffset = new Vector2(0, 0); // Mouse press offset relative to the ball that grabbedd + + gravity = 100; // World gravity + + windowPosition = GetWindowPosition(); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + float delta = GetFrameTime(); + Vector2 mousePos = GetMousePosition(); + + // Checks if a ball was grabbed + if (IsMouseButtonPressed(MouseButton.Left)) + { + for (int i = ballCount - 1; i >= 0; i--) + { + pressOffset.X = mousePos.X - balls[i].position.X; + pressOffset.Y = mousePos.Y - balls[i].position.Y; + + // If the distance between the ball position and the mouse press position + // is less than or equal to the ball radius, the event occurred inside the ball + if (MathF.Sqrt(pressOffset.X*pressOffset.X + pressOffset.Y*pressOffset.Y) <= balls[i].radius) + { + balls[i].grabbed = true; + grabbedBallIndex = i; + break; + } + } + } + + // Releases any ball the was grabbed + if (IsMouseButtonReleased(MouseButton.Left)) + { + if (grabbedBallIndex != -1) + { + balls[grabbedBallIndex].grabbed = false; + grabbedBallIndex = -1; + } + } + + // Creates a new ball + if (IsMouseButtonPressed(MouseButton.Right) || (IsKeyDown(KeyboardKey.LeftControl) && IsMouseButtonDown(MouseButton.Right))) + { + if (ballCount < MAX_BALLS) + { + balls[ballCount++] = new Ball + { + position = mousePos, + speed = new Vector2(GetRandomValue(-300, 300), GetRandomValue(-300, 300)), + prevPosition = new Vector2(0, 0), + radius = 20.0f + GetRandomValue(0, 30), + friction = 0.99f, + elasticity = 0.9f, + color = new Color(GetRandomValue(0, 255), GetRandomValue(0, 255), GetRandomValue(0, 255), 255), + grabbed = false + }; + } + } + + // Get window position change for shaking + Vector2 windowPositionDelta = Vector2Subtract(windowPosition, GetWindowPosition()); + + if (Vector2Length(windowPositionDelta) > 5.0f) + { + for (int i = 0; i < ballCount; i++) + { + if (!balls[i].grabbed) balls[i].speed = Vector2Add(balls[i].speed, Vector2Scale(windowPositionDelta, 10.0f)); + } + } + + // Shake balls + if (IsMouseButtonPressed(MouseButton.Middle)) + { + for (int i = 0; i < ballCount; i++) + { + if (!balls[i].grabbed) balls[i].speed = new Vector2(GetRandomValue(-2000, 2000), GetRandomValue(-2000, 2000)); + } + } + + // Changes gravity + gravity += GetMouseWheelMove()*5; + + // Updates each ball state + for (int i = 0; i < ballCount; i++) + { + // The ball is not grabbed + if (!balls[i].grabbed) + { + // Ball repositioning using the velocity + balls[i].position.X += balls[i].speed.X * delta; + balls[i].position.Y += balls[i].speed.Y * delta; + + // Does the ball hit the screen right boundary? + if ((balls[i].position.X + balls[i].radius) >= screenWidth) + { + balls[i].position.X = screenWidth - balls[i].radius; // Ball repositioning + balls[i].speed.X = -balls[i].speed.X*balls[i].elasticity; // Elasticity makes the ball lose 10% of its velocity on hit + } + // Does the ball hit the screen left boundary? + else if ((balls[i].position.X - balls[i].radius) <= 0) + { + balls[i].position.X = balls[i].radius; + balls[i].speed.X = -balls[i].speed.X*balls[i].elasticity; + } + + // The same for y axis + if ((balls[i].position.Y + balls[i].radius) >= screenHeight) + { + balls[i].position.Y = screenHeight - balls[i].radius; + balls[i].speed.Y = -balls[i].speed.Y*balls[i].elasticity; + } + else if ((balls[i].position.Y - balls[i].radius) <= 0) + { + balls[i].position.Y = balls[i].radius; + balls[i].speed.Y = -balls[i].speed.Y*balls[i].elasticity; + } + + // Friction makes the ball lose 1% of its velocity each frame + balls[i].speed.X = balls[i].speed.X*balls[i].friction; + // Gravity affects only the y axis + balls[i].speed.Y = balls[i].speed.Y*balls[i].friction + gravity; + } + else + { + // Ball repositioning using the mouse position + balls[i].position.X = mousePos.X - pressOffset.X; + balls[i].position.Y = mousePos.Y - pressOffset.Y; + + // While the ball is grabbed, recalculates its velocity + balls[i].speed.X = (balls[i].position.X - balls[i].prevPosition.X)/delta; + balls[i].speed.Y = (balls[i].position.Y - balls[i].prevPosition.Y)/delta; + balls[i].prevPosition = balls[i].position; + } + } + + windowPosition = GetWindowPosition(); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + for (int i = 0; i < ballCount; i++) + { + DrawCircleV(balls[i].position, balls[i].radius, balls[i].color); + DrawCircleLinesV(balls[i].position, balls[i].radius, Color.Black); + } + + DrawText("grab a ball by pressing with the mouse and throw it by releasing", 10, 10, 10, Color.DarkGray); + DrawText("right click to create new balls (keep left control pressed to create a lot)", 10, 30, 10, Color.DarkGray); + DrawText("use mouse wheel to change gravity", 10, 50, 10, Color.DarkGray); + DrawText("middle click to shake", 10, 70, 10, Color.DarkGray); + DrawText($"BALL COUNT: {ballCount}", 10, GetScreenHeight() - 70, 20, Color.Black); + DrawText($"GRAVITY: {gravity:F2}", 10, GetScreenHeight() - 40, 20, Color.Black); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - ball physics"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new BallPhysics(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/BulletHell.cs b/Examples/Shapes/BulletHell.cs new file mode 100644 index 0000000..5fdc9fd --- /dev/null +++ b/Examples/Shapes/BulletHell.cs @@ -0,0 +1,290 @@ +/******************************************************************************************* +* +* raylib [shapes] example - bullet hell +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 5.6, last time updated with raylib 5.6 +* +* Example contributed by Zero (@zerohorsepower) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Zero (@zerohorsepower) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shapes; + +public partial class BulletHell : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MAX_BULLETS = 500000; // Max bullets to be processed + + public string Name => "Shapes / Bullet Hell"; + + public string Title => "raylib [shapes] example - bullet hell"; + + //---------------------------------------------------------------------------------- + // Types and Structures Definition + //---------------------------------------------------------------------------------- + private struct Bullet + { + public Vector2 position; // Bullet position on screen + public Vector2 acceleration; // Amount of pixels to be incremented to position every frame + public bool disabled; // Skip processing and draw case out of screen + public Color color; // Bullet color + } + + // Bullets definition + private Bullet[] bullets; + private int bulletCount; + private int bulletDisabledCount; // Used to calculate how many bullets are on screen + private int bulletRadius; + private float bulletSpeed; + private int bulletRows; + private Color[] bulletColor; + + // Spawner variables + private float baseDirection; + private int angleIncrement; // After spawn all bullet rows, increment this value on the baseDirection for next the frame + private float spawnCooldown; + private float spawnCooldownTimer; + + // Magic circle + private float magicCircleRotation; + + // Used on performance drawing + private RenderTexture2D bulletTexture; + + private bool drawInPerformanceMode; // Switch between DrawCircle() and DrawTexture() + + public void Init() + { + // Bullets definition + bullets = new Bullet[MAX_BULLETS]; // Bullets array + bulletCount = 0; + bulletDisabledCount = 0; + bulletRadius = 10; + bulletSpeed = 3.0f; + bulletRows = 6; + bulletColor = new[] { Color.Red, Color.Blue }; + + // Spawner variables + baseDirection = 0; + angleIncrement = 5; + spawnCooldown = 2; + spawnCooldownTimer = spawnCooldown; + + // Magic circle + magicCircleRotation = 0; + + // Used on performance drawing + bulletTexture = LoadRenderTexture(24, 24); + + // Draw circle to bullet texture, then draw bullet using DrawTexture() + // NOTE: This is done to improve the performance, since DrawCircle() is very slow + BeginTextureMode(bulletTexture); + DrawCircle(12, 12, (float)bulletRadius, Color.White); + DrawCircleLines(12, 12, (float)bulletRadius, Color.Black); + EndTextureMode(); + + drawInPerformanceMode = true; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Reset the bullet index + // New bullets will replace the old ones that are already disabled due to out-of-screen + if (bulletCount >= MAX_BULLETS) + { + bulletCount = 0; + bulletDisabledCount = 0; + } + + spawnCooldownTimer--; + if (spawnCooldownTimer < 0) + { + spawnCooldownTimer = spawnCooldown; + + // Spawn bullets + float degreesPerRow = 360.0f/bulletRows; + for (int row = 0; row < bulletRows; row++) + { + if (bulletCount < MAX_BULLETS) + { + bullets[bulletCount].position = new Vector2((float)screenWidth/2, (float)screenHeight/2); + bullets[bulletCount].disabled = false; + bullets[bulletCount].color = bulletColor[row%2]; + + float bulletDirection = baseDirection + (degreesPerRow*row); + + // Bullet speed*bullet direction, this will determine how much pixels will be incremented/decremented + // from the bullet position every frame. Since the bullets doesn't change its direction and speed, + // only need to calculate it at the spawning time + // 0 degrees = right, 90 degrees = down, 180 degrees = left and 270 degrees = up, basically clockwise + // Case you want it to be anti-clockwise, add "* -1" at the y acceleration + bullets[bulletCount].acceleration = new Vector2( + bulletSpeed*MathF.Cos(bulletDirection*DEG2RAD), + bulletSpeed*MathF.Sin(bulletDirection*DEG2RAD) + ); + + bulletCount++; + } + } + + baseDirection += angleIncrement; + } + + // Update bullets position based on its acceleration + for (int i = 0; i < bulletCount; i++) + { + // Only update bullet if inside the screen + if (!bullets[i].disabled) + { + bullets[i].position.X += bullets[i].acceleration.X; + bullets[i].position.Y += bullets[i].acceleration.Y; + + // Disable bullet if out of screen + if ((bullets[i].position.X < -bulletRadius*2) || + (bullets[i].position.X > screenWidth + bulletRadius*2) || + (bullets[i].position.Y < -bulletRadius*2) || + (bullets[i].position.Y > screenHeight + bulletRadius*2)) + { + bullets[i].disabled = true; + bulletDisabledCount++; + } + } + } + + // Input logic + if ((IsKeyPressed(KeyboardKey.Right) || IsKeyPressed(KeyboardKey.D)) && (bulletRows < 359)) bulletRows++; + if ((IsKeyPressed(KeyboardKey.Left) || IsKeyPressed(KeyboardKey.A)) && (bulletRows > 1)) bulletRows--; + if (IsKeyPressed(KeyboardKey.Up) || IsKeyPressed(KeyboardKey.W)) bulletSpeed += 0.25f; + if ((IsKeyPressed(KeyboardKey.Down) || IsKeyPressed(KeyboardKey.S)) && (bulletSpeed > 0.50f)) bulletSpeed -= 0.25f; + if (IsKeyPressed(KeyboardKey.Z) && (spawnCooldown > 1)) spawnCooldown--; + if (IsKeyPressed(KeyboardKey.X)) spawnCooldown++; + if (IsKeyPressed(KeyboardKey.Enter)) drawInPerformanceMode = !drawInPerformanceMode; + + if (IsKeyDown(KeyboardKey.Space)) + { + angleIncrement += 1; + angleIncrement %= 360; + } + + if (IsKeyPressed(KeyboardKey.C)) + { + bulletCount = 0; + bulletDisabledCount = 0; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // Draw magic circle + magicCircleRotation++; + DrawRectanglePro(new Rectangle((float)screenWidth/2, (float)screenHeight/2, 120, 120), + new Vector2(60.0f, 60.0f), magicCircleRotation, Color.Purple); + DrawRectanglePro(new Rectangle((float)screenWidth/2, (float)screenHeight/2, 120, 120), + new Vector2(60.0f, 60.0f), magicCircleRotation + 45, Color.Purple); + DrawCircleLines(screenWidth/2, screenHeight/2, 70, Color.Black); + DrawCircleLines(screenWidth/2, screenHeight/2, 50, Color.Black); + DrawCircleLines(screenWidth/2, screenHeight/2, 30, Color.Black); + + // Draw bullets + if (drawInPerformanceMode) + { + // Draw bullets using pre-rendered texture containing circle + for (int i = 0; i < bulletCount; i++) + { + // Do not draw disabled bullets (out of screen) + if (!bullets[i].disabled) + { + DrawTexture(bulletTexture.Texture, + (int)(bullets[i].position.X - bulletTexture.Texture.Width*0.5f), + (int)(bullets[i].position.Y - bulletTexture.Texture.Height*0.5f), + bullets[i].color); + } + } + } + else + { + // Draw bullets using DrawCircle(), less performant + for (int i = 0; i < bulletCount; i++) + { + // Do not draw disabled bullets (out of screen) + if (!bullets[i].disabled) + { + DrawCircleV(bullets[i].position, (float)bulletRadius, bullets[i].color); + DrawCircleLinesV(bullets[i].position, (float)bulletRadius, Color.Black); + } + } + } + + // Draw UI + DrawRectangle(10, 10, 280, 150, new Color(0, 0, 0, 200)); + DrawText("Controls:", 20, 20, 10, Color.LightGray); + DrawText("- Right/Left or A/D: Change rows number", 40, 40, 10, Color.LightGray); + DrawText("- Up/Down or W/S: Change bullet speed", 40, 60, 10, Color.LightGray); + DrawText("- Z or X: Change spawn cooldown", 40, 80, 10, Color.LightGray); + DrawText("- Space (Hold): Change the angle increment", 40, 100, 10, Color.LightGray); + DrawText("- Enter: Switch draw method (Performance)", 40, 120, 10, Color.LightGray); + DrawText("- C: Clear bullets", 40, 140, 10, Color.LightGray); + + DrawRectangle(610, 10, 170, 30, new Color(0, 0, 0, 200)); + if (drawInPerformanceMode) DrawText("Draw method: DrawTexture(*)", 620, 20, 10, Color.Green); + else DrawText("Draw method: DrawCircle(*)", 620, 20, 10, Color.Red); + + DrawRectangle(135, 410, 530, 30, new Color(0, 0, 0, 200)); + DrawText($"[ FPS: {GetFPS()}, Bullets: {bulletCount - bulletDisabledCount}, Rows: {bulletRows}, Bullet speed: {bulletSpeed:F2}, Angle increment per frame: {angleIncrement}, Cooldown: {spawnCooldown:F0} ]", + 155, 420, 10, Color.Green); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadRenderTexture(bulletTexture); // Unload bullet texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - bullet hell"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new BulletHell(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/ClockOfClocks.cs b/Examples/Shapes/ClockOfClocks.cs new file mode 100644 index 0000000..c80d5e4 --- /dev/null +++ b/Examples/Shapes/ClockOfClocks.cs @@ -0,0 +1,240 @@ +/******************************************************************************************* +* +* raylib [shapes] example - clock of clocks +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 6.0 +* +* Example contributed by JP Mortiboys (@themushroompirates) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 JP Mortiboys (@themushroompirates) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; // Required for: Lerp(), Clamp() + +namespace Examples.Shapes; + +public partial class ClockOfClocks : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Clock of Clocks"; + + public string Title => "raylib [shapes] example - clock of clocks"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + private Color bgColor; + private Color handsColor; + + private const float clockFaceSize = 24; + private const float clockFaceSpacing = 8.0f; + private const float sectionSpacing = 16.0f; + + private static readonly Vector2 TL = new(0.0f, 90.0f); // Top-left corner + private static readonly Vector2 TR = new(90.0f, 180.0f); // Top-right corner + private static readonly Vector2 BR = new(180.0f, 270.0f); // Bottom-right corner + private static readonly Vector2 BL = new(0.0f, 270.0f); // Bottom-left corner + private static readonly Vector2 HH = new(0.0f, 180.0f); // Horizontal line + private static readonly Vector2 VV = new(90.0f, 270.0f); // Vertical line + private static readonly Vector2 ZZ = new(135.0f, 135.0f); // Not relevant + + private Vector2[,] digitAngles; + + // Time for the hands to move to the new position (in seconds); this must be <1s + private const float handsMoveDuration = 0.5f; + + private int prevSeconds; + private Vector2[,] currentAngles; + private Vector2[,] srcAngles; + private Vector2[,] dstAngles; + + private float handsMoveTimer; + private int hourMode; + + public void Init() + { + bgColor = ColorLerp(Color.DarkBlue, Color.Black, 0.75f); + handsColor = ColorLerp(Color.Yellow, Color.RayWhite, .25f); + + digitAngles = new Vector2[10, 24] + { + /* 0 */ { TL, HH, HH, TR, /* */ VV, TL, TR, VV, /* */ VV, VV, VV, VV, /* */ VV, VV, VV, VV, /* */ VV, BL, BR, VV, /* */ BL, HH, HH, BR }, + /* 1 */ { TL, HH, TR, ZZ, /* */ BL, TR, VV, ZZ, /* */ ZZ, VV, VV, ZZ, /* */ ZZ, VV, VV, ZZ, /* */ TL, BR, BL, TR, /* */ BL, HH, HH, BR }, + /* 2 */ { TL, HH, HH, TR, /* */ BL, HH, TR, VV, /* */ TL, HH, BR, VV, /* */ VV, TL, HH, BR, /* */ VV, BL, HH, TR, /* */ BL, HH, HH, BR }, + /* 3 */ { TL, HH, HH, TR, /* */ BL, HH, TR, VV, /* */ TL, HH, BR, VV, /* */ BL, HH, TR, VV, /* */ TL, HH, BR, VV, /* */ BL, HH, HH, BR }, + /* 4 */ { TL, TR, TL, TR, /* */ VV, VV, VV, VV, /* */ VV, BL, BR, VV, /* */ BL, HH, TR, VV, /* */ ZZ, ZZ, VV, VV, /* */ ZZ, ZZ, BL, BR }, + /* 5 */ { TL, HH, HH, TR, /* */ VV, TL, HH, BR, /* */ VV, BL, HH, TR, /* */ BL, HH, TR, VV, /* */ TL, HH, BR, VV, /* */ BL, HH, HH, BR }, + /* 6 */ { TL, HH, HH, TR, /* */ VV, TL, HH, BR, /* */ VV, BL, HH, TR, /* */ VV, TL, TR, VV, /* */ VV, BL, BR, VV, /* */ BL, HH, HH, BR }, + /* 7 */ { TL, HH, HH, TR, /* */ BL, HH, TR, VV, /* */ ZZ, ZZ, VV, VV, /* */ ZZ, ZZ, VV, VV, /* */ ZZ, ZZ, VV, VV, /* */ ZZ, ZZ, BL, BR }, + /* 8 */ { TL, HH, HH, TR, /* */ VV, TL, TR, VV, /* */ VV, BL, BR, VV, /* */ VV, TL, TR, VV, /* */ VV, BL, BR, VV, /* */ BL, HH, HH, BR }, + /* 9 */ { TL, HH, HH, TR, /* */ VV, TL, TR, VV, /* */ VV, BL, BR, VV, /* */ BL, HH, TR, VV, /* */ TL, HH, BR, VV, /* */ BL, HH, HH, BR }, + }; + + prevSeconds = -1; + currentAngles = new Vector2[6, 24]; + srcAngles = new Vector2[6, 24]; + dstAngles = new Vector2[6, 24]; + + handsMoveTimer = 0.0f; + hourMode = 24; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Get the current time + DateTime timeinfo = DateTime.Now; + + if (timeinfo.Second != prevSeconds) + { + // The time has changed, so we need to move the hands to the new positions + prevSeconds = timeinfo.Second; + + // Format the current time so we can access the individual digits + string clockDigits = $"{timeinfo.Hour % hourMode:D2}{timeinfo.Minute:D2}{timeinfo.Second:D2}"; + + // Fetch where we want all the hands to be + for (int digit = 0; digit < 6; digit++) + { + for (int cell = 0; cell < 24; cell++) + { + srcAngles[digit, cell] = currentAngles[digit, cell]; + dstAngles[digit, cell] = digitAngles[clockDigits[digit] - '0', cell]; + + // Quick exception for 12h mode + if ((digit == 0) && (hourMode == 12) && (clockDigits[0] == '0')) dstAngles[digit, cell] = ZZ; + if (srcAngles[digit, cell].X > dstAngles[digit, cell].X) srcAngles[digit, cell].X -= 360.0f; + if (srcAngles[digit, cell].Y > dstAngles[digit, cell].Y) srcAngles[digit, cell].Y -= 360.0f; + } + } + + // Reset the timer + handsMoveTimer = -GetFrameTime(); + } + + // Now let's animate all the hands if we need to + if (handsMoveTimer < handsMoveDuration) + { + // Increase the timer but don't go above the maximum + handsMoveTimer = Clamp(handsMoveTimer + GetFrameTime(), 0, handsMoveDuration); + + // Calculate the % completion of the animation + float t = handsMoveTimer / handsMoveDuration; + + // A little cheeky smoothstep + t = t * t * (3.0f - 2.0f * t); + + for (int digit = 0; digit < 6; digit++) + { + for (int cell = 0; cell < 24; cell++) + { + currentAngles[digit, cell].X = Lerp(srcAngles[digit, cell].X, dstAngles[digit, cell].X, t); + currentAngles[digit, cell].Y = Lerp(srcAngles[digit, cell].Y, dstAngles[digit, cell].Y, t); + } + } + } + + // Handle input + if (IsKeyPressed(KeyboardKey.Space)) hourMode = 36 - hourMode; // Toggle between 12 and 24 hour mode with space + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(bgColor); + + DrawText($"{hourMode}-h mode, space to change", 10, 30, 20, Color.RayWhite); + + float xOffset = 4.0f; + + for (int digit = 0; digit < 6; digit++) + { + for (int row = 0; row < 6; row++) + { + for (int col = 0; col < 4; col++) + { + Vector2 centre = new( + xOffset + col * (clockFaceSize + clockFaceSpacing) + clockFaceSize * 0.5f, + 100 + row * (clockFaceSize + clockFaceSpacing) + clockFaceSize * 0.5f + ); + + DrawRing(centre, clockFaceSize * 0.5f - 2.0f, clockFaceSize * 0.5f, 0, 360, 24, Color.DarkGray); + + // Big hand + DrawRectanglePro( + new Rectangle(centre.X, centre.Y, clockFaceSize * 0.5f + 4.0f, 4.0f), + new Vector2(2.0f, 2.0f), + currentAngles[digit, row * 4 + col].X, + handsColor + ); + + // Little hand + DrawRectanglePro( + new Rectangle(centre.X, centre.Y, clockFaceSize * 0.5f + 2.0f, 4.0f), + new Vector2(2.0f, 2.0f), + currentAngles[digit, row * 4 + col].Y, + handsColor + ); + } + } + + xOffset += (clockFaceSize + clockFaceSpacing) * 4; + if (digit % 2 == 1) + { + DrawRing(new Vector2(xOffset + 4.0f, 160.0f), 6.0f, 8.0f, 0.0f, 360.0f, 24, handsColor); + DrawRing(new Vector2(xOffset + 4.0f, 225.0f), 6.0f, 8.0f, 0.0f, 360.0f, 24, handsColor); + xOffset += sectionSpacing; + } + } + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - clock of clocks"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new ClockOfClocks(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/DashedLine.cs b/Examples/Shapes/DashedLine.cs new file mode 100644 index 0000000..5d50c1e --- /dev/null +++ b/Examples/Shapes/DashedLine.cs @@ -0,0 +1,130 @@ +/******************************************************************************************* +* +* raylib [shapes] example - dashed line +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.5 +* +* Example contributed by Luís Almeida (@luis605) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Luís Almeida (@luis605) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shapes; + +public partial class DashedLine : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Dashed Line"; + + public string Title => "raylib [shapes] example - dashed line"; + + // Line Properties + private Vector2 lineStartPosition; + private Vector2 lineEndPosition; + private float dashLength; + private float blankLength; + + // Color selection + private Color[] lineColors; + private int colorIndex; + + public void Init() + { + // Line Properties + lineStartPosition = new Vector2(20.0f, 50.0f); + lineEndPosition = new Vector2(780.0f, 400.0f); + dashLength = 25.0f; + blankLength = 15.0f; + + // Color selection + lineColors = new[] { Color.Red, Color.Orange, Color.Gold, Color.Green, Color.Blue, Color.Violet, Color.Pink, Color.Black }; + colorIndex = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + lineEndPosition = GetMousePosition(); // Line endpoint follows the mouse + + // Change Dash Length (UP/DOWN arrows) + if (IsKeyDown(KeyboardKey.Up)) dashLength += 1.0f; + if (IsKeyDown(KeyboardKey.Down) && dashLength > 1.0f) dashLength -= 1.0f; + + // Change Space Length (LEFT/RIGHT arrows) + if (IsKeyDown(KeyboardKey.Right)) blankLength += 1.0f; + if (IsKeyDown(KeyboardKey.Left) && blankLength > 1.0f) blankLength -= 1.0f; + + // Cycle through colors ('C' key) + if (IsKeyPressed(KeyboardKey.C)) colorIndex = (colorIndex + 1)%lineColors.Length; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + // Draw the dashed line with the current properties + DrawLineDashed(lineStartPosition, lineEndPosition, (int)dashLength, (int)blankLength, lineColors[colorIndex]); + + // Draw UI and Instructions + DrawRectangle(5, 5, 265, 95, Fade(Color.SkyBlue, 0.5f)); + DrawRectangleLines(5, 5, 265, 95, Color.Blue); + + DrawText("CONTROLS:", 15, 15, 10, Color.Black); + DrawText("UP/DOWN: Change Dash Length", 15, 35, 10, Color.Black); + DrawText("LEFT/RIGHT: Change Space Length", 15, 55, 10, Color.Black); + DrawText("C: Cycle Color", 15, 75, 10, Color.Black); + + DrawText($"Dash: {dashLength:F0} | Space: {blankLength:F0}", 15, 115, 10, Color.DarkGray); + + DrawFPS(screenWidth - 80, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - dashed line"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new DashedLine(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/DigitalClock.cs b/Examples/Shapes/DigitalClock.cs new file mode 100644 index 0000000..46981e4 --- /dev/null +++ b/Examples/Shapes/DigitalClock.cs @@ -0,0 +1,327 @@ +/******************************************************************************************* +* +* raylib [shapes] example - digital clock +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.6 +* +* Example contributed by Hamza RAHAL (@hmz-rhl) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Hamza RAHAL (@hmz-rhl) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shapes; + +public partial class DigitalClock : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int CLOCK_ANALOG = 0; + private const int CLOCK_DIGITAL = 1; + + public string Name => "Shapes / Digital Clock"; + + public string Title => "raylib [shapes] example - digital clock"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + //---------------------------------------------------------------------------------- + // Types and Structures Definition + //---------------------------------------------------------------------------------- + // Clock hand type + private struct ClockHand + { + public int value; // Time value + + // Visual elements + public float angle; // Hand angle + public int length; // Hand length + public int thickness; // Hand thickness + public Color color; // Hand color + } + + // Clock hands + private struct Clock + { + public ClockHand second; // Clock hand for seconds + public ClockHand minute; // Clock hand for minutes + public ClockHand hour; // Clock hand for hours + } + + private int clockMode; + private Clock clock; + + public void Init() + { + clockMode = CLOCK_DIGITAL; + + // Initialize clock + // NOTE: Includes visual info for analog clock + clock = new Clock(); + + clock.second.angle = 45; + clock.second.length = 140; + clock.second.thickness = 3; + clock.second.color = Color.Maroon; + + clock.minute.angle = 10; + clock.minute.length = 130; + clock.minute.thickness = 7; + clock.minute.color = Color.DarkGray; + + clock.hour.angle = 0; + clock.hour.length = 100; + clock.hour.thickness = 7; + clock.hour.color = Color.Black; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.Space)) + { + // Toggle clock mode + if (clockMode == CLOCK_DIGITAL) clockMode = CLOCK_ANALOG; + else if (clockMode == CLOCK_ANALOG) clockMode = CLOCK_DIGITAL; + } + + UpdateClock(); // Update clock required data: value and angle + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + // Draw clock in selected mode + if (clockMode == CLOCK_ANALOG) DrawClockAnalog(clock, new Vector2(400, 240)); + else if (clockMode == CLOCK_DIGITAL) + { + DrawClockDigital(clock, new Vector2(30, 60)); + + // Draw clock using default raylib font + string clockTime = $"{clock.hour.value:D2}:{clock.minute.value:D2}:{clock.second.value:D2}"; + DrawText(clockTime, GetScreenWidth() / 2 - MeasureText(clockTime, 150) / 2, 300, 150, Color.Black); + } + + DrawText($"Press [SPACE] to switch clock mode: {((clockMode == CLOCK_DIGITAL) ? "DIGITAL CLOCK" : "ANALOGUE CLOCK")}", + 10, 10, 20, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + //---------------------------------------------------------------------------------- + // Module Functions Definition + //---------------------------------------------------------------------------------- + // Update clock time + private void UpdateClock() + { + DateTime timeinfo = DateTime.Now; + + // Updating time data + clock.second.value = timeinfo.Second; + clock.minute.value = timeinfo.Minute; + clock.hour.value = timeinfo.Hour; + + clock.hour.angle = (timeinfo.Hour % 12) * 180.0f / 6.0f; + clock.hour.angle += (timeinfo.Minute % 60) * 30 / 60.0f; + clock.hour.angle -= 90; + + clock.minute.angle = (timeinfo.Minute % 60) * 6.0f; + clock.minute.angle += (timeinfo.Second % 60) * 6 / 60.0f; + clock.minute.angle -= 90; + + clock.second.angle = (timeinfo.Second % 60) * 6.0f; + clock.second.angle -= 90; + } + + // Draw analog clock + // Parameter: position, refers to center position + private static void DrawClockAnalog(Clock clock, Vector2 position) + { + // Draw clock base + DrawCircleV(position, clock.second.length + 40.0f, Color.LightGray); + DrawCircleV(position, 12.0f, Color.Gray); + + // Draw clock minutes/seconds lines + for (int i = 0; i < 60; i++) + { + DrawLineEx(new Vector2(position.X + (clock.second.length + ((i % 5) != 0 ? 10 : 6)) * MathF.Cos((6.0f * i - 90.0f) * DEG2RAD), + position.Y + (clock.second.length + ((i % 5) != 0 ? 10 : 6)) * MathF.Sin((6.0f * i - 90.0f) * DEG2RAD)), + new Vector2(position.X + (clock.second.length + 20) * MathF.Cos((6.0f * i - 90.0f) * DEG2RAD), + position.Y + (clock.second.length + 20) * MathF.Sin((6.0f * i - 90.0f) * DEG2RAD)), ((i % 5) != 0 ? 1.0f : 3.0f), Color.DarkGray); + } + + // Draw hand seconds + DrawRectanglePro(new Rectangle(position.X, position.Y, (float)clock.second.length, (float)clock.second.thickness), + new Vector2(0.0f, clock.second.thickness / 2.0f), clock.second.angle, clock.second.color); + + // Draw hand minutes + DrawRectanglePro(new Rectangle(position.X, position.Y, (float)clock.minute.length, (float)clock.minute.thickness), + new Vector2(0.0f, clock.minute.thickness / 2.0f), clock.minute.angle, clock.minute.color); + + // Draw hand hours + DrawRectanglePro(new Rectangle(position.X, position.Y, (float)clock.hour.length, (float)clock.hour.thickness), + new Vector2(0.0f, clock.hour.thickness / 2.0f), clock.hour.angle, clock.hour.color); + } + + // Draw digital clock + // PARAM: position, refers to top-left corner + private static void DrawClockDigital(Clock clock, Vector2 position) + { + // Draw clock using custom 7-segments display (made of shapes) + DrawDisplayValue(new Vector2(position.X, position.Y), clock.hour.value / 10, Color.Red, Fade(Color.LightGray, 0.3f)); + DrawDisplayValue(new Vector2(position.X + 120, position.Y), clock.hour.value % 10, Color.Red, Fade(Color.LightGray, 0.3f)); + + DrawCircle((int)position.X + 240, (int)position.Y + 70, 12, (clock.second.value % 2) != 0 ? Color.Red : Fade(Color.LightGray, 0.3f)); + DrawCircle((int)position.X + 240, (int)position.Y + 150, 12, (clock.second.value % 2) != 0 ? Color.Red : Fade(Color.LightGray, 0.3f)); + + DrawDisplayValue(new Vector2(position.X + 260, position.Y), clock.minute.value / 10, Color.Red, Fade(Color.LightGray, 0.3f)); + DrawDisplayValue(new Vector2(position.X + 380, position.Y), clock.minute.value % 10, Color.Red, Fade(Color.LightGray, 0.3f)); + + DrawCircle((int)position.X + 500, (int)position.Y + 70, 12, (clock.second.value % 2) != 0 ? Color.Red : Fade(Color.LightGray, 0.3f)); + DrawCircle((int)position.X + 500, (int)position.Y + 150, 12, (clock.second.value % 2) != 0 ? Color.Red : Fade(Color.LightGray, 0.3f)); + + DrawDisplayValue(new Vector2(position.X + 520, position.Y), clock.second.value / 10, Color.Red, Fade(Color.LightGray, 0.3f)); + DrawDisplayValue(new Vector2(position.X + 640, position.Y), clock.second.value % 10, Color.Red, Fade(Color.LightGray, 0.3f)); + } + + // Draw 7-segment display with value + private static void DrawDisplayValue(Vector2 position, int value, Color colorOn, Color colorOff) + { + switch (value) + { + case 0: Draw7SDisplay(position, 0b00111111, colorOn, colorOff); break; + case 1: Draw7SDisplay(position, 0b00000110, colorOn, colorOff); break; + case 2: Draw7SDisplay(position, 0b01011011, colorOn, colorOff); break; + case 3: Draw7SDisplay(position, 0b01001111, colorOn, colorOff); break; + case 4: Draw7SDisplay(position, 0b01100110, colorOn, colorOff); break; + case 5: Draw7SDisplay(position, 0b01101101, colorOn, colorOff); break; + case 6: Draw7SDisplay(position, 0b01111101, colorOn, colorOff); break; + case 7: Draw7SDisplay(position, 0b00000111, colorOn, colorOff); break; + case 8: Draw7SDisplay(position, 0b01111111, colorOn, colorOff); break; + case 9: Draw7SDisplay(position, 0b01101111, colorOn, colorOff); break; + default: break; + } + } + + // Draw seven segments display + // Parameter: position, refers to top-left corner of display + // Parameter: segments, defines in binary the segments to be activated + private static void Draw7SDisplay(Vector2 position, int segments, Color colorOn, Color colorOff) + { + int segmentLen = 60; + int segmentThick = 20; + float offsetYAdjust = segmentThick * 0.3f; // HACK: Adjust gap space between segment limits + + // Segment A + DrawDisplaySegment(new Vector2(position.X + segmentThick + segmentLen / 2.0f, position.Y + segmentThick), + segmentLen, segmentThick, false, (segments & 0b00000001) != 0 ? colorOn : colorOff); + // Segment B + DrawDisplaySegment(new Vector2(position.X + segmentThick + segmentLen + segmentThick / 2.0f, position.Y + 2 * segmentThick + segmentLen / 2.0f - offsetYAdjust), + segmentLen, segmentThick, true, (segments & 0b00000010) != 0 ? colorOn : colorOff); + // Segment C + DrawDisplaySegment(new Vector2(position.X + segmentThick + segmentLen + segmentThick / 2.0f, position.Y + 4 * segmentThick + segmentLen + segmentLen / 2.0f - 3 * offsetYAdjust), + segmentLen, segmentThick, true, (segments & 0b00000100) != 0 ? colorOn : colorOff); + // Segment D + DrawDisplaySegment(new Vector2(position.X + segmentThick + segmentLen / 2.0f, position.Y + 5 * segmentThick + 2 * segmentLen - 4 * offsetYAdjust), + segmentLen, segmentThick, false, (segments & 0b00001000) != 0 ? colorOn : colorOff); + // Segment E + DrawDisplaySegment(new Vector2(position.X + segmentThick / 2.0f, position.Y + 4 * segmentThick + segmentLen + segmentLen / 2.0f - 3 * offsetYAdjust), + segmentLen, segmentThick, true, (segments & 0b00010000) != 0 ? colorOn : colorOff); + // Segment F + DrawDisplaySegment(new Vector2(position.X + segmentThick / 2.0f, position.Y + 2 * segmentThick + segmentLen / 2.0f - offsetYAdjust), + segmentLen, segmentThick, true, (segments & 0b00100000) != 0 ? colorOn : colorOff); + // Segment G + DrawDisplaySegment(new Vector2(position.X + segmentThick + segmentLen / 2.0f, position.Y + 3 * segmentThick + segmentLen - 2 * offsetYAdjust), + segmentLen, segmentThick, false, (segments & 0b01000000) != 0 ? colorOn : colorOff); + } + + // Draw one 7-segment display segment, horizontal or vertical + private static void DrawDisplaySegment(Vector2 center, int length, int thick, bool vertical, Color color) + { + if (!vertical) + { + // Horizontal segment points + /* + 3___________________________5 + / \ + /1 x 6\ + \ / + \2___________________________4/ + */ + Vector2[] segmentPointsH = new Vector2[6] + { + new Vector2(center.X - length / 2.0f - thick / 2.0f, center.Y), // Point 1 + new Vector2(center.X - length / 2.0f, center.Y + thick / 2.0f), // Point 2 + new Vector2(center.X - length / 2.0f, center.Y - thick / 2.0f), // Point 3 + new Vector2(center.X + length / 2.0f, center.Y + thick / 2.0f), // Point 4 + new Vector2(center.X + length / 2.0f, center.Y - thick / 2.0f), // Point 5 + new Vector2(center.X + length / 2.0f + thick / 2.0f, center.Y), // Point 6 + }; + + DrawTriangleStrip(segmentPointsH, 6, color); + } + else + { + // Vertical segment points + Vector2[] segmentPointsV = new Vector2[6] + { + new Vector2(center.X, center.Y - length / 2.0f - thick / 2.0f), // Point 1 + new Vector2(center.X - thick / 2.0f, center.Y - length / 2.0f), // Point 2 + new Vector2(center.X + thick / 2.0f, center.Y - length / 2.0f), // Point 3 + new Vector2(center.X - thick / 2.0f, center.Y + length / 2.0f), // Point 4 + new Vector2(center.X + thick / 2.0f, center.Y + length / 2.0f), // Point 5 + new Vector2(center.X, center.Y + (float)length / 2 + thick / 2.0f), // Point 6 + }; + + DrawTriangleStrip(segmentPointsV, 6, color); + } + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - digital clock"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new DigitalClock(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/DoublePendulum.cs b/Examples/Shapes/DoublePendulum.cs new file mode 100644 index 0000000..e6ba7ef --- /dev/null +++ b/Examples/Shapes/DoublePendulum.cs @@ -0,0 +1,205 @@ +/******************************************************************************************* +* +* raylib [shapes] example - double pendulum +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.5 +* +* Example contributed by JoeCheong (@Joecheong2006) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 JoeCheong (@Joecheong2006) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shapes; + +public partial class DoublePendulum : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + // Constant for Simulation + private const int SIMULATION_STEPS = 30; + private const float G = 9.81f; + + public string Name => "Shapes / Double Pendulum"; + + public string Title => "raylib [shapes] example - double pendulum"; + + public ConfigFlags ConfigFlags => ConfigFlags.HighDpiWindow; + + // Simulation Parameters + private float l1, m1, theta1, w1; + private float l2, m2, theta2, w2; + private float lengthScaler; + private float totalM; + + private Vector2 previousPosition; + + // Scale length + private float L1; + private float L2; + + // Draw parameters + private float lineThick, trailThick; + private float fateAlpha; + + // Create framebuffer + private RenderTexture2D target; + + // Calculate pendulum end point + private static Vector2 CalculatePendulumEndPoint(float l, float theta) + { + return new(10 * l * MathF.Sin(theta), 10 * l * MathF.Cos(theta)); + } + + // Calculate double pendulum end point + private static Vector2 CalculateDoublePendulumEndPoint(float l1, float theta1, float l2, float theta2) + { + Vector2 endpoint1 = CalculatePendulumEndPoint(l1, theta1); + Vector2 endpoint2 = CalculatePendulumEndPoint(l2, theta2); + return new(endpoint1.X + endpoint2.X, endpoint1.Y + endpoint2.Y); + } + + public void Init() + { + // Simulation Parameters + l1 = 15.0f; m1 = 0.2f; theta1 = DEG2RAD * 170; w1 = 0; + l2 = 15.0f; m2 = 0.1f; theta2 = DEG2RAD * 0; w2 = 0; + lengthScaler = 0.1f; + totalM = m1 + m2; + + previousPosition = CalculateDoublePendulumEndPoint(l1, theta1, l2, theta2); + previousPosition.X += ((float)screenWidth / 2); + previousPosition.Y += ((float)screenHeight / 2 - 100); + + // Scale length + L1 = l1 * lengthScaler; + L2 = l2 * lengthScaler; + + // Draw parameters + lineThick = 20; trailThick = 2; + fateAlpha = 0.01f; + + // Create framebuffer + target = LoadRenderTexture(screenWidth, screenHeight); + SetTextureFilter(target.Texture, TextureFilter.Bilinear); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + float dt = GetFrameTime(); + float step = dt / SIMULATION_STEPS, step2 = step * step; + + // Update Physics - larger steps = better approximation + for (int i = 0; i < SIMULATION_STEPS; i++) + { + float delta = theta1 - theta2; + float sinD = MathF.Sin(delta), cosD = MathF.Cos(delta), cos2D = MathF.Cos(2 * delta); + float ww1 = w1 * w1, ww2 = w2 * w2; + + // Calculate a1 + float a1 = (-G * (2 * m1 + m2) * MathF.Sin(theta1) + - m2 * G * MathF.Sin(theta1 - 2 * theta2) + - 2 * sinD * m2 * (ww2 * L2 + ww1 * L1 * cosD)) + / (L1 * (2 * m1 + m2 - m2 * cos2D)); + + // Calculate a2 + float a2 = (2 * sinD * (ww1 * L1 * totalM + + G * totalM * MathF.Cos(theta1) + + ww2 * L2 * m2 * cosD)) + / (L2 * (2 * m1 + m2 - m2 * cos2D)); + + // Update thetas + theta1 += w1 * step + 0.5f * a1 * step2; + theta2 += w2 * step + 0.5f * a2 * step2; + + // Update omegas + w1 += a1 * step; + w2 += a2 * step; + } + + // Calculate position + Vector2 currentPosition = CalculateDoublePendulumEndPoint(l1, theta1, l2, theta2); + currentPosition.X += (float)screenWidth / 2; + currentPosition.Y += (float)screenHeight / 2 - 100; + + // Draw to render texture + BeginTextureMode(target); + // Draw a transparent rectangle - smaller alpha = longer trails + DrawRectangle(0, 0, screenWidth, screenHeight, Fade(Color.Black, fateAlpha)); + + // Draw trail + DrawCircleV(previousPosition, trailThick, Color.Red); + DrawLineEx(previousPosition, currentPosition, trailThick * 2, Color.Red); + EndTextureMode(); + + // Update previous position + previousPosition = currentPosition; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.Black); + + // Draw trails texture + DrawTextureRec(target.Texture, new Rectangle(0, 0, (float)target.Texture.Width, (float)-target.Texture.Height), new Vector2(0, 0), Color.White); + + // Draw double pendulum + DrawRectanglePro(new Rectangle(screenWidth / 2.0f, screenHeight / 2.0f - 100, 10 * l1, lineThick), + new Vector2(0, lineThick * 0.5f), 90 - RAD2DEG * theta1, Color.RayWhite); + + Vector2 endpoint1 = CalculatePendulumEndPoint(l1, theta1); + DrawRectanglePro(new Rectangle(screenWidth / 2.0f + endpoint1.X, screenHeight / 2.0f - 100 + endpoint1.Y, 10 * l2, lineThick), + new Vector2(0, lineThick * 0.5f), 90 - RAD2DEG * theta2, Color.RayWhite); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadRenderTexture(target); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.HighDpiWindow); + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - double pendulum"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new DoublePendulum(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/DrawCircleSector.cs b/Examples/Shapes/DrawCircleSector.cs index 88044be..2f593a8 100644 --- a/Examples/Shapes/DrawCircleSector.cs +++ b/Examples/Shapes/DrawCircleSector.cs @@ -75,11 +75,11 @@ public partial class DrawCircleSector : IExample // Draw GUI controls //------------------------------------------------------------------------------ - /*GuiSliderBar(new Rectangle( 600, 40, 120, 20), "StartAngle", TextFormat("%.2f", startAngle), ref startAngle, 0, 720); - GuiSliderBar(new Rectangle( 600, 70, 120, 20), "EndAngle", TextFormat("%.2f", endAngle), ref endAngle, 0, 720); + GuiSliderBar(new Rectangle(600, 40, 120, 20), "StartAngle", $"{startAngle:F2}", ref startAngle, 0, 720); + GuiSliderBar(new Rectangle(600, 70, 120, 20), "EndAngle", $"{endAngle:F2}", ref endAngle, 0, 720); - GuiSliderBar(new Rectangle( 600, 140, 120, 20), "Radius", TextFormat("%.2f", outerRadius), ref outerRadius, 0, 200); - GuiSliderBar(new Rectangle( 600, 170, 120, 20), "Segments", TextFormat("%.2f", segments), ref segments, 0, 100);*/ + GuiSliderBar(new Rectangle(600, 140, 120, 20), "Radius", $"{outerRadius:F2}", ref outerRadius, 0, 200); + GuiSliderBar(new Rectangle(600, 170, 120, 20), "Segments", $"{segments:F2}", ref segments, 0, 100); //------------------------------------------------------------------------------ minSegments = MathF.Truncate(MathF.Ceiling((endAngle - startAngle) / 90)); @@ -96,6 +96,28 @@ public partial class DrawCircleSector : IExample { } + //---------------------------------------------------------------------------------- + // Minimal raygui-like widgets (plain raylib re-implementation) + //---------------------------------------------------------------------------------- + private static void GuiSliderBar(Rectangle bounds, string textLeft, string textRight, ref float value, float minValue, float maxValue) + { + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + if (hover && IsMouseButtonDown(MouseButton.Left)) + { + value = minValue + ((mouse.X - bounds.X) / bounds.Width) * (maxValue - minValue); + if (value < minValue) value = minValue; + if (value > maxValue) value = maxValue; + } + + DrawRectangleRec(bounds, Color.LightGray); + float pct = (value - minValue) / (maxValue - minValue); + DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), Color.SkyBlue); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (!string.IsNullOrEmpty(textLeft)) DrawText(textLeft, (int)bounds.X - MeasureText(textLeft, 10) - 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + if (!string.IsNullOrEmpty(textRight)) DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + public static int Main() { // Initialization diff --git a/Examples/Shapes/DrawRectangleRounded.cs b/Examples/Shapes/DrawRectangleRounded.cs index 5700cf3..780c00e 100644 --- a/Examples/Shapes/DrawRectangleRounded.cs +++ b/Examples/Shapes/DrawRectangleRounded.cs @@ -86,15 +86,15 @@ public partial class DrawRectangleRounded : IExample // Draw GUI controls //------------------------------------------------------------------------------ - /*GuiSliderBar(new Rectangle( 640, 40, 105, 20 ), "Width", TextFormat("%.2f", width), ref width, 0, (float)GetScreenWidth() - 300); - GuiSliderBar(new Rectangle( 640, 70, 105, 20 ), "Height", TextFormat("%.2f", height), ref height, 0, (float)GetScreenHeight() - 50); - GuiSliderBar(new Rectangle( 640, 140, 105, 20 ), "Roundness", TextFormat("%.2f", roundness), ref roundness, 0.0f, 1.0f); - GuiSliderBar(new Rectangle( 640, 170, 105, 20 ), "Thickness", TextFormat("%.2f", lineThick), ref lineThick, 0, 20); - GuiSliderBar(new Rectangle( 640, 240, 105, 20), "Segments", TextFormat("%.2f", segments), ref segments, 0, 60); + GuiSliderBar(new Rectangle(640, 40, 105, 20), "Width", $"{width:F2}", ref width, 0, (float)GetScreenWidth() - 300); + GuiSliderBar(new Rectangle(640, 70, 105, 20), "Height", $"{height:F2}", ref height, 0, (float)GetScreenHeight() - 50); + GuiSliderBar(new Rectangle(640, 140, 105, 20), "Roundness", $"{roundness:F2}", ref roundness, 0.0f, 1.0f); + GuiSliderBar(new Rectangle(640, 170, 105, 20), "Thickness", $"{lineThick:F2}", ref lineThick, 0, 20); + GuiSliderBar(new Rectangle(640, 240, 105, 20), "Segments", $"{segments:F2}", ref segments, 0, 60); - GuiCheckBox(new Rectangle( 640, 320, 20, 20 ), "DrawRoundedRect", ref drawRoundedRect); - GuiCheckBox(new Rectangle( 640, 350, 20, 20 ), "DrawRoundedLines", ref drawRoundedLines); - GuiCheckBox(new Rectangle( 640, 380, 20, 20), "DrawRect", ref drawRect);*/ + GuiCheckBox(new Rectangle(640, 320, 20, 20), "DrawRoundedRect", ref drawRoundedRect); + GuiCheckBox(new Rectangle(640, 350, 20, 20), "DrawRoundedLines", ref drawRoundedLines); + GuiCheckBox(new Rectangle(640, 380, 20, 20), "DrawRect", ref drawRect); //------------------------------------------------------------------------------ var text = $"MODE: {((segments >= 4) ? "MANUAL" : "AUTO")}"; @@ -109,6 +109,39 @@ public partial class DrawRectangleRounded : IExample { } + //---------------------------------------------------------------------------------- + // Minimal raygui-like widgets (plain raylib re-implementation) + //---------------------------------------------------------------------------------- + private static void GuiSliderBar(Rectangle bounds, string textLeft, string textRight, ref float value, float minValue, float maxValue) + { + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + if (hover && IsMouseButtonDown(MouseButton.Left)) + { + value = minValue + ((mouse.X - bounds.X) / bounds.Width) * (maxValue - minValue); + if (value < minValue) value = minValue; + if (value > maxValue) value = maxValue; + } + + DrawRectangleRec(bounds, Color.LightGray); + float pct = (value - minValue) / (maxValue - minValue); + DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), Color.SkyBlue); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (!string.IsNullOrEmpty(textLeft)) DrawText(textLeft, (int)bounds.X - MeasureText(textLeft, 10) - 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + if (!string.IsNullOrEmpty(textRight)) DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + + private static void GuiCheckBox(Rectangle bounds, string text, ref bool active) + { + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + if (hover && IsMouseButtonPressed(MouseButton.Left)) active = !active; + + DrawRectangleLinesEx(bounds, 1, hover ? Color.DarkBlue : Color.Gray); + if (active) DrawRectangle((int)bounds.X + 4, (int)bounds.Y + 4, (int)bounds.Width - 8, (int)bounds.Height - 8, Color.DarkGray); + if (text != null) DrawText(text, (int)(bounds.X + bounds.Width + 8), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + public static int Main() { // Initialization diff --git a/Examples/Shapes/DrawRing.cs b/Examples/Shapes/DrawRing.cs index 8a42c03..cb4f5a3 100644 --- a/Examples/Shapes/DrawRing.cs +++ b/Examples/Shapes/DrawRing.cs @@ -112,17 +112,17 @@ public partial class DrawRing : IExample // Draw GUI controls //------------------------------------------------------------------------------ - /*GuiSliderBar(new Rectangle( 600, 40, 120, 20 ), "StartAngle", TextFormat("%.2f", startAngle), ref startAngle, -450, 450); - GuiSliderBar(new Rectangle( 600, 70, 120, 20 ), "EndAngle", TextFormat("%.2f", endAngle), ref endAngle, -450, 450); + GuiSliderBar(new Rectangle(600, 40, 120, 20), "StartAngle", $"{startAngle:F2}", ref startAngle, -450, 450); + GuiSliderBar(new Rectangle(600, 70, 120, 20), "EndAngle", $"{endAngle:F2}", ref endAngle, -450, 450); - GuiSliderBar(new Rectangle( 600, 140, 120, 20 ), "InnerRadius", TextFormat("%.2f", innerRadius), ref innerRadius, 0, 100); - GuiSliderBar(new Rectangle( 600, 170, 120, 20 ), "OuterRadius", TextFormat("%.2f", outerRadius), ref outerRadius, 0, 200); + GuiSliderBar(new Rectangle(600, 140, 120, 20), "InnerRadius", $"{innerRadius:F2}", ref innerRadius, 0, 100); + GuiSliderBar(new Rectangle(600, 170, 120, 20), "OuterRadius", $"{outerRadius:F2}", ref outerRadius, 0, 200); - GuiSliderBar(new Rectangle( 600, 240, 120, 20 ), "Segments", TextFormat("%.2f", segments), ref segments, 0, 100); + GuiSliderBar(new Rectangle(600, 240, 120, 20), "Segments", $"{segments:F2}", ref segments, 0, 100); - GuiCheckBox(new Rectangle( 600, 320, 20, 20 ), "Draw Ring", ref drawRing); - GuiCheckBox(new Rectangle( 600, 350, 20, 20 ), "Draw RingLines", ref drawRingLines); - GuiCheckBox(new Rectangle( 600, 380, 20, 20 ), "Draw CircleLines", ref drawCircleLines);*/ + GuiCheckBox(new Rectangle(600, 320, 20, 20), "Draw Ring", ref drawRing); + GuiCheckBox(new Rectangle(600, 350, 20, 20), "Draw RingLines", ref drawRingLines); + GuiCheckBox(new Rectangle(600, 380, 20, 20), "Draw CircleLines", ref drawCircleLines); //------------------------------------------------------------------------------ var minSegments = (int)MathF.Ceiling((endAngle - startAngle) / 90); @@ -139,6 +139,39 @@ public partial class DrawRing : IExample { } + //---------------------------------------------------------------------------------- + // Minimal raygui-like widgets (plain raylib re-implementation) + //---------------------------------------------------------------------------------- + private static void GuiSliderBar(Rectangle bounds, string textLeft, string textRight, ref float value, float minValue, float maxValue) + { + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + if (hover && IsMouseButtonDown(MouseButton.Left)) + { + value = minValue + ((mouse.X - bounds.X) / bounds.Width) * (maxValue - minValue); + if (value < minValue) value = minValue; + if (value > maxValue) value = maxValue; + } + + DrawRectangleRec(bounds, Color.LightGray); + float pct = (value - minValue) / (maxValue - minValue); + DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), Color.SkyBlue); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (!string.IsNullOrEmpty(textLeft)) DrawText(textLeft, (int)bounds.X - MeasureText(textLeft, 10) - 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + if (!string.IsNullOrEmpty(textRight)) DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + + private static void GuiCheckBox(Rectangle bounds, string text, ref bool active) + { + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + if (hover && IsMouseButtonPressed(MouseButton.Left)) active = !active; + + DrawRectangleLinesEx(bounds, 1, hover ? Color.DarkBlue : Color.Gray); + if (active) DrawRectangle((int)bounds.X + 4, (int)bounds.Y + 4, (int)bounds.Width - 8, (int)bounds.Height - 8, Color.DarkGray); + if (text != null) DrawText(text, (int)(bounds.X + bounds.Width + 8), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + public static int Main() { // Initialization diff --git a/Examples/Shapes/EasingsTestbed.cs b/Examples/Shapes/EasingsTestbed.cs new file mode 100644 index 0000000..7c864d7 --- /dev/null +++ b/Examples/Shapes/EasingsTestbed.cs @@ -0,0 +1,230 @@ +/******************************************************************************************* +* +* raylib [shapes] example - easings testbed +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 2.5, last time updated with raylib 2.5 +* +* Example contributed by Juan Miguel López (@flashback-fx) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2019-2025 Juan Miguel López (@flashback-fx) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using Examples.Shared; +using static Raylib_cs.Raylib; + +namespace Examples.Shapes; + +public partial class EasingsTestbed : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int FONT_SIZE = 20; + + private const float D_STEP = 20.0f; + private const float D_STEP_FINE = 2.0f; + private const float D_MIN = 1.0f; + private const float D_MAX = 10000.0f; + + public string Name => "Shapes / Easings Testbed"; + + public string Title => "raylib [shapes] example - easings testbed"; + + // Easing types + private const int EASE_LINEAR_NONE = 0; + private const int NUM_EASING_TYPES = 27; + private const int EASING_NONE = NUM_EASING_TYPES; + + // NoEase function, used when "no easing" is selected for any axis + // It just ignores all parameters besides b + private static float NoEase(float t, float b, float c, float d) + { + // Hack to avoid compiler warning (about unused variables) + float burn = t + b + c + d; + d += burn; + + return b; + } + + // Easing functions reference data + private string[] easingNames; + private Func[] easingFuncs; + + private Vector2 ballPosition; + private float t; // Current time (in any unit measure, but same unit as duration) + private float d; // Total time it should take to complete (duration) + private bool paused; + private bool boundedT; // If true, t will stop when d >= td, otherwise t will keep adding td to its value every loop + + private int easingX; // Easing selected for x axis + private int easingY; // Easing selected for y axis + + public void Init() + { + easingNames = new string[] + { + "EaseLinearNone", "EaseLinearIn", "EaseLinearOut", "EaseLinearInOut", + "EaseSineIn", "EaseSineOut", "EaseSineInOut", + "EaseCircIn", "EaseCircOut", "EaseCircInOut", + "EaseCubicIn", "EaseCubicOut", "EaseCubicInOut", + "EaseQuadIn", "EaseQuadOut", "EaseQuadInOut", + "EaseExpoIn", "EaseExpoOut", "EaseExpoInOut", + "EaseBackIn", "EaseBackOut", "EaseBackInOut", + "EaseBounceOut", "EaseBounceIn", "EaseBounceInOut", + "EaseElasticIn", "EaseElasticOut", "EaseElasticInOut", + "None", + }; + + easingFuncs = new Func[] + { + Easings.EaseLinearNone, Easings.EaseLinearIn, Easings.EaseLinearOut, Easings.EaseLinearInOut, + Easings.EaseSineIn, Easings.EaseSineOut, Easings.EaseSineInOut, + Easings.EaseCircIn, Easings.EaseCircOut, Easings.EaseCircInOut, + Easings.EaseCubicIn, Easings.EaseCubicOut, Easings.EaseCubicInOut, + Easings.EaseQuadIn, Easings.EaseQuadOut, Easings.EaseQuadInOut, + Easings.EaseExpoIn, Easings.EaseExpoOut, Easings.EaseExpoInOut, + Easings.EaseBackIn, Easings.EaseBackOut, Easings.EaseBackInOut, + Easings.EaseBounceOut, Easings.EaseBounceIn, Easings.EaseBounceInOut, + Easings.EaseElasticIn, Easings.EaseElasticOut, Easings.EaseElasticInOut, + NoEase, + }; + + ballPosition = new Vector2(100.0f, 100.0f); + + t = 0.0f; + d = 300.0f; + paused = true; + boundedT = true; + + easingX = EASING_NONE; + easingY = EASING_NONE; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.T)) boundedT = !boundedT; + + // Choose easing for the X axis + if (IsKeyPressed(KeyboardKey.Right)) + { + easingX++; + + if (easingX > EASING_NONE) easingX = 0; + } + else if (IsKeyPressed(KeyboardKey.Left)) + { + if (easingX == 0) easingX = EASING_NONE; + else easingX--; + } + + // Choose easing for the Y axis + if (IsKeyPressed(KeyboardKey.Down)) + { + easingY++; + + if (easingY > EASING_NONE) easingY = 0; + } + else if (IsKeyPressed(KeyboardKey.Up)) + { + if (easingY == 0) easingY = EASING_NONE; + else easingY--; + } + + // Change d (duration) value + if (IsKeyPressed(KeyboardKey.W) && (d < D_MAX - D_STEP)) d += D_STEP; + else if (IsKeyPressed(KeyboardKey.Q) && (d > D_MIN + D_STEP)) d -= D_STEP; + + if (IsKeyDown(KeyboardKey.S) && (d < D_MAX - D_STEP_FINE)) d += D_STEP_FINE; + else if (IsKeyDown(KeyboardKey.A) && (d > D_MIN + D_STEP_FINE)) d -= D_STEP_FINE; + + // Play, pause and restart controls + if (IsKeyPressed(KeyboardKey.Space) || IsKeyPressed(KeyboardKey.T) || + IsKeyPressed(KeyboardKey.Right) || IsKeyPressed(KeyboardKey.Left) || + IsKeyPressed(KeyboardKey.Down) || IsKeyPressed(KeyboardKey.Up) || + IsKeyPressed(KeyboardKey.W) || IsKeyPressed(KeyboardKey.Q) || + IsKeyDown(KeyboardKey.S) || IsKeyDown(KeyboardKey.A) || + (IsKeyPressed(KeyboardKey.Enter) && (boundedT == true) && (t >= d))) + { + t = 0.0f; + ballPosition.X = 100.0f; + ballPosition.Y = 100.0f; + paused = true; + } + + if (IsKeyPressed(KeyboardKey.Enter)) paused = !paused; + + // Movement computation + if (!paused && ((boundedT && t < d) || !boundedT)) + { + ballPosition.X = easingFuncs[easingX](t, 100.0f, 700.0f - 170.0f, d); + ballPosition.Y = easingFuncs[easingY](t, 100.0f, 400.0f - 170.0f, d); + t += 1.0f; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + // Draw information text + DrawText($"Easing x: {easingNames[easingX]}", 20, FONT_SIZE, FONT_SIZE, Color.LightGray); + DrawText($"Easing y: {easingNames[easingY]}", 20, FONT_SIZE * 2, FONT_SIZE, Color.LightGray); + DrawText($"t ({(boundedT == true ? 'b' : 'u')}) = {t:F2} d = {d:F2}", 20, FONT_SIZE * 3, FONT_SIZE, Color.LightGray); + + // Draw instructions text + DrawText("Use ENTER to play or pause movement, use SPACE to restart", 20, GetScreenHeight() - FONT_SIZE * 2, FONT_SIZE, Color.LightGray); + DrawText("Use Q and W or A and S keys to change duration", 20, GetScreenHeight() - FONT_SIZE * 3, FONT_SIZE, Color.LightGray); + DrawText("Use LEFT or RIGHT keys to choose easing for the x axis", 20, GetScreenHeight() - FONT_SIZE * 4, FONT_SIZE, Color.LightGray); + DrawText("Use UP or DOWN keys to choose easing for the y axis", 20, GetScreenHeight() - FONT_SIZE * 5, FONT_SIZE, Color.LightGray); + + // Draw ball + DrawCircleV(ballPosition, 16.0f, Color.Maroon); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - easings testbed"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new EasingsTestbed(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/EllipseCollision.cs b/Examples/Shapes/EllipseCollision.cs new file mode 100644 index 0000000..6647efe --- /dev/null +++ b/Examples/Shapes/EllipseCollision.cs @@ -0,0 +1,195 @@ +/******************************************************************************************* +* +* raylib [shapes] example - ellipse collision +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.5 +* +* Example contributed by Ziya (@Monjaris) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Ziya (@Monjaris) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shapes; + +public partial class EllipseCollision : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Ellipse Collision"; + + public string Title => "raylib [shapes] example - collision ellipses"; + + private Vector2 ellipseACenter; + private float ellipseARx; + private float ellipseARy; + + private Vector2 ellipseBCenter; + private float ellipseBRx; + private float ellipseBRy; + + // 0 = controlling A, 1 = controlling B + private int controlled; + + // Check if point is inside ellipse + private static bool CheckCollisionPointEllipse(Vector2 point, Vector2 center, float rx, float ry) + { + float dx = (point.X - center.X) / rx; + float dy = (point.Y - center.Y) / ry; + return (dx * dx + dy * dy) <= 1.0f; + } + + // Check if two ellipses collide + // Uses radial boundary distance in the direction between centers — scales correctly with radii + private static bool CheckCollisionEllipses(Vector2 c1, float rx1, float ry1, Vector2 c2, float rx2, float ry2) + { + float dx = c2.X - c1.X; + float dy = c2.Y - c1.Y; + float dist = MathF.Sqrt(dx * dx + dy * dy); + + // Ellipses are on top of each other + if (dist == 0.0f) + { + return true; + } + + float theta = MathF.Atan2(dy, dx); + float cosT = MathF.Cos(theta); + float sinT = MathF.Sin(theta); + + // Radial distance from center to ellipse boundary in direction theta + // r(theta) = (rx * ry) / sqrt((ry*cos)^2 + (rx*sin)^2) + float r1 = (rx1 * ry1) / MathF.Sqrt((ry1 * cosT) * (ry1 * cosT) + (rx1 * sinT) * (rx1 * sinT)); + float r2 = (rx2 * ry2) / MathF.Sqrt((ry2 * cosT) * (ry2 * cosT) + (rx2 * sinT) * (rx2 * sinT)); + + return dist <= (r1 + r2); + } + + public void Init() + { + ellipseACenter = new((float)screenWidth / 4, (float)screenHeight / 2); + ellipseARx = 120.0f; + ellipseARy = 70.0f; + + ellipseBCenter = new((float)screenWidth * 3 / 4, (float)screenHeight / 2); + ellipseBRx = 90.0f; + ellipseBRy = 140.0f; + + // 0 = controlling A, 1 = controlling B + controlled = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.A)) + { + controlled = 0; + } + if (IsKeyPressed(KeyboardKey.B)) + { + controlled = 1; + } + + if (controlled == 0) + { + ellipseACenter = GetMousePosition(); + } + else + { + ellipseBCenter = GetMousePosition(); + } + + bool ellipsesCollide = CheckCollisionEllipses( + ellipseACenter, ellipseARx, ellipseARy, + ellipseBCenter, ellipseBRx, ellipseBRy + ); + + bool mouseInA = CheckCollisionPointEllipse(GetMousePosition(), ellipseACenter, ellipseARx, ellipseARy); + bool mouseInB = CheckCollisionPointEllipse(GetMousePosition(), ellipseBCenter, ellipseBRx, ellipseBRy); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + DrawEllipse((int)ellipseACenter.X, (int)ellipseACenter.Y, ellipseARx, ellipseARy, ellipsesCollide ? Color.Red : Color.Blue); + + DrawEllipse((int)ellipseBCenter.X, (int)ellipseBCenter.Y, ellipseBRx, ellipseBRy, ellipsesCollide ? Color.Red : Color.Green); + + DrawEllipseLines((int)ellipseACenter.X, (int)ellipseACenter.Y, ellipseARx, ellipseARy, Color.White); + + DrawEllipseLines((int)ellipseBCenter.X, (int)ellipseBCenter.Y, ellipseBRx, ellipseBRy, Color.White); + + DrawCircleV(ellipseACenter, 4, Color.White); + DrawCircleV(ellipseBCenter, 4, Color.White); + + if (ellipsesCollide) + { + DrawText("ELLIPSES COLLIDE", screenWidth / 2 - 120, 40, 28, Color.Red); + } + else + { + DrawText("NO COLLISION", screenWidth / 2 - 80, 40, 28, Color.DarkGray); + } + + DrawText(controlled == 0 ? "Controlling: A" : "Controlling: B", 20, screenHeight - 40, 20, Color.Yellow); + + if (mouseInA && controlled != 0) + { + DrawText("Mouse inside ellipse A", 20, screenHeight - 70, 20, Color.Blue); + } + if (mouseInB && controlled != 1) + { + DrawText("Mouse inside ellipse B", 20, screenHeight - 70, 20, Color.Green); + } + + DrawText("Press [A] or [B] to switch control", 20, 20, 20, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - collision ellipses"); + SetTargetFPS(60); + + var game = new EllipseCollision(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/HilbertCurve.cs b/Examples/Shapes/HilbertCurve.cs new file mode 100644 index 0000000..94d80cf --- /dev/null +++ b/Examples/Shapes/HilbertCurve.cs @@ -0,0 +1,270 @@ +/******************************************************************************************* +* +* raylib [shapes] example - hilbert curve +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 5.6, last time updated with raylib 5.6 +* +* Example contributed by Hamza RAHAL (@hmz-rhl) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Hamza RAHAL (@hmz-rhl) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shapes; + +public partial class HilbertCurve : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Hilbert Curve"; + + public string Title => "raylib [shapes] example - hilbert curve"; + + private int order; + private float size; + private int strokeCount; + private Vector2[] hilbertPath; + + private int prevOrder; + private int prevSize; // NOTE: Size from slider is float but for comparison we use int + private int counter; + private float thick; + private bool animate; + + public void Init() + { + order = 2; + size = (float)GetScreenHeight(); + strokeCount = 0; + hilbertPath = LoadHilbertPath(order, size, out strokeCount); + + prevOrder = order; + prevSize = (int)size; + counter = 0; + thick = 2.0f; + animate = true; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Check if order or size have changed to regenerate + // NOTE: Size from slider is float but for comparison we use int + if ((prevOrder != order) || (prevSize != (int)size)) + { + hilbertPath = LoadHilbertPath(order, size, out strokeCount); + + if (animate) counter = 0; + else counter = strokeCount; + + prevOrder = order; + prevSize = (int)size; + } + //---------------------------------------------------------------------------------- + + // Draw + //-------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + if (counter < strokeCount) + { + // Draw Hilbert path animation, one stroke every frame + for (int i = 1; i <= counter; i++) + { + DrawLineEx(hilbertPath[i], hilbertPath[i - 1], thick, ColorFromHSV(((float)i / strokeCount) * 360.0f, 1.0f, 1.0f)); + } + + counter += 1; + } + else + { + // Draw full Hilbert path + for (int i = 1; i < strokeCount; i++) + { + DrawLineEx(hilbertPath[i], hilbertPath[i - 1], thick, ColorFromHSV(((float)i / strokeCount) * 360.0f, 1.0f, 1.0f)); + } + } + + // Draw UI (minimal raygui-like controls, raygui is not bound in raylib-cs) + GuiCheckBox(new Rectangle(450, 50, 20, 20), "ANIMATE GENERATION ON CHANGE", ref animate); + GuiSpinner(new Rectangle(585, 100, 180, 30), "HILBERT CURVE ORDER: ", ref order, 2, 8); + GuiSlider(new Rectangle(524, 150, 240, 24), "THICKNESS: ", null, ref thick, 1.0f, 10.0f); + GuiSlider(new Rectangle(524, 190, 240, 24), "TOTAL SIZE: ", null, ref size, 10.0f, GetScreenHeight() * 1.5f); + + EndDrawing(); + //-------------------------------------------------------------------------- + } + + public void Unload() + { + } + + //------------------------------------------------------------------------------------ + // Module Functions Definition + //------------------------------------------------------------------------------------ + // Load the whole Hilbert Path (including each U and their link) + private static Vector2[] LoadHilbertPath(int order, float size, out int strokeCount) + { + int N = 1 << order; + float len = size / N; + strokeCount = N * N; + + Vector2[] hilbertPath = new Vector2[strokeCount]; + + for (int i = 0; i < strokeCount; i++) + { + hilbertPath[i] = ComputeHilbertStep(order, i); + hilbertPath[i].X = hilbertPath[i].X * len + len / 2.0f; + hilbertPath[i].Y = hilbertPath[i].Y * len + len / 2.0f; + } + + return hilbertPath; + } + + // Compute Hilbert path U positions + private static Vector2 ComputeHilbertStep(int order, int index) + { + // Hilbert points base pattern + Vector2[] hilbertPoints = new Vector2[4] + { + new Vector2(0, 0), + new Vector2(0, 1), + new Vector2(1, 1), + new Vector2(1, 0), + }; + + int hilbertIndex = index & 3; + Vector2 vect = hilbertPoints[hilbertIndex]; + float temp = 0.0f; + int len = 0; + + for (int j = 1; j < order; j++) + { + index = index >> 2; + hilbertIndex = index & 3; + len = 1 << j; + + switch (hilbertIndex) + { + case 0: + { + temp = vect.X; + vect.X = vect.Y; + vect.Y = temp; + } break; + case 2: + { + vect.X += len; + vect.Y += len; + } break; + case 1: vect.Y += len; break; + case 3: + { + temp = len - 1 - vect.X; + vect.X = 2 * len - 1 - vect.Y; + vect.Y = temp; + } break; + default: break; + } + } + + return vect; + } + + //---------------------------------------------------------------------------------- + // Minimal raygui-like widgets (plain raylib re-implementation) + //---------------------------------------------------------------------------------- + private static void GuiCheckBox(Rectangle bounds, string text, ref bool active) + { + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + if (hover && IsMouseButtonPressed(MouseButton.Left)) active = !active; + + DrawRectangleLinesEx(bounds, 1, hover ? Color.DarkBlue : Color.Gray); + if (active) DrawRectangle((int)bounds.X + 4, (int)bounds.Y + 4, (int)bounds.Width - 8, (int)bounds.Height - 8, Color.DarkGray); + if (text != null) DrawText(text, (int)(bounds.X + bounds.Width + 8), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + + private static void GuiSpinner(Rectangle bounds, string text, ref int value, int minValue, int maxValue) + { + Vector2 mouse = GetMousePosition(); + Rectangle left = new Rectangle(bounds.X, bounds.Y, bounds.Height, bounds.Height); + Rectangle right = new Rectangle(bounds.X + bounds.Width - bounds.Height, bounds.Y, bounds.Height, bounds.Height); + Rectangle mid = new Rectangle(bounds.X + bounds.Height, bounds.Y, bounds.Width - 2 * bounds.Height, bounds.Height); + + if (CheckCollisionPointRec(mouse, left) && IsMouseButtonPressed(MouseButton.Left) && value > minValue) value--; + if (CheckCollisionPointRec(mouse, right) && IsMouseButtonPressed(MouseButton.Left) && value < maxValue) value++; + + DrawRectangleRec(mid, Color.RayWhite); + DrawRectangleLinesEx(mid, 1, Color.Gray); + DrawRectangleRec(left, Color.LightGray); + DrawRectangleLinesEx(left, 1, Color.Gray); + DrawRectangleRec(right, Color.LightGray); + DrawRectangleLinesEx(right, 1, Color.Gray); + DrawText("-", (int)(left.X + left.Width / 2 - 2), (int)(left.Y + left.Height / 2 - 5), 10, Color.DarkGray); + DrawText("+", (int)(right.X + right.Width / 2 - 3), (int)(right.Y + right.Height / 2 - 5), 10, Color.DarkGray); + string vs = value.ToString(); + DrawText(vs, (int)(mid.X + mid.Width / 2 - MeasureText(vs, 10) / 2), (int)(mid.Y + mid.Height / 2 - 5), 10, Color.DarkGray); + if (text != null) DrawText(text, (int)bounds.X - MeasureText(text, 10) - 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + + private static void GuiSlider(Rectangle bounds, string textLeft, string textRight, ref float value, float minValue, float maxValue) + { + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + if (hover && IsMouseButtonDown(MouseButton.Left)) + { + value = minValue + ((mouse.X - bounds.X) / bounds.Width) * (maxValue - minValue); + if (value < minValue) value = minValue; + if (value > maxValue) value = maxValue; + } + + DrawRectangleRec(bounds, Color.LightGray); + float pct = (value - minValue) / (maxValue - minValue); + float handleX = bounds.X + pct * bounds.Width; + DrawRectangle((int)(handleX - 5), (int)bounds.Y, 10, (int)bounds.Height, Color.DarkGray); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (textLeft != null) DrawText(textLeft, (int)bounds.X - MeasureText(textLeft, 10) - 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + if (textRight != null) DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - hilbert curve"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new HilbertCurve(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/Kaleidoscope.cs b/Examples/Shapes/Kaleidoscope.cs new file mode 100644 index 0000000..85827ba --- /dev/null +++ b/Examples/Shapes/Kaleidoscope.cs @@ -0,0 +1,224 @@ +/******************************************************************************************* +* +* raylib [shapes] example - kaleidoscope +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.6 +* +* Example contributed by Hugo ARNAL (@hugoarnal) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Hugo ARNAL (@hugoarnal) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; + +namespace Examples.Shapes; + +public partial class Kaleidoscope : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MAX_DRAW_LINES = 8192; + + // Line data type + private struct Line + { + public Vector2 Start; + public Vector2 End; + } + + public string Name => "Shapes / Kaleidoscope"; + + public string Title => "raylib [shapes] example - kaleidoscope"; + + public int TargetFps => 20; + + // Lines array as a global static variable to be stored + // in heap and avoid potential stack overflow (on Web platform) + private Line[] lines; + + // Line drawing properties + private int symmetry; + private float angle; + private float thickness; + private Rectangle resetButtonRec; + private Rectangle backButtonRec; + private Rectangle nextButtonRec; + private Vector2 mousePos; + private Vector2 prevMousePos; + private Vector2 scaleVector; + private Vector2 offset; + + private Camera2D camera; + + private int currentLineCounter; + private int totalLineCounter; + private bool resetButtonClicked; + private bool backButtonClicked; + private bool nextButtonClicked; + + public void Init() + { + lines = new Line[MAX_DRAW_LINES]; + + // Line drawing properties + symmetry = 6; + angle = 360.0f / (float)symmetry; + thickness = 3.0f; + resetButtonRec = new(screenWidth - 55.0f, 5.0f, 50, 25); + backButtonRec = new(screenWidth - 55.0f, screenHeight - 30.0f, 25, 25); + nextButtonRec = new(screenWidth - 30.0f, screenHeight - 30.0f, 25, 25); + mousePos = new(0, 0); + prevMousePos = new(0, 0); + scaleVector = new(1.0f, -1.0f); + offset = new((float)screenWidth / 2.0f, (float)screenHeight / 2.0f); + + camera = new(); + camera.Target = new(0, 0); + camera.Offset = offset; + camera.Rotation = 0.0f; + camera.Zoom = 1.0f; + + currentLineCounter = 0; + totalLineCounter = 0; + resetButtonClicked = false; + backButtonClicked = false; + nextButtonClicked = false; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + prevMousePos = mousePos; + mousePos = GetMousePosition(); + + Vector2 lineStart = Vector2Subtract(mousePos, offset); + Vector2 lineEnd = Vector2Subtract(prevMousePos, offset); + + if ( + IsMouseButtonDown(MouseButton.Left) + && !CheckCollisionPointRec(mousePos, resetButtonRec) + && !CheckCollisionPointRec(mousePos, backButtonRec) + && !CheckCollisionPointRec(mousePos, nextButtonRec) + ) + { + for (int s = 0; (s < symmetry) && (totalLineCounter < (MAX_DRAW_LINES - 1)); s++) + { + lineStart = Vector2Rotate(lineStart, angle * DEG2RAD); + lineEnd = Vector2Rotate(lineEnd, angle * DEG2RAD); + + // Store mouse line + lines[totalLineCounter].Start = lineStart; + lines[totalLineCounter].End = lineEnd; + + // Store reflective line + lines[totalLineCounter + 1].Start = Vector2Multiply(lineStart, scaleVector); + lines[totalLineCounter + 1].End = Vector2Multiply(lineEnd, scaleVector); + + totalLineCounter += 2; + currentLineCounter = totalLineCounter; + } + } + + if (resetButtonClicked) + { + Array.Clear(lines, 0, MAX_DRAW_LINES); + currentLineCounter = 0; + totalLineCounter = 0; + } + + if (backButtonClicked && (currentLineCounter > 0)) + { + currentLineCounter -= 1; + } + + if (nextButtonClicked && (currentLineCounter < MAX_DRAW_LINES) && ((currentLineCounter + 1) <= totalLineCounter)) + { + currentLineCounter += 1; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + BeginMode2D(camera); + + for (int s = 0; s < symmetry; s++) + { + for (int i = 0; i < currentLineCounter; i += 2) + { + DrawLineEx(lines[i].Start, lines[i].End, thickness, Color.Black); + DrawLineEx(lines[i + 1].Start, lines[i + 1].End, thickness, Color.Black); + } + } + + EndMode2D(); + + // NOTE: raygui is not bound in raylib-cs, so the on-screen back/next/reset + // controls are unavailable; drawing with the mouse still works. + //------------------------------------------------------------------------------ + /* + if ((currentLineCounter - 1) < 0) GuiDisable(); + + backButtonClicked = GuiButton(backButtonRec, "<"); + GuiEnable(); + + if ((currentLineCounter + 1) > totalLineCounter) GuiDisable(); + + nextButtonClicked = GuiButton(nextButtonRec, ">"); + GuiEnable(); + resetButtonClicked = GuiButton(resetButtonRec, "Reset"); + */ + //------------------------------------------------------------------------------ + + DrawText($"LINES: {currentLineCounter}/{MAX_DRAW_LINES}", 10, screenHeight - 30, 20, Color.Maroon); + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - kaleidoscope"); + + SetTargetFPS(20); + //-------------------------------------------------------------------------------------- + + var game = new Kaleidoscope(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/LinesDrawing.cs b/Examples/Shapes/LinesDrawing.cs new file mode 100644 index 0000000..7e4fd9e --- /dev/null +++ b/Examples/Shapes/LinesDrawing.cs @@ -0,0 +1,173 @@ +/******************************************************************************************* +* +* raylib [shapes] example - lines drawing +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 5.6 +* +* Example contributed by Robin (@RobinsAviary) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Robin (@RobinsAviary) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; + +namespace Examples.Shapes; + +public partial class LinesDrawing : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Lines Drawing"; + + public string Title => "raylib [shapes] example - lines drawing"; + + // Hint text that shows before you click the screen + private bool startText; + + // The mouse's position on the previous frame + private Vector2 mousePositionPrevious; + + // The canvas to draw lines on + private RenderTexture2D canvas; + + // The line's thickness + private float lineThickness; + // The lines hue (in HSV, from 0-360) + private float lineHue; + + public void Init() + { + // Hint text that shows before you click the screen + startText = true; + + // The mouse's position on the previous frame + mousePositionPrevious = GetMousePosition(); + + // The canvas to draw lines on + canvas = LoadRenderTexture(screenWidth, screenHeight); + + // The line's thickness + lineThickness = 8.0f; + // The lines hue (in HSV, from 0-360) + lineHue = 0.0f; + + // Clear the canvas to the background color + BeginTextureMode(canvas); + ClearBackground(Color.RayWhite); + EndTextureMode(); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Disable the hint text once the user clicks + if (IsMouseButtonPressed(MouseButton.Left) && startText) startText = false; + + // Clear the canvas when the user middle-clicks + if (IsMouseButtonPressed(MouseButton.Middle)) + { + BeginTextureMode(canvas); + ClearBackground(Color.RayWhite); + EndTextureMode(); + } + + // Store whether the left and right buttons are down + bool leftButtonDown = IsMouseButtonDown(MouseButton.Left); + bool rightButtonDown = IsMouseButtonDown(MouseButton.Right); + + if (leftButtonDown || rightButtonDown) + { + // The color for the line + Color drawColor = Color.White; + + if (leftButtonDown) + { + // Increase the hue value by the distance our cursor has moved since the last frame (divided by 3) + lineHue += Vector2Distance(mousePositionPrevious, GetMousePosition())/3.0f; + + // While the hue is >=360, subtract it to bring it down into the range 0-360 + // This is more visually accurate than resetting to zero + while (lineHue >= 360.0f) lineHue -= 360.0f; + + // Create the final color + drawColor = ColorFromHSV(lineHue, 1.0f, 1.0f); + } + else if (rightButtonDown) drawColor = Color.RayWhite; // Use the background color as an "eraser" + + // Draw the line onto the canvas + BeginTextureMode(canvas); + // Circles act as "caps", smoothing corners + DrawCircleV(mousePositionPrevious, lineThickness/2.0f, drawColor); + DrawCircleV(GetMousePosition(), lineThickness/2.0f, drawColor); + DrawLineEx(mousePositionPrevious, GetMousePosition(), lineThickness, drawColor); + EndTextureMode(); + } + + // Update line thickness based on mousewheel + lineThickness += GetMouseWheelMove(); + lineThickness = Clamp(lineThickness, 1.0f, 500.0f); + + // Update mouse's previous position + mousePositionPrevious = GetMousePosition(); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + // Draw the render texture to the screen, flipped vertically to make it appear top-side up + DrawTextureRec(canvas.Texture, new Rectangle(0.0f, 0.0f, (float)canvas.Texture.Width, (float)-canvas.Texture.Height), Vector2Zero(), Color.White); + + // Draw the preview circle + if (!leftButtonDown) DrawCircleLinesV(GetMousePosition(), lineThickness/2.0f, new Color(127, 127, 127, 127)); + + // Draw the hint text + if (startText) DrawText("try clicking and dragging!", 275, 215, 20, Color.LightGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadRenderTexture(canvas); // Unload the canvas render texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - lines drawing"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new LinesDrawing(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/MathAngleRotation.cs b/Examples/Shapes/MathAngleRotation.cs new file mode 100644 index 0000000..6ea45a0 --- /dev/null +++ b/Examples/Shapes/MathAngleRotation.cs @@ -0,0 +1,141 @@ +/******************************************************************************************* +* +* raylib [shapes] example - math angle rotation +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 5.6 +* +* Example contributed by Kris (@krispy-snacc) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Kris (@krispy-snacc) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shapes; + +public partial class MathAngleRotation : IExample +{ + private const int screenWidth = 720; + private const int screenHeight = 400; + + public string Name => "Shapes / Math Angle Rotation"; + + public string Title => "raylib [shapes] example - math angle rotation"; + + public int Width => screenWidth; + + public int Height => screenHeight; + + private Vector2 center; + private const float lineLength = 150.0f; + + // Predefined angles for fixed lines + private int[] angles; + private int numAngles; + + private float totalAngle; // Animated rotation angle + + public void Init() + { + center = new Vector2(screenWidth/2.0f, screenHeight/2.0f); + + // Predefined angles for fixed lines + angles = new[] { 0, 30, 60, 90 }; + numAngles = angles.Length; + + totalAngle = 0.0f; // Animated rotation angle + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + totalAngle += 1.0f; // degrees per frame + if (totalAngle >= 360.0f) totalAngle -= 360.0f; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.White); + + DrawText("Fixed angles + rotating line", 10, 10, 20, Color.LightGray); + + // Draw fixed-angle lines with colorful gradient + for (int i = 0; i < numAngles; i++) + { + float rad = angles[i]*DEG2RAD; + Vector2 end = new Vector2(center.X + MathF.Cos(rad)*lineLength, + center.Y + MathF.Sin(rad)*lineLength); + + // Gradient color from green → cyan → blue → magenta + Color col; + switch(i) + { + case 0: col = Color.Green; break; + case 1: col = Color.Orange; break; + case 2: col = Color.Blue; break; + case 3: col = Color.Magenta; break; + default: col = Color.White; break; + } + + DrawLineEx(center, end, 5.0f, col); + + // Draw angle label slightly offset along the line + Vector2 textPos = new Vector2(center.X + MathF.Cos(rad)*(lineLength + 20), + center.Y + MathF.Sin(rad)*(lineLength + 20)); + DrawText($"{angles[i]}°", (int)textPos.X, (int)textPos.Y, 20, col); + } + + // Draw animated rotating line with changing color + float animRad = totalAngle*DEG2RAD; + Vector2 animEnd = new Vector2(center.X + MathF.Cos(animRad)*lineLength, + center.Y + MathF.Sin(animRad)*lineLength); + + // Cycle through HSV colors for animated line + Color animCol = ColorFromHSV(totalAngle % 360.0f, 0.8f, 0.9f); + DrawLineEx(center, animEnd, 5.0f, animCol); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - math angle rotation"); + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new MathAngleRotation(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/MathSineCosine.cs b/Examples/Shapes/MathSineCosine.cs new file mode 100644 index 0000000..efe2286 --- /dev/null +++ b/Examples/Shapes/MathSineCosine.cs @@ -0,0 +1,212 @@ +/******************************************************************************************* +* +* raylib [shapes] example - math sine cosine +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Jopestpe (@jopestpe) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Jopestpe (@jopestpe) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; + +namespace Examples.Shapes; + +public partial class MathSineCosine : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + // Wave points for sine/cosine visualization + private const int WAVE_POINTS = 36; + + public string Name => "Shapes / Math Sine Cosine"; + + public string Title => "raylib [shapes] example - math sine cosine"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + private Vector2[] sinePoints; + private Vector2[] cosPoints; + private Vector2 center; + private Rectangle start; + private float radius; + private float angle; + private bool pause; + + public void Init() + { + sinePoints = new Vector2[WAVE_POINTS]; + cosPoints = new Vector2[WAVE_POINTS]; + center = new((screenWidth / 2.0f) - 30.0f, screenHeight / 2.0f); + start = new(20.0f, screenHeight - 120.0f, 200.0f, 100.0f); + radius = 130.0f; + angle = 0.0f; + pause = false; + + for (int i = 0; i < WAVE_POINTS; i++) + { + float t = i / (float)(WAVE_POINTS - 1); + float currentAngle = t * 360.0f * DEG2RAD; + sinePoints[i] = new(start.X + t * start.Width, start.Y + start.Height / 2.0f - MathF.Sin(currentAngle) * (start.Height / 2.0f)); + cosPoints[i] = new(start.X + t * start.Width, start.Y + start.Height / 2.0f - MathF.Cos(currentAngle) * (start.Height / 2.0f)); + } + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + float angleRad = angle * DEG2RAD; + float cosRad = MathF.Cos(angleRad); + float sinRad = MathF.Sin(angleRad); + + Vector2 point = new(center.X + cosRad * radius, center.Y - sinRad * radius); + Vector2 limitMin = new(center.X - radius, center.Y - radius); + Vector2 limitMax = new(center.X + radius, center.Y + radius); + + float complementary = 90.0f - angle; + float supplementary = 180.0f - angle; + float explementary = 360.0f - angle; + + float tangent = Clamp(MathF.Tan(angleRad), -10.0f, 10.0f); + float cotangent = (MathF.Abs(tangent) > 0.001f) ? Clamp(1.0f / tangent, -radius, radius) : 0.0f; + Vector2 tangentPoint = new(center.X + radius, center.Y - tangent * radius); + Vector2 cotangentPoint = new(center.X + cotangent * radius, center.Y - radius); + + angle = Wrap(angle + (!pause ? 1.0f : 0.0f), 0.0f, 360.0f); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // Cotangent (orange) + DrawLineEx(new Vector2(center.X, limitMin.Y), new Vector2(cotangentPoint.X, limitMin.Y), 2.0f, Color.Orange); + DrawLineDashed(center, cotangentPoint, 10, 4, Color.Orange); + + // Side background + DrawLine(580, 0, 580, GetScreenHeight(), new Color(218, 218, 218, 255)); + DrawRectangle(580, 0, GetScreenWidth(), GetScreenHeight(), new Color(232, 232, 232, 255)); + + // Base circle and axes + DrawCircleLinesV(center, radius, Color.Gray); + DrawLineEx(new Vector2(center.X, limitMin.Y), new Vector2(center.X, limitMax.Y), 1.0f, Color.Gray); + DrawLineEx(new Vector2(limitMin.X, center.Y), new Vector2(limitMax.X, center.Y), 1.0f, Color.Gray); + + // Wave graph axes + DrawLineEx(new Vector2(start.X, start.Y), new Vector2(start.X, start.Y + start.Height), 2.0f, Color.Gray); + DrawLineEx(new Vector2(start.X + start.Width, start.Y), new Vector2(start.X + start.Width, start.Y + start.Height), 2.0f, Color.Gray); + DrawLineEx(new Vector2(start.X, start.Y + start.Height / 2), new Vector2(start.X + start.Width, start.Y + start.Height / 2), 2.0f, Color.Gray); + + // Wave graph axis labels + DrawText("1", (int)start.X - 8, (int)start.Y, 6, Color.Gray); + DrawText("0", (int)start.X - 8, (int)start.Y + (int)start.Height / 2 - 6, 6, Color.Gray); + DrawText("-1", (int)start.X - 12, (int)start.Y + (int)start.Height - 8, 6, Color.Gray); + DrawText("0", (int)start.X - 2, (int)start.Y + (int)start.Height + 4, 6, Color.Gray); + DrawText("360", (int)start.X + (int)start.Width - 8, (int)start.Y + (int)start.Height + 4, 6, Color.Gray); + + // Sine (red - vertical) + DrawLineEx(new Vector2(center.X, center.Y), new Vector2(center.X, point.Y), 2.0f, Color.Red); + DrawLineDashed(new Vector2(point.X, center.Y), new Vector2(point.X, point.Y), 10, 4, Color.Red); + DrawText($"Sine {sinRad:0.00}", 640, 190, 6, Color.Red); + DrawCircleV(new Vector2(start.X + (angle / 360.0f) * start.Width, start.Y + ((-sinRad + 1) * start.Height / 2.0f)), 4.0f, Color.Red); + fixed (Vector2* p = sinePoints) DrawSplineLinear(p, WAVE_POINTS, 1.0f, Color.Red); + + // Cosine (blue - horizontal) + DrawLineEx(new Vector2(center.X, center.Y), new Vector2(point.X, center.Y), 2.0f, Color.Blue); + DrawLineDashed(new Vector2(center.X, point.Y), new Vector2(point.X, point.Y), 10, 4, Color.Blue); + DrawText($"Cosine {cosRad:0.00}", 640, 210, 6, Color.Blue); + DrawCircleV(new Vector2(start.X + (angle / 360.0f) * start.Width, start.Y + ((-cosRad + 1) * start.Height / 2.0f)), 4.0f, Color.Blue); + fixed (Vector2* p = cosPoints) DrawSplineLinear(p, WAVE_POINTS, 1.0f, Color.Blue); + + // Tangent (purple) + DrawLineEx(new Vector2(limitMax.X, center.Y), new Vector2(limitMax.X, tangentPoint.Y), 2.0f, Color.Purple); + DrawLineDashed(center, tangentPoint, 10, 4, Color.Purple); + DrawText($"Tangent {tangent:0.00}", 640, 230, 6, Color.Purple); + + // Cotangent (orange) + DrawText($"Cotangent {cotangent:0.00}", 640, 250, 6, Color.Orange); + + // Complementary angle (beige) + DrawCircleSectorLines(center, radius * 0.6f, -angle, -90.0f, 36, Color.Beige); + DrawText($"Complementary {complementary:0}°", 640, 150, 6, Color.Beige); + + // Supplementary angle (darkblue) + DrawCircleSectorLines(center, radius * 0.5f, -angle, -180.0f, 36, Color.DarkBlue); + DrawText($"Supplementary {supplementary:0}°", 640, 130, 6, Color.DarkBlue); + + // Explementary angle (pink) + DrawCircleSectorLines(center, radius * 0.4f, -angle, -360.0f, 36, Color.Pink); + DrawText($"Explementary {explementary:0}°", 640, 170, 6, Color.Pink); + + // Current angle - arc (lime), radius (black), endpoint (black) + DrawCircleSectorLines(center, radius * 0.7f, -angle, 0.0f, 36, Color.Lime); + DrawLineEx(new Vector2(center.X, center.Y), point, 2.0f, Color.Black); + DrawCircleV(point, 4.0f, Color.Black); + + // Draw GUI controls + // NOTE: raygui is not bound in raylib-cs, so the Pause toggle and Angle slider + // are unavailable; the angle animates continuously. + //------------------------------------------------------------------------------ + /* + GuiSetStyle(LABEL, TEXT_COLOR_NORMAL, ColorToInt(GRAY)); + GuiToggle((Rectangle){ 640, 70, 120, 20}, TextFormat("Pause"), &pause); + GuiSetStyle(LABEL, TEXT_COLOR_NORMAL, ColorToInt(LIME)); + GuiSliderBar((Rectangle){ 640, 40, 120, 20}, "Angle", TextFormat("%.0f°", angle), &angle, 0.0f, 360.0f); + + // Angle values panel + GuiGroupBox((Rectangle){ 620, 110, 140, 170}, "Angle Values"); + */ + //------------------------------------------------------------------------------ + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - math sine cosine"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new MathSineCosine(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/MouseTrail.cs b/Examples/Shapes/MouseTrail.cs new file mode 100644 index 0000000..82bc57f --- /dev/null +++ b/Examples/Shapes/MouseTrail.cs @@ -0,0 +1,127 @@ +/******************************************************************************************* +* +* raylib [shapes] example - Draw a mouse trail (position history) +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 5.6 +* +* Example contributed by Balamurugan R (@Bala050814]) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Balamurugan R (@Bala050814) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shapes; + +public partial class MouseTrail : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + // Define the maximum number of positions to store in the trail + private const int MAX_TRAIL_LENGTH = 30; + + public string Name => "Shapes / Mouse Trail"; + + public string Title => "raylib [shapes] example - mouse trail"; + + // Array to store the history of mouse positions (our fixed-size queue) + private Vector2[] trailPositions; + + public void Init() + { + // Array to store the history of mouse positions (our fixed-size queue) + trailPositions = new Vector2[MAX_TRAIL_LENGTH]; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + Vector2 mousePosition = GetMousePosition(); + + // Shift all existing positions backward by one slot in the array + // The last element (the oldest position) is dropped + for (int i = MAX_TRAIL_LENGTH - 1; i > 0; i--) + { + trailPositions[i] = trailPositions[i - 1]; + } + + // Store the new, current mouse position at the start of the array (Index 0) + trailPositions[0] = mousePosition; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.Black); + + // Draw the trail by looping through the history array + for (int i = 0; i < MAX_TRAIL_LENGTH; i++) + { + // Ensure we skip drawing if the array hasn't been fully filled on startup + if ((trailPositions[i].X != 0.0f) || (trailPositions[i].Y != 0.0f)) + { + // Calculate relative trail strength (ratio is near 1.0 for new, near 0.0 for old) + float ratio = (float)(MAX_TRAIL_LENGTH - i)/MAX_TRAIL_LENGTH; + + // Fade effect: oldest positions are more transparent + // Fade (color, alpha) - alpha is 0.5 to 1.0 based on ratio + Color trailColor = Fade(Color.SkyBlue, ratio*0.5f + 0.5f); + + // Size effect: oldest positions are smaller + float trailRadius = 15.0f*ratio; + + DrawCircleV(trailPositions[i], trailRadius, trailColor); + } + } + + // Draw a distinct white circle for the current mouse position (Index 0) + DrawCircleV(mousePosition, 15.0f, Color.White); + + DrawText("Move the mouse to see the trail effect!", 10, screenHeight - 30, 20, Color.LightGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - mouse trail"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new MouseTrail(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/PenroseTile.cs b/Examples/Shapes/PenroseTile.cs new file mode 100644 index 0000000..3dc630a --- /dev/null +++ b/Examples/Shapes/PenroseTile.cs @@ -0,0 +1,283 @@ +/******************************************************************************************* +* +* raylib [shapes] example - penrose tile +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 6.0 +* Based on: https://processing.org/examples/penrosetile.html +* +* Example contributed by David Buzatto (@davidbuzatto) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 David Buzatto (@davidbuzatto) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using System.Collections.Generic; +using System.Text; +using static Raylib_cs.Raylib; + +namespace Examples.Shapes; + +public partial class PenroseTile : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Penrose Tile"; + + public string Title => "raylib [shapes] example - penrose tile"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + public int TargetFps => 120; + + //---------------------------------------------------------------------------------- + // Types and Structures Definition + //---------------------------------------------------------------------------------- + private struct TurtleState + { + public Vector2 origin; + public float angle; + } + + private class PenroseLSystem + { + public int steps; + public StringBuilder production; + public string ruleW; + public string ruleX; + public string ruleY; + public string ruleZ; + public float drawLength; + public float theta; + } + + //---------------------------------------------------------------------------------- + // Global Variables Definition + //---------------------------------------------------------------------------------- + private Stack turtleStack; + + private const float drawLength = 460.0f; + private int minGenerations; + private int maxGenerations; + private int generations; + private PenroseLSystem ls; + + public void Init() + { + turtleStack = new Stack(); + + minGenerations = 0; + maxGenerations = 4; + generations = 0; + + // Initialize new penrose tile + ls = CreatePenroseLSystem(drawLength * (generations / (float)maxGenerations)); + for (int i = 0; i < generations; i++) BuildProductionStep(ls); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + bool rebuild = false; + if (IsKeyPressed(KeyboardKey.Up)) + { + if (generations < maxGenerations) + { + generations++; + rebuild = true; + } + } + else if (IsKeyPressed(KeyboardKey.Down)) + { + if (generations > minGenerations) + { + generations--; + if (generations > 0) rebuild = true; + } + } + + if (rebuild) + { + ls = CreatePenroseLSystem(drawLength * (generations / (float)maxGenerations)); + for (int i = 0; i < generations; i++) BuildProductionStep(ls); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + if (generations > 0) DrawPenroseLSystem(ls); + + DrawText("penrose l-system", 10, 10, 20, Color.DarkGray); + DrawText("press up or down to change generations", 10, 30, 20, Color.DarkGray); + DrawText($"generations: {generations}", 10, 50, 20, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + //---------------------------------------------------------------------------------- + // Module Functions Definition + //---------------------------------------------------------------------------------- + // Push turtle state for next step + private void PushTurtleState(TurtleState state) + { + turtleStack.Push(state); + } + + // Pop turtle state step + private TurtleState PopTurtleState() + { + if (turtleStack.Count > 0) return turtleStack.Pop(); + else TraceLog(TraceLogLevel.Warning, "TURTLE STACK UNDERFLOW!"); + + return new TurtleState(); + } + + // Create a new penrose tile structure + private static PenroseLSystem CreatePenroseLSystem(float drawLength) + { + PenroseLSystem ls = new PenroseLSystem + { + steps = 0, + ruleW = "YF++ZF4-XF[-YF4-WF]++", + ruleX = "+YF--ZF[3-WF--XF]+", + ruleY = "-WF++XF[+++YF++ZF]-", + ruleZ = "--YF++++WF[+ZF++++XF]--XF", + drawLength = drawLength, + theta = 36.0f // Degrees + }; + + ls.production = new StringBuilder("[X]++[X]++[X]++[X]++[X]"); + + return ls; + } + + // Build next penrose step + private static void BuildProductionStep(PenroseLSystem ls) + { + StringBuilder newProduction = new StringBuilder(); + + string production = ls.production.ToString(); + + for (int i = 0; i < production.Length; i++) + { + char step = production[i]; + switch (step) + { + case 'W': newProduction.Append(ls.ruleW); break; + case 'X': newProduction.Append(ls.ruleX); break; + case 'Y': newProduction.Append(ls.ruleY); break; + case 'Z': newProduction.Append(ls.ruleZ); break; + default: + { + if (step != 'F') newProduction.Append(step); + } break; + } + } + + ls.drawLength *= 0.5f; + ls.production = newProduction; + } + + // Draw penrose tile lines + private void DrawPenroseLSystem(PenroseLSystem ls) + { + Vector2 screenCenter = new Vector2(GetScreenWidth() / 2.0f, GetScreenHeight() / 2.0f); + + TurtleState turtle = new TurtleState + { + origin = new Vector2(0, 0), + angle = -90.0f + }; + + int repeats = 1; + string production = ls.production.ToString(); + int productionLength = production.Length; + ls.steps += 12; + + if (ls.steps > productionLength) ls.steps = productionLength; + + for (int i = 0; i < ls.steps; i++) + { + char step = production[i]; + if (step == 'F') + { + for (int j = 0; j < repeats; j++) + { + Vector2 startPosWorld = turtle.origin; + float radAngle = DEG2RAD * turtle.angle; + turtle.origin.X += ls.drawLength * MathF.Cos(radAngle); + turtle.origin.Y += ls.drawLength * MathF.Sin(radAngle); + Vector2 startPosScreen = new Vector2(startPosWorld.X + screenCenter.X, startPosWorld.Y + screenCenter.Y); + Vector2 endPosScreen = new Vector2(turtle.origin.X + screenCenter.X, turtle.origin.Y + screenCenter.Y); + + DrawLineEx(startPosScreen, endPosScreen, 2, Fade(Color.Black, 0.2f)); + } + + repeats = 1; + } + else if (step == '+') + { + for (int j = 0; j < repeats; j++) turtle.angle += ls.theta; + + repeats = 1; + } + else if (step == '-') + { + for (int j = 0; j < repeats; j++) turtle.angle += -ls.theta; + + repeats = 1; + } + else if (step == '[') PushTurtleState(turtle); + else if (step == ']') turtle = PopTurtleState(); + else if ((step >= 48) && (step <= 57)) repeats = (int)step - 48; + } + + turtleStack.Clear(); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - penrose tile"); + + SetTargetFPS(120); // Set our game to run at 120 frames-per-second + //--------------------------------------------------------------------------------------- + + var game = new PenroseTile(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/PieChart.cs b/Examples/Shapes/PieChart.cs new file mode 100644 index 0000000..f61adff --- /dev/null +++ b/Examples/Shapes/PieChart.cs @@ -0,0 +1,384 @@ +/******************************************************************************************* +* +* raylib [shapes] example - pie chart +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.6 +* +* Example contributed by Gideon Serfontein (@GideonSerf) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Gideon Serfontein (@GideonSerf) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using System.Text; +using static Raylib_cs.Raylib; + +namespace Examples.Shapes; + +public partial class PieChart : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MAX_PIE_SLICES = 10; // Max pie slices + + public string Name => "Shapes / Pie Chart"; + + public string Title => "raylib [shapes] example - pie chart"; + + private int sliceCount; + private float donutInnerRadius; + private float[] values; + private StringBuilder[] labels; + private bool[] editingLabel; + + private bool showValues; + private bool showPercentages; + private bool showDonut; + private int hoveredSlice; + private Vector2 scrollContentOffset; + + // UI layout parameters + private const int panelWidth = 270; + private const int panelMargin = 5; + + private Vector2 panelPos; + private Rectangle panelRect; + private Rectangle canvas; + private Vector2 center; + private const float radius = 205.0f; + + // Total value for percentage calculations + private float totalValue; + + private int framesCounter; + + // Minimal raygui-like global state + private static bool guiDisabled; + + public void Init() + { + sliceCount = 7; + donutInnerRadius = 25.0f; + values = new float[MAX_PIE_SLICES] { 300.0f, 100.0f, 450.0f, 350.0f, 600.0f, 380.0f, 750.0f, 0.0f, 0.0f, 0.0f }; + labels = new StringBuilder[MAX_PIE_SLICES]; + editingLabel = new bool[MAX_PIE_SLICES]; + + for (int i = 0; i < MAX_PIE_SLICES; i++) labels[i] = new StringBuilder($"Slice {i + 1:D2}"); + + showValues = true; + showPercentages = false; + showDonut = false; + hoveredSlice = -1; + scrollContentOffset = new Vector2(0, 0); + + // UI Panel top-left anchor + panelPos = new Vector2( + (float)screenWidth - panelMargin - panelWidth, + (float)panelMargin + ); + + // UI Panel rectangle + panelRect = new Rectangle( + panelPos.X, panelPos.Y, + (float)panelWidth, + (float)screenHeight - 2.0f * panelMargin + ); + + // Pie chart geometry + canvas = new Rectangle(0, 0, panelPos.X, (float)screenHeight); + center = new Vector2(canvas.Width / 2.0f, canvas.Height / 2.0f); + + totalValue = 0.0f; + + framesCounter = 0; + guiDisabled = false; + } + + public void Update() + { + framesCounter++; + + // Update + //---------------------------------------------------------------------------------- + // Calculate total value for percentage calculations + totalValue = 0.0f; + for (int i = 0; i < sliceCount; i++) totalValue += values[i]; + + // Check for mouse hover over slices + hoveredSlice = -1; // Reset hovered slice + Vector2 mousePos = GetMousePosition(); + if (CheckCollisionPointRec(mousePos, canvas)) // Only check if mouse is inside the canvas + { + float dx = mousePos.X - center.X; + float dy = mousePos.Y - center.Y; + float distance = MathF.Sqrt(dx * dx + dy * dy); + + if (distance <= radius) // Inside the pie radius + { + float angle = MathF.Atan2(dy, dx) * RAD2DEG; + if (angle < 0) angle += 360; + + float currentAngle = 0.0f; + for (int i = 0; i < sliceCount; i++) + { + float sweep = (totalValue > 0) ? (values[i] / totalValue) * 360.0f : 0.0f; + + if ((angle >= currentAngle) && (angle < (currentAngle + sweep))) + { + hoveredSlice = i; + break; + } + + currentAngle += sweep; + } + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // Draw the pie chart on the canvas + float startAngle = 0.0f; + for (int i = 0; i < sliceCount; i++) + { + float sweepAngle = (totalValue > 0) ? (values[i] / totalValue) * 360.0f : 0.0f; + float midAngle = startAngle + sweepAngle / 2.0f; // Middle angle for label positioning + + Color color = ColorFromHSV((float)i / sliceCount * 360.0f, 0.75f, 0.9f); + float currentRadius = radius; + + // Make the hovered slice pop out by adding pixels to its radius + if (i == hoveredSlice) currentRadius += 20.0f; + + // Draw the pie slice using raylib's DrawCircleSector function + DrawCircleSector(center, currentRadius, startAngle, startAngle + sweepAngle, 120, color); + + // Draw the label for the current slice + if (values[i] > 0) + { + string labelText; + if (showValues && showPercentages) labelText = $"{values[i]:F1} ({(values[i] / totalValue) * 100.0f:F0}%)"; + else if (showValues) labelText = $"{values[i]:F1}"; + else if (showPercentages) labelText = $"{(values[i] / totalValue) * 100.0f:F0}%"; + else labelText = ""; + + Vector2 textSize = MeasureTextEx(GetFontDefault(), labelText, 20, 1); + float labelRadius = radius * 0.7f; + Vector2 labelPos = new Vector2(center.X + MathF.Cos(midAngle * DEG2RAD) * labelRadius - textSize.X / 2.0f, + center.Y + MathF.Sin(midAngle * DEG2RAD) * labelRadius - textSize.Y / 2.0f); + DrawText(labelText, (int)labelPos.X, (int)labelPos.Y, 20, Color.White); + } + + // Draw inner circle to create donut effect + // TODO: This is a hacky solution, better use DrawRing() + if (showDonut) DrawCircleV(center, donutInnerRadius, Color.RayWhite); + + startAngle += sweepAngle; + } + + // UI control panel (minimal raygui-like controls, raygui is not bound in raylib-cs) + DrawRectangleRec(panelRect, Fade(Color.LightGray, 0.5f)); + DrawRectangleLinesEx(panelRect, 1.0f, Color.Gray); + + GuiSpinner(new Rectangle(panelPos.X + 95, (float)panelPos.Y + 12, 125, 25), "Slices ", ref sliceCount, 1, MAX_PIE_SLICES); + GuiCheckBox(new Rectangle(panelPos.X + 20, (float)panelPos.Y + 12 + 40, 20, 20), "Show Values", ref showValues); + GuiCheckBox(new Rectangle(panelPos.X + 20, (float)panelPos.Y + 12 + 70, 20, 20), "Show Percentages", ref showPercentages); + GuiCheckBox(new Rectangle(panelPos.X + 20, (float)panelPos.Y + 12 + 100, 20, 20), "Make Donut", ref showDonut); + + if (showDonut) GuiDisable(); + GuiSliderBar(new Rectangle(panelPos.X + 80, (float)panelPos.Y + 12 + 130, panelRect.Width - 100, 30), + "Inner Radius", null, ref donutInnerRadius, 5.0f, radius - 10.0f); + GuiEnable(); + + GuiLine(new Rectangle(panelPos.X + 10, (float)panelPos.Y + 12 + 170, panelRect.Width - 20, 1)); + + // Scrollable area for slice editors + float scrollTop = (float)panelPos.Y + 12 + 190; + Rectangle scrollPanelBounds = new Rectangle( + panelPos.X + panelMargin, + scrollTop, + panelRect.Width - panelMargin * 2, + (panelRect.Y + panelRect.Height) - scrollTop - panelMargin); + int contentHeight = sliceCount * 35; + + // Simple vertical scroll via mouse wheel while hovering the panel + if (CheckCollisionPointRec(GetMousePosition(), scrollPanelBounds)) + { + scrollContentOffset.Y += GetMouseWheelMove() * 20.0f; + float minOffset = MathF.Min(0.0f, scrollPanelBounds.Height - contentHeight); + if (scrollContentOffset.Y < minOffset) scrollContentOffset.Y = minOffset; + if (scrollContentOffset.Y > 0.0f) scrollContentOffset.Y = 0.0f; + } + + Rectangle view = scrollPanelBounds; + float contentX = view.X + scrollContentOffset.X; // Left of content + float contentY = view.Y + scrollContentOffset.Y; // Top of content + + BeginScissorMode((int)view.X, (int)view.Y, (int)view.Width, (int)view.Height); + + for (int i = 0; i < sliceCount; i++) + { + int rowY = (int)(contentY + 5 + i * 35); + + // Color indicator + Color color = ColorFromHSV((float)i / sliceCount * 360.0f, 0.75f, 0.9f); + DrawRectangle((int)(contentX + 15), rowY + 5, 20, 20, color); + + // Label textbox + if (GuiTextBox(new Rectangle(contentX + 45, (float)rowY, 75, 30), labels[i], 32, editingLabel[i])) editingLabel[i] = !editingLabel[i]; + + GuiSliderBar(new Rectangle(contentX + 130, (float)rowY, 110, 30), null, null, ref values[i], 0.0f, 1000.0f); + } + + EndScissorMode(); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + //---------------------------------------------------------------------------------- + // Minimal raygui-like widgets (plain raylib re-implementation) + //---------------------------------------------------------------------------------- + private static void GuiDisable() => guiDisabled = true; + private static void GuiEnable() => guiDisabled = false; + + private static void GuiLine(Rectangle bounds) + { + int y = (int)(bounds.Y + bounds.Height / 2); + DrawLine((int)bounds.X, y, (int)(bounds.X + bounds.Width), y, Color.Gray); + } + + private static void GuiCheckBox(Rectangle bounds, string text, ref bool active) + { + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + if (!guiDisabled && hover && IsMouseButtonPressed(MouseButton.Left)) active = !active; + + DrawRectangleLinesEx(bounds, 1, hover ? Color.DarkBlue : Color.Gray); + if (active) DrawRectangle((int)bounds.X + 4, (int)bounds.Y + 4, (int)bounds.Width - 8, (int)bounds.Height - 8, Color.DarkGray); + if (text != null) DrawText(text, (int)(bounds.X + bounds.Width + 8), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + + private static void GuiSpinner(Rectangle bounds, string text, ref int value, int minValue, int maxValue) + { + Vector2 mouse = GetMousePosition(); + Rectangle left = new Rectangle(bounds.X, bounds.Y, bounds.Height, bounds.Height); + Rectangle right = new Rectangle(bounds.X + bounds.Width - bounds.Height, bounds.Y, bounds.Height, bounds.Height); + Rectangle mid = new Rectangle(bounds.X + bounds.Height, bounds.Y, bounds.Width - 2 * bounds.Height, bounds.Height); + + if (!guiDisabled && CheckCollisionPointRec(mouse, left) && IsMouseButtonPressed(MouseButton.Left) && value > minValue) value--; + if (!guiDisabled && CheckCollisionPointRec(mouse, right) && IsMouseButtonPressed(MouseButton.Left) && value < maxValue) value++; + + DrawRectangleRec(mid, Color.RayWhite); + DrawRectangleLinesEx(mid, 1, Color.Gray); + DrawRectangleRec(left, Color.LightGray); + DrawRectangleLinesEx(left, 1, Color.Gray); + DrawRectangleRec(right, Color.LightGray); + DrawRectangleLinesEx(right, 1, Color.Gray); + DrawText("-", (int)(left.X + left.Width / 2 - 2), (int)(left.Y + left.Height / 2 - 5), 10, Color.DarkGray); + DrawText("+", (int)(right.X + right.Width / 2 - 3), (int)(right.Y + right.Height / 2 - 5), 10, Color.DarkGray); + string vs = value.ToString(); + DrawText(vs, (int)(mid.X + mid.Width / 2 - MeasureText(vs, 10) / 2), (int)(mid.Y + mid.Height / 2 - 5), 10, Color.DarkGray); + if (text != null) DrawText(text, (int)bounds.X - MeasureText(text, 10) - 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + + private static void GuiSliderBar(Rectangle bounds, string textLeft, string textRight, ref float value, float minValue, float maxValue) + { + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + if (!guiDisabled && hover && IsMouseButtonDown(MouseButton.Left)) + { + value = minValue + ((mouse.X - bounds.X) / bounds.Width) * (maxValue - minValue); + if (value < minValue) value = minValue; + if (value > maxValue) value = maxValue; + } + + DrawRectangleRec(bounds, guiDisabled ? Color.Gray : Color.LightGray); + float pct = (value - minValue) / (maxValue - minValue); + DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), guiDisabled ? Color.DarkGray : Color.SkyBlue); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (!string.IsNullOrEmpty(textLeft)) DrawText(textLeft, (int)bounds.X - MeasureText(textLeft, 10) - 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + if (!string.IsNullOrEmpty(textRight)) DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + + private bool GuiTextBox(Rectangle bounds, StringBuilder text, int maxChars, bool editMode) + { + bool pressed = false; + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + + if (editMode) + { + int key = GetCharPressed(); + while (key > 0) + { + if ((key >= 32) && (key <= 125) && (text.Length < maxChars - 1)) + { + text.Append((char)key); + } + + key = GetCharPressed(); + } + + if (IsKeyPressed(KeyboardKey.Backspace) && (text.Length > 0)) text.Remove(text.Length - 1, 1); + } + + DrawRectangleRec(bounds, Color.RayWhite); + DrawRectangleLinesEx(bounds, editMode ? 2 : 1, editMode ? Color.Red : (hover ? Color.Blue : Color.Gray)); + + string content = text.ToString(); + DrawText(content, (int)bounds.X + 4, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + + if (editMode && ((framesCounter / 20) % 2 == 0)) + { + DrawText("_", (int)bounds.X + 4 + MeasureText(content, 10), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + + if (hover && IsMouseButtonPressed(MouseButton.Left)) pressed = true; + + return pressed; + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - pie chart"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new PieChart(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/RectangleAdvanced.cs b/Examples/Shapes/RectangleAdvanced.cs new file mode 100644 index 0000000..7236ebb --- /dev/null +++ b/Examples/Shapes/RectangleAdvanced.cs @@ -0,0 +1,252 @@ +/******************************************************************************************* +* +* raylib [shapes] example - rectangle advanced +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.5 +* +* Example contributed by Everton Jr. (@evertonse) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2024-2025 Everton Jr. (@evertonse) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Rlgl; + +namespace Examples.Shapes; + +public partial class RectangleAdvanced : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Rectangle Advanced"; + + public string Title => "raylib [shapes] example - rectangle advanced"; + + public void Init() + { + } + + public void Update() + { + // Update rectangle bounds + //---------------------------------------------------------------------------------- + float width = GetScreenWidth() / 2.0f, height = GetScreenHeight() / 6.0f; + Rectangle rec = new Rectangle( + GetScreenWidth() / 2.0f - width / 2, + GetScreenHeight() / 2.0f - 5 * (height / 2), + width, height + ); + //-------------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // Draw All Rectangles with different roundess for each side and different gradients + DrawRectangleRoundedGradientH(rec, 0.8f, 0.8f, 36, Color.Blue, Color.Red); + + rec.Y += rec.Height + 1; + DrawRectangleRoundedGradientH(rec, 0.5f, 1.0f, 36, Color.Red, Color.Pink); + + rec.Y += rec.Height + 1; + DrawRectangleRoundedGradientH(rec, 1.0f, 0.5f, 36, Color.Red, Color.Blue); + + rec.Y += rec.Height + 1; + DrawRectangleRoundedGradientH(rec, 0.0f, 1.0f, 36, Color.Blue, Color.Black); + + rec.Y += rec.Height + 1; + DrawRectangleRoundedGradientH(rec, 1.0f, 0.0f, 36, Color.Blue, Color.Pink); + EndDrawing(); + //-------------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + //-------------------------------------------------------------------------------------- + // Module Functions Definition + //-------------------------------------------------------------------------------------- + // Draw rectangle with rounded edges and horizontal gradient, with options to choose side of roundness + // NOTE: Adapted from both 'DrawRectangleRounded()' and 'DrawRectangleGradientH()' raylib [rshapes] implementations + private static void DrawRectangleRoundedGradientH(Rectangle rec, float roundnessLeft, float roundnessRight, int segments, Color left, Color right) + { + // Neither side is rounded + if ((roundnessLeft <= 0.0f && roundnessRight <= 0.0f) || (rec.Width < 1) || (rec.Height < 1)) + { + DrawRectangleGradientEx(rec, left, left, right, right); + return; + } + + if (roundnessLeft >= 1.0f) roundnessLeft = 1.0f; + if (roundnessRight >= 1.0f) roundnessRight = 1.0f; + + // Calculate corner radius both from right and left + float recSize = rec.Width > rec.Height ? rec.Height : rec.Width; + float radiusLeft = (recSize * roundnessLeft) / 2; + float radiusRight = (recSize * roundnessRight) / 2; + + if (radiusLeft <= 0.0f) radiusLeft = 0.0f; + if (radiusRight <= 0.0f) radiusRight = 0.0f; + + if (radiusRight <= 0.0f && radiusLeft <= 0.0f) return; + + float stepLength = 90.0f / (float)segments; + + /* + Diagram Copied here for reference, original at 'DrawRectangleRounded()' source code + + P0____________________P1 + /| |\ + /1| 2 |3\ + P7 /__|____________________|__\ P2 + | |P8 P9| | + | 8 | 9 | 4 | + | __|____________________|__ | + P6 \ |P11 P10| / P3 + \7| 6 |5/ + \|____________________|/ + P5 P4 + */ + + // Coordinates of the 12 points also adapted from `DrawRectangleRounded` + Vector2[] point = new Vector2[12] + { + // PO, P1, P2 + new Vector2(rec.X + radiusLeft, rec.Y), new Vector2((rec.X + rec.Width) - radiusRight, rec.Y), new Vector2(rec.X + rec.Width, rec.Y + radiusRight), + // P3, P4 + new Vector2(rec.X + rec.Width, (rec.Y + rec.Height) - radiusRight), new Vector2((rec.X + rec.Width) - radiusRight, rec.Y + rec.Height), + // P5, P6, P7 + new Vector2(rec.X + radiusLeft, rec.Y + rec.Height), new Vector2(rec.X, (rec.Y + rec.Height) - radiusLeft), new Vector2(rec.X, rec.Y + radiusLeft), + // P8, P9 + new Vector2(rec.X + radiusLeft, rec.Y + radiusLeft), new Vector2((rec.X + rec.Width) - radiusRight, rec.Y + radiusRight), + // P10, P11 + new Vector2((rec.X + rec.Width) - radiusRight, (rec.Y + rec.Height) - radiusRight), new Vector2(rec.X + radiusLeft, (rec.Y + rec.Height) - radiusLeft) + }; + + Vector2[] centers = new Vector2[4] { point[8], point[9], point[10], point[11] }; + float[] angles = new float[4] { 180.0f, 270.0f, 0.0f, 90.0f }; + + // Here we use the 'Diagram' to guide ourselves to which point receives what color + // By choosing the color correctly associated with a point the gradient effect + // will naturally come from OpenGL interpolation + // But this time instead of Quad, we think in triangles + + Begin(DrawMode.Triangles); + // Draw all of the 4 corners: [1] Upper Left Corner, [3] Upper Right Corner, [5] Lower Right Corner, [7] Lower Left Corner + for (int k = 0; k < 4; ++k) + { + Color color = new Color(0, 0, 0, 0); + float radius = 0.0f; + if (k == 0) { color = left; radius = radiusLeft; } // [1] Upper Left Corner + if (k == 1) { color = right; radius = radiusRight; } // [3] Upper Right Corner + if (k == 2) { color = right; radius = radiusRight; } // [5] Lower Right Corner + if (k == 3) { color = left; radius = radiusLeft; } // [7] Lower Left Corner + + float angle = angles[k]; + Vector2 center = centers[k]; + + for (int i = 0; i < segments; i++) + { + Color4ub(color.R, color.G, color.B, color.A); + Vertex2f(center.X, center.Y); + Vertex2f(center.X + MathF.Cos(DEG2RAD * (angle + stepLength)) * radius, center.Y + MathF.Sin(DEG2RAD * (angle + stepLength)) * radius); + Vertex2f(center.X + MathF.Cos(DEG2RAD * angle) * radius, center.Y + MathF.Sin(DEG2RAD * angle) * radius); + angle += stepLength; + } + } + + // [2] Upper Rectangle + Color4ub(left.R, left.G, left.B, left.A); + Vertex2f(point[0].X, point[0].Y); + Vertex2f(point[8].X, point[8].Y); + Color4ub(right.R, right.G, right.B, right.A); + Vertex2f(point[9].X, point[9].Y); + Vertex2f(point[1].X, point[1].Y); + Color4ub(left.R, left.G, left.B, left.A); + Vertex2f(point[0].X, point[0].Y); + Color4ub(right.R, right.G, right.B, right.A); + Vertex2f(point[9].X, point[9].Y); + + // [4] Right Rectangle + Color4ub(right.R, right.G, right.B, right.A); + Vertex2f(point[9].X, point[9].Y); + Vertex2f(point[10].X, point[10].Y); + Vertex2f(point[3].X, point[3].Y); + Vertex2f(point[2].X, point[2].Y); + Vertex2f(point[9].X, point[9].Y); + Vertex2f(point[3].X, point[3].Y); + + // [6] Bottom Rectangle + Color4ub(left.R, left.G, left.B, left.A); + Vertex2f(point[11].X, point[11].Y); + Vertex2f(point[5].X, point[5].Y); + Color4ub(right.R, right.G, right.B, right.A); + Vertex2f(point[4].X, point[4].Y); + Vertex2f(point[10].X, point[10].Y); + Color4ub(left.R, left.G, left.B, left.A); + Vertex2f(point[11].X, point[11].Y); + Color4ub(right.R, right.G, right.B, right.A); + Vertex2f(point[4].X, point[4].Y); + + // [8] Left Rectangle + Color4ub(left.R, left.G, left.B, left.A); + Vertex2f(point[7].X, point[7].Y); + Vertex2f(point[6].X, point[6].Y); + Vertex2f(point[11].X, point[11].Y); + Vertex2f(point[8].X, point[8].Y); + Vertex2f(point[7].X, point[7].Y); + Vertex2f(point[11].X, point[11].Y); + + // [9] Middle Rectangle + Color4ub(left.R, left.G, left.B, left.A); + Vertex2f(point[8].X, point[8].Y); + Vertex2f(point[11].X, point[11].Y); + Color4ub(right.R, right.G, right.B, right.A); + Vertex2f(point[10].X, point[10].Y); + Vertex2f(point[9].X, point[9].Y); + Color4ub(left.R, left.G, left.B, left.A); + Vertex2f(point[8].X, point[8].Y); + Color4ub(right.R, right.G, right.B, right.A); + Vertex2f(point[10].X, point[10].Y); + End(); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - rectangle advanced"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new RectangleAdvanced(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/RecursiveTree.cs b/Examples/Shapes/RecursiveTree.cs new file mode 100644 index 0000000..a522956 --- /dev/null +++ b/Examples/Shapes/RecursiveTree.cs @@ -0,0 +1,199 @@ +/******************************************************************************************* +* +* raylib [shapes] example - recursive tree +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Jopestpe (@jopestpe) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Jopestpe (@jopestpe) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shapes; + +public partial class RecursiveTree : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Recursive Tree"; + + public string Title => "raylib [shapes] example - recursive tree"; + + //---------------------------------------------------------------------------------- + // Types and Structures Definition + //---------------------------------------------------------------------------------- + private struct Branch + { + public Vector2 start; + public Vector2 end; + public float angle; + public float length; + } + + private Vector2 start; + private float angle; + private float thick; + private float treeDepth; + private float branchDecay; + private float length; + private bool bezier; + + private Branch[] branches; + + public void Init() + { + start = new Vector2((screenWidth / 2.0f) - 125.0f, (float)screenHeight); + angle = 40.0f; + thick = 1.0f; + treeDepth = 10.0f; + branchDecay = 0.66f; + length = 120.0f; + bezier = false; + + branches = new Branch[1030]; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + float theta = angle * DEG2RAD; + int maxBranches = (int)(MathF.Pow(2, MathF.Floor(treeDepth))); + int count = 0; + + Vector2 initialEnd = new Vector2(start.X + length * MathF.Sin(0.0f), start.Y - length * MathF.Cos(0.0f)); + branches[count++] = new Branch { start = start, end = initialEnd, angle = 0.0f, length = length }; + + for (int i = 0; i < count; i++) + { + Branch branch = branches[i]; + if (branch.length < 2) continue; + + float nextLength = branch.length * branchDecay; + + if (count < maxBranches && nextLength >= 2) + { + Vector2 branchStart = branch.end; + + float angle1 = branch.angle + theta; + Vector2 branchEnd1 = new Vector2(branchStart.X + nextLength * MathF.Sin(angle1), branchStart.Y - nextLength * MathF.Cos(angle1)); + branches[count++] = new Branch { start = branchStart, end = branchEnd1, angle = angle1, length = nextLength }; + + float angle2 = branch.angle - theta; + Vector2 branchEnd2 = new Vector2(branchStart.X + nextLength * MathF.Sin(angle2), branchStart.Y - nextLength * MathF.Cos(angle2)); + branches[count++] = new Branch { start = branchStart, end = branchEnd2, angle = angle2, length = nextLength }; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + for (int i = 0; i < count; i++) + { + Branch branch = branches[i]; + if (branch.length >= 2) + { + if (bezier) DrawLineBezier(branch.start, branch.end, thick, Color.Red); + else DrawLineEx(branch.start, branch.end, thick, Color.Red); + } + } + + DrawLine(580, 0, 580, GetScreenHeight(), new Color(218, 218, 218, 255)); + DrawRectangle(580, 0, GetScreenWidth(), GetScreenHeight(), new Color(232, 232, 232, 255)); + + // Draw GUI controls (minimal raygui-like controls, raygui is not bound in raylib-cs) + //------------------------------------------------------------------------------ + GuiSliderBar(new Rectangle(640, 40, 120, 20), "Angle", $"{angle:F0}", ref angle, 0, 180); + GuiSliderBar(new Rectangle(640, 70, 120, 20), "Length", $"{length:F0}", ref length, 12.0f, 240.0f); + GuiSliderBar(new Rectangle(640, 100, 120, 20), "Decay", $"{branchDecay:F2}", ref branchDecay, 0.1f, 0.78f); + GuiSliderBar(new Rectangle(640, 130, 120, 20), "Depth", $"{treeDepth:F0}", ref treeDepth, 1.0f, 10.0f); + GuiSliderBar(new Rectangle(640, 160, 120, 20), "Thick", $"{thick:F0}", ref thick, 1, 8); + GuiCheckBox(new Rectangle(640, 190, 20, 20), "Bezier", ref bezier); + //------------------------------------------------------------------------------ + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + //---------------------------------------------------------------------------------- + // Minimal raygui-like widgets (plain raylib re-implementation) + //---------------------------------------------------------------------------------- + private static void GuiSliderBar(Rectangle bounds, string textLeft, string textRight, ref float value, float minValue, float maxValue) + { + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + if (hover && IsMouseButtonDown(MouseButton.Left)) + { + value = minValue + ((mouse.X - bounds.X) / bounds.Width) * (maxValue - minValue); + if (value < minValue) value = minValue; + if (value > maxValue) value = maxValue; + } + + DrawRectangleRec(bounds, Color.LightGray); + float pct = (value - minValue) / (maxValue - minValue); + DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), Color.SkyBlue); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (textLeft != null) DrawText(textLeft, (int)bounds.X - MeasureText(textLeft, 10) - 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + if (textRight != null) DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + + private static void GuiCheckBox(Rectangle bounds, string text, ref bool active) + { + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + if (hover && IsMouseButtonPressed(MouseButton.Left)) active = !active; + + DrawRectangleLinesEx(bounds, 1, hover ? Color.DarkBlue : Color.Gray); + if (active) DrawRectangle((int)bounds.X + 4, (int)bounds.Y + 4, (int)bounds.Width - 8, (int)bounds.Height - 8, Color.DarkGray); + if (text != null) DrawText(text, (int)(bounds.X + bounds.Width + 8), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - recursive tree"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new RecursiveTree(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/RlglColorWheel.cs b/Examples/Shapes/RlglColorWheel.cs new file mode 100644 index 0000000..36c7f41 --- /dev/null +++ b/Examples/Shapes/RlglColorWheel.cs @@ -0,0 +1,331 @@ +/******************************************************************************************* +* +* raylib [shapes] example - rlgl color wheel +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Robin (@RobinsAviary) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Robin (@RobinsAviary) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; +using static Raylib_cs.Rlgl; + +namespace Examples.Shapes; + +public partial class RlglColorWheel : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / RLGL Color Wheel"; + + public string Title => "raylib [shapes] example - rlgl color wheel"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + // The minimum/maximum points the circle can have + private const int pointsMin = 3; + private const int pointsMax = 256; + + // The current number of points and the radius of the circle + private int triangleCount; + private float pointScale; + + // Slider value, literally maps to value in HSV + private float value; + + // The center of the screen + private Vector2 center; + // The location of the color wheel + private Vector2 circlePosition; + + // The currently selected color + private Color color; + + // Indicates if the slider is being clicked + private bool sliderClicked; + + // Indicates if the current color going to be updated, as well as the handle position + private bool settingColor; + + // How the color wheel will be rendered + private DrawMode renderType; + + public void Init() + { + triangleCount = 64; + pointScale = 150.0f; + value = 1.0f; + + center = new Vector2((float)screenWidth / 2.0f, (float)screenHeight / 2.0f); + circlePosition = center; + + color = new Color(255, 255, 255, 255); + + sliderClicked = false; + settingColor = false; + + renderType = DrawMode.Triangles; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + triangleCount += (int)GetMouseWheelMove(); + triangleCount = (int)Clamp((float)triangleCount, (float)pointsMin, (float)pointsMax); + + Rectangle sliderRectangle = new Rectangle(42.0f, 16.0f + 64.0f + 45.0f, 64.0f, 16.0f); + Vector2 mousePosition = GetMousePosition(); + + // Checks if the user is hovering over the value slider + bool sliderHover = (mousePosition.X >= sliderRectangle.X && mousePosition.Y >= sliderRectangle.Y && mousePosition.X < sliderRectangle.X + sliderRectangle.Width && mousePosition.Y < sliderRectangle.Y + sliderRectangle.Height); + + // Copy color as hex + if (IsKeyDown(KeyboardKey.LeftControl) && IsKeyDown(KeyboardKey.C)) + { + if (IsKeyPressed(KeyboardKey.C)) + { + SetClipboardText($"#{color.R:X2}{color.G:X2}{color.B:X2}"); + } + } + + // Scale up the color wheel, adjusting the handle visually + if (IsKeyDown(KeyboardKey.Up)) + { + pointScale *= 1.025f; + + if (pointScale > (float)screenHeight / 2.0f) + { + pointScale = (float)screenHeight / 2.0f; + } + else + { + circlePosition = Vector2Add(Vector2Multiply(Vector2Subtract(circlePosition, center), new Vector2(1.025f, 1.025f)), center); + } + } + + // Scale down the wheel, adjusting the handle visually + if (IsKeyDown(KeyboardKey.Down)) + { + pointScale *= 0.975f; + + if (pointScale < 32.0f) + { + pointScale = 32.0f; + } + else + { + circlePosition = Vector2Add(Vector2Multiply(Vector2Subtract(circlePosition, center), new Vector2(0.975f, 0.975f)), center); + } + + float distanceDown = Vector2Distance(center, circlePosition) / pointScale; + float angleDown = ((Vector2Angle(new Vector2(0.0f, -pointScale), Vector2Subtract(center, circlePosition)) / MathF.PI + 1.0f) / 2.0f); + + if (distanceDown > 1.0f) + { + circlePosition = Vector2Add(new Vector2(MathF.Sin(angleDown * (MathF.PI * 2.0f)) * pointScale, -MathF.Cos(angleDown * (MathF.PI * 2.0f)) * pointScale), center); + } + } + + // Checks if the user clicked on the color wheel + if (IsMouseButtonPressed(MouseButton.Left) && Vector2Distance(GetMousePosition(), center) <= pointScale + 10.0f) + { + settingColor = true; + } + + // Update flag when mouse button is released + if (IsMouseButtonReleased(MouseButton.Left)) settingColor = false; + + // Check if the user clicked/released the slider for the color's value + if (sliderHover && IsMouseButtonPressed(MouseButton.Left)) sliderClicked = true; + + if (sliderClicked && IsMouseButtonReleased(MouseButton.Left)) sliderClicked = false; + + // Update render mode accordingly + if (IsKeyPressed(KeyboardKey.Space)) renderType = DrawMode.Lines; + + if (IsKeyReleased(KeyboardKey.Space)) renderType = DrawMode.Triangles; + + // If the slider or the wheel was clicked, update the current color + if (settingColor || sliderClicked) + { + if (settingColor) circlePosition = GetMousePosition(); + + float distance = Vector2Distance(center, circlePosition) / pointScale; + + float angle = ((Vector2Angle(new Vector2(0.0f, -pointScale), Vector2Subtract(center, circlePosition)) / MathF.PI + 1.0f) / 2.0f); + if (settingColor && distance > 1.0f) circlePosition = Vector2Add(new Vector2(MathF.Sin(angle * (MathF.PI * 2.0f)) * pointScale, -MathF.Cos(angle * (MathF.PI * 2.0f)) * pointScale), center); + + float angle360 = angle * 360.0f; + float valueActual = Clamp(distance, 0.0f, 1.0f); + color = ColorLerp(new Color((int)(value * 255.0f), (int)(value * 255.0f), (int)(value * 255.0f), 255), ColorFromHSV(angle360, Clamp(distance, 0.0f, 1.0f), 1.0f), valueActual); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + // Begin rendering color wheel + Begin(renderType); + for (int i = 0; i < triangleCount; i++) + { + float angleOffset = ((MathF.PI * 2.0f) / (float)triangleCount); + float angle = angleOffset * (float)i; + float angleOffsetCalculated = ((float)i + 1) * angleOffset; + Vector2 scale = new Vector2(pointScale, pointScale); + + Vector2 offset = Vector2Multiply(new Vector2(MathF.Sin(angle), -MathF.Cos(angle)), scale); + Vector2 offset2 = Vector2Multiply(new Vector2(MathF.Sin(angleOffsetCalculated), -MathF.Cos(angleOffsetCalculated)), scale); + + Vector2 position = Vector2Add(center, offset); + Vector2 position2 = Vector2Add(center, offset2); + + float angleNonRadian = (angle / (2.0f * MathF.PI)) * 360.0f; + float angleNonRadianOffset = (angleOffset / (2.0f * MathF.PI)) * 360.0f; + + Color currentColor = ColorFromHSV(angleNonRadian, 1.0f, 1.0f); + Color offsetColor = ColorFromHSV(angleNonRadian + angleNonRadianOffset, 1.0f, 1.0f); + + // Input vertices differently depending on mode + if (renderType == DrawMode.Triangles) + { + // RL_TRIANGLES expects three vertices per triangle + Color4ub(currentColor.R, currentColor.G, currentColor.B, currentColor.A); + Vertex2f(position.X, position.Y); + Color4f(value, value, value, 1.0f); + Vertex2f(center.X, center.Y); + Color4ub(offsetColor.R, offsetColor.G, offsetColor.B, offsetColor.A); + Vertex2f(position2.X, position2.Y); + } + else if (renderType == DrawMode.Lines) + { + // RL_LINES expects two vertices per line + Color4ub(currentColor.R, currentColor.G, currentColor.B, currentColor.A); + Vertex2f(position.X, position.Y); + Color4ub(Color.White.R, Color.White.G, Color.White.B, Color.White.A); + Vertex2f(center.X, center.Y); + + Vertex2f(center.X, center.Y); + Color4ub(offsetColor.R, offsetColor.G, offsetColor.B, offsetColor.A); + Vertex2f(position2.X, position2.Y); + + Vertex2f(position2.X, position2.Y); + Color4ub(currentColor.R, currentColor.G, currentColor.B, currentColor.A); + Vertex2f(position.X, position.Y); + } + } + End(); + + // Make the handle slightly more visible overtop darker colors + Color handleColor = Color.Black; + + if (Vector2Distance(center, circlePosition) / pointScale <= 0.5f && value <= 0.5f) + { + handleColor = Color.DarkGray; + } + + // Draw the color handle + DrawCircleLinesV(circlePosition, 4.0f, handleColor); + + // Draw the color in a preview, with a darkened outline. + DrawRectangleV(new Vector2(8.0f, 8.0f), new Vector2(64.0f, 64.0f), color); + DrawRectangleLinesEx(new Rectangle(8.0f, 8.0f, 64.0f, 64.0f), 2.0f, ColorLerp(color, Color.Black, 0.5f)); + + // Draw current color as hex and decimal + DrawText($"#{color.R:X2}{color.G:X2}{color.B:X2}\n({color.R}, {color.G}, {color.B})", 8, 8 + 64 + 8, 20, Color.DarkGray); + + // Update the visuals for the copying text + Color copyColor = Color.DarkGray; + int textOffset = 0; + if (IsKeyDown(KeyboardKey.LeftControl) && IsKeyDown(KeyboardKey.C)) + { + copyColor = Color.DarkGreen; + textOffset = 4; + } + + // Draw the copying text + DrawText("press ctrl+c to copy!", 8, 425 - textOffset, 20, copyColor); + + // Display the number of rendered triangles + DrawText($"triangle count: {triangleCount}", 8, 395, 20, Color.DarkGray); + + // Slider to change color's value (minimal raygui-like control, raygui is not bound in raylib-cs) + GuiSliderBar(sliderRectangle, "value: ", "", ref value, 0.0f, 1.0f); + + // Draw FPS next to outlined color preview + DrawFPS(64 + 16, 8); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + //---------------------------------------------------------------------------------- + // Minimal raygui-like widgets (plain raylib re-implementation) + //---------------------------------------------------------------------------------- + private static void GuiSliderBar(Rectangle bounds, string textLeft, string textRight, ref float value, float minValue, float maxValue) + { + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + if (hover && IsMouseButtonDown(MouseButton.Left)) + { + value = minValue + ((mouse.X - bounds.X) / bounds.Width) * (maxValue - minValue); + if (value < minValue) value = minValue; + if (value > maxValue) value = maxValue; + } + + DrawRectangleRec(bounds, Color.LightGray); + float pct = (value - minValue) / (maxValue - minValue); + DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), Color.SkyBlue); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (!string.IsNullOrEmpty(textLeft)) DrawText(textLeft, (int)bounds.X - MeasureText(textLeft, 10) - 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + if (!string.IsNullOrEmpty(textRight)) DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - rlgl color wheel"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new RlglColorWheel(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/RlglTriangle.cs b/Examples/Shapes/RlglTriangle.cs new file mode 100644 index 0000000..cb6ee22 --- /dev/null +++ b/Examples/Shapes/RlglTriangle.cs @@ -0,0 +1,201 @@ +/******************************************************************************************* +* +* raylib [shapes] example - rlgl triangle +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Robin (@RobinsAviary) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Robin (@RobinsAviary) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Rlgl; + +namespace Examples.Shapes; + +public partial class RlglTriangle : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / RLGL Triangle"; + + public string Title => "raylib [shapes] example - rlgl triangle"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + // Starting postions and rendered triangle positions + private Vector2[] startingPositions; + private Vector2[] trianglePositions; + + // Currently selected vertex, -1 means none + private int triangleIndex; + private bool linesMode; + private float handleRadius; + + public void Init() + { + // Starting postions and rendered triangle positions + startingPositions = [new(400.0f, 150.0f), new(300.0f, 300.0f), new(500.0f, 300.0f)]; + trianglePositions = [startingPositions[0], startingPositions[1], startingPositions[2]]; + + // Currently selected vertex, -1 means none + triangleIndex = -1; + linesMode = false; + handleRadius = 8.0f; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.Space)) linesMode = !linesMode; + + // Check selected vertex + for (int i = 0; i < 3; i++) + { + // If the mouse is within the handle circle + if (CheckCollisionPointCircle(GetMousePosition(), trianglePositions[i], handleRadius) && + IsMouseButtonDown(MouseButton.Left)) + { + triangleIndex = i; + break; + } + } + + // If the user has selected a vertex, offset it by the mouse's delta this frame + if (triangleIndex != -1) + { + Vector2 mouseDelta = GetMouseDelta(); + trianglePositions[triangleIndex].X += mouseDelta.X; + trianglePositions[triangleIndex].Y += mouseDelta.Y; + } + + // Reset index on release + if (IsMouseButtonReleased(MouseButton.Left)) triangleIndex = -1; + + // Enable/disable backface culling (2-sided triangles, slower to render) + if (IsKeyPressed(KeyboardKey.Left)) EnableBackfaceCulling(); + if (IsKeyPressed(KeyboardKey.Right)) DisableBackfaceCulling(); + + // Reset triangle vertices to starting positions and reset backface culling + if (IsKeyPressed(KeyboardKey.R)) + { + trianglePositions[0] = startingPositions[0]; + trianglePositions[1] = startingPositions[1]; + trianglePositions[2] = startingPositions[2]; + + EnableBackfaceCulling(); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + if (linesMode) + { + // Draw triangle with lines + Begin(DrawMode.Lines); + // Three lines, six points + // Define color for next vertex + Color4ub(255, 0, 0, 255); + // Define vertex + Vertex2f(trianglePositions[0].X, trianglePositions[0].Y); + Color4ub(0, 255, 0, 255); + Vertex2f(trianglePositions[1].X, trianglePositions[1].Y); + + Color4ub(0, 255, 0, 255); + Vertex2f(trianglePositions[1].X, trianglePositions[1].Y); + Color4ub(0, 0, 255, 255); + Vertex2f(trianglePositions[2].X, trianglePositions[2].Y); + + Color4ub(0, 0, 255, 255); + Vertex2f(trianglePositions[2].X, trianglePositions[2].Y); + Color4ub(255, 0, 0, 255); + Vertex2f(trianglePositions[0].X, trianglePositions[0].Y); + End(); + } + else + { + // Draw triangle as a triangle + Begin(DrawMode.Triangles); + // One triangle, three points + // Define color for next vertex + Color4ub(255, 0, 0, 255); + // Define vertex + Vertex2f(trianglePositions[0].X, trianglePositions[0].Y); + Color4ub(0, 255, 0, 255); + Vertex2f(trianglePositions[1].X, trianglePositions[1].Y); + Color4ub(0, 0, 255, 255); + Vertex2f(trianglePositions[2].X, trianglePositions[2].Y); + End(); + } + + // Render the vertex handles, reacting to mouse movement/input + for (int i = 0; i < 3; i++) + { + // Draw handle fill focused by mouse + if (CheckCollisionPointCircle(GetMousePosition(), trianglePositions[i], handleRadius)) + DrawCircleV(trianglePositions[i], handleRadius, ColorAlpha(Color.DarkGray, 0.5f)); + + // Draw handle fill selected + if (i == triangleIndex) DrawCircleV(trianglePositions[i], handleRadius, Color.DarkGray); + + // Draw handle outline + DrawCircleLinesV(trianglePositions[i], handleRadius, Color.Black); + } + + // Draw controls + DrawText("SPACE: Toggle lines mode", 10, 10, 20, Color.DarkGray); + DrawText("LEFT-RIGHT: Toggle backface culling", 10, 40, 20, Color.DarkGray); + DrawText("MOUSE: Click and drag vertex points", 10, 70, 20, Color.DarkGray); + DrawText("R: Reset triangle to start positions", 10, 100, 20, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - rlgl triangle"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new RlglTriangle(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/SimpleParticles.cs b/Examples/Shapes/SimpleParticles.cs new file mode 100644 index 0000000..34d0eb6 --- /dev/null +++ b/Examples/Shapes/SimpleParticles.cs @@ -0,0 +1,308 @@ +/******************************************************************************************* +* +* raylib [shapes] example - simple particles +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.6, last time updated with raylib 5.6 +* +* Example contributed by Jordi Santonja (@JordSant) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Jordi Santonja (@JordSant) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shapes; + +public partial class SimpleParticles : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MAX_PARTICLES = 3000; // Max number of particles + + public string Name => "Shapes / Simple Particles"; + + public string Title => "raylib [shapes] example - simple particles"; + + //---------------------------------------------------------------------------------- + // Types and Structures Definition + //---------------------------------------------------------------------------------- + private enum ParticleType + { + Water = 0, + Smoke, + Fire + } + + private static readonly string[] particleTypeNames = ["WATER", "SMOKE", "FIRE"]; + + private struct Particle + { + public ParticleType type; // Particle type (WATER, SMOKE, FIRE) + public Vector2 position; // Particle position on screen + public Vector2 velocity; // Particle current speed and direction + public float radius; // Particle radius + public Color color; // Particle color + + public float lifeTime; // Particle life time + public bool alive; // Particle alive: inside screen and life time + } + + // Circular buffer state + private int head; // Index for the next write + private int tail; // Index for the next read + private Particle[] buffer; // Particle buffer array + + // Particle emitter parameters + private int emissionRate; // Negative: on average every -X frames. Positive: particles per frame + private ParticleType currentType; + private Vector2 emitterPosition; + + private Random random; + + public void Init() + { + // Definition of particles + buffer = new Particle[MAX_PARTICLES]; // Particle array + head = 0; + tail = 0; + + // Particle emitter parameters + emissionRate = -2; // Negative: on average every -X frames. Positive: particles per frame + currentType = ParticleType.Water; + emitterPosition = new(screenWidth / 2.0f, screenHeight / 2.0f); + + random = new Random(); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Emit new particles: when emissionRate is 1, emit every frame + if (emissionRate < 0) + { + if (random.Next(-emissionRate) == 0) EmitParticle(emitterPosition, currentType); + } + else + { + for (int i = 0; i <= emissionRate; i++) EmitParticle(emitterPosition, currentType); + } + + // Update the parameters of each particle + UpdateParticles(screenWidth, screenHeight); + + // Remove dead particles from the circular buffer + UpdateCircularBuffer(); + + // Change Particle Emission Rate (UP/DOWN arrows) + if (IsKeyPressed(KeyboardKey.Up)) emissionRate++; + if (IsKeyPressed(KeyboardKey.Down)) emissionRate--; + + // Change Particle Type (LEFT/RIGHT arrows) + if (IsKeyPressed(KeyboardKey.Right)) currentType = (currentType == ParticleType.Fire) ? ParticleType.Water : (ParticleType)((int)currentType + 1); + if (IsKeyPressed(KeyboardKey.Left)) currentType = (currentType == ParticleType.Water) ? ParticleType.Fire : (ParticleType)((int)currentType - 1); + + if (IsMouseButtonDown(MouseButton.Left)) emitterPosition = GetMousePosition(); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + // Call the function with a loop to draw all particles + DrawParticles(); + + // Draw UI and Instructions + DrawRectangle(5, 5, 315, 75, Fade(Color.SkyBlue, 0.5f)); + DrawRectangleLines(5, 5, 315, 75, Color.Blue); + + DrawText("CONTROLS:", 15, 15, 10, Color.Black); + DrawText("UP/DOWN: Change Particle Emission Rate", 15, 35, 10, Color.Black); + DrawText("LEFT/RIGHT: Change Particle Type (Water, Smoke, Fire)", 15, 55, 10, Color.Black); + + if (emissionRate < 0) DrawText($"Particles every {-emissionRate} frames | Type: {particleTypeNames[(int)currentType]}", 15, 95, 10, Color.DarkGray); + else DrawText($"{emissionRate + 1} Particles per frame | Type: {particleTypeNames[(int)currentType]}", 15, 95, 10, Color.DarkGray); + + DrawFPS(screenWidth - 80, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + //---------------------------------------------------------------------------------- + // Module Functions Definition + //---------------------------------------------------------------------------------- + private void EmitParticle(Vector2 emitterPosition, ParticleType type) + { + int index = AddToCircularBuffer(); + + // If buffer is full, index is -1 + if (index != -1) + { + ref Particle newParticle = ref buffer[index]; + + // Fill particle properties + newParticle.position = emitterPosition; + newParticle.alive = true; + newParticle.lifeTime = 0.0f; + newParticle.type = type; + float speed = (float)(random.Next(10)) / 5.0f; + switch (type) + { + case ParticleType.Water: + { + newParticle.radius = 5.0f; + newParticle.color = Color.Blue; + } break; + case ParticleType.Smoke: + { + newParticle.radius = 7.0f; + newParticle.color = Color.Gray; + } break; + case ParticleType.Fire: + { + newParticle.radius = 10.0f; + newParticle.color = Color.Yellow; + speed /= 10.0f; + } break; + default: break; + } + + float direction = (float)(random.Next(360)); + newParticle.velocity = new(speed * MathF.Cos(direction * DEG2RAD), speed * MathF.Sin(direction * DEG2RAD)); + } + } + + private int AddToCircularBuffer() + { + int index = -1; + + // Check if buffer full + if (((head + 1) % MAX_PARTICLES) != tail) + { + // Add new particle to the head position and advance head + index = head; + head = (head + 1) % MAX_PARTICLES; + } + + return index; + } + + private void UpdateParticles(int screenWidth, int screenHeight) + { + for (int i = tail; i != head; i = (i + 1) % MAX_PARTICLES) + { + // Update particle life and positions + buffer[i].lifeTime += 1.0f / 60.0f; // 60 FPS -> 1/60 seconds per frame + + switch (buffer[i].type) + { + case ParticleType.Water: + { + buffer[i].position.X += buffer[i].velocity.X; + buffer[i].velocity.Y += 0.2f; // Gravity + buffer[i].position.Y += buffer[i].velocity.Y; + } break; + case ParticleType.Smoke: + { + buffer[i].position.X += buffer[i].velocity.X; + buffer[i].velocity.Y -= 0.05f; // Upwards + buffer[i].position.Y += buffer[i].velocity.Y; + buffer[i].radius += 0.5f; // Increment radius: smoke expands + buffer[i].color.A -= 4; // Decrement alpha: smoke fades + + // If alpha transparent, particle dies + if (buffer[i].color.A < 4) buffer[i].alive = false; + } break; + case ParticleType.Fire: + { + // Add a little horizontal oscillation to fire particles + buffer[i].position.X += buffer[i].velocity.X + MathF.Cos(buffer[i].lifeTime * 215.0f); + buffer[i].velocity.Y -= 0.05f; // Upwards + buffer[i].position.Y += buffer[i].velocity.Y; + buffer[i].radius -= 0.15f; // Decrement radius: fire shrinks + buffer[i].color.G -= 3; // Decrement green: fire turns reddish starting from yellow + + // If radius too small, particle dies + if (buffer[i].radius <= 0.02f) buffer[i].alive = false; + } break; + default: break; + } + + // Disable particle when out of screen + Vector2 center = buffer[i].position; + float radius = buffer[i].radius; + + if ((center.X < -radius) || (center.X > (screenWidth + radius)) || + (center.Y < -radius) || (center.Y > (screenHeight + radius))) + { + buffer[i].alive = false; + } + } + } + + private void UpdateCircularBuffer() + { + // Update circular buffer: advance tail over dead particles + while ((tail != head) && !buffer[tail].alive) + { + tail = (tail + 1) % MAX_PARTICLES; + } + } + + private void DrawParticles() + { + for (int i = tail; i != head; i = (i + 1) % MAX_PARTICLES) + { + if (buffer[i].alive) + { + DrawCircleV(buffer[i].position, + buffer[i].radius, + buffer[i].color); + } + } + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - simple particles"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new SimpleParticles(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/SplinesDrawing.cs b/Examples/Shapes/SplinesDrawing.cs new file mode 100644 index 0000000..b8598d4 --- /dev/null +++ b/Examples/Shapes/SplinesDrawing.cs @@ -0,0 +1,387 @@ +/******************************************************************************************* +* +* raylib [shapes] example - splines drawing +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 5.0, last time updated with raylib 5.0 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2023-2025 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shapes; + +public partial class SplinesDrawing : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MAX_SPLINE_POINTS = 32; + + public string Name => "Shapes / Splines Drawing"; + + public string Title => "raylib [shapes] example - splines drawing"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + //---------------------------------------------------------------------------------- + // Types and Structures Definition + //---------------------------------------------------------------------------------- + // Cubic Bezier spline control points + // NOTE: Every segment has two control points + private struct ControlPoint + { + public Vector2 start; + public Vector2 end; + } + + // Spline types + private const int SPLINE_LINEAR = 0; // Linear + private const int SPLINE_BASIS = 1; // B-Spline + private const int SPLINE_CATMULLROM = 2; // Catmull-Rom + private const int SPLINE_BEZIER = 3; // Cubic Bezier + + private Vector2[] points; + private Vector2[] pointsInterleaved; + + private int pointCount; + private int selectedPoint; + private int focusedPoint; + private int selectedControlIndex; // -1 when none + private bool selectedControlStart; + private int focusedControlIndex; // -1 when none + private bool focusedControlStart; + + private ControlPoint[] control; + + // Spline config variables + private float splineThickness; + private int splineTypeActive; // 0-Linear, 1-BSpline, 2-CatmullRom, 3-Bezier + private bool splineTypeEditMode; + private bool splineHelpersActive; + + // Minimal raygui-like global lock + private static bool guiLocked; + + public void Init() + { + points = new Vector2[MAX_SPLINE_POINTS]; + points[0] = new Vector2(50.0f, 400.0f); + points[1] = new Vector2(160.0f, 220.0f); + points[2] = new Vector2(340.0f, 380.0f); + points[3] = new Vector2(520.0f, 60.0f); + points[4] = new Vector2(710.0f, 260.0f); + + // Array required for spline bezier-cubic, + // including control points interleaved with start-end segment points + pointsInterleaved = new Vector2[3 * (MAX_SPLINE_POINTS - 1) + 1]; + + pointCount = 5; + selectedPoint = -1; + focusedPoint = -1; + selectedControlIndex = -1; + focusedControlIndex = -1; + + // Cubic Bezier control points initialization + control = new ControlPoint[MAX_SPLINE_POINTS - 1]; + for (int i = 0; i < pointCount - 1; i++) + { + control[i].start = new Vector2(points[i].X + 50, points[i].Y); + control[i].end = new Vector2(points[i + 1].X - 50, points[i + 1].Y); + } + + splineThickness = 8.0f; + splineTypeActive = SPLINE_LINEAR; + splineTypeEditMode = false; + splineHelpersActive = true; + + guiLocked = false; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Spline points creation logic (at the end of spline) + if (IsMouseButtonPressed(MouseButton.Right) && (pointCount < MAX_SPLINE_POINTS)) + { + points[pointCount] = GetMousePosition(); + int i = pointCount - 1; + control[i].start = new Vector2(points[i].X + 50, points[i].Y); + control[i].end = new Vector2(points[i + 1].X - 50, points[i + 1].Y); + pointCount++; + } + + // Spline point focus and selection logic + if ((selectedPoint == -1) && ((splineTypeActive != SPLINE_BEZIER) || (selectedControlIndex == -1))) + { + focusedPoint = -1; + for (int i = 0; i < pointCount; i++) + { + if (CheckCollisionPointCircle(GetMousePosition(), points[i], 8.0f)) + { + focusedPoint = i; + break; + } + } + if (IsMouseButtonPressed(MouseButton.Left)) selectedPoint = focusedPoint; + } + + // Spline point movement logic + if (selectedPoint >= 0) + { + points[selectedPoint] = GetMousePosition(); + if (IsMouseButtonReleased(MouseButton.Left)) selectedPoint = -1; + } + + // Cubic Bezier spline control points logic + if ((splineTypeActive == SPLINE_BEZIER) && (focusedPoint == -1)) + { + // Spline control point focus and selection logic + if (selectedControlIndex == -1) + { + focusedControlIndex = -1; + for (int i = 0; i < pointCount - 1; i++) + { + if (CheckCollisionPointCircle(GetMousePosition(), control[i].start, 6.0f)) + { + focusedControlIndex = i; + focusedControlStart = true; + break; + } + else if (CheckCollisionPointCircle(GetMousePosition(), control[i].end, 6.0f)) + { + focusedControlIndex = i; + focusedControlStart = false; + break; + } + } + if (IsMouseButtonPressed(MouseButton.Left)) + { + selectedControlIndex = focusedControlIndex; + selectedControlStart = focusedControlStart; + } + } + + // Spline control point movement logic + if (selectedControlIndex != -1) + { + if (selectedControlStart) control[selectedControlIndex].start = GetMousePosition(); + else control[selectedControlIndex].end = GetMousePosition(); + if (IsMouseButtonReleased(MouseButton.Left)) selectedControlIndex = -1; + } + } + + // Spline selection logic + if (IsKeyPressed(KeyboardKey.One)) splineTypeActive = 0; + else if (IsKeyPressed(KeyboardKey.Two)) splineTypeActive = 1; + else if (IsKeyPressed(KeyboardKey.Three)) splineTypeActive = 2; + else if (IsKeyPressed(KeyboardKey.Four)) splineTypeActive = 3; + + // Clear selection when changing to a spline without control points + if (IsKeyPressed(KeyboardKey.One) || IsKeyPressed(KeyboardKey.Two) || IsKeyPressed(KeyboardKey.Three)) selectedControlIndex = -1; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + if (splineTypeActive == SPLINE_LINEAR) + { + // Draw spline: linear + DrawSplineLinear(points, pointCount, splineThickness, Color.Red); + } + else if (splineTypeActive == SPLINE_BASIS) + { + // Draw spline: basis + DrawSplineBasis(points, pointCount, splineThickness, Color.Red); // Provide connected points array + } + else if (splineTypeActive == SPLINE_CATMULLROM) + { + // Draw spline: catmull-rom + DrawSplineCatmullRom(points, pointCount, splineThickness, Color.Red); // Provide connected points array + } + else if (splineTypeActive == SPLINE_BEZIER) + { + // NOTE: Cubic-bezier spline requires the 2 control points of each segment to be + // provided interleaved with the start and end point of every segment + for (int i = 0; i < (pointCount - 1); i++) + { + pointsInterleaved[3 * i] = points[i]; + pointsInterleaved[3 * i + 1] = control[i].start; + pointsInterleaved[3 * i + 2] = control[i].end; + } + + pointsInterleaved[3 * (pointCount - 1)] = points[pointCount - 1]; + + // Draw spline: cubic-bezier (with control points) + DrawSplineBezierCubic(pointsInterleaved, 3 * (pointCount - 1) + 1, splineThickness, Color.Red); + + // Draw spline control points + for (int i = 0; i < pointCount - 1; i++) + { + // Every cubic bezier point have two control points + DrawCircleV(control[i].start, 6, Color.Gold); + DrawCircleV(control[i].end, 6, Color.Gold); + if (focusedControlIndex == i && focusedControlStart) DrawCircleV(control[i].start, 8, Color.Green); + else if (focusedControlIndex == i && !focusedControlStart) DrawCircleV(control[i].end, 8, Color.Green); + DrawLineEx(points[i], control[i].start, 1.0f, Color.LightGray); + DrawLineEx(points[i + 1], control[i].end, 1.0f, Color.LightGray); + + // Draw spline control lines + DrawLineV(points[i], control[i].start, Color.Gray); + DrawLineV(control[i].end, points[i + 1], Color.Gray); + } + } + + if (splineHelpersActive) + { + // Draw spline point helpers + for (int i = 0; i < pointCount; i++) + { + DrawCircleLinesV(points[i], (focusedPoint == i) ? 12.0f : 8.0f, (focusedPoint == i) ? Color.Blue : Color.DarkBlue); + if ((splineTypeActive != SPLINE_LINEAR) && + (splineTypeActive != SPLINE_BEZIER) && + (i < pointCount - 1)) DrawLineV(points[i], points[i + 1], Color.Gray); + + DrawText($"[{points[i].X:F0}, {points[i].Y:F0}]", (int)points[i].X, (int)points[i].Y + 10, 10, Color.Black); + } + } + + // Check all possible UI states that require controls lock + if (splineTypeEditMode || (selectedPoint != -1) || (selectedControlIndex != -1)) GuiLock(); + + // Draw spline config + GuiLabel(new Rectangle(12, 62, 140, 24), $"Spline thickness: {(int)splineThickness}"); + GuiSliderBar(new Rectangle(12, 60 + 24, 140, 16), null, null, ref splineThickness, 1.0f, 40.0f); + + GuiCheckBox(new Rectangle(12, 110, 20, 20), "Show point helpers", ref splineHelpersActive); + + if (splineTypeEditMode) GuiUnlock(); + + GuiLabel(new Rectangle(12, 10, 140, 24), "Spline type:"); + if (GuiDropdownBox(new Rectangle(12, 8 + 24, 140, 28), "LINEAR;BSPLINE;CATMULLROM;BEZIER", ref splineTypeActive, splineTypeEditMode)) splineTypeEditMode = !splineTypeEditMode; + + GuiUnlock(); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + //---------------------------------------------------------------------------------- + // Minimal raygui-like widgets (plain raylib re-implementation) + //---------------------------------------------------------------------------------- + private static void GuiLock() => guiLocked = true; + private static void GuiUnlock() => guiLocked = false; + + private static void GuiLabel(Rectangle bounds, string text) + { + DrawText(text, (int)bounds.X, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + + private static void GuiSliderBar(Rectangle bounds, string textLeft, string textRight, ref float value, float minValue, float maxValue) + { + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + if (!guiLocked && hover && IsMouseButtonDown(MouseButton.Left)) + { + value = minValue + ((mouse.X - bounds.X) / bounds.Width) * (maxValue - minValue); + if (value < minValue) value = minValue; + if (value > maxValue) value = maxValue; + } + + DrawRectangleRec(bounds, Color.LightGray); + float pct = (value - minValue) / (maxValue - minValue); + DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), Color.SkyBlue); + DrawRectangleLinesEx(bounds, 1, Color.Gray); + if (!string.IsNullOrEmpty(textLeft)) DrawText(textLeft, (int)bounds.X - MeasureText(textLeft, 10) - 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + if (!string.IsNullOrEmpty(textRight)) DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + + private static void GuiCheckBox(Rectangle bounds, string text, ref bool active) + { + Vector2 mouse = GetMousePosition(); + bool hover = CheckCollisionPointRec(mouse, bounds); + if (!guiLocked && hover && IsMouseButtonPressed(MouseButton.Left)) active = !active; + + DrawRectangleLinesEx(bounds, 1, hover ? Color.DarkBlue : Color.Gray); + if (active) DrawRectangle((int)bounds.X + 4, (int)bounds.Y + 4, (int)bounds.Width - 8, (int)bounds.Height - 8, Color.DarkGray); + if (text != null) DrawText(text, (int)(bounds.X + bounds.Width + 8), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + } + + private static bool GuiDropdownBox(Rectangle bounds, string text, ref int active, bool editMode) + { + string[] items = text.Split(';'); + bool pressed = false; + Vector2 mouse = GetMousePosition(); + bool clickable = !guiLocked; + + bool hoverMain = CheckCollisionPointRec(mouse, bounds); + DrawRectangleRec(bounds, editMode ? Color.SkyBlue : (hoverMain ? Color.LightGray : Color.RayWhite)); + DrawRectangleLinesEx(bounds, 1, editMode ? Color.Blue : Color.Gray); + DrawText(items[active], (int)bounds.X + 6, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray); + + if (editMode) + { + for (int i = 0; i < items.Length; i++) + { + Rectangle itemRec = new Rectangle(bounds.X, bounds.Y + bounds.Height * (i + 1), bounds.Width, bounds.Height); + bool hoverItem = CheckCollisionPointRec(mouse, itemRec); + DrawRectangleRec(itemRec, hoverItem ? Color.SkyBlue : Color.RayWhite); + DrawRectangleLinesEx(itemRec, 1, Color.Gray); + DrawText(items[i], (int)itemRec.X + 6, (int)(itemRec.Y + itemRec.Height / 2 - 5), 10, Color.DarkGray); + if (clickable && hoverItem && IsMouseButtonPressed(MouseButton.Left)) + { + active = i; + pressed = true; + } + } + } + + if (clickable && hoverMain && IsMouseButtonPressed(MouseButton.Left)) pressed = true; + + return pressed; + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - splines drawing"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new SplinesDrawing(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/StarfieldEffect.cs b/Examples/Shapes/StarfieldEffect.cs new file mode 100644 index 0000000..bcd64ea --- /dev/null +++ b/Examples/Shapes/StarfieldEffect.cs @@ -0,0 +1,180 @@ +/******************************************************************************************* +* +* raylib [shapes] example - starfield effect +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 6.0 +* +* Example contributed by JP Mortiboys (@themushroompirates) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 JP Mortiboys (@themushroompirates) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; // Required for: Lerp() + +namespace Examples.Shapes; + +public partial class StarfieldEffect : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int STAR_COUNT = 420; + + public string Name => "Shapes / Starfield Effect"; + + public string Title => "raylib [shapes] example - starfield effect"; + + private Color bgColor; + + // Speed at which we fly forward + private float speed; + + // We're either drawing lines or circles + private bool drawLines; + + private Vector3[] stars; + private Vector2[] starsScreenPos; + + public void Init() + { + bgColor = ColorLerp(Color.DarkBlue, Color.Black, 0.69f); + + // Speed at which we fly forward + speed = 10.0f / 9.0f; + + // We're either drawing lines or circles + drawLines = true; + + stars = new Vector3[STAR_COUNT]; + starsScreenPos = new Vector2[STAR_COUNT]; + + // Setup the stars with a random position + for (int i = 0; i < STAR_COUNT; i++) + { + stars[i].X = (float)GetRandomValue(-screenWidth / 2, (int)screenWidth / 2); + stars[i].Y = (float)GetRandomValue(-screenHeight / 2, (int)screenHeight / 2); + stars[i].Z = 1.0f; + } + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Change speed based on mouse + float mouseMove = GetMouseWheelMove(); + if ((int)mouseMove != 0) speed += 2.0f * mouseMove / 9.0f; + if (speed < 0.0f) speed = 0.1f; + else if (speed > 2.0f) speed = 2.0f; + + // Toggle lines / points with space bar + if (IsKeyPressed(KeyboardKey.Space)) drawLines = !drawLines; + + float dt = GetFrameTime(); + for (int i = 0; i < STAR_COUNT; i++) + { + // Update star's timer + stars[i].Z -= dt * speed; + + // Calculate the screen position + starsScreenPos[i] = new Vector2( + screenWidth * 0.5f + stars[i].X / stars[i].Z, + screenHeight * 0.5f + stars[i].Y / stars[i].Z + ); + + // If the star is too old, or offscreen, it dies and we make a new random one + if ((stars[i].Z < 0.0f) || (starsScreenPos[i].X < 0) || (starsScreenPos[i].Y < 0.0f) || + (starsScreenPos[i].X > screenWidth) || (starsScreenPos[i].Y > screenHeight)) + { + stars[i].X = (float)GetRandomValue(-screenWidth / 2, screenWidth / 2); + stars[i].Y = (float)GetRandomValue(-screenHeight / 2, screenHeight / 2); + stars[i].Z = 1.0f; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(bgColor); + + for (int i = 0; i < STAR_COUNT; i++) + { + if (drawLines) + { + // Get the time a little while ago for this star, but clamp it + float t = Clamp(stars[i].Z + 1.0f / 32.0f, 0.0f, 1.0f); + + // If it's different enough from the current time, we proceed + if ((t - stars[i].Z) > 1e-3) + { + // Calculate the screen position of the old point + Vector2 startPos = new Vector2( + screenWidth * 0.5f + stars[i].X / t, + screenHeight * 0.5f + stars[i].Y / t + ); + + // Draw a line connecting the old point to the current point + DrawLineV(startPos, starsScreenPos[i], Color.RayWhite); + } + } + else + { + // Make the radius grow as the star ages + float radius = Lerp(stars[i].Z, 1.0f, 5.0f); + + // Draw the circle + DrawCircleV(starsScreenPos[i], radius, Color.RayWhite); + } + } + + DrawText($"[MOUSE WHEEL] Current Speed: {9.0f * speed / 2.0f:F0}", 10, 40, 20, Color.RayWhite); + DrawText($"[SPACE] Current draw mode: {(drawLines ? "Lines" : "Circles")}", 10, 70, 20, Color.RayWhite); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - starfield effect"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new StarfieldEffect(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/TopDownLights.cs b/Examples/Shapes/TopDownLights.cs new file mode 100644 index 0000000..83e1f6a --- /dev/null +++ b/Examples/Shapes/TopDownLights.cs @@ -0,0 +1,406 @@ +/******************************************************************************************* +* +* raylib [shapes] example - top down lights +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 4.2, last time updated with raylib 4.2 +* +* Example contributed by Jeffery Myers (@JeffM2501) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2022-2025 Jeffery Myers (@JeffM2501) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; +using static Raylib_cs.Rlgl; + +namespace Examples.Shapes; + +public partial class TopDownLights : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + // Custom Blend Modes + private const int RLGL_SRC_ALPHA = 0x0302; + private const int RLGL_MIN = 0x8007; + private const int RLGL_MAX = 0x8008; + + private const int MAX_BOXES = 20; + private const int MAX_SHADOWS = MAX_BOXES * 3; // MAX_BOXES*3 - Each box can cast up to two shadow volumes for the edges it is away from, and one for the box itself + private const int MAX_LIGHTS = 16; + + public string Name => "Shapes / Top Down Lights"; + + public string Title => "raylib [shapes] example - top down lights"; + + //---------------------------------------------------------------------------------- + // Types and Structures Definition + //---------------------------------------------------------------------------------- + // Shadow geometry type + private class ShadowGeometry + { + public Vector2[] vertices = new Vector2[4]; + } + + // Light info type + private class LightInfo + { + public bool active; // Is this light slot active? + public bool dirty; // Does this light need to be updated? + public bool valid; // Is this light in a valid position? + + public Vector2 position; // Light position + public RenderTexture2D mask; // Alpha mask for the light + public float outerRadius; // The distance the light touches + public Rectangle bounds; // A cached rectangle of the light bounds to help with culling + + public ShadowGeometry[] shadows = new ShadowGeometry[MAX_SHADOWS]; + public int shadowCount; + + public LightInfo() + { + for (int i = 0; i < MAX_SHADOWS; i++) shadows[i] = new ShadowGeometry(); + } + } + + //------------------------------------------------------------------------------------ + // Global Variables Definition + //------------------------------------------------------------------------------------ + private LightInfo[] lights; + + private int boxCount; + private Rectangle[] boxes; + private Texture2D backgroundTexture; + private RenderTexture2D lightMask; + private int nextLight; + private bool showLines; + + public void Init() + { + lights = new LightInfo[MAX_LIGHTS]; + for (int i = 0; i < MAX_LIGHTS; i++) lights[i] = new LightInfo(); + + // Initialize our 'world' of boxes + boxCount = 0; + boxes = new Rectangle[MAX_BOXES]; + SetupBoxes(); + + // Create a checkerboard ground texture + Image img = GenImageChecked(64, 64, 32, 32, Color.DarkBrown, Color.DarkGray); + backgroundTexture = LoadTextureFromImage(img); + UnloadImage(img); + + // Create a global light mask to hold all the blended lights + lightMask = LoadRenderTexture(GetScreenWidth(), GetScreenHeight()); + + // Setup initial light + SetupLight(0, 600, 400, 300); + nextLight = 1; + + showLines = false; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Drag light 0 + if (IsMouseButtonDown(MouseButton.Left)) MoveLight(0, GetMousePosition().X, GetMousePosition().Y); + + // Make a new light + if (IsMouseButtonPressed(MouseButton.Right) && (nextLight < MAX_LIGHTS)) + { + SetupLight(nextLight, GetMousePosition().X, GetMousePosition().Y, 200); + nextLight++; + } + + // Toggle debug info + if (IsKeyPressed(KeyboardKey.F1)) showLines = !showLines; + + // Update the lights and keep track if any were dirty so we know if we need to update the master light mask + bool dirtyLights = false; + for (int i = 0; i < MAX_LIGHTS; i++) + { + if (UpdateLight(i, boxes, boxCount)) dirtyLights = true; + } + + // Update the light mask + if (dirtyLights) + { + // Build up the light mask + BeginTextureMode(lightMask); + + ClearBackground(Color.Black); + + // Force the blend mode to only set the alpha of the destination + SetBlendFactors(RLGL_SRC_ALPHA, RLGL_SRC_ALPHA, RLGL_MIN); + SetBlendMode(BlendMode.Custom); + + // Merge in all the light masks + for (int i = 0; i < MAX_LIGHTS; i++) + { + if (lights[i].active) DrawTextureRec(lights[i].mask.Texture, new Rectangle(0, 0, (float)GetScreenWidth(), -(float)GetScreenHeight()), Vector2Zero(), Color.White); + } + + DrawRenderBatchActive(); + + // Go back to normal blend + SetBlendMode(BlendMode.Alpha); + EndTextureMode(); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.Black); + + // Draw the tile background + DrawTextureRec(backgroundTexture, new Rectangle(0, 0, (float)GetScreenWidth(), (float)GetScreenHeight()), Vector2Zero(), Color.White); + + // Overlay the shadows from all the lights + DrawTextureRec(lightMask.Texture, new Rectangle(0, 0, (float)GetScreenWidth(), -(float)GetScreenHeight()), Vector2Zero(), ColorAlpha(Color.White, showLines ? 0.75f : 1.0f)); + + // Draw the lights + for (int i = 0; i < MAX_LIGHTS; i++) + { + if (lights[i].active) DrawCircle((int)lights[i].position.X, (int)lights[i].position.Y, 10, (i == 0) ? Color.Yellow : Color.White); + } + + if (showLines) + { + for (int s = 0; s < lights[0].shadowCount; s++) + { + DrawTriangleFan(lights[0].shadows[s].vertices, 4, Color.DarkPurple); + } + + for (int b = 0; b < boxCount; b++) + { + if (CheckCollisionRecs(boxes[b], lights[0].bounds)) DrawRectangleRec(boxes[b], Color.Purple); + + DrawRectangleLines((int)boxes[b].X, (int)boxes[b].Y, (int)boxes[b].Width, (int)boxes[b].Height, Color.DarkBlue); + } + + DrawText("(F1) Hide Shadow Volumes", 10, 50, 10, Color.Green); + } + else + { + DrawText("(F1) Show Shadow Volumes", 10, 50, 10, Color.Green); + } + + DrawFPS(screenWidth - 80, 10); + DrawText("Drag to move light #1", 10, 10, 10, Color.DarkGreen); + DrawText("Right click to add new light", 10, 30, 10, Color.DarkGreen); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(backgroundTexture); + UnloadRenderTexture(lightMask); + for (int i = 0; i < MAX_LIGHTS; i++) + { + if (lights[i].active) UnloadRenderTexture(lights[i].mask); + } + } + + //------------------------------------------------------------------------------------ + // Module Functions Definition + //------------------------------------------------------------------------------------ + // Move a light and mark it as dirty so that we update it's mask next frame + private void MoveLight(int slot, float x, float y) + { + lights[slot].dirty = true; + lights[slot].position.X = x; + lights[slot].position.Y = y; + + // update the cached bounds + lights[slot].bounds.X = x - lights[slot].outerRadius; + lights[slot].bounds.Y = y - lights[slot].outerRadius; + } + + // Compute a shadow volume for the edge + // It takes the edge and projects it back by the light radius and turns it into a quad + private void ComputeShadowVolumeForEdge(int slot, Vector2 sp, Vector2 ep) + { + if (lights[slot].shadowCount >= MAX_SHADOWS) return; + + float extension = lights[slot].outerRadius * 2; + + Vector2 spVector = Vector2Normalize(Vector2Subtract(sp, lights[slot].position)); + Vector2 spProjection = Vector2Add(sp, Vector2Scale(spVector, extension)); + + Vector2 epVector = Vector2Normalize(Vector2Subtract(ep, lights[slot].position)); + Vector2 epProjection = Vector2Add(ep, Vector2Scale(epVector, extension)); + + lights[slot].shadows[lights[slot].shadowCount].vertices[0] = sp; + lights[slot].shadows[lights[slot].shadowCount].vertices[1] = ep; + lights[slot].shadows[lights[slot].shadowCount].vertices[2] = epProjection; + lights[slot].shadows[lights[slot].shadowCount].vertices[3] = spProjection; + + lights[slot].shadowCount++; + } + + // Setup a light + private void SetupLight(int slot, float x, float y, float radius) + { + lights[slot].active = true; + lights[slot].valid = false; // The light must prove it is valid + lights[slot].mask = LoadRenderTexture(GetScreenWidth(), GetScreenHeight()); + lights[slot].outerRadius = radius; + + lights[slot].bounds.Width = radius * 2; + lights[slot].bounds.Height = radius * 2; + + MoveLight(slot, x, y); + + // Force the render texture to have something in it + DrawLightMask(slot); + } + + // See if a light needs to update it's mask + private bool UpdateLight(int slot, Rectangle[] boxes, int count) + { + if (!lights[slot].active || !lights[slot].dirty) return false; + + lights[slot].dirty = false; + lights[slot].shadowCount = 0; + lights[slot].valid = false; + + for (int i = 0; i < count; i++) + { + // Are we in a box? if so we are not valid + if (CheckCollisionPointRec(lights[slot].position, boxes[i])) return false; + + // If this box is outside our bounds, we can skip it + if (!CheckCollisionRecs(lights[slot].bounds, boxes[i])) continue; + + // Check the edges that are on the same side we are, and cast shadow volumes out from them + + // Top + Vector2 sp = new Vector2(boxes[i].X, boxes[i].Y); + Vector2 ep = new Vector2(boxes[i].X + boxes[i].Width, boxes[i].Y); + + if (lights[slot].position.Y > ep.Y) ComputeShadowVolumeForEdge(slot, sp, ep); + + // Right + sp = ep; + ep.Y += boxes[i].Height; + if (lights[slot].position.X < ep.X) ComputeShadowVolumeForEdge(slot, sp, ep); + + // Bottom + sp = ep; + ep.X -= boxes[i].Width; + if (lights[slot].position.Y < ep.Y) ComputeShadowVolumeForEdge(slot, sp, ep); + + // Left + sp = ep; + ep.Y -= boxes[i].Height; + if (lights[slot].position.X > ep.X) ComputeShadowVolumeForEdge(slot, sp, ep); + + // The box itself + lights[slot].shadows[lights[slot].shadowCount].vertices[0] = new Vector2(boxes[i].X, boxes[i].Y); + lights[slot].shadows[lights[slot].shadowCount].vertices[1] = new Vector2(boxes[i].X, boxes[i].Y + boxes[i].Height); + lights[slot].shadows[lights[slot].shadowCount].vertices[2] = new Vector2(boxes[i].X + boxes[i].Width, boxes[i].Y + boxes[i].Height); + lights[slot].shadows[lights[slot].shadowCount].vertices[3] = new Vector2(boxes[i].X + boxes[i].Width, boxes[i].Y); + lights[slot].shadowCount++; + } + + lights[slot].valid = true; + + DrawLightMask(slot); + + return true; + } + + // Draw the light and shadows to the mask for a light + private void DrawLightMask(int slot) + { + // Use the light mask + BeginTextureMode(lights[slot].mask); + + ClearBackground(Color.White); + + // Force the blend mode to only set the alpha of the destination + SetBlendFactors(RLGL_SRC_ALPHA, RLGL_SRC_ALPHA, RLGL_MIN); + SetBlendMode(BlendMode.Custom); + + // If we are valid, then draw the light radius to the alpha mask + if (lights[slot].valid) DrawCircleGradient(lights[slot].position, lights[slot].outerRadius, ColorAlpha(Color.White, 0), Color.White); + + DrawRenderBatchActive(); + + // Cut out the shadows from the light radius by forcing the alpha to maximum + SetBlendMode(BlendMode.Alpha); + SetBlendFactors(RLGL_SRC_ALPHA, RLGL_SRC_ALPHA, RLGL_MAX); + SetBlendMode(BlendMode.Custom); + + // Draw the shadows to the alpha mask + for (int i = 0; i < lights[slot].shadowCount; i++) + { + DrawTriangleFan(lights[slot].shadows[i].vertices, 4, Color.White); + } + + DrawRenderBatchActive(); + + // Go back to normal blend mode + SetBlendMode(BlendMode.Alpha); + + EndTextureMode(); + } + + // Set up some boxes + private void SetupBoxes() + { + boxes[0] = new Rectangle(150, 80, 40, 40); + boxes[1] = new Rectangle(1200, 700, 40, 40); + boxes[2] = new Rectangle(200, 600, 40, 40); + boxes[3] = new Rectangle(1000, 50, 40, 40); + boxes[4] = new Rectangle(500, 350, 40, 40); + + for (int i = 5; i < MAX_BOXES; i++) + { + boxes[i] = new Rectangle((float)GetRandomValue(0, GetScreenWidth()), (float)GetRandomValue(0, GetScreenHeight()), (float)GetRandomValue(10, 100), (float)GetRandomValue(10, 100)); + } + + boxCount = MAX_BOXES; + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - top down lights"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new TopDownLights(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/TriangleStrip.cs b/Examples/Shapes/TriangleStrip.cs new file mode 100644 index 0000000..fc1ad44 --- /dev/null +++ b/Examples/Shapes/TriangleStrip.cs @@ -0,0 +1,141 @@ +/******************************************************************************************* +* +* raylib [shapes] example - triangle strip +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Jopestpe (@jopestpe) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Jopestpe (@jopestpe) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Shapes; + +public partial class TriangleStrip : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Triangle Strip"; + + public string Title => "raylib [shapes] example - triangle strip"; + + private Vector2[] points; + private Vector2 center; + private float segments; + private float insideRadius; + private float outsideRadius; + private bool outline; + + public void Init() + { + points = new Vector2[122]; + center = new((screenWidth / 2.0f) - 125.0f, screenHeight / 2.0f); + segments = 6.0f; + insideRadius = 100.0f; + outsideRadius = 150.0f; + outline = true; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + int pointCount = (int)(segments); + float angleStep = (360.0f / pointCount) * DEG2RAD; + + for (int i = 0, i2 = 0; i < pointCount; i++, i2 += 2) + { + float angle1 = i * angleStep; + points[i2] = new Vector2(center.X + MathF.Cos(angle1) * insideRadius, center.Y + MathF.Sin(angle1) * insideRadius); + float angle2 = angle1 + angleStep / 2.0f; + points[i2 + 1] = new Vector2(center.X + MathF.Cos(angle2) * outsideRadius, center.Y + MathF.Sin(angle2) * outsideRadius); + } + + points[pointCount * 2] = points[0]; + points[pointCount * 2 + 1] = points[1]; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + for (int i = 0; i < pointCount; i++) + { + Vector2 a = points[i * 2]; + Vector2 b = points[i * 2 + 1]; + Vector2 c = points[i * 2 + 2]; + Vector2 d = points[i * 2 + 3]; + + float angle1 = i * angleStep; + DrawTriangle(c, b, a, ColorFromHSV(angle1 * RAD2DEG, 1.0f, 1.0f)); + DrawTriangle(d, b, c, ColorFromHSV((angle1 + angleStep / 2) * RAD2DEG, 1.0f, 1.0f)); + + if (outline) + { + DrawTriangleLines(a, b, c, Color.Black); + DrawTriangleLines(c, b, d, Color.Black); + } + } + + DrawLine(580, 0, 580, GetScreenHeight(), new Color(218, 218, 218, 255)); + DrawRectangle(580, 0, GetScreenWidth(), GetScreenHeight(), new Color(232, 232, 232, 255)); + + // Draw GUI controls + //------------------------------------------------------------------------------ + // NOTE: raygui is not bound in raylib-cs, so the interactive controls are omitted + // and 'segments'/'outline' keep their initial values. + //GuiSliderBar(new Rectangle(640, 40, 120, 20), "Segments", TextFormat("%.0f", segments), ref segments, 6.0f, 60.0f); + //GuiCheckBox(new Rectangle(640, 70, 20, 20), "Outline", ref outline); + //------------------------------------------------------------------------------ + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - triangle strip"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new TriangleStrip(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Shapes/VectorAngle.cs b/Examples/Shapes/VectorAngle.cs new file mode 100644 index 0000000..7cb07ea --- /dev/null +++ b/Examples/Shapes/VectorAngle.cs @@ -0,0 +1,154 @@ +/******************************************************************************************* +* +* raylib [shapes] example - vector angle +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 1.0, last time updated with raylib 5.0 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2023-2025 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; // Required for: Vector2LineAngle() + +namespace Examples.Shapes; + +public partial class VectorAngle : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Vector Angle"; + + public string Title => "raylib [shapes] example - vector angle"; + + private Vector2 v0; + private Vector2 v1; + private Vector2 v2; // Updated with mouse position + + private float angle; // Angle in degrees + private int angleMode; // 0-Vector2Angle(), 1-Vector2LineAngle() + + public void Init() + { + v0 = new(screenWidth / 2.0f, screenHeight / 2.0f); + v1 = Vector2Add(v0, new Vector2(100.0f, 80.0f)); + v2 = new(0, 0); // Updated with mouse position + + angle = 0.0f; // Angle in degrees + angleMode = 0; // 0-Vector2Angle(), 1-Vector2LineAngle() + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + float startangle = 0.0f; + + if (angleMode == 0) startangle = -Vector2LineAngle(v0, v1) * RAD2DEG; + if (angleMode == 1) startangle = 0.0f; + + v2 = GetMousePosition(); + + if (IsKeyPressed(KeyboardKey.Space)) angleMode = (angleMode == 0) ? 1 : 0; + + if ((angleMode == 0) && IsMouseButtonDown(MouseButton.Right)) v1 = GetMousePosition(); + + if (angleMode == 0) + { + // Calculate angle between two vectors, considering a common origin (v0) + Vector2 v1Normal = Vector2Normalize(Vector2Subtract(v1, v0)); + Vector2 v2Normal = Vector2Normalize(Vector2Subtract(v2, v0)); + + angle = Vector2Angle(v1Normal, v2Normal) * RAD2DEG; + } + else if (angleMode == 1) + { + // Calculate angle defined by a two vectors line, in reference to horizontal line + angle = Vector2LineAngle(v0, v2) * RAD2DEG; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + if (angleMode == 0) + { + DrawText("MODE 0: Angle between V1 and V2", 10, 10, 20, Color.Black); + DrawText("Right Click to Move V2", 10, 30, 20, Color.DarkGray); + + DrawLineEx(v0, v1, 2.0f, Color.Black); + DrawLineEx(v0, v2, 2.0f, Color.Red); + + DrawCircleSector(v0, 40.0f, startangle, startangle + angle, 32, Fade(Color.Green, 0.6f)); + } + else if (angleMode == 1) + { + DrawText("MODE 1: Angle formed by line V1 to V2", 10, 10, 20, Color.Black); + + DrawLine(0, screenHeight / 2, screenWidth, screenHeight / 2, Color.LightGray); + DrawLineEx(v0, v2, 2.0f, Color.Red); + + DrawCircleSector(v0, 40.0f, startangle, startangle - angle, 32, Fade(Color.Green, 0.6f)); + } + + DrawText("v0", (int)v0.X, (int)v0.Y, 10, Color.DarkGray); + + // If the line from v0 to v1 would overlap the text, move it's position up 10 + if (angleMode == 0 && Vector2Subtract(v0, v1).Y > 0.0f) DrawText("v1", (int)v1.X, (int)v1.Y - 10, 10, Color.DarkGray); + if (angleMode == 0 && Vector2Subtract(v0, v1).Y < 0.0f) DrawText("v1", (int)v1.X, (int)v1.Y, 10, Color.DarkGray); + + // If angle mode 1, use v1 to emphasize the horizontal line + if (angleMode == 1) DrawText("v1", (int)v0.X + 40, (int)v0.Y, 10, Color.DarkGray); + + // position adjusted by -10 so it isn't hidden by cursor + DrawText("v2", (int)v2.X - 10, (int)v2.Y - 10, 10, Color.DarkGray); + + DrawText("Press SPACE to change MODE", 460, 10, 20, Color.DarkGray); + DrawText($"ANGLE: {angle:F2}", 10, 70, 20, Color.Lime); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - vector angle"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new VectorAngle(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Text/InlineStyling.cs b/Examples/Text/InlineStyling.cs new file mode 100644 index 0000000..c61a696 --- /dev/null +++ b/Examples/Text/InlineStyling.cs @@ -0,0 +1,312 @@ +/******************************************************************************************* +* +* raylib [text] example - inline styling +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Wagner Barongello (@SultansOfCode) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Wagner Barongello (@SultansOfCode) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using System.Text; +using Raylib_cs; +using static Raylib_cs.Raylib; + +namespace Examples.Text; + +public partial class InlineStyling : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Text / Inline Styling"; + + public string Title => "raylib [text] example - inline styling"; + + private Vector2 textSize; // Measure text box for provided font and text + private Color colRandom; // Random color used on text + private int frameCounter; // Used to generate a new random color every certain frames + + public void Init() + { + textSize = new(0, 0); + colRandom = Color.Red; + frameCounter = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + frameCounter++; + + if ((frameCounter % 20) == 0) + { + colRandom.R = (byte)GetRandomValue(0, 255); + colRandom.G = (byte)GetRandomValue(0, 255); + colRandom.B = (byte)GetRandomValue(0, 255); + colRandom.A = 255; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + // Text inline styling strategy used: [ ] delimiters for format + // - Define foreground color: [cRRGGBBAA] + // - Define background color: [bRRGGBBAA] + // - Reset formating: [r] + // Colors defined with [cRRGGBBAA] or [bRRGGBBAA] are multiplied by the base color alpha + // This allows global transparency control while keeping per-section styling (ex. text fade effects) + // Example: [bAA00AAFF][cFF0000FF]red text on gray background[r] normal text + + DrawTextStyled(GetFontDefault(), "This changes the [cFF0000FF]foreground color[r] of provided text!!!", + new Vector2(100, 80), 20.0f, 2.0f, Color.Black); + + DrawTextStyled(GetFontDefault(), "This changes the [bFF00FFFF]background color[r] of provided text!!!", + new Vector2(100, 120), 20.0f, 2.0f, Color.Black); + + DrawTextStyled(GetFontDefault(), "This changes the [c00ff00ff][bff0000ff]foreground and background colors[r]!!!", + new Vector2(100, 160), 20.0f, 2.0f, Color.Black); + + DrawTextStyled(GetFontDefault(), "This changes the [c00ff00ff]alpha[r] relative [cffffffff][b000000ff]from source[r] [cff000088]color[r]!!!", + new Vector2(100, 200), 20.0f, 2.0f, new Color(0, 0, 0, 100)); + + // Get pointer to formated text + string text = $"Let's be [c{colRandom.R:x2}{colRandom.G:x2}{colRandom.B:x2}FF]CREATIVE[r] !!!"; + DrawTextStyled(GetFontDefault(), text, new Vector2(100, 240), 40.0f, 2.0f, Color.Black); + + textSize = MeasureTextStyled(GetFontDefault(), text, 40.0f, 2.0f); + DrawRectangleLines(100, 240, (int)textSize.X, (int)textSize.Y, Color.Green); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + //---------------------------------------------------------------------------------- + // Module Functions Definition + //---------------------------------------------------------------------------------- + // Draw text using inline styling + // PARAM: color is the default text color, background color is BLANK by default + // NOTE: Using input color as the base alpha multiplied to inline styles + private static unsafe void DrawTextStyled(Font font, string text, Vector2 position, float fontSize, float spacing, Color color) + { + // Text inline styling strategy used: [ ] delimiters for format + // - Define foreground color: [cRRGGBBAA] + // - Define background color: [bRRGGBBAA] + // - Reset formating: [r] + // Example: [bAA00AAFF][cFF0000FF]red text on gray background[r] normal text + + if (font.Texture.Id == 0) + { + font = GetFontDefault(); + } + + using var textNative = new Utf8Buffer(text); + sbyte* t = textNative.AsPointer(); + int textLen = Encoding.UTF8.GetByteCount(text); + + Color colFront = color; + Color colBack = Color.Blank; + int backRecPadding = 4; // Background rectangle padding + + float textOffsetY = 0.0f; + float textOffsetX = 0.0f; + float textLineSpacing = 0.0f; + float scaleFactor = fontSize / font.BaseSize; + + for (int i = 0; i < textLen;) + { + int codepointByteCount = 0; + int codepoint = GetCodepointNext(&t[i], &codepointByteCount); + + if (codepoint == '\n') + { + textOffsetY += (fontSize + textLineSpacing); + textOffsetX = 0.0f; + } + else + { + if (codepoint == '[') // Process pipe styling + { + if (((i + 2) < textLen) && ((char)t[i + 1] == 'r') && ((char)t[i + 2] == ']')) // Reset styling + { + colFront = color; + colBack = Color.Blank; + + i += 3; // Skip "[r]" + continue; // Do not draw characters + } + else if (((i + 1) < textLen) && (((char)t[i + 1] == 'c') || ((char)t[i + 1] == 'b'))) + { + i += 2; // Skip "[c" or "[b" to start parsing color + + // Parse following color + var colHexText = new StringBuilder(); + int colHexCount = 0; + while ((i + colHexCount < textLen) && (t[i + colHexCount] != 0) && ((char)t[i + colHexCount] != ']')) + { + char ch = (char)t[i + colHexCount]; + if (((ch >= '0') && (ch <= '9')) || + ((ch >= 'A') && (ch <= 'F')) || + ((ch >= 'a') && (ch <= 'f'))) + { + colHexText.Append(ch); + colHexCount++; + } + else break; // Only affects while loop + } + + // Convert hex color text into actual Color + uint colHexValue = colHexText.Length > 0 ? Convert.ToUInt32(colHexText.ToString(), 16) : 0; + if ((char)t[i - 1] == 'c') + { + colFront = GetColor(colHexValue); + } + else if ((char)t[i - 1] == 'b') + { + colBack = GetColor(colHexValue); + } + + i += (colHexCount + 1); // Skip color value retrieved and ']' + continue; // Do not draw characters + } + } + + int index = GetGlyphIndex(font, codepoint); + float increaseX = 0.0f; + + if (font.Glyphs[index].AdvanceX == 0) increaseX = (font.Recs[index].Width * scaleFactor + spacing); + else increaseX += (font.Glyphs[index].AdvanceX * scaleFactor + spacing); + + // Draw background rectangle color (if required) + if (colBack.A > 0) DrawRectangleRec(new Rectangle(position.X + textOffsetX, position.Y + textOffsetY - backRecPadding, increaseX, fontSize + 2 * backRecPadding), colBack); + + if ((codepoint != ' ') && (codepoint != '\t')) + { + DrawTextCodepoint(font, codepoint, new Vector2(position.X + textOffsetX, position.Y + textOffsetY), fontSize, colFront); + } + + textOffsetX += increaseX; + } + + i += codepointByteCount; + } + } + + // Measure inline styled text + // NOTE: Measuring styled text requires skipping styling data + // WARNING: Not considering line breaks + private static unsafe Vector2 MeasureTextStyled(Font font, string text, float fontSize, float spacing) + { + Vector2 textSize = new(0, 0); + + if ((font.Texture.Id == 0) || (text == null) || (text.Length == 0)) return textSize; // Security check + + using var textNative = new Utf8Buffer(text); + sbyte* t = textNative.AsPointer(); + int textLen = Encoding.UTF8.GetByteCount(text); // Get size in bytes of text + + float textWidth = 0.0f; + float textHeight = fontSize; + float scaleFactor = fontSize / (float)font.BaseSize; + + int codepoint = 0; // Current character + int index = 0; // Index position in sprite font + int validCodepointCounter = 0; + + for (int i = 0; i < textLen;) + { + int codepointByteCount = 0; + codepoint = GetCodepointNext(&t[i], &codepointByteCount); + + if (codepoint == '[') // Ignore pipe inline styling + { + if (((i + 2) < textLen) && ((char)t[i + 1] == 'r') && ((char)t[i + 2] == ']')) // Reset styling + { + i += 3; // Skip "[r]" + continue; // Do not measure characters + } + else if (((i + 1) < textLen) && (((char)t[i + 1] == 'c') || ((char)t[i + 1] == 'b'))) + { + i += 2; // Skip "[c" or "[b" to start parsing color + + int colHexCount = 0; + while ((i + colHexCount < textLen) && (t[i + colHexCount] != 0) && ((char)t[i + colHexCount] != ']')) + { + char ch = (char)t[i + colHexCount]; + if (((ch >= '0') && (ch <= '9')) || + ((ch >= 'A') && (ch <= 'F')) || + ((ch >= 'a') && (ch <= 'f'))) + { + colHexCount++; + } + else break; // Only affects while loop + } + + i += (colHexCount + 1); // Skip color value retrieved and ']' + continue; // Do not measure characters + } + } + else if (codepoint != '\n') + { + index = GetGlyphIndex(font, codepoint); + + if (font.Glyphs[index].AdvanceX > 0) textWidth += font.Glyphs[index].AdvanceX; + else textWidth += (font.Recs[index].Width + font.Glyphs[index].OffsetX); + + validCodepointCounter++; + i += codepointByteCount; + } + } + + textSize.X = textWidth * scaleFactor + (validCodepointCounter - 1) * spacing; + textSize.Y = textHeight; + + return textSize; + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [text] example - inline styling"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new InlineStyling(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Text/StringsManagement.cs b/Examples/Text/StringsManagement.cs new file mode 100644 index 0000000..79ec512 --- /dev/null +++ b/Examples/Text/StringsManagement.cs @@ -0,0 +1,530 @@ +/******************************************************************************************* +* +* raylib [text] example - strings management +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by David Buzatto (@davidbuzatto) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 David Buzatto (@davidbuzatto) +* +********************************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; +using Raylib_cs; +using static Raylib_cs.Raylib; + +namespace Examples.Text; + +public partial class StringsManagement : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MaxTextLength = 100; + private const int MaxTextParticles = 100; + private const int FontSize = 30; + + public string Name => "Text / Strings Management"; + + public string Title => "raylib [text] example - strings management"; + + //---------------------------------------------------------------------------------- + // Types and Structures Definition + //---------------------------------------------------------------------------------- + private class TextParticle + { + public string Text; + public Rectangle Rect; // Boundary + public Vector2 Vel; // Velocity + public Vector2 Ppos; // Previous position + public float Padding; + public float BorderWidth; + public float Friction; + public float Elasticity; + public Color Color; + public bool Grabbed; + } + + private List textParticles; + private TextParticle grabbedTextParticle; + private Vector2 pressOffset; + + public void Init() + { + textParticles = new(); + grabbedTextParticle = null; + pressOffset = new(0, 0); + + PrepareFirstTextParticle("raylib => fun videogames programming!"); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + float delta = GetFrameTime(); + Vector2 mousePos = GetMousePosition(); + + // Checks if a text particle was grabbed + if (IsMouseButtonPressed(MouseButton.Left)) + { + for (int i = textParticles.Count - 1; i >= 0; i--) + { + TextParticle tp = textParticles[i]; + if (CheckCollisionPointRec(mousePos, tp.Rect)) + { + pressOffset.X = mousePos.X - tp.Rect.X; + pressOffset.Y = mousePos.Y - tp.Rect.Y; + tp.Grabbed = true; + grabbedTextParticle = tp; + break; + } + } + } + + // Releases any text particle the was grabbed + if (IsMouseButtonReleased(MouseButton.Left)) + { + if (grabbedTextParticle != null) + { + grabbedTextParticle.Grabbed = false; + grabbedTextParticle = null; + } + } + + // Slice os shatter a text particle + if (IsMouseButtonPressed(MouseButton.Right)) + { + for (int i = textParticles.Count - 1; i >= 0; i--) + { + TextParticle tp = textParticles[i]; + if (CheckCollisionPointRec(mousePos, tp.Rect)) + { + if (IsKeyDown(KeyboardKey.LeftShift)) + { + ShatterTextParticle(tp, i); + } + else + { + SliceTextParticle(tp, i, tp.Text.Length / 2); + } + break; + } + } + } + + // Shake text particles + if (IsMouseButtonPressed(MouseButton.Middle)) + { + for (int i = 0; i < textParticles.Count; i++) + { + if (!textParticles[i].Grabbed) + { + textParticles[i].Vel = new Vector2(GetRandomValue(-2000, 2000), GetRandomValue(-2000, 2000)); + } + } + } + + // Reset using TextTo* functions + if (IsKeyPressed(KeyboardKey.One)) PrepareFirstTextParticle("raylib => fun videogames programming!"); + if (IsKeyPressed(KeyboardKey.Two)) PrepareFirstTextParticle(TextToUpper("raylib => fun videogames programming!")); + if (IsKeyPressed(KeyboardKey.Three)) PrepareFirstTextParticle(TextToLower("raylib => fun videogames programming!")); + if (IsKeyPressed(KeyboardKey.Four)) PrepareFirstTextParticle(TextToPascal("raylib_fun_videogames_programming")); + if (IsKeyPressed(KeyboardKey.Five)) PrepareFirstTextParticle(TextToSnake("RaylibFunVideogamesProgramming")); + if (IsKeyPressed(KeyboardKey.Six)) PrepareFirstTextParticle(TextToCamel("raylib_fun_videogames_programming")); + + // Slice by char pressed only when we have one text particle + int charPressed = GetCharPressed(); + if ((charPressed >= 'A') && (charPressed <= 'z') && (textParticles.Count == 1)) + { + SliceTextParticleByChar(textParticles[0], (char)charPressed); + } + + // Updates each text particle state + for (int i = 0; i < textParticles.Count; i++) + { + TextParticle tp = textParticles[i]; + + // The text particle is not grabbed + if (!tp.Grabbed) + { + // text particle repositioning using the velocity + tp.Rect.X += tp.Vel.X * delta; + tp.Rect.Y += tp.Vel.Y * delta; + + // Does the text particle hit the screen right boundary? + if ((tp.Rect.X + tp.Rect.Width) >= screenWidth) + { + tp.Rect.X = screenWidth - tp.Rect.Width; // Text particle repositioning + tp.Vel.X = -tp.Vel.X * tp.Elasticity; // Elasticity makes the text particle lose 10% of its velocity on hit + } + // Does the text particle hit the screen left boundary? + else if (tp.Rect.X <= 0) + { + tp.Rect.X = 0.0f; + tp.Vel.X = -tp.Vel.X * tp.Elasticity; + } + + // The same for y axis + if ((tp.Rect.Y + tp.Rect.Height) >= screenHeight) + { + tp.Rect.Y = screenHeight - tp.Rect.Height; + tp.Vel.Y = -tp.Vel.Y * tp.Elasticity; + } + else if (tp.Rect.Y <= 0) + { + tp.Rect.Y = 0.0f; + tp.Vel.Y = -tp.Vel.Y * tp.Elasticity; + } + + // Friction makes the text particle lose 1% of its velocity each frame + tp.Vel.X = tp.Vel.X * tp.Friction; + tp.Vel.Y = tp.Vel.Y * tp.Friction; + } + else + { + // Text particle repositioning using the mouse position + tp.Rect.X = mousePos.X - pressOffset.X; + tp.Rect.Y = mousePos.Y - pressOffset.Y; + + // While the text particle is grabbed, recalculates its velocity + tp.Vel.X = (tp.Rect.X - tp.Ppos.X) / delta; + tp.Vel.Y = (tp.Rect.Y - tp.Ppos.Y) / delta; + tp.Ppos.X = tp.Rect.X; + tp.Ppos.Y = tp.Rect.Y; + + // Glue text particles when dragging and pressing left ctrl + if (IsKeyDown(KeyboardKey.LeftControl)) + { + for (int j = 0; j < textParticles.Count; j++) + { + if (textParticles[j] != grabbedTextParticle && grabbedTextParticle.Grabbed) + { + if (CheckCollisionRecs(grabbedTextParticle.Rect, textParticles[j].Rect)) + { + GlueTextParticles(grabbedTextParticle, textParticles[j]); + grabbedTextParticle = textParticles[textParticles.Count - 1]; + } + } + } + } + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + for (int i = 0; i < textParticles.Count; i++) + { + TextParticle tp = textParticles[i]; + DrawRectangleRec(new Rectangle(tp.Rect.X - tp.BorderWidth, tp.Rect.Y - tp.BorderWidth, tp.Rect.Width + tp.BorderWidth * 2, tp.Rect.Height + tp.BorderWidth * 2), Color.Black); + DrawRectangleRec(tp.Rect, tp.Color); + DrawText(tp.Text, (int)(tp.Rect.X + tp.Padding), (int)(tp.Rect.Y + tp.Padding), FontSize, Color.Black); + } + + DrawText("grab a text particle by pressing with the mouse and throw it by releasing", 10, 10, 10, Color.DarkGray); + DrawText("slice a text particle by pressing it with the mouse right button", 10, 30, 10, Color.DarkGray); + DrawText("shatter a text particle keeping left shift pressed and pressing it with the mouse right button", 10, 50, 10, Color.DarkGray); + DrawText("glue text particles by grabbing than and keeping left control pressed", 10, 70, 10, Color.DarkGray); + DrawText("1 to 6 to reset", 10, 90, 10, Color.DarkGray); + DrawText("when you have only one text particle, you can slice it by pressing a char", 10, 110, 10, Color.DarkGray); + DrawText($"TEXT PARTICLE COUNT: {textParticles.Count}", 10, GetScreenHeight() - 30, 20, Color.Black); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + //---------------------------------------------------------------------------------- + // Module Functions Definition + //---------------------------------------------------------------------------------- + private void PrepareFirstTextParticle(string text) + { + TextParticle first = CreateTextParticle( + text, + GetScreenWidth() / 2.0f, + GetScreenHeight() / 2.0f, + Color.RayWhite + ); + + textParticles.Clear(); + textParticles.Add(first); + } + + private static TextParticle CreateTextParticle(string text, float x, float y, Color color) + { + TextParticle tp = new() + { + Text = "", + Rect = new Rectangle(x, y, 30, 30), + Vel = new Vector2(GetRandomValue(-200, 200), GetRandomValue(-200, 200)), + Ppos = new Vector2(0, 0), + Padding = 5.0f, + BorderWidth = 5.0f, + Friction = 0.99f, + Elasticity = 0.9f, + Color = color, + Grabbed = false + }; + + // Emulate C TextCopy() into a fixed size buffer + if (text.Length > MaxTextLength - 1) + { + text = text.Substring(0, MaxTextLength - 1); + } + tp.Text = text; + + tp.Rect.Width = MeasureText(tp.Text, FontSize) + tp.Padding * 2; + tp.Rect.Height = FontSize + tp.Padding * 2; + return tp; + } + + private void SliceTextParticle(TextParticle tp, int particlePos, int sliceLength) + { + int length = tp.Text.Length; + + if ((length > 1) && ((textParticles.Count + length) < MaxTextParticles)) + { + for (int i = 0; i < length; i += sliceLength) + { + string text = sliceLength == 1 ? tp.Text[i].ToString() : Subtext(tp.Text, i, sliceLength); + textParticles.Add(CreateTextParticle( + text, + tp.Rect.X + i * tp.Rect.Width / length, + tp.Rect.Y, + new Color(GetRandomValue(0, 255), GetRandomValue(0, 255), GetRandomValue(0, 255), 255) + )); + } + RealocateTextParticles(particlePos); + } + } + + private void SliceTextParticleByChar(TextParticle tp, char charToSlice) + { + string[] tokens = tp.Text.Split(charToSlice); + int tokenCount = tokens.Length; + + if (tokenCount > 1) + { + int textLength = tp.Text.Length; + for (int i = 0; i < textLength; i++) + { + if (tp.Text[i] == charToSlice) + { + textParticles.Add(CreateTextParticle( + charToSlice.ToString(), + tp.Rect.X, + tp.Rect.Y, + new Color(GetRandomValue(0, 255), GetRandomValue(0, 255), GetRandomValue(0, 255), 255) + )); + } + } + for (int i = 0; i < tokenCount; i++) + { + int tokenLength = tokens[i].Length; + textParticles.Add(CreateTextParticle( + tokens[i], + tp.Rect.X + i * tp.Rect.Width / tokenLength, + tp.Rect.Y, + new Color(GetRandomValue(0, 255), GetRandomValue(0, 255), GetRandomValue(0, 255), 255) + )); + } + RealocateTextParticles(0); + } + } + + private void ShatterTextParticle(TextParticle tp, int particlePos) + { + SliceTextParticle(tp, particlePos, 1); + } + + private void GlueTextParticles(TextParticle grabbed, TextParticle target) + { + int p1 = textParticles.IndexOf(grabbed); + int p2 = textParticles.IndexOf(target); + + if ((p1 != -1) && (p2 != -1)) + { + TextParticle tp = CreateTextParticle( + grabbed.Text + target.Text, + grabbed.Rect.X, + grabbed.Rect.Y, + Color.RayWhite + ); + tp.Grabbed = true; + textParticles.Add(tp); + grabbed.Grabbed = false; + if (p1 < p2) + { + RealocateTextParticles(p2); + RealocateTextParticles(p1); + } + else + { + RealocateTextParticles(p1); + RealocateTextParticles(p2); + } + } + } + + private void RealocateTextParticles(int particlePos) + { + textParticles.RemoveAt(particlePos); + } + + // Extract a substring, clamping length to the available characters (like raylib TextSubtext) + private static string Subtext(string text, int position, int length) + { + if (position >= text.Length) + { + return ""; + } + + int maxLength = text.Length - position; + if (length > maxLength) + { + length = maxLength; + } + + return text.Substring(position, length); + } + + // C# equivalents of raylib TextTo* helpers (behaviour kept identical) + private static string TextToUpper(string text) + { + var sb = new StringBuilder(text.Length); + foreach (char c in text) + { + sb.Append((c >= 'a' && c <= 'z') ? (char)(c - 32) : c); + } + return sb.ToString(); + } + + private static string TextToLower(string text) + { + var sb = new StringBuilder(text.Length); + foreach (char c in text) + { + sb.Append((c >= 'A' && c <= 'Z') ? (char)(c + 32) : c); + } + return sb.ToString(); + } + + private static string TextToPascal(string text) + { + var sb = new StringBuilder(text.Length); + + if (text.Length > 0) + { + sb.Append(char.ToUpperInvariant(text[0])); + + for (int i = 1; i < text.Length; i++) + { + if (text[i] == '_' && (i + 1) < text.Length) + { + sb.Append(char.ToUpperInvariant(text[i + 1])); + i++; + } + else + { + sb.Append(text[i]); + } + } + } + + return sb.ToString(); + } + + private static string TextToSnake(string text) + { + var sb = new StringBuilder(text.Length); + + for (int i = 0; i < text.Length; i++) + { + char c = text[i]; + if (c >= 'A' && c <= 'Z') + { + if (i > 0) + { + sb.Append('_'); + } + sb.Append((char)(c + 32)); + } + else + { + sb.Append(c); + } + } + + return sb.ToString(); + } + + private static string TextToCamel(string text) + { + var sb = new StringBuilder(text.Length); + + if (text.Length > 0) + { + sb.Append(char.ToLowerInvariant(text[0])); + + for (int i = 1; i < text.Length; i++) + { + if (text[i] == '_' && (i + 1) < text.Length) + { + sb.Append(char.ToUpperInvariant(text[i + 1])); + i++; + } + else + { + sb.Append(text[i]); + } + } + } + + return sb.ToString(); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [text] example - strings management"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new StringsManagement(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Text/Text3D.cs b/Examples/Text/Text3D.cs new file mode 100644 index 0000000..8340300 --- /dev/null +++ b/Examples/Text/Text3D.cs @@ -0,0 +1,739 @@ +/******************************************************************************************* +* +* raylib [text] example - 3d drawing +* +* Example complexity rating: [★★★★] 4/4 +* +* NOTE: Draw a 2D text in 3D space, each letter is drawn in a quad (or 2 quads if backface is set) +* where the texture coodinates of each quad map to the texture coordinates of the glyphs +* inside the font texture +* +* A more efficient approach, i believe, would be to render the text in a render texture and +* map that texture to a plane and render that, or maybe a shader but my method allows more +* flexibility...for example to change position of each letter individually to make somethink +* like a wavy text effect +* +* Special thanks to: +* @Nighten for the DrawTextStyle() code https://github.com/NightenDushi/Raylib_DrawTextStyle +* Chris Camacho (codifies - http://bedroomcoders.co.uk/) for the alpha discard shader +* +* Example originally created with raylib 3.5, last time updated with raylib 4.0 +* +* Example contributed by Vlad Adrian (@demizdor) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2021-2025 Vlad Adrian (@demizdor) +* +********************************************************************************************/ + +using System; +using System.Numerics; +using System.Text; +using Raylib_cs; +using static Raylib_cs.Raylib; + +namespace Examples.Text; + +public partial class Text3D : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + //-------------------------------------------------------------------------------------- + // Global variables + //-------------------------------------------------------------------------------------- + private const float LetterBoundrySize = 0.25f; + private const int TextMaxLayers = 32; + private static readonly Color LetterBoundryColor = Color.Violet; + + private bool showLetterBoundry; + private bool showTextBoundry; + + //-------------------------------------------------------------------------------------- + // Types and Structures Definition + //-------------------------------------------------------------------------------------- + // Configuration structure for waving the text + private struct WaveTextConfig + { + public Vector3 WaveRange; + public Vector3 WaveSpeed; + public Vector3 WaveOffset; + } + + public string Name => "Text / Text 3D"; + + public string Title => "raylib [text] example - 3d drawing"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint | ConfigFlags.VSyncHint; + + public bool CursorDisabled => true; + + private bool spin; // Spin the camera? + private bool multicolor; // Multicolor mode + + private Camera3D camera; + private CameraMode cameraMode; + + private Vector3 cubePosition; + private Vector3 cubeSize; + + private Font font; + private float fontSize; + private float fontSpacing; + private float lineSpacing; + + private string text; + private Vector3 tbox; + private int layers; + private int quads; + private float layerDistance; + + private WaveTextConfig wcfg; + private float time; + + private Color light; + private Color dark; + + private Shader alphaDiscard; + private Color[] multi; + + public void Init() + { + spin = true; // Spin the camera? + multicolor = false; // Multicolor mode + showLetterBoundry = false; + showTextBoundry = false; + + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(-10.0f, 15.0f, -10.0f); // Camera position + camera.Target = new Vector3(0.0f, 0.0f, 0.0f); // Camera looking at point + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target) + camera.FovY = 45.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + + cameraMode = CameraMode.Orbital; + + cubePosition = new Vector3(0.0f, 1.0f, 0.0f); + cubeSize = new Vector3(2.0f, 2.0f, 2.0f); + + // Use the default font + font = GetFontDefault(); + fontSize = 0.8f; + fontSpacing = 0.05f; + lineSpacing = -0.1f; + + // Set the text (using markdown!) + text = "Hello ~~World~~ in 3D!"; + tbox = new Vector3(0, 0, 0); + layers = 1; + quads = 0; + layerDistance = 0.01f; + + wcfg = new WaveTextConfig(); + wcfg.WaveSpeed.X = wcfg.WaveSpeed.Y = 3.0f; wcfg.WaveSpeed.Z = 0.5f; + wcfg.WaveOffset.X = wcfg.WaveOffset.Y = wcfg.WaveOffset.Z = 0.35f; + wcfg.WaveRange.X = wcfg.WaveRange.Y = wcfg.WaveRange.Z = 0.45f; + + time = 0.0f; + + // Setup a light and dark color + light = Color.Maroon; + dark = Color.Red; + + // Load the alpha discard shader + alphaDiscard = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/alpha_discard.fs"); + + // Array filled with multiple random colors (when multicolor mode is set) + multi = new Color[TextMaxLayers]; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, cameraMode); + + // Handle font files dropped + if (IsFileDropped()) + { + string[] droppedFiles = GetDroppedFiles(); + + // NOTE: We only support first ttf file dropped + if (IsFileExtension(droppedFiles[0], ".ttf")) + { + UnloadFont(font); + font = LoadFontEx(droppedFiles[0], (int)fontSize, null, 0); + } + else if (IsFileExtension(droppedFiles[0], ".fnt")) + { + UnloadFont(font); + font = LoadFont(droppedFiles[0]); + fontSize = (float)font.BaseSize; + } + } + + // Handle Events + if (IsKeyPressed(KeyboardKey.F1)) showLetterBoundry = !showLetterBoundry; + if (IsKeyPressed(KeyboardKey.F2)) showTextBoundry = !showTextBoundry; + if (IsKeyPressed(KeyboardKey.F3)) + { + // Handle camera change + spin = !spin; + // we need to reset the camera when changing modes + camera = new(); + camera.Target = new Vector3(0.0f, 0.0f, 0.0f); // Camera looking at point + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target) + camera.FovY = 45.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera mode type + + if (spin) + { + camera.Position = new Vector3(-10.0f, 15.0f, -10.0f); // Camera position + cameraMode = CameraMode.Orbital; + } + else + { + camera.Position = new Vector3(10.0f, 10.0f, -10.0f); // Camera position + cameraMode = CameraMode.Free; + } + } + + // Handle clicking the cube + if (IsMouseButtonPressed(MouseButton.Left)) + { + Ray ray = GetScreenToWorldRay(GetMousePosition(), camera); + + // Check collision between ray and box + RayCollision collision = GetRayCollisionBox(ray, + new BoundingBox( + new Vector3(cubePosition.X - cubeSize.X / 2, cubePosition.Y - cubeSize.Y / 2, cubePosition.Z - cubeSize.Z / 2), + new Vector3(cubePosition.X + cubeSize.X / 2, cubePosition.Y + cubeSize.Y / 2, cubePosition.Z + cubeSize.Z / 2))); + if (collision.Hit) + { + // Generate new random colors + light = GenerateRandomColor(0.5f, 0.78f); + dark = GenerateRandomColor(0.4f, 0.58f); + } + } + + // Handle text layers changes + if (IsKeyPressed(KeyboardKey.Home)) { if (layers > 1) --layers; } + else if (IsKeyPressed(KeyboardKey.End)) { if (layers < TextMaxLayers) ++layers; } + + // Handle text changes + if (IsKeyPressed(KeyboardKey.Left)) fontSize -= 0.5f; + else if (IsKeyPressed(KeyboardKey.Right)) fontSize += 0.5f; + else if (IsKeyPressed(KeyboardKey.Up)) fontSpacing -= 0.1f; + else if (IsKeyPressed(KeyboardKey.Down)) fontSpacing += 0.1f; + else if (IsKeyPressed(KeyboardKey.PageUp)) lineSpacing -= 0.1f; + else if (IsKeyPressed(KeyboardKey.PageDown)) lineSpacing += 0.1f; + else if (IsKeyDown(KeyboardKey.Insert)) layerDistance -= 0.001f; + else if (IsKeyDown(KeyboardKey.Delete)) layerDistance += 0.001f; + else if (IsKeyPressed(KeyboardKey.Tab)) + { + multicolor = !multicolor; // Enable /disable multicolor mode + + if (multicolor) + { + // Fill color array with random colors + for (int i = 0; i < TextMaxLayers; i++) + { + multi[i] = GenerateRandomColor(0.5f, 0.8f); + multi[i].A = (byte)GetRandomValue(0, 255); + } + } + } + + // Handle text input + int ch = GetCharPressed(); + if (IsKeyPressed(KeyboardKey.Backspace)) + { + // Remove last char + if (text.Length > 0) text = text.Substring(0, text.Length - 1); + } + else if (IsKeyPressed(KeyboardKey.Enter)) + { + // handle newline + if (text.Length < 63) text += '\n'; + } + else + { + // append only printable chars + if ((ch != 0) && (text.Length < 63)) text += char.ConvertFromUtf32(ch); + } + + // Measure 3D text so we can center it + tbox = MeasureTextWave3D(font, text, fontSize, fontSpacing, lineSpacing); + + quads = 0; // Reset quad counter + time += GetFrameTime(); // Update timer needed by `DrawTextWave3D()` + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + DrawCubeV(cubePosition, cubeSize, dark); + DrawCubeWires(cubePosition, 2.1f, 2.1f, 2.1f, light); + + DrawGrid(10, 2.0f); + + // Use a shader to handle the depth buffer issue with transparent textures + // NOTE: more info at https://bedroomcoders.co.uk/posts/198 + BeginShaderMode(alphaDiscard); + + // Draw the 3D text above the red cube + Rlgl.PushMatrix(); + Rlgl.Rotatef(90.0f, 1.0f, 0.0f, 0.0f); + Rlgl.Rotatef(90.0f, 0.0f, 0.0f, -1.0f); + + for (int i = 0; i < layers; i++) + { + Color clr = light; + if (multicolor) clr = multi[i]; + DrawTextWave3D(font, text, new Vector3(-tbox.X / 2.0f, layerDistance * i, -4.5f), fontSize, fontSpacing, lineSpacing, true, wcfg, time, clr); + } + + // Draw the text boundry if set + if (showTextBoundry) DrawCubeWiresV(new Vector3(0.0f, 0.0f, -4.5f + tbox.Z / 2), tbox, dark); + Rlgl.PopMatrix(); + + // Don't draw the letter boundries for the 3D text below + bool slb = showLetterBoundry; + showLetterBoundry = false; + + // Draw 3D options (use default font) + //------------------------------------------------------------------------- + Rlgl.PushMatrix(); + Rlgl.Rotatef(180.0f, 0.0f, 1.0f, 0.0f); + string opt = $"< SIZE: {fontSize:0.0} >"; + quads += opt.Length; + Vector2 m = MeasureTextEx(GetFontDefault(), opt, 0.8f, 0.1f); + Vector3 pos = new(-m.X / 2.0f, 0.01f, 2.0f); + DrawText3D(GetFontDefault(), opt, pos, 0.8f, 0.1f, 0.0f, false, Color.Blue); + pos.Z += 0.5f + m.Y; + + opt = $"< SPACING: {fontSpacing:0.0} >"; + quads += opt.Length; + m = MeasureTextEx(GetFontDefault(), opt, 0.8f, 0.1f); + pos.X = -m.X / 2.0f; + DrawText3D(GetFontDefault(), opt, pos, 0.8f, 0.1f, 0.0f, false, Color.Blue); + pos.Z += 0.5f + m.Y; + + opt = $"< LINE: {lineSpacing:0.0} >"; + quads += opt.Length; + m = MeasureTextEx(GetFontDefault(), opt, 0.8f, 0.1f); + pos.X = -m.X / 2.0f; + DrawText3D(GetFontDefault(), opt, pos, 0.8f, 0.1f, 0.0f, false, Color.Blue); + pos.Z += 0.5f + m.Y; + + opt = $"< LBOX: {(slb ? "ON" : "OFF"),3} >"; + quads += opt.Length; + m = MeasureTextEx(GetFontDefault(), opt, 0.8f, 0.1f); + pos.X = -m.X / 2.0f; + DrawText3D(GetFontDefault(), opt, pos, 0.8f, 0.1f, 0.0f, false, Color.Red); + pos.Z += 0.5f + m.Y; + + opt = $"< TBOX: {(showTextBoundry ? "ON" : "OFF"),3} >"; + quads += opt.Length; + m = MeasureTextEx(GetFontDefault(), opt, 0.8f, 0.1f); + pos.X = -m.X / 2.0f; + DrawText3D(GetFontDefault(), opt, pos, 0.8f, 0.1f, 0.0f, false, Color.Red); + pos.Z += 0.5f + m.Y; + + opt = $"< LAYER DISTANCE: {layerDistance:0.000} >"; + quads += opt.Length; + m = MeasureTextEx(GetFontDefault(), opt, 0.8f, 0.1f); + pos.X = -m.X / 2.0f; + DrawText3D(GetFontDefault(), opt, pos, 0.8f, 0.1f, 0.0f, false, Color.DarkPurple); + Rlgl.PopMatrix(); + //------------------------------------------------------------------------- + + // Draw 3D info text (use default font) + //------------------------------------------------------------------------- + opt = "All the text displayed here is in 3D"; + quads += 36; + m = MeasureTextEx(GetFontDefault(), opt, 1.0f, 0.05f); + pos = new Vector3(-m.X / 2.0f, 0.01f, 2.0f); + DrawText3D(GetFontDefault(), opt, pos, 1.0f, 0.05f, 0.0f, false, Color.DarkBlue); + pos.Z += 1.5f + m.Y; + + opt = "press [Left]/[Right] to change the font size"; + quads += 44; + m = MeasureTextEx(GetFontDefault(), opt, 0.6f, 0.05f); + pos.X = -m.X / 2.0f; + DrawText3D(GetFontDefault(), opt, pos, 0.6f, 0.05f, 0.0f, false, Color.DarkBlue); + pos.Z += 0.5f + m.Y; + + opt = "press [Up]/[Down] to change the font spacing"; + quads += 44; + m = MeasureTextEx(GetFontDefault(), opt, 0.6f, 0.05f); + pos.X = -m.X / 2.0f; + DrawText3D(GetFontDefault(), opt, pos, 0.6f, 0.05f, 0.0f, false, Color.DarkBlue); + pos.Z += 0.5f + m.Y; + + opt = "press [PgUp]/[PgDown] to change the line spacing"; + quads += 48; + m = MeasureTextEx(GetFontDefault(), opt, 0.6f, 0.05f); + pos.X = -m.X / 2.0f; + DrawText3D(GetFontDefault(), opt, pos, 0.6f, 0.05f, 0.0f, false, Color.DarkBlue); + pos.Z += 0.5f + m.Y; + + opt = "press [F1] to toggle the letter boundry"; + quads += 39; + m = MeasureTextEx(GetFontDefault(), opt, 0.6f, 0.05f); + pos.X = -m.X / 2.0f; + DrawText3D(GetFontDefault(), opt, pos, 0.6f, 0.05f, 0.0f, false, Color.DarkBlue); + pos.Z += 0.5f + m.Y; + + opt = "press [F2] to toggle the text boundry"; + quads += 37; + m = MeasureTextEx(GetFontDefault(), opt, 0.6f, 0.05f); + pos.X = -m.X / 2.0f; + DrawText3D(GetFontDefault(), opt, pos, 0.6f, 0.05f, 0.0f, false, Color.DarkBlue); + //------------------------------------------------------------------------- + + showLetterBoundry = slb; + EndShaderMode(); + + EndMode3D(); + + // Draw 2D info text & stats + //------------------------------------------------------------------------- + DrawText("Drag & drop a font file to change the font!\nType something, see what happens!\n\n" + + "Press [F3] to toggle the camera", 10, 35, 10, Color.Black); + + quads += TextLengthUtf8(text) * 2 * layers; + string tmp = $"{layers,2} layer(s) | {(spin ? "ORBITAL" : "FREE")} camera | {quads,4} quads ({quads * 4,4} verts)"; + int width = MeasureText(tmp, 10); + DrawText(tmp, screenWidth - 20 - width, 10, 10, Color.DarkGreen); + + tmp = "[Home]/[End] to add/remove 3D text layers"; + width = MeasureText(tmp, 10); + DrawText(tmp, screenWidth - 20 - width, 25, 10, Color.DarkGray); + + tmp = "[Insert]/[Delete] to increase/decrease distance between layers"; + width = MeasureText(tmp, 10); + DrawText(tmp, screenWidth - 20 - width, 40, 10, Color.DarkGray); + + tmp = "click the [CUBE] for a random color"; + width = MeasureText(tmp, 10); + DrawText(tmp, screenWidth - 20 - width, 55, 10, Color.DarkGray); + + tmp = "[Tab] to toggle multicolor mode"; + width = MeasureText(tmp, 10); + DrawText(tmp, screenWidth - 20 - width, 70, 10, Color.DarkGray); + //------------------------------------------------------------------------- + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadFont(font); + } + + //-------------------------------------------------------------------------------------- + // Module Functions Definitions + //-------------------------------------------------------------------------------------- + // Get the total length in bytes of a UTF-8 string (raylib TextLength equivalent) + private static int TextLengthUtf8(string text) + { + return Encoding.UTF8.GetByteCount(text); + } + + // Draw codepoint at specified position in 3D space + private unsafe void DrawTextCodepoint3D(Font font, int codepoint, Vector3 position, float fontSize, bool backface, Color tint) + { + // Character index position in sprite font + // NOTE: In case a codepoint is not available in the font, index returned points to '?' + int index = GetGlyphIndex(font, codepoint); + float scale = fontSize / (float)font.BaseSize; + + // Character destination rectangle on screen + // NOTE: We consider charsPadding on drawing + position.X += (font.Glyphs[index].OffsetX - font.GlyphPadding) * scale; + position.Z += (font.Glyphs[index].OffsetY - font.GlyphPadding) * scale; + + // Character source rectangle from font texture atlas + // NOTE: We consider chars padding when drawing, it could be required for outline/glow shader effects + Rectangle srcRec = new(font.Recs[index].X - font.GlyphPadding, font.Recs[index].Y - font.GlyphPadding, + font.Recs[index].Width + 2.0f * font.GlyphPadding, font.Recs[index].Height + 2.0f * font.GlyphPadding); + + float width = (font.Recs[index].Width + 2.0f * font.GlyphPadding) * scale; + float height = (font.Recs[index].Height + 2.0f * font.GlyphPadding) * scale; + + if (font.Texture.Id > 0) + { + const float x = 0.0f; + const float y = 0.0f; + const float z = 0.0f; + + // normalized texture coordinates of the glyph inside the font texture (0.0f -> 1.0f) + float tx = srcRec.X / font.Texture.Width; + float ty = srcRec.Y / font.Texture.Height; + float tw = (srcRec.X + srcRec.Width) / font.Texture.Width; + float th = (srcRec.Y + srcRec.Height) / font.Texture.Height; + + if (showLetterBoundry) DrawCubeWiresV(new Vector3(position.X + width / 2, position.Y, position.Z + height / 2), new Vector3(width, LetterBoundrySize, height), LetterBoundryColor); + + Rlgl.CheckRenderBatchLimit(4 + 4 * (backface ? 1 : 0)); + Rlgl.SetTexture(font.Texture.Id); + + Rlgl.PushMatrix(); + Rlgl.Translatef(position.X, position.Y, position.Z); + + Rlgl.Begin(DrawMode.Quads); + Rlgl.Color4ub(tint.R, tint.G, tint.B, tint.A); + + // Front Face + Rlgl.Normal3f(0.0f, 1.0f, 0.0f); // Normal Pointing Up + Rlgl.TexCoord2f(tx, ty); Rlgl.Vertex3f(x, y, z); // Top Left Of The Texture and Quad + Rlgl.TexCoord2f(tx, th); Rlgl.Vertex3f(x, y, z + height); // Bottom Left Of The Texture and Quad + Rlgl.TexCoord2f(tw, th); Rlgl.Vertex3f(x + width, y, z + height); // Bottom Right Of The Texture and Quad + Rlgl.TexCoord2f(tw, ty); Rlgl.Vertex3f(x + width, y, z); // Top Right Of The Texture and Quad + + if (backface) + { + // Back Face + Rlgl.Normal3f(0.0f, -1.0f, 0.0f); // Normal Pointing Down + Rlgl.TexCoord2f(tx, ty); Rlgl.Vertex3f(x, y, z); // Top Right Of The Texture and Quad + Rlgl.TexCoord2f(tw, ty); Rlgl.Vertex3f(x + width, y, z); // Top Left Of The Texture and Quad + Rlgl.TexCoord2f(tw, th); Rlgl.Vertex3f(x + width, y, z + height); // Bottom Left Of The Texture and Quad + Rlgl.TexCoord2f(tx, th); Rlgl.Vertex3f(x, y, z + height); // Bottom Right Of The Texture and Quad + } + Rlgl.End(); + Rlgl.PopMatrix(); + + Rlgl.SetTexture(0); + } + } + + // Draw a 2D text in 3D space + private unsafe void DrawText3D(Font font, string text, Vector3 position, float fontSize, float fontSpacing, float lineSpacing, bool backface, Color tint) + { + using var textNative = new Utf8Buffer(text); + sbyte* t = textNative.AsPointer(); + int length = Encoding.UTF8.GetByteCount(text); // Total length in bytes of the text, scanned by codepoints in loop + + float textOffsetY = 0.0f; // Offset between lines (on line break '\n') + float textOffsetX = 0.0f; // Offset X to next character to draw + + float scale = fontSize / (float)font.BaseSize; + + for (int i = 0; i < length;) + { + // Get next codepoint from byte string and glyph index in font + int codepointByteCount = 0; + int codepoint = GetCodepoint(&t[i], &codepointByteCount); + int index = GetGlyphIndex(font, codepoint); + + // NOTE: Normally we exit the decoding sequence as soon as a bad byte is found (and return 0x3f) + // but we need to draw all of the bad bytes using the '?' symbol moving one byte + if (codepoint == 0x3f) codepointByteCount = 1; + + if (codepoint == '\n') + { + // NOTE: Fixed line spacing of 1.5 line-height + // TODO: Support custom line spacing defined by user + textOffsetY += fontSize + lineSpacing; + textOffsetX = 0.0f; + } + else + { + if ((codepoint != ' ') && (codepoint != '\t')) + { + DrawTextCodepoint3D(font, codepoint, new Vector3(position.X + textOffsetX, position.Y, position.Z + textOffsetY), fontSize, backface, tint); + } + + if (font.Glyphs[index].AdvanceX == 0) textOffsetX += font.Recs[index].Width * scale + fontSpacing; + else textOffsetX += font.Glyphs[index].AdvanceX * scale + fontSpacing; + } + + i += codepointByteCount; // Move text bytes counter to next codepoint + } + } + + // Draw a 2D text in 3D space and wave the parts that start with `~~` and end with `~~` + // This is a modified version of the original code by @Nighten found here https://github.com/NightenDushi/Raylib_DrawTextStyle + private unsafe void DrawTextWave3D(Font font, string text, Vector3 position, float fontSize, float fontSpacing, float lineSpacing, bool backface, WaveTextConfig config, float time, Color tint) + { + using var textNative = new Utf8Buffer(text); + sbyte* t = textNative.AsPointer(); + int length = Encoding.UTF8.GetByteCount(text); // Total length in bytes of the text, scanned by codepoints in loop + + float textOffsetY = 0.0f; // Offset between lines (on line break '\n') + float textOffsetX = 0.0f; // Offset X to next character to draw + + float scale = fontSize / (float)font.BaseSize; + + bool wave = false; + + for (int i = 0, k = 0; i < length; ++k) + { + // Get next codepoint from byte string and glyph index in font + int codepointByteCount = 0; + int codepoint = GetCodepoint(&t[i], &codepointByteCount); + int index = GetGlyphIndex(font, codepoint); + + // NOTE: Normally we exit the decoding sequence as soon as a bad byte is found (and return 0x3f) + // but we need to draw all of the bad bytes using the '?' symbol moving one byte + if (codepoint == 0x3f) codepointByteCount = 1; + + if (codepoint == '\n') + { + // NOTE: Fixed line spacing of 1.5 line-height + // TODO: Support custom line spacing defined by user + textOffsetY += fontSize + lineSpacing; + textOffsetX = 0.0f; + k = 0; + } + else if (codepoint == '~') + { + if (GetCodepoint(&t[i + 1], &codepointByteCount) == '~') + { + codepointByteCount += 1; + wave = !wave; + } + } + else + { + if ((codepoint != ' ') && (codepoint != '\t')) + { + Vector3 pos = position; + if (wave) // Apply the wave effect + { + pos.X += MathF.Sin(time * config.WaveSpeed.X - k * config.WaveOffset.X) * config.WaveRange.X; + pos.Y += MathF.Sin(time * config.WaveSpeed.Y - k * config.WaveOffset.Y) * config.WaveRange.Y; + pos.Z += MathF.Sin(time * config.WaveSpeed.Z - k * config.WaveOffset.Z) * config.WaveRange.Z; + } + + DrawTextCodepoint3D(font, codepoint, new Vector3(pos.X + textOffsetX, pos.Y, pos.Z + textOffsetY), fontSize, backface, tint); + } + + if (font.Glyphs[index].AdvanceX == 0) textOffsetX += font.Recs[index].Width * scale + fontSpacing; + else textOffsetX += font.Glyphs[index].AdvanceX * scale + fontSpacing; + } + + i += codepointByteCount; // Move text bytes counter to next codepoint + } + } + + // Measure a text in 3D ignoring the `~~` chars + private unsafe Vector3 MeasureTextWave3D(Font font, string text, float fontSize, float fontSpacing, float lineSpacing) + { + using var textNative = new Utf8Buffer(text); + sbyte* t = textNative.AsPointer(); + int len = Encoding.UTF8.GetByteCount(text); + int tempLen = 0; // Used to count longer text line num chars + int lenCounter = 0; + + float tempTextWidth = 0.0f; // Used to count longer text line width + + float scale = fontSize / (float)font.BaseSize; + float textHeight = scale; + float textWidth = 0.0f; + + int letter = 0; // Current character + int index = 0; // Index position in sprite font + + for (int i = 0; i < len; i++) + { + int next = 0; + letter = GetCodepoint(&t[i], &next); + index = GetGlyphIndex(font, letter); + + // NOTE: normally we exit the decoding sequence as soon as a bad byte is found (and return 0x3f) + // but we need to draw all of the bad bytes using the '?' symbol so to not skip any we set next = 1 + if (letter == 0x3f) next = 1; + i += next - 1; + + if (letter != '\n') + { + if (letter == '~' && GetCodepoint(&t[i + 1], &next) == '~') + { + i++; + } + else + { + lenCounter++; + if (font.Glyphs[index].AdvanceX != 0) textWidth += font.Glyphs[index].AdvanceX * scale; + else textWidth += (font.Recs[index].Width + font.Glyphs[index].OffsetX) * scale; + } + } + else + { + if (tempTextWidth < textWidth) tempTextWidth = textWidth; + lenCounter = 0; + textWidth = 0.0f; + textHeight += fontSize + lineSpacing; + } + + if (tempLen < lenCounter) tempLen = lenCounter; + } + + if (tempTextWidth < textWidth) tempTextWidth = textWidth; + + Vector3 vec = new(0, 0, 0); + vec.X = tempTextWidth + ((tempLen - 1) * fontSpacing); // Adds chars spacing to measure + vec.Y = 0.25f; + vec.Z = textHeight; + + return vec; + } + + // Generates a nice color with a random hue + private static Color GenerateRandomColor(float s, float v) + { + const float Phi = 0.618033988749895f; // Golden ratio conjugate + float h = (float)GetRandomValue(0, 360); + h = (h + h * Phi) % 360.0f; + return ColorFromHSV(h, s, v); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint | ConfigFlags.VSyncHint); + InitWindow(screenWidth, screenHeight, "raylib [text] example - 3d drawing"); + + DisableCursor(); // Limit cursor to relative movement inside the window + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new Text3D(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Text/Unicode.cs b/Examples/Text/Unicode.cs index b24a9c6..70dbff0 100644 --- a/Examples/Text/Unicode.cs +++ b/Examples/Text/Unicode.cs @@ -22,11 +22,20 @@ using static Raylib_cs.Raylib; namespace Examples.Text; -public class Unicode +public partial class Unicode : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + const int EmojiPerWidth = 8; const int EmojiPerHeight = 4; + public string Name => "Text / Unicode Emojis"; + + public string Title => "raylib [text] example - unicode emojis"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint | ConfigFlags.VSyncHint; + // Arrays that holds the random emojis struct EmojiInfo { @@ -35,10 +44,10 @@ public class Unicode public Color Color; // Emoji color } - static EmojiInfo[] emoji = new EmojiInfo[EmojiPerWidth * EmojiPerHeight]; + private EmojiInfo[] emoji; - static int hovered = -1; - static int selected = -1; + private int hovered; + private int selected; struct Message { @@ -130,191 +139,187 @@ public class Unicode new Message("\xED\x95\x9C\xEA\xB5\xAD\xEB\xA7\x90\x20\xED\x95\x98\xEC\x8B\xA4\x20\xEC\xA4\x84\x20\xEC\x95\x84\xEC\x84\xB8\xEC\x9A\x94\x3F", "Korean"), }; - public static int Main() - { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + private Font fontDefault; + private Font fontAsian; + private Font fontEmoji; - SetConfigFlags(ConfigFlags.Msaa4xHint | ConfigFlags.VSyncHint); - InitWindow(screenWidth, screenHeight, "raylib [text] example - unicode emojis"); + private Vector2 hoveredPos; + private Vector2 selectedPos; + + public void Init() + { + emoji = new EmojiInfo[EmojiPerWidth * EmojiPerHeight]; + + hovered = -1; + selected = -1; // Load the font resources // NOTE: fontAsian is for asian languages, // fontEmoji is the emojis and fontDefault is used for everything else - Font fontDefault = LoadFont("resources/fonts/dejavu.fnt"); // Requires "resources/fonts/dejavu.png" - Font fontAsian = LoadFont("resources/fonts/noto_cjk.fnt"); // Requires "resources/fonts/noto_cjk.png" - Font fontEmoji = LoadFont("resources/fonts/symbola.fnt"); // Requires "resources/fonts/symbola.png" + fontDefault = LoadFont("resources/fonts/dejavu.fnt"); // Requires "resources/fonts/dejavu.png" + fontAsian = LoadFont("resources/fonts/noto_cjk.fnt"); // Requires "resources/fonts/noto_cjk.png" + fontEmoji = LoadFont("resources/fonts/symbola.fnt"); // Requires "resources/fonts/symbola.png" - Vector2 hoveredPos = new(0.0f, 0.0f); - Vector2 selectedPos = new(0.0f, 0.0f); + hoveredPos = new(0.0f, 0.0f); + selectedPos = new(0.0f, 0.0f); // Set a random set of emojis when starting up RandomizeEmoji(); + } - SetTargetFPS(60); // Set our game to run at 60 frames-per-second - //-------------------------------------------------------------------------------------- - - // Main loop - while (!WindowShouldClose()) // Detect window close button or ESC key + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Add a new set of emojis when SPACE is pressed + if (IsKeyPressed(KeyboardKey.Space)) { - // Update - //---------------------------------------------------------------------------------- - // Add a new set of emojis when SPACE is pressed - if (IsKeyPressed(KeyboardKey.Space)) - { - RandomizeEmoji(); - } - - // Set the selected emoji - if (IsMouseButtonPressed(MouseButton.Left) && (hovered != -1) && (hovered != selected)) - { - selected = hovered; - selectedPos = hoveredPos; - } - - Vector2 mouse = GetMousePosition(); - Vector2 position = new(28.8f, 10.0f); - hovered = -1; - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // Draw random emojis in the background - //------------------------------------------------------------------------------ - for (int i = 0; i < emoji.Length; i++) - { - string txt = GetEmojiAt(emoji[i].Index); - Rectangle emojiRect = new(position.X, position.Y, fontEmoji.BaseSize, fontEmoji.BaseSize); - - if (!CheckCollisionPointRec(mouse, emojiRect)) - { - DrawTextEx(fontEmoji, txt, position, fontEmoji.BaseSize, 1.0f, selected == i ? emoji[i].Color : Fade(Color.LightGray, 0.4f)); - } - else - { - DrawTextEx(fontEmoji, txt, position, fontEmoji.BaseSize, 1.0f, emoji[i].Color); - hovered = i; - hoveredPos = position; - } - - if ((i != 0) && (i % EmojiPerWidth == 0)) - { - position.Y += fontEmoji.BaseSize + 24.25f; - position.X = 28.8f; - } - else - { - position.X += fontEmoji.BaseSize + 28.8f; - } - } - //------------------------------------------------------------------------------ - - // Draw the message when a emoji is selected - //------------------------------------------------------------------------------ - if (selected != -1) - { - int message = emoji[selected].Message; - const int horizontalPadding = 20; - const int verticalPadding = 30; - Font font = fontDefault; - - // Set correct font for asian languages - if ((messages[message].Language == "Chinese") || - (messages[message].Language == "Korean") || - (messages[message].Language == "Japanese")) - { - font = fontAsian; - } - - // Calculate size for the message box (approximate the height and width) - Vector2 sz = MeasureTextEx(font, messages[message].Text, font.BaseSize, 1.0f); - if (sz.X > 300) - { - sz.Y *= sz.X / 300; - sz.X = 300; - } - else if (sz.X < 160) - { - sz.X = 160; - } - - Rectangle msgRect = new(selectedPos.X - 38.8f, selectedPos.Y, 2 * horizontalPadding + sz.X, 2 * verticalPadding + sz.Y); - msgRect.Y -= msgRect.Height; - - // Coordinates for the chat bubble triangle - Vector2 a = new(selectedPos.X, msgRect.Y + msgRect.Height); - Vector2 b = new(a.X + 8, a.Y + 10); - Vector2 c = new(a.X + 10, a.Y); - - // Don't go outside the screen - if (msgRect.X < 10) - { - msgRect.X += 28; - } - - if (msgRect.Y < 10) - { - msgRect.Y = selectedPos.Y + 84; - a.Y = msgRect.Y; - c.Y = a.Y; - b.Y = a.Y - 10; - - // Swap values so we can actually render the triangle :( - Vector2 tmp = a; - a = b; - b = tmp; - } - - if (msgRect.X + msgRect.Width > screenWidth) - { - msgRect.X -= (msgRect.X + msgRect.Width) - screenWidth + 10; - } - - // Draw chat bubble - DrawRectangleRec(msgRect, emoji[selected].Color); - DrawTriangle(a, b, c, emoji[selected].Color); - - // Draw the main text message - Rectangle textRect = new(msgRect.X + (float)horizontalPadding / 2, msgRect.Y + (float)verticalPadding / 2, msgRect.Width - horizontalPadding, msgRect.Height); - DrawTextBoxed(font, messages[message].Text, textRect, font.BaseSize, 1.0f, true, Color.White); - - // Draw the info text below the main message - int size = Encoding.UTF8.GetByteCount(messages[message].Text); - int length = GetCodepointCount(messages[message].Text); - string info = $"{messages[message].Language} {length} characters {size} bytes"; - sz = MeasureTextEx(GetFontDefault(), info, 10, 1.0f); - - DrawText(info, (int)(textRect.X + textRect.Width - sz.X), (int)(msgRect.Y + msgRect.Height - sz.Y - 2), 10, Color.RayWhite); - } - //------------------------------------------------------------------------------ - - // Draw the info text - DrawText("These emojis have something to tell you, click each to find out!", (screenWidth - 650) / 2, screenHeight - 40, 20, Color.Gray); - DrawText("Each emoji is a unicode character from a font, not a texture... Press [SPACEBAR] to refresh", (screenWidth - 484) / 2, screenHeight - 16, 10, Color.Gray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + RandomizeEmoji(); } - // De-Initialization - //-------------------------------------------------------------------------------------- + // Set the selected emoji + if (IsMouseButtonPressed(MouseButton.Left) && (hovered != -1) && (hovered != selected)) + { + selected = hovered; + selectedPos = hoveredPos; + } + + Vector2 mouse = GetMousePosition(); + Vector2 position = new(28.8f, 10.0f); + hovered = -1; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // Draw random emojis in the background + //------------------------------------------------------------------------------ + for (int i = 0; i < emoji.Length; i++) + { + string txt = GetEmojiAt(emoji[i].Index); + Rectangle emojiRect = new(position.X, position.Y, fontEmoji.BaseSize, fontEmoji.BaseSize); + + if (!CheckCollisionPointRec(mouse, emojiRect)) + { + DrawTextEx(fontEmoji, txt, position, fontEmoji.BaseSize, 1.0f, selected == i ? emoji[i].Color : Fade(Color.LightGray, 0.4f)); + } + else + { + DrawTextEx(fontEmoji, txt, position, fontEmoji.BaseSize, 1.0f, emoji[i].Color); + hovered = i; + hoveredPos = position; + } + + if ((i != 0) && (i % EmojiPerWidth == 0)) + { + position.Y += fontEmoji.BaseSize + 24.25f; + position.X = 28.8f; + } + else + { + position.X += fontEmoji.BaseSize + 28.8f; + } + } + //------------------------------------------------------------------------------ + + // Draw the message when a emoji is selected + //------------------------------------------------------------------------------ + if (selected != -1) + { + int message = emoji[selected].Message; + const int horizontalPadding = 20; + const int verticalPadding = 30; + Font font = fontDefault; + + // Set correct font for asian languages + if ((messages[message].Language == "Chinese") || + (messages[message].Language == "Korean") || + (messages[message].Language == "Japanese")) + { + font = fontAsian; + } + + // Calculate size for the message box (approximate the height and width) + Vector2 sz = MeasureTextEx(font, messages[message].Text, font.BaseSize, 1.0f); + if (sz.X > 300) + { + sz.Y *= sz.X / 300; + sz.X = 300; + } + else if (sz.X < 160) + { + sz.X = 160; + } + + Rectangle msgRect = new(selectedPos.X - 38.8f, selectedPos.Y, 2 * horizontalPadding + sz.X, 2 * verticalPadding + sz.Y); + msgRect.Y -= msgRect.Height; + + // Coordinates for the chat bubble triangle + Vector2 a = new(selectedPos.X, msgRect.Y + msgRect.Height); + Vector2 b = new(a.X + 8, a.Y + 10); + Vector2 c = new(a.X + 10, a.Y); + + // Don't go outside the screen + if (msgRect.X < 10) + { + msgRect.X += 28; + } + + if (msgRect.Y < 10) + { + msgRect.Y = selectedPos.Y + 84; + a.Y = msgRect.Y; + c.Y = a.Y; + b.Y = a.Y - 10; + + // Swap values so we can actually render the triangle :( + Vector2 tmp = a; + a = b; + b = tmp; + } + + if (msgRect.X + msgRect.Width > screenWidth) + { + msgRect.X -= (msgRect.X + msgRect.Width) - screenWidth + 10; + } + + // Draw chat bubble + DrawRectangleRec(msgRect, emoji[selected].Color); + DrawTriangle(a, b, c, emoji[selected].Color); + + // Draw the main text message + Rectangle textRect = new(msgRect.X + (float)horizontalPadding / 2, msgRect.Y + (float)verticalPadding / 2, msgRect.Width - horizontalPadding, msgRect.Height); + DrawTextBoxed(font, messages[message].Text, textRect, font.BaseSize, 1.0f, true, Color.White); + + // Draw the info text below the main message + int size = Encoding.UTF8.GetByteCount(messages[message].Text); + int length = GetCodepointCount(messages[message].Text); + string info = $"{messages[message].Language} {length} characters {size} bytes"; + sz = MeasureTextEx(GetFontDefault(), info, 10, 1.0f); + + DrawText(info, (int)(textRect.X + textRect.Width - sz.X), (int)(msgRect.Y + msgRect.Height - sz.Y - 2), 10, Color.RayWhite); + } + //------------------------------------------------------------------------------ + + // Draw the info text + DrawText("These emojis have something to tell you, click each to find out!", (screenWidth - 650) / 2, screenHeight - 40, 20, Color.Gray); + DrawText("Each emoji is a unicode character from a font, not a texture... Press [SPACEBAR] to refresh", (screenWidth - 484) / 2, screenHeight - 16, 10, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { UnloadFont(fontDefault); // Unload font resource UnloadFont(fontAsian); // Unload font resource UnloadFont(fontEmoji); // Unload font resource - - CloseWindow(); // Close window and OpenGL context - //-------------------------------------------------------------------------------------- - - return 0; } // Fills the emoji array with random emoji (only those emojis present in fontEmoji) - static void RandomizeEmoji() + void RandomizeEmoji() { hovered = selected = -1; int start = GetRandomValue(45, 360); @@ -510,4 +515,33 @@ public class Unicode textOffsetX += glyphWidth; } } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint | ConfigFlags.VSyncHint); + InitWindow(screenWidth, screenHeight, "raylib [text] example - unicode emojis"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new Unicode(); + game.Init(); + + // Main loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } } diff --git a/Examples/Text/UnicodeRanges.cs b/Examples/Text/UnicodeRanges.cs new file mode 100644 index 0000000..2979c73 --- /dev/null +++ b/Examples/Text/UnicodeRanges.cs @@ -0,0 +1,225 @@ +/******************************************************************************************* +* +* raylib [text] example - unicode ranges +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.6 +* +* Example contributed by Vadim Gunko (@GuvaCode) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Vadim Gunko (@GuvaCode) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System.Numerics; +using Raylib_cs; +using static Raylib_cs.Raylib; + +namespace Examples.Text; + +public partial class UnicodeRanges : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + // path differs from upstream: font lives under resources/fonts/ + private const string FontPath = "resources/fonts/NotoSansTC-Regular.ttf"; + + public string Name => "Text / Unicode Ranges"; + + public string Title => "raylib [text] example - unicode ranges"; + + private Font font; + private int unicodeRange; // Track the ranges of codepoints added to font + private int prevUnicodeRange; // Previous Unicode range to avoid reloading every frame + + public void Init() + { + // Load font with default Unicode range: Basic ASCII [32-127] + font = LoadFont(FontPath); + SetTextureFilter(font.Texture, TextureFilter.Bilinear); + + unicodeRange = 0; + prevUnicodeRange = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (unicodeRange != prevUnicodeRange) + { + UnloadFont(font); + + // Load font with default Unicode range: Basic ASCII [32-127] + font = LoadFont(FontPath); + + // Add required ranges to loaded font + // NOTE: The upstream switch uses fall-through so range N also loads all lower + // ranges, in the same order (4 -> 3 -> 2 -> 1); the descending if-chain preserves that. + + /* + if (unicodeRange >= 5) + { + // Unicode range: Devanari, Arabic, Hebrew + // WARNING: Glyphs not available on provided font! + AddCodepointRange(ref font, FontPath, 0x900, 0x97f); // Devanagari + AddCodepointRange(ref font, FontPath, 0x600, 0x6ff); // Arabic + AddCodepointRange(ref font, FontPath, 0x5d0, 0x5ea); // Hebrew + } + */ + if (unicodeRange >= 4) + { + // Unicode range: CJK (Japanese and Chinese) + // WARNING: Loading thousands of codepoints requires lot of time! + // A better strategy is prefilter the required codepoints for the text + // in the game and just load the required ones + AddCodepointRange(ref font, FontPath, 0x4e00, 0x9fff); + AddCodepointRange(ref font, FontPath, 0x3400, 0x4dbf); + AddCodepointRange(ref font, FontPath, 0x3000, 0x303f); + AddCodepointRange(ref font, FontPath, 0x3040, 0x309f); + AddCodepointRange(ref font, FontPath, 0x30A0, 0x30ff); + AddCodepointRange(ref font, FontPath, 0x31f0, 0x31ff); + AddCodepointRange(ref font, FontPath, 0xff00, 0xffef); + AddCodepointRange(ref font, FontPath, 0xac00, 0xd7af); + AddCodepointRange(ref font, FontPath, 0x1100, 0x11ff); + } + if (unicodeRange >= 3) + { + // Unicode range: Cyrillic + AddCodepointRange(ref font, FontPath, 0x400, 0x4ff); + AddCodepointRange(ref font, FontPath, 0x500, 0x52f); + AddCodepointRange(ref font, FontPath, 0x2de0, 0x2Dff); + AddCodepointRange(ref font, FontPath, 0xa640, 0xA69f); + } + if (unicodeRange >= 2) + { + // Unicode range: Greek + AddCodepointRange(ref font, FontPath, 0x370, 0x3ff); + AddCodepointRange(ref font, FontPath, 0x1f00, 0x1fff); + } + if (unicodeRange >= 1) + { + // Unicode range: European Languages + AddCodepointRange(ref font, FontPath, 0xc0, 0x17f); + AddCodepointRange(ref font, FontPath, 0x180, 0x24f); + //AddCodepointRange(ref font, FontPath, 0x1e00, 0x1eff); + //AddCodepointRange(ref font, FontPath, 0x2c60, 0x2c7f); + } + + prevUnicodeRange = unicodeRange; + SetTextureFilter(font.Texture, TextureFilter.Bilinear); // Set font atlas scale filter + } + + if (IsKeyPressed(KeyboardKey.Zero)) unicodeRange = 0; + else if (IsKeyPressed(KeyboardKey.One)) unicodeRange = 1; + else if (IsKeyPressed(KeyboardKey.Two)) unicodeRange = 2; + else if (IsKeyPressed(KeyboardKey.Three)) unicodeRange = 3; + else if (IsKeyPressed(KeyboardKey.Four)) unicodeRange = 4; + //else if (IsKeyPressed(KeyboardKey.Five)) unicodeRange = 5; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + DrawText("ADD CODEPOINTS: [1][2][3][4]", 20, 20, 20, Color.Maroon); + + // Render test strings in different languages + DrawTextEx(font, "> English: Hello World!", new Vector2(50, 70), 32, 1, Color.DarkGray); // English + DrawTextEx(font, "> Español: Hola mundo!", new Vector2(50, 120), 32, 1, Color.DarkGray); // Spanish + DrawTextEx(font, "> Ελληνικά: Γειά σου κόσμε!", new Vector2(50, 170), 32, 1, Color.DarkGray); // Greek + DrawTextEx(font, "> Русский: Привет мир!", new Vector2(50, 220), 32, 0, Color.DarkGray); // Russian + DrawTextEx(font, "> 中文: 你好世界!", new Vector2(50, 270), 32, 1, Color.DarkGray); // Chinese + DrawTextEx(font, "> 日本語: こんにちは世界!", new Vector2(50, 320), 32, 1, Color.DarkGray); // Japanese + //DrawTextEx(font, "देवनागरी: होला मुंडो!", new Vector2(50, 350), 32, 1, Color.DarkGray); // Devanagari (glyphs not available in font) + + // Draw font texture scaled to screen + float atlasScale = 380.0f / font.Texture.Width; + DrawRectangleRec(new Rectangle(400.0f, 16.0f, font.Texture.Width * atlasScale, font.Texture.Height * atlasScale), Color.Black); + DrawTexturePro(font.Texture, new Rectangle(0, 0, font.Texture.Width, font.Texture.Height), + new Rectangle(400.0f, 16.0f, font.Texture.Width * atlasScale, font.Texture.Height * atlasScale), new Vector2(0, 0), 0.0f, Color.White); + DrawRectangleLines(400, 16, 380, 380, Color.Red); + + DrawText($"ATLAS SIZE: {font.Texture.Width}x{font.Texture.Height} px (x{atlasScale:00.00})", 20, 380, 20, Color.Blue); + DrawText($"CODEPOINTS GLYPHS LOADED: {font.GlyphCount}", 20, 410, 20, Color.Lime); + + // Display font attribution + DrawText("Font: Noto Sans TC. License: SIL Open Font License 1.1", screenWidth - 300, screenHeight - 20, 10, Color.Gray); + + if (prevUnicodeRange != unicodeRange) + { + DrawRectangle(0, 0, screenWidth, screenHeight, Fade(Color.White, 0.8f)); + DrawRectangle(0, 125, screenWidth, 200, Color.Gray); + DrawText("GENERATING FONT ATLAS...", 120, 210, 40, Color.Black); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadFont(font); // Unload font resource + } + + //-------------------------------------------------------------------------------------- + // Module Functions Definition + //-------------------------------------------------------------------------------------- + // Add codepoint range to existing font + private static unsafe void AddCodepointRange(ref Font font, string fontPath, int start, int stop) + { + int rangeSize = stop - start + 1; + int currentRangeSize = font.GlyphCount; + + // TODO: Load glyphs from provided vector font (if available), + // add them to existing font, regenerating font image and texture + + int updatedCodepointCount = currentRangeSize + rangeSize; + int[] updatedCodepoints = new int[updatedCodepointCount]; + + // Get current codepoint list + for (int i = 0; i < currentRangeSize; i++) updatedCodepoints[i] = font.Glyphs[i].Value; + + // Add new codepoints to list (provided range) + for (int i = currentRangeSize; i < updatedCodepointCount; i++) + updatedCodepoints[i] = start + (i - currentRangeSize); + + UnloadFont(font); + font = LoadFontEx(fontPath, 32, updatedCodepoints, updatedCodepointCount); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [text] example - unicode ranges"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new UnicodeRanges(); + game.Init(); + + // Main loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Text/WordsAlignment.cs b/Examples/Text/WordsAlignment.cs new file mode 100644 index 0000000..75c6e10 --- /dev/null +++ b/Examples/Text/WordsAlignment.cs @@ -0,0 +1,167 @@ +/******************************************************************************************* +* +* raylib [text] example - words alignment +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by JP Mortiboys (@themushroompirates) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 JP Mortiboys (@themushroompirates) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; // Required for: Lerp() + +namespace Examples.Text; + +public partial class WordsAlignment : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + // TextAlignment values: Left/Top = 0, Centre/Middle = 1, Right/Bottom = 2 + + public string Name => "Text / Words Alignment"; + + public string Title => "raylib [text] example - words alignment"; + + // Define the rectangle we will draw the text in + private Rectangle textContainerRect; + + // Some text to display the current alignment + private static readonly string[] textAlignNameH = { "Left", "Centre", "Right" }; + private static readonly string[] textAlignNameV = { "Top", "Middle", "Bottom" }; + + // Define the text we're going to draw in the rectangle + private int wordIndex; + private int wordCount; + private string[] words; + + // Initialize the font size we're going to use + private int fontSize; + + // And of course the font... + private Font font; + + // Initialize the alignment variables + private int hAlign; + private int vAlign; + + public void Init() + { + // Define the rectangle we will draw the text in + textContainerRect = new Rectangle((float)screenWidth / 2 - (float)screenWidth / 4, (float)screenHeight / 2 - (float)screenHeight / 3, (float)screenWidth / 2, (float)screenHeight * 2 / 3); + + // Define the text we're going to draw in the rectangle + wordIndex = 0; + words = "raylib is a simple and easy-to-use library to enjoy videogames programming".Split(' '); + wordCount = words.Length; + + // Initialize the font size we're going to use + fontSize = 40; + + // And of course the font... + font = GetFontDefault(); + + // Initialize the alignment variables + hAlign = 1; // TEXT_ALIGN_CENTRE + vAlign = 1; // TEXT_ALIGN_MIDDLE + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.Left)) + { + if (hAlign > 0) hAlign = hAlign - 1; + } + + if (IsKeyPressed(KeyboardKey.Right)) + { + hAlign = hAlign + 1; + if (hAlign > 2) hAlign = 2; + } + + if (IsKeyPressed(KeyboardKey.Up)) + { + if (vAlign > 0) vAlign = vAlign - 1; + } + + if (IsKeyPressed(KeyboardKey.Down)) + { + vAlign = vAlign + 1; + if (vAlign > 2) vAlign = 2; + } + + // One word per second + if (wordCount > 0) wordIndex = (int)GetTime() % wordCount; + else wordIndex = 0; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.DarkBlue); + + DrawText("Use Arrow Keys to change the text alignment", 20, 20, 20, Color.LightGray); + DrawText($"Alignment: Horizontal = {textAlignNameH[hAlign]}, Vertical = {textAlignNameV[vAlign]}", 20, 40, 20, Color.LightGray); + + DrawRectangleRec(textContainerRect, Color.Blue); + + // Get the size of the text to draw + Vector2 textSize = MeasureTextEx(font, words[wordIndex], fontSize, fontSize * .1f); + + // Calculate the top-left text position based on the rectangle and alignment + Vector2 textPos = new Vector2( + textContainerRect.X + Lerp(0.0f, textContainerRect.Width - textSize.X, hAlign * 0.5f), + textContainerRect.Y + Lerp(0.0f, textContainerRect.Height - textSize.Y, vAlign * 0.5f) + ); + + // Draw the text + DrawTextEx(font, words[wordIndex], textPos, fontSize, fontSize * .1f, Color.RayWhite); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [text] example - words alignment"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new WordsAlignment(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Textures/CellularAutomata.cs b/Examples/Textures/CellularAutomata.cs new file mode 100644 index 0000000..ee558e9 --- /dev/null +++ b/Examples/Textures/CellularAutomata.cs @@ -0,0 +1,248 @@ +/******************************************************************************************* +* +* raylib [textures] example - cellular automata +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.6, last time updated with raylib 5.6 +* +* Example contributed by Jordi Santonja (@JordSant) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Jordi Santonja (@JordSant) +* +********************************************************************************************/ + +using static Raylib_cs.Raylib; + +namespace Examples.Textures; + +public partial class CellularAutomata : IExample +{ + // Initialization constants + //-------------------------------------------------------------------------------------- + private const int screenWidth = 800; + private const int screenHeight = 450; + private const int imageWidth = 800; + private const int imageHeight = 800 / 2; + + // Rule button sizes and positions + private const int drawRuleStartX = 585; + private const int drawRuleStartY = 10; + private const int drawRuleSpacing = 15; + private const int drawRuleGroupSpacing = 50; + private const int drawRuleSize = 14; + private const int drawRuleInnerSize = 10; + + // Preset button sizes + private const int presetsSizeX = 42; + private const int presetsSizeY = 22; + + private const int linesUpdatedPerFrame = 4; + + public string Name => "Textures / Cellular Automata"; + + public string Title => "raylib [textures] example - cellular automata"; + + // Some interesting rules + private static readonly int[] presetValues = { 18, 30, 60, 86, 102, 124, 126, 150, 182, 225 }; + private const int presetsCount = 10; + + private Image image; + private Texture2D texture; + private int rule; + private int line; + + private static void ComputeLine(ref Image image, int line, int rule) + { + // Compute next line pixels. Boundaries are not computed, always 0 + for (var i = 1; i < imageWidth - 1; i++) + { + // Get, from the previous line, the 3 pixels states as a binary value + var prevValue = ((GetImageColor(image, i - 1, line - 1).R < 5) ? 4 : 0) + // Left pixel + ((GetImageColor(image, i, line - 1).R < 5) ? 2 : 0) + // Center pixel + ((GetImageColor(image, i + 1, line - 1).R < 5) ? 1 : 0); // Right pixel + // Get next value from rule bitmask + var currValue = (rule & (1 << prevValue)) != 0; + // Update pixel color + ImageDrawPixel(ref image, i, line, currValue ? Color.Black : Color.RayWhite); + } + } + + public void Init() + { + // Image that contains the cellular automaton + image = GenImageColor(imageWidth, imageHeight, Color.RayWhite); + // The top central pixel set as black + ImageDrawPixel(ref image, imageWidth / 2, 0, Color.Black); + + texture = LoadTextureFromImage(image); + + // Variables + rule = 30; // Starting rule + line = 1; // Line to compute, starting from line 1. One point in line 0 is already set + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Handle mouse + var mouse = GetMousePosition(); + var mouseInCell = -1; // -1: outside any button; 0-7: rule cells; 8+: preset cells + + // Check mouse on rule cells + for (var i = 0; i < 8; i++) + { + var cellX = drawRuleStartX - drawRuleGroupSpacing * i + drawRuleSpacing; + var cellY = drawRuleStartY + drawRuleSpacing; + if ((mouse.X >= cellX) && (mouse.X <= cellX + drawRuleSize) && + (mouse.Y >= cellY) && (mouse.Y <= cellY + drawRuleSize)) + { + mouseInCell = i; // 0-7: rule cells + break; + } + } + + // Check mouse on preset cells + if (mouseInCell < 0) + { + for (var i = 0; i < presetsCount; i++) + { + var cellX = 4 + (presetsSizeX + 2) * (i / 2); + var cellY = 2 + (presetsSizeY + 2) * (i % 2); + if ((mouse.X >= cellX) && (mouse.X <= cellX + presetsSizeX) && + (mouse.Y >= cellY) && (mouse.Y <= cellY + presetsSizeY)) + { + mouseInCell = i + 8; // 8+: preset cells + break; + } + } + } + + if (IsMouseButtonPressed(MouseButton.Left) && (mouseInCell >= 0)) + { + // Rule changed both by selecting a preset or toggling a bit + if (mouseInCell < 8) + { + rule ^= (1 << mouseInCell); + } + else + { + rule = presetValues[mouseInCell - 8]; + } + + // Reset image + ImageClearBackground(ref image, Color.RayWhite); + ImageDrawPixel(ref image, imageWidth / 2, 0, Color.Black); + line = 1; + } + + // Compute next lines + //---------------------------------------------------------------------------------- + if (line < imageHeight) + { + for (var i = 0; (i < linesUpdatedPerFrame) && (line + i < imageHeight); i++) + { + ComputeLine(ref image, line + i, rule); + } + line += linesUpdatedPerFrame; + + UpdateTexture(texture, image.Data); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // Draw cellular automaton texture + DrawTexture(texture, 0, screenHeight - imageHeight, Color.White); + + // Draw preset values + for (var i = 0; i < presetsCount; i++) + { + DrawText($"{presetValues[i]}", 8 + (presetsSizeX + 2) * (i / 2), 4 + (presetsSizeY + 2) * (i % 2), 20, Color.Gray); + DrawRectangleLines(4 + (presetsSizeX + 2) * (i / 2), 2 + (presetsSizeY + 2) * (i % 2), presetsSizeX, presetsSizeY, Color.Blue); + + // If the mouse is on this preset, highlight it + if (mouseInCell == i + 8) + { + DrawRectangleLinesEx(new Rectangle(2 + (presetsSizeX + 2.0f) * (i / 2), + (presetsSizeY + 2.0f) * (i % 2), + presetsSizeX + 4.0f, presetsSizeY + 4.0f), 3, Color.Red); + } + } + + // Draw rule bits + for (var i = 0; i < 8; i++) + { + // The three input bits + for (var j = 0; j < 3; j++) + { + DrawRectangleLines(drawRuleStartX - drawRuleGroupSpacing * i + drawRuleSpacing * j, drawRuleStartY, drawRuleSize, drawRuleSize, Color.Gray); + if ((i & (4 >> j)) != 0) + { + DrawRectangle(drawRuleStartX + 2 - drawRuleGroupSpacing * i + drawRuleSpacing * j, drawRuleStartY + 2, drawRuleInnerSize, drawRuleInnerSize, Color.Black); + } + } + + // The output bit + DrawRectangleLines(drawRuleStartX - drawRuleGroupSpacing * i + drawRuleSpacing, drawRuleStartY + drawRuleSpacing, drawRuleSize, drawRuleSize, Color.Blue); + if ((rule & (1 << i)) != 0) + { + DrawRectangle(drawRuleStartX + 2 - drawRuleGroupSpacing * i + drawRuleSpacing, drawRuleStartY + 2 + drawRuleSpacing, drawRuleInnerSize, drawRuleInnerSize, Color.Black); + } + + // If the mouse is on this rule bit, highlight it + if (mouseInCell == i) + { + DrawRectangleLinesEx(new Rectangle(drawRuleStartX - drawRuleGroupSpacing * i + drawRuleSpacing - 2.0f, + drawRuleStartY + drawRuleSpacing - 2.0f, + drawRuleSize + 4.0f, drawRuleSize + 4.0f), 3, Color.Red); + } + } + + DrawText($"RULE: {rule}", drawRuleStartX + drawRuleSpacing * 4, drawRuleStartY + 1, 30, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadImage(image); + UnloadTexture(texture); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - cellular automata"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new CellularAutomata(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Textures/ClipboardImage.cs b/Examples/Textures/ClipboardImage.cs new file mode 100644 index 0000000..3c0e157 --- /dev/null +++ b/Examples/Textures/ClipboardImage.cs @@ -0,0 +1,144 @@ +/******************************************************************************************* +* +* raylib [textures] example - clipboard image +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Maicon Santana (@maiconpintoabreu) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2026 Maicon Santana (@maiconpintoabreu) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Textures; + +[ExcludeFromBrowser("GetClipboardImage() is a desktop-only OS clipboard feature")] +public partial class ClipboardImage : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MaxTextureCollection = 20; + + public string Name => "Textures / Clipboard Image"; + + public string Title => "raylib [textures] example - clipboard image"; + + private struct TextureCollection + { + public Texture2D Texture; + public Vector2 Position; + } + + private TextureCollection[] collection; + private int currentCollectionIndex; + + public void Init() + { + collection = new TextureCollection[MaxTextureCollection]; + currentCollectionIndex = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.R)) // Reset image collection + { + // Unload textures to avoid memory leaks + for (var i = 0; i < MaxTextureCollection; i++) + { + UnloadTexture(collection[i].Texture); + } + + currentCollectionIndex = 0; + } + + if (IsKeyDown(KeyboardKey.LeftControl) && IsKeyPressed(KeyboardKey.V) && + (currentCollectionIndex < MaxTextureCollection)) + { + var image = GetClipboardImage(); + + if (IsImageValid(image)) + { + collection[currentCollectionIndex].Texture = LoadTextureFromImage(image); + collection[currentCollectionIndex].Position = GetMousePosition(); + currentCollectionIndex++; + UnloadImage(image); + } + else + { + TraceLog(TraceLogLevel.Info, "IMAGE: Could not retrieve image from clipboard"); + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + for (var i = 0; i < currentCollectionIndex; i++) + { + if (IsTextureValid(collection[i].Texture)) + { + DrawTexturePro(collection[i].Texture, + new Rectangle(0, 0, collection[i].Texture.Width, collection[i].Texture.Height), + new Rectangle(collection[i].Position.X, collection[i].Position.Y, collection[i].Texture.Width, collection[i].Texture.Height), + new Vector2(collection[i].Texture.Width * 0.5f, collection[i].Texture.Height * 0.5f), + 0.0f, Color.White); + } + } + + DrawRectangle(0, 0, screenWidth, 40, Color.Black); + DrawText("Clipboard Image - Ctrl+V to Paste and R to Reset ", 120, 10, 20, Color.LightGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + for (var i = 0; i < MaxTextureCollection; i++) + { + UnloadTexture(collection[i].Texture); + } + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - clipboard image"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new ClipboardImage(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Textures/FogOfWar.cs b/Examples/Textures/FogOfWar.cs new file mode 100644 index 0000000..8d95eea --- /dev/null +++ b/Examples/Textures/FogOfWar.cs @@ -0,0 +1,233 @@ +/******************************************************************************************* +* +* raylib [textures] example - fog of war +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 4.2, last time updated with raylib 4.2 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2018-2025 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Textures; + +public partial class FogOfWar : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MapTileSize = 32; // Tiles size 32x32 pixels + private const int PlayerSize = 16; // Player size + private const int PlayerTileVisibility = 2; // Player can see 2 tiles around its position + + public string Name => "Textures / Fog of War"; + + public string Title => "raylib [textures] example - fog of war"; + + // Map data type + private struct Map + { + public uint TilesX; // Number of tiles in X axis + public uint TilesY; // Number of tiles in Y axis + public byte[] TileIds; // Tile ids (tilesX*tilesY), defines type of tile to draw + public byte[] TileFog; // Tile fog state (tilesX*tilesY), defines if a tile has fog or half-fog + } + + private Map map; + private Vector2 playerPosition; + private int playerTileX; + private int playerTileY; + private RenderTexture2D fogOfWar; + + public void Init() + { + map = new Map(); + map.TilesX = 25; + map.TilesY = 15; + + // NOTE: We can have up to 256 values for tile ids and for tile fog state, + // probably we don't need that many values for fog state, it can be optimized + // to use only 2 bits per fog state (reducing size by 4) but logic will be a bit more complex + map.TileIds = new byte[map.TilesX * map.TilesY]; + map.TileFog = new byte[map.TilesX * map.TilesY]; + + // Load map tiles (generating 2 random tile ids for testing) + // NOTE: Map tile ids should be probably loaded from an external map file + for (uint i = 0; i < map.TilesY * map.TilesX; i++) + { + map.TileIds[i] = (byte)GetRandomValue(0, 1); + } + + // Player position on the screen (pixel coordinates, not tile coordinates) + playerPosition = new Vector2(180, 130); + playerTileX = 0; + playerTileY = 0; + + // Render texture to render fog of war + // NOTE: To get an automatic smooth-fog effect we use a render texture to render fog + // at a smaller size (one pixel per tile) and scale it on drawing with bilinear filtering + fogOfWar = LoadRenderTexture((int)map.TilesX, (int)map.TilesY); + SetTextureFilter(fogOfWar.Texture, TextureFilter.Bilinear); + SetTextureWrap(fogOfWar.Texture, TextureWrap.Clamp); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Move player around + if (IsKeyDown(KeyboardKey.Right)) + { + playerPosition.X += 5; + } + if (IsKeyDown(KeyboardKey.Left)) + { + playerPosition.X -= 5; + } + if (IsKeyDown(KeyboardKey.Down)) + { + playerPosition.Y += 5; + } + if (IsKeyDown(KeyboardKey.Up)) + { + playerPosition.Y -= 5; + } + + // Check player position to avoid moving outside tilemap limits + if (playerPosition.X < 0) + { + playerPosition.X = 0; + } + else if ((playerPosition.X + PlayerSize) > (map.TilesX * MapTileSize)) + { + playerPosition.X = (float)map.TilesX * MapTileSize - PlayerSize; + } + if (playerPosition.Y < 0) + { + playerPosition.Y = 0; + } + else if ((playerPosition.Y + PlayerSize) > (map.TilesY * MapTileSize)) + { + playerPosition.Y = (float)map.TilesY * MapTileSize - PlayerSize; + } + + // Previous visited tiles are set to partial fog + for (uint i = 0; i < map.TilesX * map.TilesY; i++) + { + if (map.TileFog[i] == 1) + { + map.TileFog[i] = 2; + } + } + + // Get current tile position from player pixel position + playerTileX = (int)((playerPosition.X + (float)MapTileSize / 2) / MapTileSize); + playerTileY = (int)((playerPosition.Y + (float)MapTileSize / 2) / MapTileSize); + + // Check visibility and update fog + // NOTE: We check tilemap limits to avoid processing tiles out-of-array-bounds (it could crash program) + for (var y = (playerTileY - PlayerTileVisibility); y < (playerTileY + PlayerTileVisibility); y++) + { + for (var x = (playerTileX - PlayerTileVisibility); x < (playerTileX + PlayerTileVisibility); x++) + { + if ((x >= 0) && (x < (int)map.TilesX) && (y >= 0) && (y < (int)map.TilesY)) + { + map.TileFog[y * map.TilesX + x] = 1; + } + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + // Draw fog of war to a small render texture for automatic smoothing on scaling + BeginTextureMode(fogOfWar); + ClearBackground(Color.Blank); + for (uint y = 0; y < map.TilesY; y++) + { + for (uint x = 0; x < map.TilesX; x++) + { + if (map.TileFog[y * map.TilesX + x] == 0) + { + DrawRectangle((int)x, (int)y, 1, 1, Color.Black); + } + else if (map.TileFog[y * map.TilesX + x] == 2) + { + DrawRectangle((int)x, (int)y, 1, 1, Fade(Color.Black, 0.8f)); + } + } + } + EndTextureMode(); + + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + for (uint y = 0; y < map.TilesY; y++) + { + for (uint x = 0; x < map.TilesX; x++) + { + // Draw tiles from id (and tile borders) + DrawRectangle((int)x * MapTileSize, (int)y * MapTileSize, MapTileSize, MapTileSize, + (map.TileIds[y * map.TilesX + x] == 0) ? Color.Blue : Fade(Color.Blue, 0.9f)); + DrawRectangleLines((int)x * MapTileSize, (int)y * MapTileSize, MapTileSize, MapTileSize, Fade(Color.DarkBlue, 0.5f)); + } + } + + // Draw player + DrawRectangleV(playerPosition, new Vector2(PlayerSize, PlayerSize), Color.Red); + + // Draw fog of war (scaled to full map, bilinear filtering) + DrawTexturePro(fogOfWar.Texture, + new Rectangle(0, 0, (float)fogOfWar.Texture.Width, (float)-fogOfWar.Texture.Height), + new Rectangle(0, 0, (float)map.TilesX * MapTileSize, (float)map.TilesY * MapTileSize), + new Vector2(0, 0), 0.0f, Color.White); + + // Draw player current tile + DrawText($"Current tile: [{playerTileX},{playerTileY}]", 10, 10, 20, Color.RayWhite); + DrawText("ARROW KEYS to move", 10, screenHeight - 25, 20, Color.RayWhite); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadRenderTexture(fogOfWar); // Unload render texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - fog of war"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new FogOfWar(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Textures/FramebufferRendering.cs b/Examples/Textures/FramebufferRendering.cs new file mode 100644 index 0000000..cd435d4 --- /dev/null +++ b/Examples/Textures/FramebufferRendering.cs @@ -0,0 +1,248 @@ +/******************************************************************************************* +* +* raylib [textures] example - framebuffer rendering +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.6, last time updated with raylib 5.6 +* +* Example contributed by Jack Boakes (@jackboakes) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2026 Jack Boakes (@jackboakes) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; +using static Raylib_cs.Raymath; + +namespace Examples.Textures; + +public partial class FramebufferRendering : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + private const int splitWidth = screenWidth / 2; + + public string Name => "Textures / Framebuffer Rendering"; + + public string Title => "raylib [textures] example - framebuffer rendering"; + + public bool CursorDisabled => true; + + private Camera3D subjectCamera; + private Camera3D observerCamera; + + private RenderTexture2D observerTarget; + private Rectangle observerSource; + private Rectangle observerDest; + + private RenderTexture2D subjectTarget; + private Rectangle subjectSource; + private Rectangle subjectDest; + private float textureAspectRatio; + + private const float captureSize = 128.0f; + private Rectangle cropSource; + private Rectangle cropDest; + + public void Init() + { + // Camera to look at the 3D world + subjectCamera = new Camera3D(); + subjectCamera.Position = new Vector3(5.0f, 5.0f, 5.0f); + subjectCamera.Target = new Vector3(0.0f, 0.0f, 0.0f); + subjectCamera.Up = new Vector3(0.0f, 1.0f, 0.0f); + subjectCamera.FovY = 45.0f; + subjectCamera.Projection = CameraProjection.Perspective; + + // Camera to observe the subject camera and 3D world + observerCamera = new Camera3D(); + observerCamera.Position = new Vector3(10.0f, 10.0f, 10.0f); + observerCamera.Target = new Vector3(0.0f, 0.0f, 0.0f); + observerCamera.Up = new Vector3(0.0f, 1.0f, 0.0f); + observerCamera.FovY = 45.0f; + observerCamera.Projection = CameraProjection.Perspective; + + // Set up render textures + observerTarget = LoadRenderTexture(splitWidth, screenHeight); + observerSource = new Rectangle(0.0f, 0.0f, observerTarget.Texture.Width, -observerTarget.Texture.Height); + observerDest = new Rectangle(0.0f, 0.0f, splitWidth, screenHeight); + + subjectTarget = LoadRenderTexture(splitWidth, screenHeight); + subjectSource = new Rectangle(0.0f, 0.0f, subjectTarget.Texture.Width, -subjectTarget.Texture.Height); + subjectDest = new Rectangle(splitWidth, 0.0f, splitWidth, screenHeight); + textureAspectRatio = (float)subjectTarget.Texture.Width / subjectTarget.Texture.Height; + + // Rectangles for cropping render texture + cropSource = new Rectangle((subjectTarget.Texture.Width - captureSize) / 2.0f, (subjectTarget.Texture.Height - captureSize) / 2.0f, captureSize, -captureSize); + cropDest = new Rectangle(splitWidth + 20.0f, 20.0f, captureSize, captureSize); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref observerCamera, CameraMode.Free); + UpdateCamera(ref subjectCamera, CameraMode.Orbital); + + if (IsKeyPressed(KeyboardKey.R)) + { + observerCamera.Target = new Vector3(0.0f, 0.0f, 0.0f); + } + + // Build LHS observer view texture + BeginTextureMode(observerTarget); + + ClearBackground(Color.RayWhite); + + BeginMode3D(observerCamera); + + DrawGrid(10, 1.0f); + DrawCube(new Vector3(0.0f, 0.0f, 0.0f), 2.0f, 2.0f, 2.0f, Color.Gold); + DrawCubeWires(new Vector3(0.0f, 0.0f, 0.0f), 2.0f, 2.0f, 2.0f, Color.Pink); + DrawCameraPrism(subjectCamera, textureAspectRatio, Color.Green); + + EndMode3D(); + + DrawText("Observer View", 10, observerTarget.Texture.Height - 30, 20, Color.Black); + DrawText("WASD + Mouse to Move", 10, 10, 20, Color.DarkGray); + DrawText("Scroll to Zoom", 10, 30, 20, Color.DarkGray); + DrawText("R to Reset Observer Target", 10, 50, 20, Color.DarkGray); + + EndTextureMode(); + + // Build RHS subject view texture + BeginTextureMode(subjectTarget); + + ClearBackground(Color.RayWhite); + + BeginMode3D(subjectCamera); + + DrawCube(new Vector3(0.0f, 0.0f, 0.0f), 2.0f, 2.0f, 2.0f, Color.Gold); + DrawCubeWires(new Vector3(0.0f, 0.0f, 0.0f), 2.0f, 2.0f, 2.0f, Color.Pink); + DrawGrid(10, 1.0f); + + EndMode3D(); + + DrawRectangleLines((int)((subjectTarget.Texture.Width - captureSize) / 2.0f), (int)((subjectTarget.Texture.Height - captureSize) / 2.0f), (int)captureSize, (int)captureSize, Color.Green); + DrawText("Subject View", 10, subjectTarget.Texture.Height - 30, 20, Color.Black); + + EndTextureMode(); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.Black); + + // Draw observer texture LHS + DrawTexturePro(observerTarget.Texture, observerSource, observerDest, new Vector2(0.0f, 0.0f), 0.0f, Color.White); + + // Draw subject texture RHS + DrawTexturePro(subjectTarget.Texture, subjectSource, subjectDest, new Vector2(0.0f, 0.0f), 0.0f, Color.White); + + // Draw the small crop overlay on top + DrawTexturePro(subjectTarget.Texture, cropSource, cropDest, new Vector2(0.0f, 0.0f), 0.0f, Color.White); + DrawRectangleLinesEx(cropDest, 2, Color.Black); + + // Draw split screen divider line + DrawLine(splitWidth, 0, splitWidth, screenHeight, Color.Black); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadRenderTexture(observerTarget); + UnloadRenderTexture(subjectTarget); + } + + //---------------------------------------------------------------------------------- + // Module Functions Definition + //---------------------------------------------------------------------------------- + private static void DrawCameraPrism(Camera3D camera, float aspect, Color color) + { + float length = Vector3Distance(camera.Position, camera.Target); + // Define the 4 corners of the camera's prism plane sliced at the target in Normalized Device Coordinates + Vector3[] planeNDC = + { + new(-1.0f, -1.0f, 1.0f), // Bottom Left + new( 1.0f, -1.0f, 1.0f), // Bottom Right + new( 1.0f, 1.0f, 1.0f), // Top Right + new(-1.0f, 1.0f, 1.0f) // Top Left + }; + + // Build the matrices + Matrix4x4 view = GetCameraMatrix(camera); + Matrix4x4 proj = MatrixPerspective(camera.FovY * DEG2RAD, aspect, 0.05f, length); + // Combine view and projection so we can reverse the full camera transform + Matrix4x4 viewProj = MatrixMultiply(view, proj); + // Invert the view-projection matrix to unproject points from NDC space back into world space + Matrix4x4 inverseViewProj = MatrixInvert(viewProj); + + // Transform the 4 plane corners from NDC into world space + Vector3[] corners = new Vector3[4]; + for (int i = 0; i < 4; i++) + { + float x = planeNDC[i].X; + float y = planeNDC[i].Y; + float z = planeNDC[i].Z; + + // Multiply NDC position by the inverse view-projection matrix + // This produces a homogeneous (x, y, z, w) position in world space + float vx = inverseViewProj.M11 * x + inverseViewProj.M12 * y + inverseViewProj.M13 * z + inverseViewProj.M14; + float vy = inverseViewProj.M21 * x + inverseViewProj.M22 * y + inverseViewProj.M23 * z + inverseViewProj.M24; + float vz = inverseViewProj.M31 * x + inverseViewProj.M32 * y + inverseViewProj.M33 * z + inverseViewProj.M34; + float vw = inverseViewProj.M41 * x + inverseViewProj.M42 * y + inverseViewProj.M43 * z + inverseViewProj.M44; + + corners[i] = new Vector3(vx / vw, vy / vw, vz / vw); + } + + // Draw the far plane sliced at the target + DrawLine3D(corners[0], corners[1], color); + DrawLine3D(corners[1], corners[2], color); + DrawLine3D(corners[2], corners[3], color); + DrawLine3D(corners[3], corners[0], color); + + // Draw the prism lines from the far plane to the camera position + for (int i = 0; i < 4; i++) + { + DrawLine3D(camera.Position, corners[i], color); + } + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - framebuffer rendering"); + + SetTargetFPS(60); + DisableCursor(); + //-------------------------------------------------------------------------------------- + + var game = new FramebufferRendering(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Textures/GifPlayer.cs b/Examples/Textures/GifPlayer.cs new file mode 100644 index 0000000..62f5628 --- /dev/null +++ b/Examples/Textures/GifPlayer.cs @@ -0,0 +1,171 @@ +/******************************************************************************************* +* +* raylib [textures] example - gif player +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 4.2, last time updated with raylib 4.2 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2021-2025 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using static Raylib_cs.Raylib; + +namespace Examples.Textures; + +public partial class GifPlayer : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MaxFrameDelay = 20; + private const int MinFrameDelay = 1; + + public string Name => "Textures / Gif Player"; + + public string Title => "raylib [textures] example - gif player"; + + private int animFrames; + private Image imScarfyAnim; + private Texture2D texScarfyAnim; + private uint nextFrameDataOffset; // Current byte offset to next frame in image.data + private int currentAnimFrame; // Current animation frame to load and draw + private int frameDelay; // Frame delay to switch between animation frames + private int frameCounter; // General frames counter + + public void Init() + { + animFrames = 0; + + // Load all GIF animation frames into a single Image + // NOTE: GIF data is always loaded as RGBA (32bit) by default + // NOTE: Frames are just appended one after another in image.data memory + imScarfyAnim = LoadImageAnim("resources/scarfy_run.gif", out animFrames); + + // Load texture from image + // NOTE: We will update this texture when required with next frame data + // WARNING: It's not recommended to use this technique for sprites animation, + // use spritesheets instead, like illustrated in textures_sprite_anim example + texScarfyAnim = LoadTextureFromImage(imScarfyAnim); + + nextFrameDataOffset = 0; + + currentAnimFrame = 0; + frameDelay = 8; + frameCounter = 0; + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + frameCounter++; + if (frameCounter >= frameDelay) + { + // Move to next frame + // NOTE: If final frame is reached we return to first frame + currentAnimFrame++; + if (currentAnimFrame >= animFrames) + { + currentAnimFrame = 0; + } + + // Get memory offset position for next frame data in image.data + nextFrameDataOffset = (uint)(imScarfyAnim.Width * imScarfyAnim.Height * 4 * currentAnimFrame); + + // Update GPU texture data with next frame image data + // WARNING: Data size (frame size) and pixel format must match already created texture + UpdateTexture(texScarfyAnim, (byte*)imScarfyAnim.Data + nextFrameDataOffset); + + frameCounter = 0; + } + + // Control frames delay + if (IsKeyPressed(KeyboardKey.Right)) + { + frameDelay++; + } + else if (IsKeyPressed(KeyboardKey.Left)) + { + frameDelay--; + } + + if (frameDelay > MaxFrameDelay) + { + frameDelay = MaxFrameDelay; + } + else if (frameDelay < MinFrameDelay) + { + frameDelay = MinFrameDelay; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + DrawText($"TOTAL GIF FRAMES: {animFrames:D2}", 50, 30, 20, Color.LightGray); + DrawText($"CURRENT FRAME: {currentAnimFrame:D2}", 50, 60, 20, Color.Gray); + DrawText($"CURRENT FRAME IMAGE.DATA OFFSET: {nextFrameDataOffset:D2}", 50, 90, 20, Color.Gray); + + DrawText("FRAMES DELAY: ", 100, 305, 10, Color.DarkGray); + DrawText($"{frameDelay:D2} frames", 620, 305, 10, Color.DarkGray); + DrawText("PRESS RIGHT/LEFT KEYS to CHANGE SPEED!", 290, 350, 10, Color.DarkGray); + + for (var i = 0; i < MaxFrameDelay; i++) + { + if (i < frameDelay) + { + DrawRectangle(190 + 21 * i, 300, 20, 20, Color.Red); + } + DrawRectangleLines(190 + 21 * i, 300, 20, 20, Color.Maroon); + } + + DrawTexture(texScarfyAnim, GetScreenWidth() / 2 - texScarfyAnim.Width / 2, 140, Color.White); + + DrawText("(c) Scarfy sprite by Eiden Marsal", screenWidth - 200, screenHeight - 20, 10, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texScarfyAnim); // Unload texture + UnloadImage(imScarfyAnim); // Unload image (contains all frames) + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - gif player"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new GifPlayer(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Textures/ImageChannel.cs b/Examples/Textures/ImageChannel.cs new file mode 100644 index 0000000..5741994 --- /dev/null +++ b/Examples/Textures/ImageChannel.cs @@ -0,0 +1,149 @@ +/******************************************************************************************* +* +* raylib [textures] example - image channel +* +* NOTE: Images are loaded in CPU memory (RAM); textures are loaded in GPU memory (VRAM) +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.5 +* +* Example contributed by Bruno Cabral (@brccabral) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2024-2025 Bruno Cabral (@brccabral) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Textures; + +public partial class ImageChannel : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / Image Channel"; + + public string Title => "raylib [textures] example - image channel"; + + private Texture2D fudesumiTexture; + private Texture2D textureAlpha; + private Texture2D textureRed; + private Texture2D textureGreen; + private Texture2D textureBlue; + private Texture2D backgroundTexture; + + private Rectangle fudesumiRec; + private Rectangle fudesumiPos; + private Rectangle redPos; + private Rectangle greenPos; + private Rectangle bluePos; + private Rectangle alphaPos; + + public void Init() + { + var fudesumiImage = LoadImage("resources/fudesumi.png"); + + var imageAlpha = ImageFromChannel(fudesumiImage, 3); + ImageAlphaMask(ref imageAlpha, imageAlpha); + + var imageRed = ImageFromChannel(fudesumiImage, 0); + ImageAlphaMask(ref imageRed, imageAlpha); + + var imageGreen = ImageFromChannel(fudesumiImage, 1); + ImageAlphaMask(ref imageGreen, imageAlpha); + + var imageBlue = ImageFromChannel(fudesumiImage, 2); + ImageAlphaMask(ref imageBlue, imageAlpha); + + var backgroundImage = GenImageChecked(screenWidth, screenHeight, screenWidth / 20, screenHeight / 20, Color.Orange, Color.Yellow); + + fudesumiTexture = LoadTextureFromImage(fudesumiImage); + textureAlpha = LoadTextureFromImage(imageAlpha); + textureRed = LoadTextureFromImage(imageRed); + textureGreen = LoadTextureFromImage(imageGreen); + textureBlue = LoadTextureFromImage(imageBlue); + backgroundTexture = LoadTextureFromImage(backgroundImage); + + fudesumiRec = new Rectangle(0, 0, fudesumiImage.Width, fudesumiImage.Height); + + fudesumiPos = new Rectangle(50, 10, fudesumiImage.Width * 0.8f, fudesumiImage.Height * 0.8f); + redPos = new Rectangle(410, 10, fudesumiPos.Width / 2.0f, fudesumiPos.Height / 2.0f); + greenPos = new Rectangle(600, 10, fudesumiPos.Width / 2.0f, fudesumiPos.Height / 2.0f); + bluePos = new Rectangle(410, 230, fudesumiPos.Width / 2.0f, fudesumiPos.Height / 2.0f); + alphaPos = new Rectangle(600, 230, fudesumiPos.Width / 2.0f, fudesumiPos.Height / 2.0f); + + UnloadImage(fudesumiImage); + UnloadImage(imageAlpha); + UnloadImage(imageRed); + UnloadImage(imageGreen); + UnloadImage(imageBlue); + UnloadImage(backgroundImage); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Nothing to update... + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + DrawTexture(backgroundTexture, 0, 0, Color.White); + DrawTexturePro(fudesumiTexture, fudesumiRec, fudesumiPos, new Vector2(0, 0), 0, Color.White); + + DrawTexturePro(textureRed, fudesumiRec, redPos, new Vector2(0, 0), 0, Color.Red); + DrawTexturePro(textureGreen, fudesumiRec, greenPos, new Vector2(0, 0), 0, Color.Green); + DrawTexturePro(textureBlue, fudesumiRec, bluePos, new Vector2(0, 0), 0, Color.Blue); + DrawTexturePro(textureAlpha, fudesumiRec, alphaPos, new Vector2(0, 0), 0, Color.White); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(backgroundTexture); + UnloadTexture(fudesumiTexture); + UnloadTexture(textureRed); + UnloadTexture(textureGreen); + UnloadTexture(textureBlue); + UnloadTexture(textureAlpha); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - image channel"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new ImageChannel(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Textures/ImageKernel.cs b/Examples/Textures/ImageKernel.cs new file mode 100644 index 0000000..62cd563 --- /dev/null +++ b/Examples/Textures/ImageKernel.cs @@ -0,0 +1,165 @@ +/******************************************************************************************* +* +* raylib [textures] example - image kernel +* +* Example complexity rating: [★★★★] 4/4 +* +* NOTE: Images are loaded in CPU memory (RAM); textures are loaded in GPU memory (VRAM) +* +* Example contributed by Karim Salem (@kimo-s) and reviewed by Ramon Santamaria (@raysan5) +* +* Example originally created with raylib 1.3, last time updated with raylib 1.3 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2015-2025 Karim Salem (@kimo-s) +* +********************************************************************************************/ + +using static Raylib_cs.Raylib; + +namespace Examples.Textures; + +public partial class ImageKernel : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / Image Kernel"; + + public string Title => "raylib [textures] example - image kernel"; + + private Texture2D texture; + private Texture2D catSharpendTexture; + private Texture2D catSobelTexture; + private Texture2D catGaussianTexture; + + private static void NormalizeKernel(float[] kernel, int size) + { + var sum = 0.0f; + for (var i = 0; i < size; i++) + { + sum += kernel[i]; + } + + if (sum != 0.0f) + { + for (var i = 0; i < size; i++) + { + kernel[i] /= sum; + } + } + } + + public void Init() + { + var image = LoadImage("resources/cat.png"); // Loaded in CPU memory (RAM) + + float[] gaussiankernel = { + 1.0f, 2.0f, 1.0f, + 2.0f, 4.0f, 2.0f, + 1.0f, 2.0f, 1.0f + }; + + float[] sobelkernel = { + 1.0f, 0.0f, -1.0f, + 2.0f, 0.0f, -2.0f, + 1.0f, 0.0f, -1.0f + }; + + float[] sharpenkernel = { + 0.0f, -1.0f, 0.0f, + -1.0f, 5.0f, -1.0f, + 0.0f, -1.0f, 0.0f + }; + + NormalizeKernel(gaussiankernel, 9); + NormalizeKernel(sharpenkernel, 9); + NormalizeKernel(sobelkernel, 9); + + var catSharpend = ImageCopy(image); + ImageKernelConvolution(ref catSharpend, sharpenkernel); + + var catSobel = ImageCopy(image); + ImageKernelConvolution(ref catSobel, sobelkernel); + + var catGaussian = ImageCopy(image); + + for (var i = 0; i < 6; i++) + { + ImageKernelConvolution(ref catGaussian, gaussiankernel); + } + + ImageCrop(ref image, new Rectangle(0, 0, 200, 450)); + ImageCrop(ref catGaussian, new Rectangle(0, 0, 200, 450)); + ImageCrop(ref catSobel, new Rectangle(0, 0, 200, 450)); + ImageCrop(ref catSharpend, new Rectangle(0, 0, 200, 450)); + + // Images converted to texture, GPU memory (VRAM) + texture = LoadTextureFromImage(image); + catSharpendTexture = LoadTextureFromImage(catSharpend); + catSobelTexture = LoadTextureFromImage(catSobel); + catGaussianTexture = LoadTextureFromImage(catGaussian); + + // Once images have been converted to texture and uploaded to VRAM, + // they can be unloaded from RAM + UnloadImage(image); + UnloadImage(catGaussian); + UnloadImage(catSobel); + UnloadImage(catSharpend); + } + + public void Update() + { + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + DrawTexture(catSharpendTexture, 0, 0, Color.White); + DrawTexture(catSobelTexture, 200, 0, Color.White); + DrawTexture(catGaussianTexture, 400, 0, Color.White); + DrawTexture(texture, 600, 0, Color.White); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texture); + UnloadTexture(catGaussianTexture); + UnloadTexture(catSobelTexture); + UnloadTexture(catSharpendTexture); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - image kernel"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new ImageKernel(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Textures/ImageRotate.cs b/Examples/Textures/ImageRotate.cs new file mode 100644 index 0000000..86c250a --- /dev/null +++ b/Examples/Textures/ImageRotate.cs @@ -0,0 +1,121 @@ +/******************************************************************************************* +* +* raylib [textures] example - image rotate +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 1.0, last time updated with raylib 1.0 +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2014-2025 Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + +using static Raylib_cs.Raylib; + +namespace Examples.Textures; + +public partial class ImageRotate : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int NumTextures = 3; + + public string Name => "Textures / Image Rotate"; + + public string Title => "raylib [textures] example - image rotate"; + + private Texture2D[] textures; + private int currentTexture; + + public void Init() + { + // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) + var image45 = LoadImage("resources/raylib_logo.png"); + var image90 = LoadImage("resources/raylib_logo.png"); + var imageNeg90 = LoadImage("resources/raylib_logo.png"); + + ImageRotate(ref image45, 45); + ImageRotate(ref image90, 90); + ImageRotate(ref imageNeg90, -90); + + textures = new Texture2D[NumTextures]; + + textures[0] = LoadTextureFromImage(image45); + textures[1] = LoadTextureFromImage(image90); + textures[2] = LoadTextureFromImage(imageNeg90); + + UnloadImage(image45); + UnloadImage(image90); + UnloadImage(imageNeg90); + + currentTexture = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsMouseButtonPressed(MouseButton.Left) || IsKeyPressed(KeyboardKey.Right)) + { + currentTexture = (currentTexture + 1) % NumTextures; // Cycle between the textures + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + DrawTexture( + textures[currentTexture], + screenWidth / 2 - textures[currentTexture].Width / 2, + screenHeight / 2 - textures[currentTexture].Height / 2, + Color.White); + + DrawText("Press LEFT MOUSE BUTTON to rotate the image clockwise", 250, 420, 10, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + for (var i = 0; i < NumTextures; i++) + { + UnloadTexture(textures[i]); + } + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - image rotate"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new ImageRotate(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Textures/MagnifyingGlass.cs b/Examples/Textures/MagnifyingGlass.cs new file mode 100644 index 0000000..1c77d50 --- /dev/null +++ b/Examples/Textures/MagnifyingGlass.cs @@ -0,0 +1,160 @@ +/******************************************************************************************* +* +* raylib textures example - magnifying glass +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 5.6, last time updated with raylib 5.6 +* +* Example contributed by Luke Vaughan (@badram) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2026 Luke Vaughan (@badram) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Textures; + +public partial class MagnifyingGlass : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / Magnifying Glass"; + + public string Title => "raylib [textures] example - magnifying glass"; + + private Texture2D bunny; + private Texture2D parrots; + private Texture2D mask; + private RenderTexture2D magnifiedWorld; + private Camera2D camera; + + public void Init() + { + bunny = LoadTexture("resources/raybunny.png"); + parrots = LoadTexture("resources/parrots.png"); + + // Use image draw to generate a mask texture instead of loading it from a file. + var circle = GenImageColor(256, 256, Color.Blank); + ImageDrawCircle(ref circle, 128, 128, 128, Color.White); + mask = LoadTextureFromImage(circle); // Copy the mask image from RAM to VRAM + UnloadImage(circle); // Unload the image from RAM + + magnifiedWorld = LoadRenderTexture(256, 256); + + camera = new Camera2D(); + // Set magnifying glass zoom + camera.Zoom = 2; + // Offset by half the size of the magnifying glass to counteract drawing the texture centered on the mouse position + camera.Offset = new Vector2(128, 128); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + var mPos = GetMousePosition(); + camera.Target = mPos; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + // Draw the normal version of the world + DrawTexture(parrots, 144, 33, Color.White); + DrawText("Use the magnifying glass to find hidden bunnies!", 154, 6, 20, Color.Black); + + // Render to a the magnifying glass + BeginTextureMode(magnifiedWorld); + ClearBackground(Color.RayWhite); + + BeginMode2D(camera); + // Draw the same things in the magnified world as were in the normal version + DrawTexture(parrots, 144, 33, Color.White); + DrawText("Use the magnifying glass to find hidden bunnies!", 154, 6, 20, Color.Black); + + // Draw bunnies only in the magnified world. + // BLEND_MULTIPLIED lets them take on the color of the image below them. + BeginBlendMode(BlendMode.Multiplied); + DrawTexture(bunny, 250, 350, Color.White); + DrawTexture(bunny, 500, 100, Color.White); + DrawTexture(bunny, 420, 300, Color.White); + DrawTexture(bunny, 650, 10, Color.White); + EndBlendMode(); + EndMode2D(); + + // Mask the magnifying glass view texture to a circle + // To make the mask affect only alpha, a CUSTOM blend mode is used with SEPARATE color/alpha functions + BeginBlendMode(BlendMode.CustomSeparate); + // C: Color, A: Alpha, s: source (texture to draw), d: destination (texture drawn to) + // glSrcRGB: RL_ZERO - Cs * 0 = 0 - discard source rgb because we don't want to draw our texture's colors at all + // glDstRGB: RL_ONE - Cd * 1 = Cd - use destination colors unmodified + // glSrcAlpha: RL_ONE - As * 1 = As - use source alpha unmodified + // glDstAlpha: RL_ZERO - Ad * 0 = 0 - discard destination alpha + // glEqRGB: RL_FUNC_ADD - Cs(0) + Cd = Cd - destination color is unmodified + // glEqAlpha: RL_FUNC_ADD - As + Ad(0) = As - destination alpha is set to source alpha + Rlgl.SetBlendFactorsSeparate(Rlgl.ZERO, Rlgl.ONE, Rlgl.ONE, Rlgl.ZERO, Rlgl.FUNC_ADD, Rlgl.FUNC_ADD); + DrawTexture(mask, 0, 0, Color.White); + EndBlendMode(); + EndTextureMode(); + + // Draw magnifiedWorld to screen, centered on cursor + DrawTextureRec(magnifiedWorld.Texture, new Rectangle(0, 0, 256, -256), new Vector2(mPos.X - 128, mPos.Y - 128), Color.White); + + // Draw the outer ring of the magnifying glass + DrawRing(mPos, 126, 130, 0, 360, 64, Color.Black); + + // Draw floating specular highlight on the glass + var rx = mPos.X / 800; + var ry = mPos.Y / 800; + DrawCircle((int)(mPos.X - 64 * rx) - 32, (int)(mPos.Y - 64 * ry) - 32, 4, ColorAlpha(Color.White, 0.5f)); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(parrots); + UnloadTexture(bunny); + UnloadTexture(mask); + UnloadRenderTexture(magnifiedWorld); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - magnifying glass"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new MagnifyingGlass(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Textures/ScreenBuffer.cs b/Examples/Textures/ScreenBuffer.cs new file mode 100644 index 0000000..2501434 --- /dev/null +++ b/Examples/Textures/ScreenBuffer.cs @@ -0,0 +1,181 @@ +/******************************************************************************************* +* +* raylib [textures] example - screen buffer +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.5, last time updated with raylib 5.5 +* +* Example contributed by Agnis Aldiņš (@nezvers) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 Agnis Aldiņš (@nezvers) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Textures; + +public partial class ScreenBuffer : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MaxColors = 256; + private const int ScaleFactor = 2; + + private const int imageWidth = screenWidth / ScaleFactor; + private const int imageHeight = screenHeight / ScaleFactor; + private const int flameWidth = screenWidth / ScaleFactor; + + public string Name => "Textures / Screen Buffer"; + + public string Title => "raylib [textures] example - screen buffer"; + + private Color[] palette; + private byte[] indexBuffer; + private byte[] flameRootBuffer; + private Image screenImage; + private Texture2D screenTexture; + + public void Init() + { + palette = new Color[MaxColors]; + indexBuffer = new byte[imageWidth * imageWidth]; + flameRootBuffer = new byte[flameWidth]; + + screenImage = GenImageColor(imageWidth, imageHeight, Color.Black); + screenTexture = LoadTextureFromImage(screenImage); + + // Generate flame color palette + for (var i = 0; i < MaxColors; i++) + { + var t = (float)i / (float)(MaxColors - 1); + var hue = t * t; + var saturation = t; + var value = t; + + palette[i] = ColorFromHSV(250.0f + 150.0f * hue, saturation, value); + } + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Grow flameRoot + for (var x = 2; x < flameWidth; x++) + { + var flame = (int)flameRootBuffer[x]; + flame += GetRandomValue(0, 2); + flameRootBuffer[x] = (flame > 255) ? (byte)255 : (byte)flame; + } + + // Transfer flameRoot to indexBuffer + for (var x = 0; x < flameWidth; x++) + { + var i = x + (imageHeight - 1) * imageWidth; + indexBuffer[i] = flameRootBuffer[x]; + } + + // Clear top row, because it can't move any higher + for (var x = 0; x < imageWidth; x++) + { + if (indexBuffer[x] != 0) + { + indexBuffer[x] = 0; + } + } + + // Skip top row, it is already cleared + for (var y = 1; y < imageHeight; y++) + { + for (var x = 0; x < imageWidth; x++) + { + var i = x + y * imageWidth; + int colorIndex = indexBuffer[i]; + + if (colorIndex != 0) + { + // Move pixel a row above + indexBuffer[i] = 0; + var moveX = GetRandomValue(0, 2) - 1; + var newX = x + moveX; + + if ((newX > 0) && (newX < imageWidth)) + { + var iabove = i - imageWidth + moveX; + var decay = GetRandomValue(0, 3); + colorIndex -= (decay < colorIndex) ? decay : colorIndex; + indexBuffer[iabove] = (byte)colorIndex; + } + } + } + } + + // Update screenImage with palette colors + for (var y = 1; y < imageHeight; y++) + { + for (var x = 0; x < imageWidth; x++) + { + var i = x + y * imageWidth; + int colorIndex = indexBuffer[i]; + var col = palette[colorIndex]; + + ImageDrawPixel(ref screenImage, x, y, col); + } + } + + UpdateTexture(screenTexture, screenImage.Data); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + DrawTextureEx(screenTexture, new Vector2(0, 0), 0.0f, 2.0f, Color.White); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(screenTexture); + UnloadImage(screenImage); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - screen buffer"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new ScreenBuffer(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Textures/SpriteStacking.cs b/Examples/Textures/SpriteStacking.cs new file mode 100644 index 0000000..41e3de1 --- /dev/null +++ b/Examples/Textures/SpriteStacking.cs @@ -0,0 +1,140 @@ +/******************************************************************************************* +* +* raylib [textures] example - sprite stacking +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 6.0, last time updated with raylib 6.0 +* +* Example contributed by Robin (@RobinsAviary) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Redbooth model (c) 2017-2025 @kluchek under https://creativecommons.org/licenses/by/4.0/ https://github.com/kluchek/vox-models/ +* Copyright (c) 2025 Robin (@RobinsAviary) +* +********************************************************************************************/ + +using System.Numerics; +using static Raylib_cs.Raylib; + +namespace Examples.Textures; + +public partial class SpriteStacking : IExample +{ + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const float speedChange = 0.25f; // Amount speed will change by when the user presses A/D + + public string Name => "Textures / Sprite Stacking"; + + public string Title => "raylib [textures] example - sprite stacking"; + + private Texture2D booth; + private float stackScale; // Overall scale of the stacked sprite + private float stackSpacing; // Vertical spacing between each layer + private uint stackCount; // Number of layers, used for calculating the size of a single slice + private float rotationSpeed; // Stacked sprites rotation speed + private float rotation; // Current rotation of the stacked sprite + + public void Init() + { + booth = LoadTexture("resources/booth.png"); + + stackScale = 3.0f; + stackSpacing = 2.0f; + stackCount = 122; + rotationSpeed = 30.0f; + rotation = 0.0f; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Use mouse wheel to affect stack separation + stackSpacing += GetMouseWheelMove() * 0.1f; + stackSpacing = Math.Clamp(stackSpacing, 0.0f, 5.0f); + + // Add a positive/negative offset to spin right/left at different speeds + if (IsKeyDown(KeyboardKey.Left) || IsKeyDown(KeyboardKey.A)) + { + rotationSpeed -= speedChange; + } + if (IsKeyDown(KeyboardKey.Right) || IsKeyDown(KeyboardKey.D)) + { + rotationSpeed += speedChange; + } + + rotation += rotationSpeed * GetFrameTime(); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + // Get the size of a single slice + var frameWidth = (float)booth.Width; + var frameHeight = (float)booth.Height / (float)stackCount; + + // Get the scaled resolution to draw at + var scaledWidth = frameWidth * stackScale; + var scaledHeight = frameHeight * stackScale; + + // Draw the stacked sprite, rotated to the correct angle, with an vertical offset applied based on its y location + for (var i = (int)stackCount - 1; i >= 0; i--) + { + // Center vertically + Rectangle source = new(0.0f, (float)i * frameHeight, frameWidth, frameHeight); + Rectangle dest = new(screenWidth / 2.0f, (screenHeight / 2.0f) + (i * stackSpacing) - (stackSpacing * stackCount / 2.0f), scaledWidth, scaledHeight); + Vector2 origin = new(scaledWidth / 2.0f, scaledHeight / 2.0f); + + DrawTexturePro(booth, source, dest, origin, rotation, Color.White); + } + + DrawText("A/D to spin\nmouse wheel to change separation (aka 'angle')", 10, 10, 20, Color.DarkGray); + DrawText($"current spacing: {stackSpacing:F1}", 10, 50, 20, Color.DarkGray); + DrawText($"current speed: {rotationSpeed:F2}", 10, 70, 20, Color.DarkGray); + DrawText("redbooth model (c) kluchek under cc 4.0", 10, 420, 20, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(booth); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - sprite stacking"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new SpriteStacking(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Web/Host.cs b/Examples/Web/Host.cs index c5ff082..6ddcd6d 100644 --- a/Examples/Web/Host.cs +++ b/Examples/Web/Host.cs @@ -8,13 +8,18 @@ namespace Examples.Web; /// Browser host: owns the single raylib window and the "current" example. The frame loop is /// driven from JavaScript (main.js, via requestAnimationFrame) since a blocking C# loop would /// freeze the page. Methods marked [JSExport] are called from main.js. +/// +/// +/// Every switch applies and resets the cursor to visible. +/// Cursor hiding, , and per-example window size are not +/// honored in the browser; the canvas stays 800x450 and the pointer stays visible. +/// /// public partial class Host { public const int screenWidth = 800; public const int screenHeight = 450; - // Examples discovered from the assembly, minus desktop-only ones (see ExampleRegistry). private static readonly List _examples = new(ExampleRegistry.BrowserExamples); private static IExample _current; @@ -24,48 +29,76 @@ public partial class Host InitWindow(screenWidth, screenHeight, "raylib-cs web examples"); SetTargetFPS(60); - SetExample(_examples[0].Name); + // main.js selects the first example, so its init failures surface through SetExample. } - /// Render one frame of the current example (called every requestAnimationFrame tick). + /// Render one frame of the current example; false if it threw and was torn down. [JSExport] - public static void UpdateFrame() + public static bool UpdateFrame() { + if (_current == null) + { + return true; + } + try { - _current?.Update(); + _current.Update(); + return true; } catch (Exception ex) { // Don't let one misbehaving example kill the whole page; log and stop driving it. - Console.WriteLine($"[Examples.Web] '{_current?.Name}' threw during Update: {ex}"); + Console.WriteLine($"[Examples.Web] '{_current.Name}' threw during Update: {ex}"); + TryUnload(_current); _current = null; + return false; } } - /// Switch the active example by name (called from the nav dropdown). + /// Switch the active example by name; false on unknown name or failed Init. [JSExport] - public static void SetExample(string name) + public static bool SetExample(string name) { var next = _examples.Find(e => e.Name == name); if (next == null) { - return; + return false; } + if (_current != null) + { + TryUnload(_current); + _current = null; + } + + // Reset the baseline; an example may hide/lock the cursor from its own Update(), and + // CursorDisabled/CursorHidden are deliberately never applied in the browser. + EnableCursor(); + ShowCursor(); + SetTargetFPS(next.TargetFps); + try { - _current?.Unload(); + next.Init(); _current = next; - _current.Init(); + return true; } catch (Exception ex) { Console.WriteLine($"[Examples.Web] '{next.Name}' failed to initialize: {ex}"); - _current = null; + TryUnload(next); + return false; } } + /// Target FPS of the current example; main.js paces the frame loop with it. + [JSExport] + public static int GetCurrentTargetFps() + { + return _current?.TargetFps ?? 60; + } + /// Newline-separated example names, used to populate the nav dropdown. [JSExport] public static string GetExampleNames() @@ -83,4 +116,17 @@ public partial class Host SetMouseOffset(0, 0); SetMouseScale(scaleX, scaleY); } + + /// Best-effort Unload that never lets a failure cascade. + private static void TryUnload(IExample example) + { + try + { + example.Unload(); + } + catch (Exception ex) + { + Console.WriteLine($"[Examples.Web] '{example.Name}' threw during Unload: {ex}"); + } + } } diff --git a/Examples/Web/README.md b/Examples/Web/README.md index b11d7eb..b6d38df 100644 --- a/Examples/Web/README.md +++ b/Examples/Web/README.md @@ -1,15 +1,11 @@ # Examples/Web — raylib-cs in the browser (WebAssembly) -A small browser app that runs raylib examples in the browser via WebAssembly, with a dropdown to -switch between them. It exists to **prove the `Raylib-cs` NuGet package's `browser-wasm` support -works end-to-end**: the package's `buildTransitive/Raylib-cs.targets` automatically links the -shipped `runtimes/browser-wasm/native/raylib.a` (and adds `-sUSE_GLFW=3`) into the .NET wasm -runtime. The browser host lives in `Examples/Web`, while `Examples.csproj` remains the single -examples project. +Runs the raylib examples in the browser via WebAssembly, with a dropdown to switch between them. +It proves the `Raylib-cs` NuGet package's `browser-wasm` support end-to-end: the package's +`buildTransitive` targets link the shipped `raylib.a` into the .NET wasm runtime. -The browser-wasm configuration is enabled only when publishing `Examples` with -`RuntimeIdentifier=browser-wasm`, so normal solution builds do not require the `wasm-tools` -workload. +The browser-wasm configuration only activates when publishing with +`RuntimeIdentifier=browser-wasm`; normal solution builds don't need the `wasm-tools` workload. ## Prerequisites @@ -27,23 +23,11 @@ dotnet publish Examples -f net10.0 -r browser-wasm -c Release ### Toolchain caveat -If the link step fails with: - -``` -wasm-opt: Unknown option '--enable-bulk-memory-opt' -``` - -your installed `wasm-tools` workload (Binaryen) is out of sync with the SDK — emscripten passes a -feature flag the bundled `wasm-opt` doesn't understand (commonly caused by a stale workload band -or a conflicting system EMSDK on PATH/`$EMSDK`). Proper fix: - -```bash -dotnet workload update -``` - -`Examples.csproj` defaults browser-wasm publishes to unoptimized native linking so the standard -publish command works on affected local toolchains. A fully optimized publish can override those -MSBuild properties after updating the workload/toolchain. +If the link step fails with `wasm-opt: Unknown option '--enable-bulk-memory-opt'`, the installed +`wasm-tools` workload is out of sync with the SDK (stale workload band or a system EMSDK on +PATH); fix with `dotnet workload update`. As a safety net, `Examples.csproj` defaults +browser-wasm publishes to unoptimized native linking; an optimized publish can override those +properties. ## Run @@ -58,51 +42,40 @@ Open the printed URL and use the **Example** dropdown to switch examples. ## Canvas scaling modes -The browser host keeps raylib's internal render buffer fixed to `800x450` and applies display -scaling in CSS. Use the **Scale** dropdown (or `?scale=` query param) to choose behavior: +The render buffer stays fixed at `800x450`; display scaling is CSS-only. Pick with the **Scale** +dropdown (or `?scale=` query param): -- `native` (default): exact `800x450` CSS pixels (1x), no resizing. -- `integer`: largest whole-number multiple that fits the viewport, centered with letterboxing. - Best for pixel-perfect presentation. -- `fit`: fills available viewport while preserving aspect ratio (can be fractional, less crisp on - some DPI/zoom combinations). +- `native` (default): exact `800x450` CSS pixels. +- `integer`: largest whole-number multiple that fits, centered with letterboxing (pixel-perfect). +- `fit`: fills the viewport preserving aspect ratio (can be fractional, less crisp). -Scaling is computed in **device pixels first** and then converted back to CSS pixels using current -`devicePixelRatio`, which decouples the modes from needing a specific browser zoom level on -125%/150% OS scale displays. - -`main.js` also shows runtime diagnostics in the status bar (`DPR`, CSS size, backing size, scale) -to help debug OS scaling / browser zoom behavior across monitors. - -### Manual validation matrix (expected behavior) - -- OS scale `100%`, browser zoom `100%`: `integer` should appear crisp and stable (`Scale 1.00` or - higher if viewport allows). -- OS scale `125%` and `150%`, browser zoom `100%`: `integer` should stay crisp (no subpixel CSS - scaling); perceived physical size changes with OS scale are expected. -- Browser zoom `80%`, `100%`, `125%`: `integer` should continue using whole-number scaling; - `fit` may show softening at non-integer effective scales. -- `native` mode should always report `CSS 800x450` and `Scale 1.00`. -- During window resize, there should be no frame-by-frame jitter because scale updates run on - resize/mode changes, not per animation frame. +Scaling is computed in device pixels and converted back to CSS via `devicePixelRatio`, so the +modes behave consistently across OS scale and browser zoom. The status bar shows the live numbers +(`DPR`, CSS size, backing size, scale): `integer` should stay crisp at any OS scale or zoom, and +`native` should always report `Scale 1.00`. ## How it works A browser can't run raylib's blocking `while (!WindowShouldClose())` loop (it would freeze the page), so frames are driven from JavaScript: -- `Host.Main()` calls `InitWindow` once and selects the default example (`main.js` runs it via - `runMain()`). -- `main.js` binds the page `` to the runtime, then calls `Host.UpdateFrame()` every - `requestAnimationFrame` tick. `UpdateFrame`, `SetExample`, and `GetExampleNames` are `[JSExport]`. -- Each base example implements `IExample` (`Init` / `Update` / `Unload`) in `#if BROWSER` partials. - The host owns the window; - examples never call `InitWindow`/`CloseWindow`. +- `Host.Main()` calls `InitWindow` once; `main.js` then populates the dropdown and selects the + first example via `Host.SetExample`, so init failures surface in the on-page error banner. +- `main.js` binds the page `` to the runtime and calls `Host.UpdateFrame()` from a + `requestAnimationFrame` loop paced to the current example's `TargetFps` (raylib's own limiter + busy-waits and would peg the main thread). The `Host` methods it calls are `[JSExport]`. +- On switch, `Host.SetExample` unloads the previous example, resets the cursor to visible, and + applies the next `TargetFps`. Cursor hiding and `ConfigFlags` are not honored in the browser. + If `Init` or `Update` throws, the example is unloaded and an error banner is shown. +- Each example is a single `.cs` file implementing `IExample` (`Init` / `Update` / `Unload`); the + host owns the window, so examples never call `InitWindow`/`CloseWindow`. Platform differences + (e.g. GLSL 100 vs 330) are handled inline with `#if BROWSER` guards, not separate files. ## Adding more examples -For a new desktop example (`../Core/...`, `../Shaders/...`, etc.), add a browser partial -(`*.Browser.cs`) that implements `IExample` and mirrors the split of its monolithic `Main`: +Examples are **auto-discovered by reflection** (`ExampleRegistry.DiscoverAll`) — no list to edit. +Drop a new `.cs` file in the matching category folder implementing `IExample`, splitting the +original monolithic `Main` as: ``` Main() { ; while(!WindowShouldClose()){ } } @@ -111,8 +84,8 @@ Main() { ; while(!WindowShouldClose()){ } } Unload(){ } ``` -Then add `new YourExample()` to the `Examples` list in `Host.cs`. +Keep the standalone `static Main()` as a thin driver so the example still runs on its own. -Examples that load files (`resources/...`) also need those assets in the wasm virtual filesystem. -`Examples.csproj` bundles `../resources/` into the browser app when publishing with -`RuntimeIdentifier=browser-wasm`. +If an example can't run on single-threaded wasm, add its type to `DesktopExcludedFromBrowser` in +`ExampleRegistry.cs`; `BrowserOnly` is the inverse list. Assets under `resources/` are bundled +into the wasm virtual filesystem automatically. diff --git a/Examples/Web/index.html b/Examples/Web/index.html index 18b86fc..3609b22 100644 --- a/Examples/Web/index.html +++ b/Examples/Web/index.html @@ -46,6 +46,14 @@ background: #000; } #status { color: #888; font-size: .85rem; margin: 0; } + #error { + color: #ff6b6b; + background: #3a1d1d; + border: 1px solid #6b2b2b; + border-radius: 4px; + padding: .4rem .6rem; + font-size: .9rem; + } @@ -60,6 +68,7 @@ +
diff --git a/Examples/Web/main.js b/Examples/Web/main.js index 99bf3a1..fe5d157 100644 --- a/Examples/Web/main.js +++ b/Examples/Web/main.js @@ -7,6 +7,7 @@ import { } from './scaleUtils.js' const status = document.getElementById('status'); +const errorBanner = document.getElementById('error'); const select = document.getElementById('examples'); const canvas = document.getElementById('canvas'); const viewport = document.getElementById('viewport'); @@ -74,7 +75,6 @@ function applyDisplayScale() { canvas.style.width = `${display.cssWidth}px`; canvas.style.height = `${display.cssHeight}px`; - // Center canvas inside viewport, leaving letterboxing around it. viewport.style.justifyContent = 'center'; viewport.style.alignItems = 'center'; @@ -133,11 +133,17 @@ Host = exports.Examples.Web.Host; // raylib's GLFW/emscripten backend renders into this canvas. dotnet.instance.Module['canvas'] = canvas; -// Runs Host.Main() -> InitWindow + default example Init(). Must come after the canvas is bound. +// Runs Host.Main() -> InitWindow. Must come after the canvas is bound. await runMain(); applyDisplayScale(); -// Populate the navigation dropdown from the registered examples. +let targetFps = 60; +function selectExample(name) { + const ok = Host.SetExample(name); + targetFps = Host.GetCurrentTargetFps(); + errorBanner.hidden = ok; +} + select.innerHTML = ''; for (const name of Host.GetExampleNames().split('\n').filter(n => n.length > 0)) { const opt = document.createElement('option'); @@ -145,13 +151,26 @@ for (const name of Host.GetExampleNames().split('\n').filter(n => n.length > 0)) opt.textContent = name; select.appendChild(opt); } -select.addEventListener('change', () => Host.SetExample(select.value)); +select.addEventListener('change', () => selectExample(select.value)); +selectExample(select.value); setScaleMode(scaleMode, false); -// Drive raylib one frame per animation tick (never block the browser). -function mainLoop() { - Host.UpdateFrame(); +// Drive raylib one frame per animation tick, paced to the example's target FPS here rather +// than by raylib's limiter (which busy-waits in EndDrawing and would peg the main thread). +let nextFrameTime = 0; +function mainLoop(timestamp) { requestAnimationFrame(mainLoop); + + if (timestamp < nextFrameTime) { + return; + } + + // Schedule one interval ahead; resync to now if we've fallen behind (hidden tab). + nextFrameTime = Math.max(nextFrameTime + 1000 / targetFps, timestamp); + + if (!Host.UpdateFrame()) { + errorBanner.hidden = false; + } } requestAnimationFrame(mainLoop); diff --git a/Examples/resources/booth.png b/Examples/resources/booth.png new file mode 100644 index 0000000..b4147f7 Binary files /dev/null and b/Examples/resources/booth.png differ diff --git a/Examples/resources/raybunny.png b/Examples/resources/raybunny.png new file mode 100644 index 0000000..b608147 Binary files /dev/null and b/Examples/resources/raybunny.png differ diff --git a/Examples/resources/scarfy_run.gif b/Examples/resources/scarfy_run.gif new file mode 100644 index 0000000..f0f712c Binary files /dev/null and b/Examples/resources/scarfy_run.gif differ diff --git a/Examples/resources/shaders/glsl100/eratosthenes.fs b/Examples/resources/shaders/glsl100/eratosthenes.fs index 739d7b9..27e5e16 100644 --- a/Examples/resources/shaders/glsl100/eratosthenes.fs +++ b/Examples/resources/shaders/glsl100/eratosthenes.fs @@ -1,6 +1,11 @@ #version 100 +// value reaches scale*scale = 1,000,000, beyond mediump's guaranteed precision +#ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +#else precision mediump float; +#endif /************************************************************************************* @@ -9,13 +14,16 @@ precision mediump float; The screen is divided into a square grid of boxes, each representing an integer value Each integer is tested to see if it is a prime number. Primes are colored white - Non-primes are colored with a color that indicates the smallest factor which evenly divdes our integer + Non-primes are colored with a color that indicates the smallest factor which evenly divides our integer You can change the scale variable to make a larger or smaller grid Total number of integers displayed = scale squared, so scale = 100 tests the first 10,000 integers WARNING: If you make scale too large, your GPU may bog down! + NOTE: GLSL ES 100 requires constant loop bounds, so the factor loop runs to the worst + case (scale) and breaks out at sqrt(value), matching the glsl330 version + ***************************************************************************************/ // Input vertex attributes (from vertex shader) @@ -38,23 +46,25 @@ vec4 Colorizer(float counter, float maxSize) void main() { vec4 color = vec4(1.0); - float scale = 1000.0; // Makes 100x100 square grid. Change this variable to make a smaller or larger grid + float scale = 1000.0; // Makes 100x100 square grid, change this variable to make a smaller or larger grid float value = scale*floor(fragTexCoord.y*scale) + floor(fragTexCoord.x*scale); // Group pixels into boxes representing integer values - int valuei = int(value); - //if ((valuei == 0) || (valuei == 1) || (valuei == 2)) gl_FragColor = vec4(1.0); - //else + if (value <= 2.0) gl_FragColor = vec4(1.0); + else { - //for (int i = 2; (i < int(max(2.0, sqrt(value) + 1.0))); i++) - // NOTE: On GLSL 100 for loops are restricted and loop condition must be a constant - // Tested on RPI, it seems loops are limited around 60 iteractions - for (int i = 2; i < 48; i++) + float maxFactor = max(2.0, sqrt(value) + 1.0); + + for (int i = 2; i < 1001; i++) // Constant bound = scale (worst-case sqrt(value)) { + if (float(i) >= maxFactor) break; + if ((value - float(i)*floor(value/float(i))) <= 0.0) { - gl_FragColor = Colorizer(float(i), scale); + color = Colorizer(float(i), scale); //break; // Uncomment to color by the largest factor instead } } + + gl_FragColor = color; } } diff --git a/Examples/resources/text_file.txt b/Examples/resources/text_file.txt new file mode 100644 index 0000000..72b1a8b --- /dev/null +++ b/Examples/resources/text_file.txt @@ -0,0 +1,18 @@ +Starting of the Lorem ipsum dolor sit amet file +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at interdum ex, et iaculis quam. Mauris consectetur, magna vel malesuada aliquam, mauris enim venenatis arcu, nec cursus orci ante a massa. Curabitur libero elit, cursus eu odio eget, suscipit rhoncus nibh. Nam justo elit, ullamcorper eget dolor et, ullamcorper tristique nulla. Nullam sagittis dolor in tristique tincidunt. Duis ac porttitor erat, a molestie sapien. Aliquam finibus in ipsum quis venenatis. +Mauris odio lorem, pharetra ut egestas quis, tristique eu mauris. Cras sed gravida velit. Suspendisse potenti. Suspendisse lobortis eleifend fermentum. Donec eu dolor est. Etiam ac felis eu ligula auctor feugiat eu quis diam. Sed eleifend id nibh porta viverra. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi at tristique dui, nec pretium est. Mauris mollis massa quis massa aliquet efficitur. Phasellus posuere, elit id tempus consequat, massa ante scelerisque odio, finibus gravida elit tellus eget lectus. +Donec rutrum sagittis ligula a auctor. Aliquam aliquet tincidunt pulvinar. Aenean a porta ex. Aenean at sagittis nulla. Morbi congue luctus est nec gravida. In hac habitasse platea dictumst. Praesent commodo efficitur congue. Duis interdum enim in pharetra dapibus. Proin vestibulum finibus mauris vitae mollis. +Sed ultricies sed enim vel interdum. Nullam nec sagittis est, quis lobortis nunc. Donec auctor elementum velit vel pulvinar. Sed quis efficitur felis, at mollis elit. Integer id elit ante. Ut gravida ante vitae erat scelerisque scelerisque vel in massa. Praesent varius massa eu purus feugiat, non venenatis urna rhoncus. Pellentesque vehicula, tellus eu venenatis efficitur, libero eros lobortis justo, at ultricies lacus diam non libero. Sed metus nulla, consectetur in justo vitae, mollis maximus orci. Vestibulum dapibus ultrices leo, et facilisis odio molestie a. Phasellus facilisis vitae lorem quis viverra. Etiam imperdiet urna dolor, quis interdum ligula tristique id. Aliquam id dapibus enim. +Curabitur congue elit in magna tristique, sit amet porta quam viverra. Integer nec placerat libero. Praesent sem dolor, tristique eu augue non, ultricies mollis nibh. Fusce finibus, lorem sollicitudin eleifend gravida, eros quam tempus leo, ut iaculis libero ex vitae libero. Sed placerat fringilla accumsan. Nulla laoreet cursus justo nec elementum. Proin facilisis lobortis velit, a sollicitudin leo mollis eu. Aenean et leo est. +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at interdum ex, et iaculis quam. Mauris consectetur, magna vel malesuada aliquam, mauris enim venenatis arcu, nec cursus orci ante a massa. Curabitur libero elit, cursus eu odio eget, suscipit rhoncus nibh. Nam justo elit, ullamcorper eget dolor et, ullamcorper tristique nulla. Nullam sagittis dolor in tristique tincidunt. Duis ac porttitor erat, a molestie sapien. Aliquam finibus in ipsum quis venenatis. +Mauris odio lorem, pharetra ut egestas quis, tristique eu mauris. Cras sed gravida velit. Suspendisse potenti. Suspendisse lobortis eleifend fermentum. Donec eu dolor est. Etiam ac felis eu ligula auctor feugiat eu quis diam. Sed eleifend id nibh porta viverra. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi at tristique dui, nec pretium est. Mauris mollis massa quis massa aliquet efficitur. Phasellus posuere, elit id tempus consequat, massa ante scelerisque odio, finibus gravida elit tellus eget lectus. +Donec rutrum sagittis ligula a auctor. Aliquam aliquet tincidunt pulvinar. Aenean a porta ex. Aenean at sagittis nulla. Morbi congue luctus est nec gravida. In hac habitasse platea dictumst. Praesent commodo efficitur congue. Duis interdum enim in pharetra dapibus. Proin vestibulum finibus mauris vitae mollis. +Sed ultricies sed enim vel interdum. Nullam nec sagittis est, quis lobortis nunc. Donec auctor elementum velit vel pulvinar. Sed quis efficitur felis, at mollis elit. Integer id elit ante. Ut gravida ante vitae erat scelerisque scelerisque vel in massa. Praesent varius massa eu purus feugiat, non venenatis urna rhoncus. Pellentesque vehicula, tellus eu venenatis efficitur, libero eros lobortis justo, at ultricies lacus diam non libero. Sed metus nulla, consectetur in justo vitae, mollis maximus orci. Vestibulum dapibus ultrices leo, et facilisis odio molestie a. Phasellus facilisis vitae lorem quis viverra. Etiam imperdiet urna dolor, quis interdum ligula tristique id. Aliquam id dapibus enim. +wrapping text from the last available space wrapping text from the last available space wrapping text from the last available space +Curabitur congue elit in magna tristique, sit amet porta quam viverra. Integer nec placerat libero. Praesent sem dolor, tristique eu augue non, ultricies mollis nibh. Fusce finibus, lorem sollicitudin eleifend gravida, eros quam tempus leo, ut iaculis libero ex vitae libero. Sed placerat fringilla accumsan. Nulla laoreet cursus justo nec elementum. Proin facilisis lobortis velit, a sollicitudin leo mollis eu. Aenean et leo est. +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at interdum ex, et iaculis quam. Mauris consectetur, magna vel malesuada aliquam, mauris enim venenatis arcu, nec cursus orci ante a massa. Curabitur libero elit, cursus eu odio eget, suscipit rhoncus nibh. Nam justo elit, ullamcorper eget dolor et, ullamcorper tristique nulla. Nullam sagittis dolor in tristique tincidunt. Duis ac porttitor erat, a molestie sapien. Aliquam finibus in ipsum quis venenatis. +Mauris odio lorem, pharetra ut egestas quis, tristique eu mauris. Cras sed gravida velit. Suspendisse potenti. Suspendisse lobortis eleifend fermentum. Donec eu dolor est. Etiam ac felis eu ligula auctor feugiat eu quis diam. Sed eleifend id nibh porta viverra. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi at tristique dui, nec pretium est. Mauris mollis massa quis massa aliquet efficitur. Phasellus posuere, elit id tempus consequat, massa ante scelerisque odio, finibus gravida elit tellus eget lectus. +Donec rutrum sagittis ligula a auctor. Aliquam aliquet tincidunt pulvinar. Aenean a porta ex. Aenean at sagittis nulla. Morbi congue luctus est nec gravida. In hac habitasse platea dictumst. Praesent commodo efficitur congue. Duis interdum enim in pharetra dapibus. Proin vestibulum finibus mauris vitae mollis. +Sed ultricies sed enim vel interdum. Nullam nec sagittis est, quis lobortis nunc. Donec auctor elementum velit vel pulvinar. Sed quis efficitur felis, at mollis elit. Integer id elit ante. Ut gravida ante vitae erat scelerisque scelerisque vel in massa. Praesent varius massa eu purus feugiat, non venenatis urna rhoncus. Pellentesque vehicula, tellus eu venenatis efficitur, libero eros lobortis justo, at ultricies lacus diam non libero. Sed metus nulla, consectetur in justo vitae, mollis maximus orci. Vestibulum dapibus ultrices leo, et facilisis odio molestie a. Phasellus facilisis vitae lorem quis viverra. Etiam imperdiet urna dolor, quis interdum ligula tristique id. Aliquam id dapibus enim. +Curabitur congue elit in magna tristique, sit amet porta quam viverra. Integer nec placerat libero. Praesent sem dolor, tristique eu augue non, ultricies mollis nibh. Fusce finibus, lorem sollicitudin eleifend gravida, eros quam tempus leo, ut iaculis libero ex vitae libero. Sed placerat fringilla accumsan. Nulla laoreet cursus justo nec elementum. Proin facilisis lobortis velit, a sollicitudin leo mollis eu. Aenean et leo est. +Ending of the Lorem ipsum dolor sit amet file