From 732b7aa34617f9c120099b73f799562327f92432 Mon Sep 17 00:00:00 2001 From: tiger tiger tiger Date: Fri, 17 Jul 2026 08:22:54 +0200 Subject: [PATCH] feat: backporting existing raylib-cs examples and new official raylib examples to WASM, adopting raylib's original code style --- Examples/Audio/AmpEnvelope.cs | 263 ++++++ Examples/Audio/MixedProcessor.cs | 159 ++++ Examples/Audio/ModulePlaying.cs | 289 +++--- Examples/Audio/MusicStreamDemo.cs | 238 +++-- Examples/Audio/RawStream.cs | 182 ++++ Examples/Audio/SoundLoading.cs | 116 ++- Examples/Audio/SoundMulti.cs | 114 +++ Examples/Audio/SoundPositioning.cs | 154 ++++ Examples/Audio/StreamCallback.cs | 306 +++++++ Examples/Audio/StreamEffects.cs | 222 +++++ Examples/Core/AutomationEvents.cs | 379 ++++++++ Examples/Core/BasicScreenManager.cs | 279 +++--- Examples/Core/BasicWindow.cs | 59 +- Examples/Core/Camera2dDemo.cs | 261 +++--- Examples/Core/Camera2dMouseZoom.cs | 184 ++++ Examples/Core/Camera2dPlatformer.cs | 297 ++++--- Examples/Core/Camera3dFirstPerson.cs | 335 +++---- Examples/Core/Camera3dFps.cs | 348 ++++++++ Examples/Core/Camera3dFree.cs | 142 +-- Examples/Core/Camera3dMode.cs | 115 ++- Examples/Core/ClipboardText.cs | 286 ++++++ Examples/Core/ComputeHash.cs | 253 ++++++ Examples/Core/CustomFrameControl.cs | 188 ++++ Examples/Core/Customlogging.cs | 90 +- Examples/Core/DeltaTime.cs | 199 +++-- Examples/Core/DirectoryFiles.cs | 205 +++++ Examples/Core/DropFiles.cs | 148 ++-- Examples/Core/HighDpiDemo.cs | 173 ++++ Examples/Core/HighDpiTestbed.cs | 145 +++ Examples/Core/InputActions.cs | 224 +++++ Examples/Core/InputGamepad.cs | 825 +++++++++++------- Examples/Core/InputGestures.cs | 273 +++--- Examples/Core/InputGesturesTestBed.cs | 747 ++++++++-------- Examples/Core/InputKeys.cs | 111 ++- Examples/Core/InputMouse.cs | 177 ++-- Examples/Core/InputMouseWheel.cs | 78 +- Examples/Core/InputMultitouch.cs | 130 +-- Examples/Core/InputVirtualControls.cs | 261 +++--- Examples/Core/KeyboardTestbed.cs | 374 ++++++++ Examples/Core/LoadingThread.cs | 249 +++--- Examples/Core/MonitorDetector.cs | 209 +++++ Examples/Core/Picking3D.cs | 224 +++-- Examples/Core/RandomSequence.cs | 183 ++++ Examples/Core/RandomValues.cs | 110 ++- Examples/Core/RenderTexture.cs | 139 +++ Examples/Core/ScissorTest.cs | 130 +-- Examples/Core/ScreenRecording.cs | 382 ++++++++ Examples/Core/SmoothPixelperfect.cs | 254 ++++-- Examples/Core/SplitScreen.cs | 229 ++--- Examples/Core/SplitScreen2D.cs | 204 +++++ Examples/Core/StorageValues.cs | 167 ++-- Examples/Core/TextFileLoading.cs | 209 +++++ Examples/Core/UndoRedo.cs | 333 +++++++ Examples/Core/ViewportScaling.cs | 361 ++++++++ Examples/Core/VrSimulator.cs | 241 ++--- Examples/Core/WindowFlags.cs | 464 +++++----- Examples/Core/WindowLetterbox.cs | 240 ++--- Examples/Core/WindowShouldClose.cs | 130 +++ Examples/Core/WorldScreen.cs | 163 ++-- Examples/ExampleRegistry.cs | 53 ++ Examples/Examples.csproj | 53 +- Examples/ExcludeFromBrowserAttribute.cs | 17 + Examples/IExample.cs | 55 ++ Examples/Models/AnimationBlendCustom.cs | 390 +++++++++ Examples/Models/AnimationBlending.cs | 474 ++++++++++ Examples/Models/AnimationGpuSkinning.cs | 160 ++++ Examples/Models/AnimationTiming.cs | 292 +++++++ Examples/Models/BasicVoxel.cs | 202 +++++ Examples/Models/BillboardDemo.cs | 220 ++--- Examples/Models/BoneSocket.cs | 246 ++++++ Examples/Models/BoxCollisions.cs | 252 +++--- Examples/Models/CubicmapDemo.cs | 175 ++-- Examples/Models/Decals.cs | 592 +++++++++++++ Examples/Models/DirectionalBillboard.cs | 148 ++++ Examples/Models/DynamicMesh.cs | 166 ++-- Examples/Models/FirstPersonMaze.cs | 243 +++--- Examples/Models/GeometricShapes.cs | 127 +-- Examples/Models/HeightmapDemo.cs | 141 +-- Examples/Models/LoadingGltf.cs | 169 ++-- Examples/Models/LoadingIqm.cs | 172 ++-- Examples/Models/LoadingM3d.cs | 173 ++++ Examples/Models/LoadingVox.cs | 246 ++++++ Examples/Models/MeshDemo.cs | 101 ++- Examples/Models/MeshGeneration.cs | 252 +++--- Examples/Models/MeshPicking.cs | 436 ++++----- Examples/Models/ModelCubeTexture.cs | 146 ++-- Examples/Models/ModelLoading.cs | 277 +++--- Examples/Models/OrthographicProjection.cs | 175 ++-- Examples/Models/PointRendering.cs | 235 +++++ Examples/Models/RotatingCube.cs | 122 +++ Examples/Models/SkyboxDemo.cs | 265 +++--- Examples/Models/SolarSystem.cs | 255 +++--- Examples/Models/TesseractView.cs | 163 ++++ Examples/Models/WavingCubes.cs | 190 ++-- Examples/Models/YawPitchRoll.cs | 298 ++++--- Examples/Program.cs | 243 ++---- Examples/Shaders/AsciiRendering.cs | 166 ++++ Examples/Shaders/BasicLighting.cs | 298 +++---- Examples/Shaders/BasicPbr.cs | 333 ++++--- Examples/Shaders/CelShading.cs | 252 ++++++ Examples/Shaders/ColorCorrection.cs | 184 ++++ Examples/Shaders/CustomUniform.cs | 233 ++--- Examples/Shaders/DeferredRendering.cs | 400 +++++++++ Examples/Shaders/DepthRendering.cs | 226 +++++ Examples/Shaders/Eratosthenes.cs | 152 ++-- Examples/Shaders/Fog.cs | 249 +++--- Examples/Shaders/GameOfLife.cs | 433 +++++++++ Examples/Shaders/HotReloading.cs | 256 +++--- Examples/Shaders/HybridRender.cs | 219 +++-- Examples/Shaders/JuliaSet.cs | 398 +++++---- Examples/Shaders/LightmapRendering.cs | 213 +++++ Examples/Shaders/MandelbrotSet.cs | 265 ++++++ Examples/Shaders/MeshInstancing.cs | 419 ++++----- Examples/Shaders/ModelShader.cs | 178 ++-- Examples/Shaders/MultiSample2d.cs | 193 ++-- Examples/Shaders/NormalmapRendering.cs | 233 +++++ Examples/Shaders/PaletteSwitch.cs | 207 +++-- Examples/Shaders/PostProcessing.cs | 261 +++--- Examples/Shaders/Raymarching.cs | 208 +++-- Examples/Shaders/RlglCompute.cs | 242 +++++ Examples/Shaders/RoundedRectangle.cs | 243 ++++++ Examples/Shaders/ShadowmapRendering.cs | 316 +++++++ Examples/Shaders/ShapesTextures.cs | 194 ++-- Examples/Shaders/SimpleMask.cs | 203 +++-- Examples/Shaders/Spotlight.cs | 349 ++++---- Examples/Shaders/TextureDrawing.cs | 132 +-- Examples/Shaders/TextureOutline.cs | 162 ++-- Examples/Shaders/TextureTiling.cs | 143 +++ Examples/Shaders/TextureWaves.cs | 149 ++-- Examples/Shaders/VertexDisplacement.cs | 148 ++++ Examples/Shaders/WriteDepth.cs | 167 ++-- Examples/Shapes/BallPhysics.cs | 275 ++++++ Examples/Shapes/BasicShapes.cs | 139 +-- Examples/Shapes/BouncingBall.cs | 185 ++-- Examples/Shapes/BulletHell.cs | 290 ++++++ Examples/Shapes/ClockOfClocks.cs | 240 +++++ Examples/Shapes/CollisionArea.cs | 248 +++--- Examples/Shapes/ColorsPalette.cs | 281 +++--- Examples/Shapes/DashedLine.cs | 130 +++ Examples/Shapes/DigitalClock.cs | 327 +++++++ Examples/Shapes/DoublePendulum.cs | 205 +++++ Examples/Shapes/DrawCircleSector.cs | 175 ++-- Examples/Shapes/DrawRectangleRounded.cs | 213 +++-- Examples/Shapes/DrawRing.cs | 266 +++--- Examples/Shapes/EasingsBallAnim.cs | 220 +++-- Examples/Shapes/EasingsBoxAnim.cs | 253 +++--- Examples/Shapes/EasingsRectangleArray.cs | 214 +++-- Examples/Shapes/EasingsTestbed.cs | 230 +++++ Examples/Shapes/EllipseCollision.cs | 195 +++++ Examples/Shapes/FollowingEyes.cs | 187 ++-- Examples/Shapes/HilbertCurve.cs | 270 ++++++ Examples/Shapes/Kaleidoscope.cs | 224 +++++ Examples/Shapes/LinesBezier.cs | 134 ++- Examples/Shapes/LinesDrawing.cs | 173 ++++ Examples/Shapes/LogoRaylibAnim.cs | 358 ++++---- Examples/Shapes/LogoRaylibShape.cs | 83 +- Examples/Shapes/MathAngleRotation.cs | 141 +++ Examples/Shapes/MathSineCosine.cs | 212 +++++ Examples/Shapes/MouseTrail.cs | 127 +++ Examples/Shapes/PenroseTile.cs | 283 ++++++ Examples/Shapes/PieChart.cs | 384 ++++++++ Examples/Shapes/RectangleAdvanced.cs | 252 ++++++ Examples/Shapes/RectangleScaling.cs | 218 +++-- Examples/Shapes/RecursiveTree.cs | 199 +++++ Examples/Shapes/RlglColorWheel.cs | 331 +++++++ Examples/Shapes/RlglTriangle.cs | 201 +++++ Examples/Shapes/SimpleParticles.cs | 308 +++++++ Examples/Shapes/SplinesDrawing.cs | 387 ++++++++ Examples/Shapes/StarfieldEffect.cs | 180 ++++ Examples/Shapes/TopDownLights.cs | 406 +++++++++ Examples/Shapes/TriangleStrip.cs | 141 +++ Examples/Shapes/VectorAngle.cs | 154 ++++ Examples/Shared/Easings.cs | 38 +- Examples/Shared/PbrLights.cs | 12 +- Examples/Shared/Rlights.cs | 12 +- Examples/Text/CodepointsLoading.cs | 169 ++-- Examples/Text/FontFilters.cs | 247 +++--- Examples/Text/FontLoading.cs | 145 +-- Examples/Text/FontSdf.cs | 259 +++--- Examples/Text/FontSpritefont.cs | 137 +-- Examples/Text/FormatText.cs | 93 +- Examples/Text/InlineStyling.cs | 312 +++++++ Examples/Text/InputBox.cs | 298 ++++--- Examples/Text/RaylibFonts.cs | 139 +-- Examples/Text/RectangleBounds.cs | 327 ++++--- Examples/Text/StringsManagement.cs | 530 +++++++++++ Examples/Text/Text3D.cs | 739 ++++++++++++++++ Examples/Text/Unicode.cs | 661 +++++++++----- Examples/Text/UnicodeRanges.cs | 225 +++++ Examples/Text/WordsAlignment.cs | 167 ++++ Examples/Text/WritingAnim.cs | 117 ++- Examples/Textures/BackgroundScrolling.cs | 197 +++-- Examples/Textures/BlendModes.cs | 193 ++-- Examples/Textures/Bunnymark.cs | 198 +++-- Examples/Textures/CellularAutomata.cs | 248 ++++++ Examples/Textures/ClipboardImage.cs | 144 +++ Examples/Textures/DrawTiled.cs | 402 +++++---- Examples/Textures/FogOfWar.cs | 233 +++++ Examples/Textures/FramebufferRendering.cs | 248 ++++++ Examples/Textures/GifPlayer.cs | 171 ++++ Examples/Textures/ImageChannel.cs | 149 ++++ Examples/Textures/ImageDrawing.cs | 126 +-- Examples/Textures/ImageGeneration.cs | 219 +++-- Examples/Textures/ImageKernel.cs | 165 ++++ Examples/Textures/ImageLoading.cs | 100 ++- Examples/Textures/ImageProcessing.cs | 379 ++++---- Examples/Textures/ImageRotate.cs | 121 +++ Examples/Textures/ImageText.cs | 151 ++-- Examples/Textures/LogoRaylibTexture.cs | 95 +- Examples/Textures/MagnifyingGlass.cs | 160 ++++ Examples/Textures/MousePainting.cs | 537 ++++++------ Examples/Textures/NpatchDrawing.cs | 183 ++-- Examples/Textures/ParticlesBlending.cs | 255 +++--- Examples/Textures/Polygon.cs | 175 ++-- Examples/Textures/RawData.cs | 145 +-- Examples/Textures/ScreenBuffer.cs | 181 ++++ Examples/Textures/SpriteAnim.cs | 207 +++-- Examples/Textures/SpriteButton.cs | 181 ++-- Examples/Textures/SpriteExplosion.cs | 227 ++--- Examples/Textures/SpriteStacking.cs | 140 +++ Examples/Textures/SrcRecDstRec.cs | 140 +-- Examples/Textures/TexturedCurve.cs | 207 +++-- Examples/Textures/ToImage.cs | 107 ++- Examples/Web/Host.cs | 132 +++ Examples/Web/README.md | 91 ++ Examples/Web/index.html | 79 ++ Examples/Web/main.js | 176 ++++ Examples/Web/scaleUtils.js | 67 ++ Examples/Web/scaleUtils.test.js | 92 ++ Examples/resources/booth.png | Bin 0 -> 63521 bytes Examples/resources/raybunny.png | Bin 0 -> 466 bytes Examples/resources/scarfy_run.gif | Bin 0 -> 21261 bytes .../resources/shaders/glsl100/eratosthenes.fs | 30 +- Examples/resources/text_file.txt | 18 + 234 files changed, 39927 insertions(+), 11506 deletions(-) create mode 100644 Examples/Audio/AmpEnvelope.cs create mode 100644 Examples/Audio/MixedProcessor.cs create mode 100644 Examples/Audio/RawStream.cs create mode 100644 Examples/Audio/SoundMulti.cs create mode 100644 Examples/Audio/SoundPositioning.cs create mode 100644 Examples/Audio/StreamCallback.cs create mode 100644 Examples/Audio/StreamEffects.cs create mode 100644 Examples/Core/AutomationEvents.cs create mode 100644 Examples/Core/Camera2dMouseZoom.cs create mode 100644 Examples/Core/Camera3dFps.cs create mode 100644 Examples/Core/ClipboardText.cs create mode 100644 Examples/Core/ComputeHash.cs create mode 100644 Examples/Core/CustomFrameControl.cs create mode 100644 Examples/Core/DirectoryFiles.cs create mode 100644 Examples/Core/HighDpiDemo.cs create mode 100644 Examples/Core/HighDpiTestbed.cs create mode 100644 Examples/Core/InputActions.cs create mode 100644 Examples/Core/KeyboardTestbed.cs create mode 100644 Examples/Core/MonitorDetector.cs create mode 100644 Examples/Core/RandomSequence.cs create mode 100644 Examples/Core/RenderTexture.cs create mode 100644 Examples/Core/ScreenRecording.cs create mode 100644 Examples/Core/SplitScreen2D.cs create mode 100644 Examples/Core/TextFileLoading.cs create mode 100644 Examples/Core/UndoRedo.cs create mode 100644 Examples/Core/ViewportScaling.cs create mode 100644 Examples/Core/WindowShouldClose.cs create mode 100644 Examples/ExampleRegistry.cs create mode 100644 Examples/ExcludeFromBrowserAttribute.cs create mode 100644 Examples/IExample.cs create mode 100644 Examples/Models/AnimationBlendCustom.cs create mode 100644 Examples/Models/AnimationBlending.cs create mode 100644 Examples/Models/AnimationGpuSkinning.cs create mode 100644 Examples/Models/AnimationTiming.cs create mode 100644 Examples/Models/BasicVoxel.cs create mode 100644 Examples/Models/BoneSocket.cs create mode 100644 Examples/Models/Decals.cs create mode 100644 Examples/Models/DirectionalBillboard.cs create mode 100644 Examples/Models/LoadingM3d.cs create mode 100644 Examples/Models/LoadingVox.cs create mode 100644 Examples/Models/PointRendering.cs create mode 100644 Examples/Models/RotatingCube.cs create mode 100644 Examples/Models/TesseractView.cs create mode 100644 Examples/Shaders/AsciiRendering.cs create mode 100644 Examples/Shaders/CelShading.cs create mode 100644 Examples/Shaders/ColorCorrection.cs create mode 100644 Examples/Shaders/DeferredRendering.cs create mode 100644 Examples/Shaders/DepthRendering.cs create mode 100644 Examples/Shaders/GameOfLife.cs create mode 100644 Examples/Shaders/LightmapRendering.cs create mode 100644 Examples/Shaders/MandelbrotSet.cs create mode 100644 Examples/Shaders/NormalmapRendering.cs create mode 100644 Examples/Shaders/RlglCompute.cs create mode 100644 Examples/Shaders/RoundedRectangle.cs create mode 100644 Examples/Shaders/ShadowmapRendering.cs create mode 100644 Examples/Shaders/TextureTiling.cs create mode 100644 Examples/Shaders/VertexDisplacement.cs create mode 100644 Examples/Shapes/BallPhysics.cs create mode 100644 Examples/Shapes/BulletHell.cs create mode 100644 Examples/Shapes/ClockOfClocks.cs create mode 100644 Examples/Shapes/DashedLine.cs create mode 100644 Examples/Shapes/DigitalClock.cs create mode 100644 Examples/Shapes/DoublePendulum.cs create mode 100644 Examples/Shapes/EasingsTestbed.cs create mode 100644 Examples/Shapes/EllipseCollision.cs create mode 100644 Examples/Shapes/HilbertCurve.cs create mode 100644 Examples/Shapes/Kaleidoscope.cs create mode 100644 Examples/Shapes/LinesDrawing.cs create mode 100644 Examples/Shapes/MathAngleRotation.cs create mode 100644 Examples/Shapes/MathSineCosine.cs create mode 100644 Examples/Shapes/MouseTrail.cs create mode 100644 Examples/Shapes/PenroseTile.cs create mode 100644 Examples/Shapes/PieChart.cs create mode 100644 Examples/Shapes/RectangleAdvanced.cs create mode 100644 Examples/Shapes/RecursiveTree.cs create mode 100644 Examples/Shapes/RlglColorWheel.cs create mode 100644 Examples/Shapes/RlglTriangle.cs create mode 100644 Examples/Shapes/SimpleParticles.cs create mode 100644 Examples/Shapes/SplinesDrawing.cs create mode 100644 Examples/Shapes/StarfieldEffect.cs create mode 100644 Examples/Shapes/TopDownLights.cs create mode 100644 Examples/Shapes/TriangleStrip.cs create mode 100644 Examples/Shapes/VectorAngle.cs create mode 100644 Examples/Text/InlineStyling.cs create mode 100644 Examples/Text/StringsManagement.cs create mode 100644 Examples/Text/Text3D.cs create mode 100644 Examples/Text/UnicodeRanges.cs create mode 100644 Examples/Text/WordsAlignment.cs create mode 100644 Examples/Textures/CellularAutomata.cs create mode 100644 Examples/Textures/ClipboardImage.cs create mode 100644 Examples/Textures/FogOfWar.cs create mode 100644 Examples/Textures/FramebufferRendering.cs create mode 100644 Examples/Textures/GifPlayer.cs create mode 100644 Examples/Textures/ImageChannel.cs create mode 100644 Examples/Textures/ImageKernel.cs create mode 100644 Examples/Textures/ImageRotate.cs create mode 100644 Examples/Textures/MagnifyingGlass.cs create mode 100644 Examples/Textures/ScreenBuffer.cs create mode 100644 Examples/Textures/SpriteStacking.cs create mode 100644 Examples/Web/Host.cs create mode 100644 Examples/Web/README.md create mode 100644 Examples/Web/index.html create mode 100644 Examples/Web/main.js create mode 100644 Examples/Web/scaleUtils.js create mode 100644 Examples/Web/scaleUtils.test.js create mode 100644 Examples/resources/booth.png create mode 100644 Examples/resources/raybunny.png create mode 100644 Examples/resources/scarfy_run.gif create mode 100644 Examples/resources/text_file.txt 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/ModulePlaying.cs b/Examples/Audio/ModulePlaying.cs index be9ab92..39d7829 100644 --- a/Examples/Audio/ModulePlaying.cs +++ b/Examples/Audio/ModulePlaying.cs @@ -1,13 +1,15 @@ /******************************************************************************************* * -* raylib [audio] example - Module playing (streaming) +* raylib [audio] example - module playing * -* NOTE: This example requires OpenAL Soft library installed +* Example complexity rating: [★☆☆☆] 1/4 * -* This example has been created using raylib 1.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.5, last time updated with raylib 3.5 * -* Copyright (c) 2016 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) 2016-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -16,11 +18,20 @@ using static Raylib_cs.Raylib; namespace Examples.Audio; -public class ModulePlaying +public partial class ModulePlaying : IExample { - const int MaxCircles = 64; + private const int screenWidth = 800; + private const int screenHeight = 450; - struct CircleWave + private const int MaxCircles = 64; + + public string Name => "Audio / Module Playing"; + + public string Title => "raylib [audio] example - module playing"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + private struct CircleWave { public Vector2 Position; public float Radius; @@ -29,20 +40,18 @@ public class ModulePlaying public Color Color; } - public static int Main() + private Color[] colors; + private CircleWave[] circles; + private Music music; + private float pitch; + private float timePlayed; + private bool pause; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitAudioDevice(); // Initialize audio device - SetConfigFlags(ConfigFlags.Msaa4xHint); // NOTE: Try to enable MSAA 4X - - InitWindow(screenWidth, screenHeight, "raylib [audio] example - module playing (streaming)"); - - InitAudioDevice(); - - Color[] colors = new Color[14] { + colors = new Color[14] { Color.Orange, Color.Red, Color.Gold, @@ -59,133 +68,163 @@ public class ModulePlaying Color.Beige }; - // Creates ome circles for visual effect - CircleWave[] circles = new CircleWave[MaxCircles]; + // Creates some circles for visual effect + circles = new CircleWave[MaxCircles]; - for (int i = MaxCircles - 1; i >= 0; i--) + for (var i = MaxCircles - 1; i >= 0; i--) { circles[i].Alpha = 0.0f; circles[i].Radius = GetRandomValue(10, 40); circles[i].Position.X = GetRandomValue((int)circles[i].Radius, screenWidth - (int)circles[i].Radius); circles[i].Position.Y = GetRandomValue((int)circles[i].Radius, screenHeight - (int)circles[i].Radius); - circles[i].Speed = (float)GetRandomValue(1, 100) / 20000.0f; + circles[i].Speed = (float)GetRandomValue(1, 100) / 2000.0f; circles[i].Color = colors[GetRandomValue(0, 13)]; } - Music music = LoadMusicStream("resources/audio/mini1111.xm"); + music = LoadMusicStream("resources/audio/mini1111.xm"); music.Looping = false; - float pitch = 1.0f; + pitch = 1.0f; PlayMusicStream(music); - float timePlayed = 0.0f; - bool pause = false; + timePlayed = 0.0f; + pause = false; + } - SetTargetFPS(60); + 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); + } + } + + if (IsKeyDown(KeyboardKey.Down)) + { + pitch -= 0.01f; + } + else if (IsKeyDown(KeyboardKey.Up)) + { + pitch += 0.01f; + } + + SetMusicPitch(music, pitch); + + // Get timePlayed scaled to bar dimensions + timePlayed = GetMusicTimePlayed(music) / GetMusicTimeLength(music) * (screenWidth - 40); + + // Color circles animation + for (var i = MaxCircles - 1; (i >= 0) && !pause; i--) + { + circles[i].Alpha += circles[i].Speed; + circles[i].Radius += circles[i].Speed * 10.0f; + + if (circles[i].Alpha > 1.0f) + { + circles[i].Speed *= -1; + } + + if (circles[i].Alpha <= 0.0f) + { + circles[i].Alpha = 0.0f; + circles[i].Radius = GetRandomValue(10, 40); + circles[i].Position.X = GetRandomValue( + (int)circles[i].Radius, + screenWidth - (int)circles[i].Radius + ); + circles[i].Position.Y = GetRandomValue( + (int)circles[i].Radius, + screenHeight - (int)circles[i].Radius + ); + circles[i].Color = colors[GetRandomValue(0, 13)]; + circles[i].Speed = (float)GetRandomValue(1, 100) / 2000.0f; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + for (var i = MaxCircles - 1; i >= 0; i--) + { + DrawCircleV( + circles[i].Position, + circles[i].Radius, + Fade(circles[i].Color, circles[i].Alpha) + ); + } + + // Draw time bar + DrawRectangle(20, screenHeight - 20 - 12, screenWidth - 40, 12, Color.LightGray); + DrawRectangle(20, screenHeight - 20 - 12, (int)timePlayed, 12, Color.Maroon); + DrawRectangleLines(20, screenHeight - 20 - 12, screenWidth - 40, 12, Color.Gray); + + // Draw help instructions + DrawRectangle(20, 20, 425, 145, Color.White); + DrawRectangleLines(20, 20, 425, 145, Color.Gray); + DrawText("PRESS SPACE TO RESTART MUSIC", 40, 40, 20, Color.Black); + DrawText("PRESS P TO PAUSE/RESUME", 40, 70, 20, Color.Black); + DrawText("PRESS UP/DOWN TO CHANGE SPEED", 40, 100, 20, Color.Black); + DrawText($"SPEED: {pitch:F6}", 40, 130, 20, Color.Maroon); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadMusicStream(music); // Unload music stream buffers from RAM + + CloseAudioDevice(); // Close audio device (music streaming is automatically stopped) + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); // NOTE: Try to enable MSAA 4X + + InitWindow(screenWidth, screenHeight, "raylib [audio] example - module playing"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new ModulePlaying(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // 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); - } - } - - if (IsKeyDown(KeyboardKey.Down)) - { - pitch -= 0.01f; - } - else if (IsKeyDown(KeyboardKey.Up)) - { - pitch += 0.01f; - } - - SetMusicPitch(music, pitch); - - // Get timePlayed scaled to bar dimensions - timePlayed = GetMusicTimePlayed(music) / GetMusicTimeLength(music) * (screenWidth - 40); - - // Color circles animation - for (int i = MaxCircles - 1; (i >= 0) && !pause; i--) - { - circles[i].Alpha += circles[i].Speed; - circles[i].Radius += circles[i].Speed * 10.0f; - - if (circles[i].Alpha > 1.0f) - { - circles[i].Speed *= -1; - } - - if (circles[i].Alpha <= 0.0f) - { - circles[i].Alpha = 0.0f; - circles[i].Radius = GetRandomValue(10, 40); - circles[i].Position.X = GetRandomValue( - (int)circles[i].Radius, - screenWidth - (int)circles[i].Radius - ); - circles[i].Position.Y = GetRandomValue( - (int)circles[i].Radius, - screenHeight - (int)circles[i].Radius - ); - circles[i].Color = colors[GetRandomValue(0, 13)]; - circles[i].Speed = (float)GetRandomValue(1, 100) / 2000.0f; - } - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - for (int i = MaxCircles - 1; i >= 0; i--) - { - DrawCircleV( - circles[i].Position, - circles[i].Radius, - ColorAlpha(circles[i].Color, circles[i].Alpha) - ); - } - - // Draw time bar - DrawRectangle(20, screenHeight - 20 - 12, screenWidth - 40, 12, Color.LightGray); - DrawRectangle(20, screenHeight - 20 - 12, (int)timePlayed, 12, Color.Maroon); - DrawRectangleLines(20, screenHeight - 20 - 12, screenWidth - 40, 12, Color.Gray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadMusicStream(music); - - CloseAudioDevice(); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Audio/MusicStreamDemo.cs b/Examples/Audio/MusicStreamDemo.cs index bf8ed9e..c25d0b8 100644 --- a/Examples/Audio/MusicStreamDemo.cs +++ b/Examples/Audio/MusicStreamDemo.cs @@ -1,13 +1,15 @@ /******************************************************************************************* * -* raylib [audio] example - IntPtr playing (streaming) +* raylib [audio] example - music stream * -* NOTE: This example requires OpenAL Soft library installed +* Example complexity rating: [★☆☆☆] 1/4 * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.3, last time updated with raylib 4.2 * -* Copyright (c) 2015 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) 2015-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -15,90 +17,176 @@ using static Raylib_cs.Raylib; namespace Examples.Audio; -public class MusicStreamDemo +public partial class MusicStreamDemo : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Audio / Music Stream Demo"; + + public string Title => "raylib [audio] example - music stream"; + + public int TargetFps => 30; + + private Music music; + private float timePlayed; + private bool pause; + private float pan; + private float volume; + + public void Init() + { + InitAudioDevice(); // Initialize audio device + + music = LoadMusicStream("resources/audio/country.mp3"); + + PlayMusicStream(music); + + timePlayed = 0.0f; // Time played normalized [0.0f..1.0f] + pause = false; // Music playing paused + + pan = 0.0f; // Default audio pan center [-1.0f..1.0f] + SetMusicPan(music, pan); + + volume = 0.8f; // Default audio volume [0.0f..1.0f] + SetMusicVolume(music, volume); + } + + 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); + } + } + + // Set audio pan + if (IsKeyDown(KeyboardKey.Left)) + { + pan -= 0.05f; + if (pan < -1.0f) + { + pan = -1.0f; + } + SetMusicPan(music, pan); + } + else if (IsKeyDown(KeyboardKey.Right)) + { + pan += 0.05f; + if (pan > 1.0f) + { + pan = 1.0f; + } + SetMusicPan(music, pan); + } + + // Set audio volume + if (IsKeyDown(KeyboardKey.Down)) + { + volume -= 0.05f; + if (volume < 0.0f) + { + volume = 0.0f; + } + SetMusicVolume(music, volume); + } + else if (IsKeyDown(KeyboardKey.Up)) + { + volume += 0.05f; + if (volume > 1.0f) + { + volume = 1.0f; + } + SetMusicVolume(music, volume); + } + + // 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!", 255, 150, 20, Color.LightGray); + + DrawText("LEFT-RIGHT for PAN CONTROL", 320, 74, 10, Color.DarkBlue); + DrawRectangle(300, 100, 200, 12, Color.LightGray); + DrawRectangleLines(300, 100, 200, 12, Color.Gray); + DrawRectangle((int)(300 + (pan + 1.0f) / 2.0f * 200 - 5), 92, 10, 28, Color.DarkGray); + + DrawRectangle(200, 200, 400, 12, Color.LightGray); + DrawRectangle(200, 200, (int)(timePlayed * 400.0f), 12, Color.Maroon); + DrawRectangleLines(200, 200, 400, 12, Color.Gray); + + DrawText("PRESS SPACE TO RESTART MUSIC", 215, 250, 20, Color.LightGray); + DrawText("PRESS P TO PAUSE/RESUME MUSIC", 208, 280, 20, Color.LightGray); + + DrawText("UP-DOWN for VOLUME CONTROL", 320, 334, 10, Color.DarkGreen); + DrawRectangle(300, 360, 200, 12, Color.LightGray); + DrawRectangleLines(300, 360, 200, 12, Color.Gray); + DrawRectangle((int)(300 + volume * 200 - 5), 352, 10, 28, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadMusicStream(music); // Unload music stream buffers from RAM + + CloseAudioDevice(); // Close audio device (music streaming is automatically stopped) + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [audio] example - music stream"); - InitWindow(screenWidth, screenHeight, "raylib [audio] example - music playing (streaming)"); - InitAudioDevice(); - - Music music = LoadMusicStream("resources/audio/country.mp3"); - PlayMusicStream(music); - - float timePlayed = 0.0f; - bool pause = false; - - SetTargetFPS(60); + SetTargetFPS(30); // Set our game to run at 30 frames-per-second //-------------------------------------------------------------------------------------- + var game = new MusicStreamDemo(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // 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); - } - } - - // Get timePlayed scaled to bar dimensions (400 pixels) - timePlayed = GetMusicTimePlayed(music) / GetMusicTimeLength(music) * 400; - - if (timePlayed > 400) - { - StopMusicStream(music); - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("MUSIC SHOULD BE PLAYING!", 255, 150, 20, Color.LightGray); - - DrawRectangle(200, 200, 400, 12, Color.LightGray); - DrawRectangle(200, 200, (int)timePlayed, 12, Color.Maroon); - DrawRectangleLines(200, 200, 400, 12, Color.Gray); - - DrawText("PRESS SPACE TO RESTART MUSIC", 215, 250, 20, Color.LightGray); - DrawText("PRESS P TO PAUSE/RESUME MUSIC", 208, 280, 20, Color.LightGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadMusicStream(music); - - CloseAudioDevice(); - - CloseWindow(); + 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/SoundLoading.cs b/Examples/Audio/SoundLoading.cs index 1aed2bb..025852c 100644 --- a/Examples/Audio/SoundLoading.cs +++ b/Examples/Audio/SoundLoading.cs @@ -1,13 +1,15 @@ /******************************************************************************************* * -* raylib [audio] example - Sound loading and playing +* raylib [audio] example - sound loading * -* NOTE: This example requires OpenAL Soft library installed +* Example complexity rating: [★☆☆☆] 1/4 * -* This example has been created using raylib 1.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.1, last time updated with raylib 3.5 * -* Copyright (c) 2014 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) 2014-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -15,60 +17,84 @@ using static Raylib_cs.Raylib; namespace Examples.Audio; -public class SoundLoading +public partial class SoundLoading : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Audio / Sound Loading"; + + public string Title => "raylib [audio] example - sound loading"; + + private Sound fxWav; + private Sound fxOgg; + + public void Init() + { + InitAudioDevice(); // Initialize audio device + + fxWav = LoadSound("resources/audio/sound.wav"); // Load WAV audio file + fxOgg = LoadSound("resources/audio/target.ogg"); // Load OGG audio file + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.Space)) + { + PlaySound(fxWav); // Play WAV sound + } + + if (IsKeyPressed(KeyboardKey.Enter)) + { + PlaySound(fxOgg); // Play OGG sound + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText("Press SPACE to PLAY the WAV sound!", 200, 180, 20, Color.LightGray); + DrawText("Press ENTER to PLAY the OGG sound!", 200, 220, 20, Color.LightGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadSound(fxWav); // Unload sound data + UnloadSound(fxOgg); // Unload sound data + + CloseAudioDevice(); // Close audio device + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [audio] example - sound loading"); - InitWindow(screenWidth, screenHeight, "raylib [audio] example - sound loading and playing"); - InitAudioDevice(); - - Sound fxWav = LoadSound("resources/audio/sound.wav"); - Sound fxOgg = LoadSound("resources/audio/target.ogg"); - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new SoundLoading(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (IsKeyPressed(KeyboardKey.Space)) - { - PlaySound(fxWav); - } - - if (IsKeyPressed(KeyboardKey.Enter)) - { - PlaySound(fxOgg); - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("Press SPACE to PLAY the WAV sound!", 200, 180, 20, Color.LightGray); - DrawText("Press ENTER to PLAY the OGG sound!", 200, 220, 20, Color.LightGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadSound(fxWav); - UnloadSound(fxOgg); - - CloseAudioDevice(); - - CloseWindow(); + 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/BasicScreenManager.cs b/Examples/Core/BasicScreenManager.cs index 62de113..308d5a0 100644 --- a/Examples/Core/BasicScreenManager.cs +++ b/Examples/Core/BasicScreenManager.cs @@ -1,21 +1,17 @@ /******************************************************************************************* * -* raylib [core] example - Basic window +* raylib [core] example - basic screen manager * -* Welcome to raylib! +* Example complexity rating: [★☆☆☆] 1/4 * -* To test examples, just press F6 and execute raylib_compile_execute script -* Note that compiled executable is placed in the same folder as .c file +* NOTE: This example illustrates a very simple screen manager based on a states machines * -* You can find all basic examples on C:\raylib\raylib\examples folder or -* raylib official webpage: www.raylib.com +* Example originally created with raylib 4.0, last time updated with raylib 4.0 * -* Enjoy using raylib. :) +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software * -* This example has been created using raylib 1.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) -* -* Copyright (c) 2013-2016 Ramon Santamaria (@raysan5) +* Copyright (c) 2021-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -23,7 +19,7 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -enum GameScreen +internal enum GameScreen { Logo = 0, Title, @@ -31,143 +27,160 @@ enum GameScreen Ending } -public class BasicScreenManager +public partial class BasicScreenManager : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Basic Screen Manager"; + + public string Title => "raylib [core] example - basic screen manager"; + + private GameScreen currentScreen; + private int framesCounter; + + public void Init() + { + currentScreen = GameScreen.Logo; + + // TODO: Initialize all required variables and load all required data here! + + framesCounter = 0; // Useful to count frames + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + switch (currentScreen) + { + case GameScreen.Logo: + { + // TODO: Update LOGO screen variables here! + + framesCounter++; // Count frames + + // Wait for 2 seconds (120 frames) before jumping to TITLE screen + if (framesCounter > 120) + { + currentScreen = GameScreen.Title; + } + } + break; + case GameScreen.Title: + { + // TODO: Update TITLE screen variables here! + + // Press enter to change to GAMEPLAY screen + if (IsKeyPressed(KeyboardKey.Enter) || IsGestureDetected(Gesture.Tap)) + { + currentScreen = GameScreen.Gameplay; + } + } + break; + case GameScreen.Gameplay: + { + // TODO: Update GAMEPLAY screen variables here! + + // Press enter to change to ENDING screen + if (IsKeyPressed(KeyboardKey.Enter) || IsGestureDetected(Gesture.Tap)) + { + currentScreen = GameScreen.Ending; + } + } + break; + case GameScreen.Ending: + { + // TODO: Update ENDING screen variables here! + + // Press enter to return to TITLE screen + if (IsKeyPressed(KeyboardKey.Enter) || IsGestureDetected(Gesture.Tap)) + { + currentScreen = GameScreen.Title; + } + } + break; + default: + break; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + switch (currentScreen) + { + case GameScreen.Logo: + { + // TODO: Draw LOGO screen here! + DrawText("LOGO SCREEN", 20, 20, 40, Color.LightGray); + DrawText("WAIT for 2 SECONDS...", 290, 220, 20, Color.Gray); + } + break; + case GameScreen.Title: + { + // TODO: Draw TITLE screen here! + DrawRectangle(0, 0, screenWidth, screenHeight, Color.Green); + DrawText("TITLE SCREEN", 20, 20, 40, Color.DarkGreen); + DrawText("PRESS ENTER or TAP to JUMP to GAMEPLAY SCREEN", 120, 220, 20, Color.DarkGreen); + } + break; + case GameScreen.Gameplay: + { + // TODO: Draw GAMEPLAY screen here! + DrawRectangle(0, 0, screenWidth, screenHeight, Color.Purple); + DrawText("GAMEPLAY SCREEN", 20, 20, 40, Color.Maroon); + DrawText("PRESS ENTER or TAP to JUMP to ENDING SCREEN", 130, 220, 20, Color.Maroon); + + } + break; + case GameScreen.Ending: + { + // TODO: Draw ENDING screen here! + DrawRectangle(0, 0, screenWidth, screenHeight, Color.Blue); + DrawText("ENDING SCREEN", 20, 20, 40, Color.DarkBlue); + DrawText("PRESS ENTER or TAP to RETURN to TITLE SCREEN", 120, 220, 20, Color.DarkBlue); + } + break; + default: + break; + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + // TODO: Unload all loaded data (textures, fonts, audio) here! + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [core] example - basic screen manager"); - GameScreen currentScreen = GameScreen.Logo; - - // TODO: Initialize all required variables and load all required data here! - - // Useful to count frames - int framesCounter = 0; - SetTargetFPS(60); // Set desired framerate (frames-per-second) //-------------------------------------------------------------------------------------- + var game = new BasicScreenManager(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - switch (currentScreen) - { - case GameScreen.Logo: - { - // TODO: Update LOGO screen variables here! - - // Count frames - framesCounter++; - - // Wait for 2 seconds (120 frames) before jumping to TITLE screen - if (framesCounter > 120) - { - currentScreen = GameScreen.Title; - } - } - break; - case GameScreen.Title: - { - // TODO: Update TITLE screen variables here! - - // Press enter to change to GAMEPLAY screen - if (IsKeyPressed(KeyboardKey.Enter) || IsGestureDetected(Gesture.Tap)) - { - currentScreen = GameScreen.Gameplay; - } - } - break; - case GameScreen.Gameplay: - { - // TODO: Update GAMEPLAY screen variables here! - - // Press enter to change to ENDING screen - if (IsKeyPressed(KeyboardKey.Enter) || IsGestureDetected(Gesture.Tap)) - { - currentScreen = GameScreen.Ending; - } - } - break; - case GameScreen.Ending: - { - // TODO: Update ENDING screen variables here! - - // Press enter to return to TITLE screen - if (IsKeyPressed(KeyboardKey.Enter) || IsGestureDetected(Gesture.Tap)) - { - currentScreen = GameScreen.Title; - } - } - break; - default: - break; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - - ClearBackground(Color.RayWhite); - - switch (currentScreen) - { - case GameScreen.Logo: - { - // TODO: Draw LOGO screen here! - DrawText("LOGO SCREEN", 20, 20, 40, Color.LightGray); - DrawText("WAIT for 2 SECONDS...", 290, 220, 20, Color.Gray); - - } - break; - case GameScreen.Title: - { - // TODO: Draw TITLE screen here! - DrawRectangle(0, 0, screenWidth, screenHeight, Color.Green); - DrawText("TITLE SCREEN", 20, 20, 40, Color.DarkGreen); - DrawText("PRESS ENTER or TAP to JUMP to GAMEPLAY SCREEN", 120, 220, 20, Color.DarkGreen); - - } - break; - case GameScreen.Gameplay: - { - // TODO: Draw GAMEPLAY screen here! - DrawRectangle(0, 0, screenWidth, screenHeight, Color.Purple); - DrawText("GAMEPLAY SCREEN", 20, 20, 40, Color.Maroon); - DrawText("PRESS ENTER or TAP to JUMP to ENDING SCREEN", 130, 220, 20, Color.Maroon); - - } - break; - case GameScreen.Ending: - { - // TODO: Draw ENDING screen here! - DrawRectangle(0, 0, screenWidth, screenHeight, Color.Blue); - DrawText("ENDING SCREEN", 20, 20, 40, Color.DarkBlue); - DrawText("PRESS ENTER or TAP to RETURN to TITLE SCREEN", 120, 220, 20, Color.DarkBlue); - - } - break; - default: - break; - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - - // TODO: Unload all loaded data (textures, fonts, audio) here! - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Core/BasicWindow.cs b/Examples/Core/BasicWindow.cs index 4180912..f24c930 100644 --- a/Examples/Core/BasicWindow.cs +++ b/Examples/Core/BasicWindow.cs @@ -33,43 +33,56 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class BasicWindow +public partial class BasicWindow : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Basic Window"; + + public string Title => "raylib [core] example - basic window"; + + public void Init() + { + } + + public void Update() + { + + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText("Congrats! You created your first window!", 190, 200, 20, Color.LightGray); + + EndDrawing(); + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [core] example - basic window"); - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + + var game = new BasicWindow(); + game.Init(); // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // TODO: Update your variables here - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("Congrats! You created your first window!", 190, 200, 20, Color.LightGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); - //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context return 0; } diff --git a/Examples/Core/Camera2dDemo.cs b/Examples/Core/Camera2dDemo.cs index 1463c24..bd9aebf 100644 --- a/Examples/Core/Camera2dDemo.cs +++ b/Examples/Core/Camera2dDemo.cs @@ -13,31 +13,37 @@ * ********************************************************************************************/ +using System; using System.Numerics; using static Raylib_cs.Raylib; namespace Examples.Core; -public class Camera2dDemo +public partial class Camera2dDemo : IExample { public const int MaxBuildings = 100; - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Camera 2D Demo"; + + public string Title => "raylib [core] example - 2d camera"; + + private Rectangle player; + private Rectangle[] buildings; + private Color[] buildColors; + private Camera2D camera; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + player = new(400, 280, 40, 40); + buildings = new Rectangle[MaxBuildings]; + buildColors = new Color[MaxBuildings]; - InitWindow(screenWidth, screenHeight, "raylib [core] example - 2d camera"); + var spacing = 0; - Rectangle player = new(400, 280, 40, 40); - Rectangle[] buildings = new Rectangle[MaxBuildings]; - Color[] buildColors = new Color[MaxBuildings]; - - int spacing = 0; - - for (int i = 0; i < MaxBuildings; i++) + for (var i = 0; i < MaxBuildings; i++) { buildings[i].Width = GetRandomValue(50, 200); buildings[i].Height = GetRandomValue(100, 800); @@ -54,124 +60,139 @@ public class Camera2dDemo ); } - Camera2D camera = new(); + camera = new(); camera.Target = new Vector2(player.X + 20, player.Y + 20); - camera.Offset = new Vector2(screenWidth / 2, screenHeight / 2); + camera.Offset = new Vector2(screenWidth / 2.0f, screenHeight / 2.0f); camera.Rotation = 0.0f; camera.Zoom = 1.0f; + } - SetTargetFPS(60); + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Player movement + if (IsKeyDown(KeyboardKey.Right)) + { + player.X += 2; + } + else if (IsKeyDown(KeyboardKey.Left)) + { + player.X -= 2; + } + + // Camera target follows player + camera.Target = new Vector2(player.X + 20, player.Y + 20); + + // Camera rotation controls + if (IsKeyDown(KeyboardKey.A)) + { + camera.Rotation--; + } + else if (IsKeyDown(KeyboardKey.S)) + { + camera.Rotation++; + } + + // Limit camera rotation to 80 degrees (-40 to 40) + if (camera.Rotation > 40) + { + camera.Rotation = 40; + } + else if (camera.Rotation < -40) + { + camera.Rotation = -40; + } + + // Camera zoom controls + // Uses log scaling to provide consistent zoom speed + camera.Zoom = MathF.Exp(MathF.Log(camera.Zoom) + ((float)GetMouseWheelMove() * 0.1f)); + + if (camera.Zoom > 3.0f) + { + camera.Zoom = 3.0f; + } + else if (camera.Zoom < 0.1f) + { + camera.Zoom = 0.1f; + } + + // Camera reset (zoom and rotation) + if (IsKeyPressed(KeyboardKey.R)) + { + camera.Zoom = 1.0f; + camera.Rotation = 0.0f; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode2D(camera); + + DrawRectangle(-6000, 320, 13000, 8000, Color.DarkGray); + + for (var i = 0; i < MaxBuildings; i++) + { + DrawRectangleRec(buildings[i], buildColors[i]); + } + + DrawRectangleRec(player, Color.Red); + + DrawLine((int)camera.Target.X, -screenHeight * 10, (int)camera.Target.X, screenHeight * 10, Color.Green); + DrawLine(-screenWidth * 10, (int)camera.Target.Y, screenWidth * 10, (int)camera.Target.Y, Color.Green); + + EndMode2D(); + + DrawText("SCREEN AREA", 640, 10, 20, Color.Red); + + DrawRectangle(0, 0, screenWidth, 5, Color.Red); + DrawRectangle(0, 5, 5, screenHeight - 10, Color.Red); + DrawRectangle(screenWidth - 5, 5, 5, screenHeight - 10, Color.Red); + DrawRectangle(0, screenHeight - 5, screenWidth, 5, Color.Red); + + DrawRectangle(10, 10, 250, 113, Fade(Color.SkyBlue, 0.5f)); + DrawRectangleLines(10, 10, 250, 113, Color.Blue); + + DrawText("Free 2D camera controls:", 20, 20, 10, Color.Black); + DrawText("- Right/Left to move player", 40, 40, 10, Color.DarkGray); + DrawText("- Mouse Wheel to Zoom in-out", 40, 60, 10, Color.DarkGray); + DrawText("- A / S to Rotate", 40, 80, 10, Color.DarkGray); + DrawText("- R to reset Zoom and Rotation", 40, 100, 10, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - 2d camera"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new Camera2dDemo(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - - // Player movement - if (IsKeyDown(KeyboardKey.Right)) - { - player.X += 2; - } - else if (IsKeyDown(KeyboardKey.Left)) - { - player.X -= 2; - } - - // Camera3D target follows player - camera.Target = new Vector2(player.X + 20, player.Y + 20); - - // Camera3D rotation controls - if (IsKeyDown(KeyboardKey.A)) - { - camera.Rotation--; - } - else if (IsKeyDown(KeyboardKey.S)) - { - camera.Rotation++; - } - - // Limit camera rotation to 80 degrees (-40 to 40) - if (camera.Rotation > 40) - { - camera.Rotation = 40; - } - else if (camera.Rotation < -40) - { - camera.Rotation = -40; - } - - // Camera3D zoom controls - camera.Zoom += ((float)GetMouseWheelMove() * 0.05f); - - if (camera.Zoom > 3.0f) - { - camera.Zoom = 3.0f; - } - else if (camera.Zoom < 0.1f) - { - camera.Zoom = 0.1f; - } - - // Camera3D reset (zoom and rotation) - if (IsKeyPressed(KeyboardKey.R)) - { - camera.Zoom = 1.0f; - camera.Rotation = 0.0f; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode2D(camera); - - DrawRectangle(-6000, 320, 13000, 8000, Color.DarkGray); - - for (int i = 0; i < MaxBuildings; i++) - { - DrawRectangleRec(buildings[i], buildColors[i]); - } - - DrawRectangleRec(player, Color.Red); - - DrawRectangle((int)camera.Target.X, -500, 1, (int)(screenHeight * 4), Color.Green); - DrawLine( - (int)(-screenWidth * 10), - (int)camera.Target.Y, - (int)(screenWidth * 10), - (int)camera.Target.Y, - Color.Green - ); - - EndMode2D(); - - DrawText("SCREEN AREA", 640, 10, 20, Color.Red); - - DrawRectangle(0, 0, (int)screenWidth, 5, Color.Red); - DrawRectangle(0, 5, 5, (int)screenHeight - 10, Color.Red); - DrawRectangle((int)screenWidth - 5, 5, 5, (int)screenHeight - 10, Color.Red); - DrawRectangle(0, (int)screenHeight - 5, (int)screenWidth, 5, Color.Red); - - DrawRectangle(10, 10, 250, 113, ColorAlpha(Color.SkyBlue, 0.5f)); - DrawRectangleLines(10, 10, 250, 113, Color.Blue); - - DrawText("Free 2d camera controls:", 20, 20, 10, Color.Black); - DrawText("- Right/Left to move Offset", 40, 40, 10, Color.DarkGray); - DrawText("- Mouse Wheel to Zoom in-out", 40, 60, 10, Color.DarkGray); - DrawText("- A / S to Rotate", 40, 80, 10, Color.DarkGray); - DrawText("- R to reset Zoom and Rotation", 40, 100, 10, Color.DarkGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + 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/Camera2dPlatformer.cs b/Examples/Core/Camera2dPlatformer.cs index c86d820..9f54911 100644 --- a/Examples/Core/Camera2dPlatformer.cs +++ b/Examples/Core/Camera2dPlatformer.cs @@ -2,12 +2,16 @@ * * raylib [core] example - 2d camera platformer * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 2.5, last time updated with raylib 3.0 * * Example contributed by arvyy (@arvyy) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2019 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) 2019-2025 arvyy (@arvyy) * ********************************************************************************************/ @@ -18,20 +22,27 @@ using static Raylib_cs.Raymath; namespace Examples.Core; -public class Camera2dPlatformer +public partial class Camera2dPlatformer : IExample { - const int G = 400; - const float PlayerJumpSpeed = 350.0f; - const float PlayerHorSpeed = 200.0f; + private const int screenWidth = 800; + private const int screenHeight = 450; - struct Player + private const int G = 400; + private const float PlayerJumpSpeed = 350.0f; + private const float PlayerHorSpeed = 200.0f; + + public string Name => "Core / 2D Camera Platformer"; + + public string Title => "raylib [core] example - 2d camera platformer"; + + private struct Player { public Vector2 Position; public float Speed; public bool CanJump; } - struct EnvItem + private struct EnvItem { public Rectangle Rect; public int Blocking; @@ -45,7 +56,7 @@ public class Camera2dPlatformer } } - delegate void CameraUpdaterCallback( + private delegate void CameraUpdaterCallback( ref Camera2D camera, ref Player player, EnvItem[] envItems, @@ -54,21 +65,22 @@ public class Camera2dPlatformer int height ); - public static int Main() + private Player player; + private EnvItem[] envItems; + private Camera2D camera; + private CameraUpdaterCallback[] cameraUpdaters; + private int cameraOption; + private int cameraUpdatersLength; + private string[] cameraDescriptions; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [core] example - 2d camera"); - - Player player = new(); + player = new(); player.Position = new Vector2(400, 280); player.Speed = 0; player.CanJump = false; - EnvItem[] envItems = new EnvItem[] + envItems = new EnvItem[] { new EnvItem(new Rectangle(0, 0, 1000, 400), 0, Color.LightGray), new EnvItem(new Rectangle(0, 400, 1000, 200), 1, Color.Gray), @@ -77,14 +89,14 @@ public class Camera2dPlatformer new EnvItem(new Rectangle(650, 300, 100, 10), 1, Color.Gray) }; - Camera2D camera = new(); + camera = new(); camera.Target = player.Position; - camera.Offset = new Vector2(screenWidth / 2, screenHeight / 2); + camera.Offset = new Vector2(screenWidth / 2.0f, screenHeight / 2.0f); camera.Rotation = 0.0f; camera.Zoom = 1.0f; - // Store callbacks to the multiple update camera functions - CameraUpdaterCallback[] cameraUpdaters = new CameraUpdaterCallback[] + // Store pointers to the multiple update camera functions + cameraUpdaters = new CameraUpdaterCallback[] { UpdateCameraCenter, UpdateCameraCenterInsideMap, @@ -93,93 +105,89 @@ public class Camera2dPlatformer UpdateCameraPlayerBoundsPush }; - int cameraOption = 0; - int cameraUpdatersLength = cameraUpdaters.Length; + cameraOption = 0; + cameraUpdatersLength = cameraUpdaters.Length; - string[] cameraDescriptions = new string[]{ + cameraDescriptions = new string[]{ "Follow player center", "Follow player center, but clamp to map edges", "Follow player center; smoothed", "Follow player center horizontally; update player center vertically after landing", "Player push camera on getting too close to screen edge" }; - - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- - - // Main game loop - while (!WindowShouldClose()) - { - // Update - //---------------------------------------------------------------------------------- - float deltaTime = GetFrameTime(); - - UpdatePlayer(ref player, envItems, deltaTime); - - 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; - } - - if (IsKeyPressed(KeyboardKey.R)) - { - camera.Zoom = 1.0f; - player.Position = new Vector2(400, 280); - } - - if (IsKeyPressed(KeyboardKey.C)) - { - cameraOption = (cameraOption + 1) % cameraUpdatersLength; - } - - // Call update camera function by its pointer - cameraUpdaters[cameraOption](ref camera, ref player, envItems, deltaTime, screenWidth, screenHeight); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.LightGray); - - BeginMode2D(camera); - - for (int i = 0; i < envItems.Length; i++) - { - DrawRectangleRec(envItems[i].Rect, envItems[i].Color); - } - - Rectangle playerRect = new(player.Position.X - 20, player.Position.Y - 40, 40, 40); - DrawRectangleRec(playerRect, Color.Red); - - EndMode2D(); - - DrawText("Controls:", 20, 20, 10, Color.Black); - DrawText("- Right/Left to move", 40, 40, 10, Color.DarkGray); - DrawText("- Space to jump", 40, 60, 10, Color.DarkGray); - DrawText("- Mouse Wheel to Zoom in-out, R to reset zoom", 40, 80, 10, Color.DarkGray); - DrawText("- C to change camera mode", 40, 100, 10, Color.DarkGray); - DrawText("Current camera mode:", 20, 120, 10, Color.Black); - DrawText(cameraDescriptions[cameraOption], 40, 140, 10, Color.DarkGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- - } - - // De-Initialization - //-------------------------------------------------------------------------------------- - CloseWindow(); - //-------------------------------------------------------------------------------------- - - return 0; } - static void UpdatePlayer(ref Player player, EnvItem[] envItems, float delta) + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + var deltaTime = GetFrameTime(); + + UpdatePlayer(ref player, envItems, deltaTime); + + 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; + } + + if (IsKeyPressed(KeyboardKey.R)) + { + camera.Zoom = 1.0f; + player.Position = new Vector2(400, 280); + } + + if (IsKeyPressed(KeyboardKey.C)) + { + cameraOption = (cameraOption + 1) % cameraUpdatersLength; + } + + // Call update camera function by its pointer + cameraUpdaters[cameraOption](ref camera, ref player, envItems, deltaTime, screenWidth, screenHeight); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.LightGray); + + BeginMode2D(camera); + + for (var i = 0; i < envItems.Length; i++) + { + DrawRectangleRec(envItems[i].Rect, envItems[i].Color); + } + + Rectangle playerRect = new(player.Position.X - 20, player.Position.Y - 40, 40.0f, 40.0f); + DrawRectangleRec(playerRect, Color.Red); + + DrawCircleV(player.Position, 5.0f, Color.Gold); + + EndMode2D(); + + DrawText("Controls:", 20, 20, 10, Color.Black); + DrawText("- Right/Left to move", 40, 40, 10, Color.DarkGray); + DrawText("- Space to jump", 40, 60, 10, Color.DarkGray); + DrawText("- Mouse Wheel to Zoom in-out", 40, 80, 10, Color.DarkGray); + DrawText("- R to reset position + zoom", 40, 100, 10, Color.DarkGray); + DrawText("- C to change camera mode", 40, 120, 10, Color.DarkGray); + DrawText("Current camera mode:", 20, 140, 10, Color.Black); + DrawText(cameraDescriptions[cameraOption], 40, 160, 10, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + private static void UpdatePlayer(ref Player player, EnvItem[] envItems, float delta) { if (IsKeyDown(KeyboardKey.Left)) { @@ -197,11 +205,11 @@ public class Camera2dPlatformer player.CanJump = false; } - int hitObstacle = 0; - for (int i = 0; i < envItems.Length; i++) + var hitObstacle = 0; + for (var i = 0; i < envItems.Length; i++) { - EnvItem ei = envItems[i]; - Vector2 p = player.Position; + var ei = envItems[i]; + var p = player.Position; if (ei.Blocking != 0 && ei.Rect.X <= p.X && ei.Rect.X + ei.Rect.Width >= p.X && @@ -211,6 +219,7 @@ public class Camera2dPlatformer hitObstacle = 1; player.Speed = 0.0f; player.Position.Y = ei.Rect.Y; + break; } } @@ -226,7 +235,7 @@ public class Camera2dPlatformer } } - static void UpdateCameraCenter( + private static void UpdateCameraCenter( ref Camera2D camera, ref Player player, EnvItem[] envItems, @@ -235,11 +244,11 @@ public class Camera2dPlatformer int height ) { - camera.Offset = new Vector2(width / 2, height / 2); + camera.Offset = new Vector2(width / 2.0f, height / 2.0f); camera.Target = player.Position; } - static void UpdateCameraCenterInsideMap( + private static void UpdateCameraCenterInsideMap( ref Camera2D camera, ref Player player, EnvItem[] envItems, @@ -248,43 +257,43 @@ public class Camera2dPlatformer int height) { camera.Target = player.Position; - camera.Offset = new Vector2(width / 2, height / 2); + camera.Offset = new Vector2(width / 2.0f, height / 2.0f); float minX = 1000, minY = 1000, maxX = -1000, maxY = -1000; - for (int i = 0; i < envItems.Length; i++) + for (var i = 0; i < envItems.Length; i++) { - EnvItem ei = envItems[i]; + var ei = envItems[i]; minX = Math.Min(ei.Rect.X, minX); maxX = Math.Max(ei.Rect.X + ei.Rect.Width, maxX); minY = Math.Min(ei.Rect.Y, minY); maxY = Math.Max(ei.Rect.Y + ei.Rect.Height, maxY); } - Vector2 max = GetWorldToScreen2D(new Vector2(maxX, maxY), camera); - Vector2 min = GetWorldToScreen2D(new Vector2(minX, minY), camera); + var max = GetWorldToScreen2D(new Vector2(maxX, maxY), camera); + var min = GetWorldToScreen2D(new Vector2(minX, minY), camera); if (max.X < width) { - camera.Offset.X = width - (max.X - width / 2); + camera.Offset.X = width - (max.X - width / 2.0f); } if (max.Y < height) { - camera.Offset.Y = height - (max.Y - height / 2); + camera.Offset.Y = height - (max.Y - height / 2.0f); } if (min.X > 0) { - camera.Offset.X = width / 2 - min.X; + camera.Offset.X = width / 2.0f - min.X; } if (min.Y > 0) { - camera.Offset.Y = height / 2 - min.Y; + camera.Offset.Y = height / 2.0f - min.Y; } } - static void UpdateCameraCenterSmoothFollow( + private static void UpdateCameraCenterSmoothFollow( ref Camera2D camera, ref Player player, EnvItem[] envItems, @@ -297,18 +306,18 @@ public class Camera2dPlatformer const float minEffectLength = 10; const float fractionSpeed = 0.8f; - camera.Offset = new Vector2(width / 2, height / 2); - Vector2 diff = Vector2Subtract(player.Position, camera.Target); - float length = Vector2Length(diff); + camera.Offset = new Vector2(width / 2.0f, height / 2.0f); + var diff = Vector2Subtract(player.Position, camera.Target); + var length = Vector2Length(diff); if (length > minEffectLength) { - float speed = Math.Max(fractionSpeed * length, minSpeed); + var speed = Math.Max(fractionSpeed * length, minSpeed); camera.Target = Vector2Add(camera.Target, Vector2Scale(diff, speed * delta / length)); } } - static void UpdateCameraEvenOutOnLanding( + private static void UpdateCameraEvenOutOnLanding( ref Camera2D camera, ref Player player, EnvItem[] envItems, @@ -318,10 +327,10 @@ public class Camera2dPlatformer ) { float evenOutSpeed = 700; - int eveningOut = 0; - float evenOutTarget = 0.0f; + var eveningOut = 0; + var evenOutTarget = 0.0f; - camera.Offset = new Vector2(width / 2, height / 2); + camera.Offset = new Vector2(width / 2.0f, height / 2.0f); camera.Target.X = player.Position.X; if (eveningOut != 0) @@ -357,7 +366,7 @@ public class Camera2dPlatformer } } - static void UpdateCameraPlayerBoundsPush( + private static void UpdateCameraPlayerBoundsPush( ref Camera2D camera, ref Player player, EnvItem[] envItems, @@ -368,11 +377,11 @@ public class Camera2dPlatformer { Vector2 bbox = new(0.2f, 0.2f); - Vector2 bboxWorldMin = GetScreenToWorld2D( + var bboxWorldMin = GetScreenToWorld2D( new Vector2((1 - bbox.X) * 0.5f * width, (1 - bbox.Y) * 0.5f * height), camera ); - Vector2 bboxWorldMax = GetScreenToWorld2D( + var bboxWorldMax = GetScreenToWorld2D( new Vector2((1 + bbox.X) * 0.5f * width, (1 + bbox.Y) * 0.5f * height), camera @@ -399,4 +408,32 @@ public class Camera2dPlatformer camera.Target.Y = bboxWorldMin.Y + (player.Position.Y - bboxWorldMax.Y); } } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - 2d camera platformer"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new Camera2dPlatformer(); + 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/Core/Camera3dFirstPerson.cs b/Examples/Core/Camera3dFirstPerson.cs index 5d91bdb..7f7b951 100644 --- a/Examples/Core/Camera3dFirstPerson.cs +++ b/Examples/Core/Camera3dFirstPerson.cs @@ -2,10 +2,14 @@ * * raylib [core] example - 3d camera first person * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2015 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 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,171 +18,202 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class Camera3dFirstPerson +public partial class Camera3dFirstPerson : IExample { public const int MaxColumns = 20; + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Camera 3D First Person"; + + public string Title => "raylib [core] example - 3d camera first person"; + + public bool CursorDisabled => true; + + private Camera3D camera; + private CameraMode cameraMode; + private float[] heights; + private Vector3[] positions; + private Color[] colors; + + public void Init() + { + // Define the camera to look into our 3d world (position, target, up vector) + camera = new(); + camera.Position = new Vector3(0.0f, 2.0f, 4.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 = 60.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + + cameraMode = CameraMode.FirstPerson; + + // Generates some random columns + heights = new float[MaxColumns]; + positions = new Vector3[MaxColumns]; + colors = new Color[MaxColumns]; + + for (var i = 0; i < MaxColumns; i++) + { + heights[i] = (float)GetRandomValue(1, 12); + positions[i] = new Vector3(GetRandomValue(-15, 15), heights[i] / 2.0f, GetRandomValue(-15, 15)); + colors[i] = new Color(GetRandomValue(20, 255), GetRandomValue(10, 55), 30, 255); + } + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Switch camera mode + if (IsKeyPressed(KeyboardKey.One)) + { + cameraMode = CameraMode.Free; + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); + } + + if (IsKeyPressed(KeyboardKey.Two)) + { + cameraMode = CameraMode.FirstPerson; + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); + } + + if (IsKeyPressed(KeyboardKey.Three)) + { + cameraMode = CameraMode.ThirdPerson; + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); + } + + if (IsKeyPressed(KeyboardKey.Four)) + { + cameraMode = CameraMode.Orbital; + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); + } + + // Switch camera projection + if (IsKeyPressed(KeyboardKey.P)) + { + if (camera.Projection == CameraProjection.Perspective) + { + // Create isometric view + cameraMode = CameraMode.ThirdPerson; + // Note: The target distance is related to the render distance in the orthographic projection + camera.Position = new Vector3(0.0f, 2.0f, -100.0f); + camera.Target = new Vector3(0.0f, 2.0f, 0.0f); + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); + camera.Projection = CameraProjection.Orthographic; + camera.FovY = 20.0f; // near plane width in CAMERA_ORTHOGRAPHIC + // CameraYaw(&camera, -135 * DEG2RAD, true); + // CameraPitch(&camera, -45 * DEG2RAD, true, true, false); + } + else if (camera.Projection == CameraProjection.Orthographic) + { + // Reset to default view + cameraMode = CameraMode.ThirdPerson; + camera.Position = new Vector3(0.0f, 2.0f, 10.0f); + camera.Target = new Vector3(0.0f, 2.0f, 0.0f); + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); + camera.Projection = CameraProjection.Perspective; + camera.FovY = 60.0f; + } + } + + // Update camera computes movement internally depending on the camera mode + // Some default standard keyboard/mouse inputs are hardcoded to simplify use + // For advanced camera controls, it's recommended to compute camera movement manually + UpdateCamera(ref camera, cameraMode); // Update camera + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + // Draw ground + DrawPlane(new Vector3(0.0f, 0.0f, 0.0f), new Vector2(32.0f, 32.0f), Color.LightGray); + + // Draw a blue wall + DrawCube(new Vector3(-16.0f, 2.5f, 0.0f), 1.0f, 5.0f, 32.0f, Color.Blue); + + // Draw a green wall + DrawCube(new Vector3(16.0f, 2.5f, 0.0f), 1.0f, 5.0f, 32.0f, Color.Lime); + + // Draw a yellow wall + DrawCube(new Vector3(0.0f, 2.5f, 16.0f), 32.0f, 5.0f, 1.0f, Color.Gold); + + // Draw some cubes around + for (var i = 0; i < MaxColumns; i++) + { + DrawCube(positions[i], 2.0f, heights[i], 2.0f, colors[i]); + DrawCubeWires(positions[i], 2.0f, heights[i], 2.0f, Color.Maroon); + } + + // Draw player cube + if (cameraMode == CameraMode.ThirdPerson) + { + DrawCube(camera.Target, 0.5f, 0.5f, 0.5f, Color.Purple); + DrawCubeWires(camera.Target, 0.5f, 0.5f, 0.5f, Color.DarkPurple); + } + + EndMode3D(); + + // Draw info boxes + DrawRectangle(5, 5, 330, 100, Fade(Color.SkyBlue, 0.5f)); + DrawRectangleLines(5, 5, 330, 100, 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); + DrawText("- Camera mode keys: 1, 2, 3, 4", 15, 60, 10, Color.Black); + DrawText("- Zoom keys: num-plus, num-minus or mouse scroll", 15, 75, 10, Color.Black); + DrawText("- Camera projection key: P", 15, 90, 10, Color.Black); + + DrawRectangle(600, 5, 195, 100, Fade(Color.SkyBlue, 0.5f)); + DrawRectangleLines(600, 5, 195, 100, Color.Blue); + + DrawText("Camera status:", 610, 15, 10, Color.Black); + DrawText($"- Mode: {cameraMode}", 610, 30, 10, Color.Black); + DrawText($"- Projection: {camera.Projection}", 610, 45, 10, Color.Black); + DrawText($"- Position: {camera.Position}", 610, 60, 10, Color.Black); + DrawText($"- Target: {camera.Target}", 610, 75, 10, Color.Black); + DrawText($"- Up: {camera.Up}", 610, 90, 10, Color.Black); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [core] example - 3d camera first person"); - // Define the camera to look into our 3d world (position, target, up vector) - Camera3D camera = new(); - camera.Position = new Vector3(4.0f, 2.0f, 4.0f); - camera.Target = new Vector3(0.0f, 1.8f, 0.0f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.FovY = 60.0f; - camera.Projection = CameraProjection.Perspective; + DisableCursor(); // Limit cursor to relative movement inside the window - // Generates some random columns - float[] heights = new float[MaxColumns]; - Vector3[] positions = new Vector3[MaxColumns]; - Color[] colors = new Color[MaxColumns]; - - for (int i = 0; i < MaxColumns; i++) - { - heights[i] = (float)GetRandomValue(1, 12); - positions[i] = new Vector3(GetRandomValue(-15, 15), heights[i] / 2, GetRandomValue(-15, 15)); - colors[i] = new Color(GetRandomValue(20, 255), GetRandomValue(10, 55), 30, 255); - } - - CameraMode cameraMode = CameraMode.FirstPerson; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new Camera3dFirstPerson(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // Switch camera mode - if (IsKeyPressed(KeyboardKey.One)) - { - cameraMode = CameraMode.Free; - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - } - - if (IsKeyPressed(KeyboardKey.Two)) - { - cameraMode = CameraMode.FirstPerson; - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - } - - if (IsKeyPressed(KeyboardKey.Three)) - { - cameraMode = CameraMode.ThirdPerson; - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - } - - if (IsKeyPressed(KeyboardKey.Four)) - { - cameraMode = CameraMode.Orbital; - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - } - - // Switch camera projection - if (IsKeyPressed(KeyboardKey.P)) - { - if (camera.Projection == CameraProjection.Perspective) - { - // Create isometric view - cameraMode = CameraMode.ThirdPerson; - // Note: The target distance is related to the render distance in the orthographic projection - camera.Position = new Vector3(0.0f, 2.0f, -100.0f); - camera.Target = new Vector3(0.0f, 2.0f, 0.0f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.Projection = CameraProjection.Orthographic; - camera.FovY = 20.0f; // near plane width in CAMERA_ORTHOGRAPHIC - // CameraYaw(&camera, -135 * DEG2RAD, true); - // CameraPitch(&camera, -45 * DEG2RAD, true, true, false); - } - else if (camera.Projection == CameraProjection.Orthographic) - { - // Reset to default view - cameraMode = CameraMode.ThirdPerson; - camera.Position = new Vector3(0.0f, 2.0f, 10.0f); - camera.Target = new Vector3(0.0f, 2.0f, 0.0f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.Projection = CameraProjection.Perspective; - camera.FovY = 60.0f; - } - } - - // Update camera computes movement internally depending on the camera mode - // Some default standard keyboard/mouse inputs are hardcoded to simplify use - // For advance camera controls, it's reecommended to compute camera movement manually - UpdateCamera(ref camera, cameraMode); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - // Draw ground - DrawPlane(new Vector3(0.0f, 0.0f, 0.0f), new Vector2(32.0f, 32.0f), Color.LightGray); - - // Draw a blue wall - DrawCube(new Vector3(-16.0f, 2.5f, 0.0f), 1.0f, 5.0f, 32.0f, Color.Blue); - - // Draw a green wall - DrawCube(new Vector3(16.0f, 2.5f, 0.0f), 1.0f, 5.0f, 32.0f, Color.Lime); - - // Draw a yellow wall - DrawCube(new Vector3(0.0f, 2.5f, 16.0f), 32.0f, 5.0f, 1.0f, Color.Gold); - - // Draw some cubes around - for (int i = 0; i < MaxColumns; i++) - { - DrawCube(positions[i], 2.0f, heights[i], 2.0f, colors[i]); - DrawCubeWires(positions[i], 2.0f, heights[i], 2.0f, Color.Maroon); - } - - // Draw player cube - if (cameraMode == CameraMode.ThirdPerson) - { - DrawCube(camera.Target, 0.5f, 0.5f, 0.5f, Color.Purple); - DrawCubeWires(camera.Target, 0.5f, 0.5f, 0.5f, Color.DarkPurple); - } - - EndMode3D(); - - // Draw info boxes - DrawRectangle(5, 5, 330, 100, ColorAlpha(Color.SkyBlue, 0.5f)); - DrawRectangleLines(10, 10, 330, 100, 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); - DrawText("- Camera mode keys: 1, 2, 3, 4", 15, 60, 10, Color.Black); - DrawText("- Zoom keys: num-plus, num-minus or mouse scroll", 15, 75, 10, Color.Black); - DrawText("- Camera projection key: P", 15, 90, 10, Color.Black); - - DrawRectangle(600, 5, 195, 100, Fade(Color.SkyBlue, 0.5f)); - DrawRectangleLines(600, 5, 195, 100, Color.Blue); - - DrawText("Camera status:", 610, 15, 10, Color.Black); - DrawText($"- Mode: {cameraMode}", 610, 30, 10, Color.Black); - DrawText($"- Projection: {camera.Projection}", 610, 45, 10, Color.Black); - DrawText($"- Position: {camera.Position}", 610, 60, 10, Color.Black); - DrawText($"- Target: {camera.Target}", 610, 75, 10, Color.Black); - DrawText($"- Up: {camera.Up}", 610, 90, 10, Color.Black); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + 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/Camera3dFree.cs b/Examples/Core/Camera3dFree.cs index cea2e8f..95cd179 100644 --- a/Examples/Core/Camera3dFree.cs +++ b/Examples/Core/Camera3dFree.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [core] example - Initialize 3d camera free +* raylib [core] example - 3d camera free * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 * -* Copyright (c) 2015 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 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,74 +18,100 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class Camera3dFree +public partial class Camera3dFree : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Camera 3D Free"; + + public string Title => "raylib [core] example - 3d camera free"; + + public bool CursorDisabled => true; + + private Camera3D camera; + private Vector3 cubePosition; + + public 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 + + cubePosition = new(0.0f, 0.0f, 0.0f); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Free); + + if (IsKeyPressed(KeyboardKey.Z)) + { + camera.Target = new Vector3(0.0f, 0.0f, 0.0f); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawCube(cubePosition, 2.0f, 2.0f, 2.0f, Color.Red); + DrawCubeWires(cubePosition, 2.0f, 2.0f, 2.0f, Color.Maroon); + + DrawGrid(10, 1.0f); + + EndMode3D(); + + DrawRectangle(10, 10, 320, 93, Fade(Color.SkyBlue, 0.5f)); + DrawRectangleLines(10, 10, 320, 93, Color.Blue); + + DrawText("Free camera default controls:", 20, 20, 10, Color.Black); + DrawText("- Mouse Wheel to Zoom in-out", 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() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [core] example - 3d camera free"); - // Define the camera to look into our 3d world - Camera3D camera; - camera.Position = new Vector3(10.0f, 10.0f, 10.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; + DisableCursor(); // Limit cursor to relative movement inside the window - Vector3 cubePosition = new(0.0f, 0.0f, 0.0f); - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new Camera3dFree(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Free); - - if (IsKeyDown(KeyboardKey.Z)) - { - camera.Target = new Vector3(0.0f, 0.0f, 0.0f); - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawCube(cubePosition, 2.0f, 2.0f, 2.0f, Color.Red); - DrawCubeWires(cubePosition, 2.0f, 2.0f, 2.0f, Color.Maroon); - - DrawGrid(10, 1.0f); - - EndMode3D(); - - DrawRectangle(10, 10, 320, 133, ColorAlpha(Color.SkyBlue, 0.5f)); - DrawRectangleLines(10, 10, 320, 133, Color.Blue); - - DrawText("Free camera default controls:", 20, 20, 10, Color.Black); - DrawText("- Mouse Wheel to Zoom in-out", 40, 40, 10, Color.DarkGray); - DrawText("- Mouse Wheel Pressed to Pan", 40, 60, 10, Color.DarkGray); - DrawText("- Alt + Mouse Wheel Pressed to Rotate", 40, 80, 10, Color.DarkGray); - DrawText("- Alt + Ctrl + Mouse Wheel Pressed for Smooth Zoom", 40, 100, 10, Color.DarkGray); - DrawText("- Z to zoom to (0, 0, 0)", 40, 120, 10, Color.DarkGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Core/Camera3dMode.cs b/Examples/Core/Camera3dMode.cs index 61510a2..14c58fa 100644 --- a/Examples/Core/Camera3dMode.cs +++ b/Examples/Core/Camera3dMode.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [core] example - Initialize 3d camera mode +* raylib [core] example - 3d camera mode * -* This example has been created using raylib 1.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 * -* Copyright (c) 2014 Ramon Santamaria (@raysan5) +* 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) * ********************************************************************************************/ @@ -14,63 +18,82 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class Camera3dMode +public partial class Camera3dMode : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + private Camera3D camera; + private Vector3 cubePosition; + + public string Name => "Core / Camera 3D Mode"; + + public string Title => "raylib [core] example - 3d camera mode"; + + public void Init() + { + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(0.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 mode type + + cubePosition = new(0.0f, 0.0f, 0.0f); + } + + public void Update() + { + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawCube(cubePosition, 2.0f, 2.0f, 2.0f, Color.Red); + DrawCubeWires(cubePosition, 2.0f, 2.0f, 2.0f, Color.Maroon); + + DrawGrid(10, 1.0f); + + EndMode3D(); + + DrawText("Welcome to the third dimension!", 10, 40, 20, Color.DarkGray); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [core] example - 3d camera mode"); - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(0.0f, 10.0f, 10.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; - - Vector3 cubePosition = new(0.0f, 0.0f, 0.0f); - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new Camera3dMode(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // TODO: Update your variables here - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawCube(cubePosition, 2.0f, 2.0f, 2.0f, Color.Red); - DrawCubeWires(cubePosition, 2.0f, 2.0f, 2.0f, Color.Maroon); - - DrawGrid(10, 1.0f); - - EndMode3D(); - - DrawText("Welcome to the third dimension!", 10, 40, 20, Color.DarkGray); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + 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/Customlogging.cs b/Examples/Core/Customlogging.cs index 3fdf64a..e171cc4 100644 --- a/Examples/Core/Customlogging.cs +++ b/Examples/Core/Customlogging.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [core] example - Custom logging +* raylib [core] example - custom logging * -* This example has been created using raylib 2.1 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 2.5, last time updated with raylib 2.5 * * Example contributed by Pablo Marcos Oltra (@pamarcos) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2018 Pablo Marcos Oltra (@pamarcos) and 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) 2018-2025 Pablo Marcos Oltra (@pamarcos) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -17,11 +21,23 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public unsafe class CustomLogging +public unsafe partial class CustomLogging : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Custom Logging"; + + public string Title => "raylib [core] example - custom logging"; + [UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })] private static void LogCustom(int logLevel, sbyte* text, sbyte* args) { +#if BROWSER + // WebAssembly can't invoke the C varargs vsprintf/vsnprintf that Logging.GetLogMessage relies on + // (the wasm runtime traps with "function signature mismatch"), so log the raw, unformatted text. + string message = Marshal.PtrToStringUTF8(new IntPtr(text)) ?? string.Empty; +#else var message = Logging.GetLogMessage(new IntPtr(text), new IntPtr(args)); /*Console.ForegroundColor = (TraceLogLevel)logLevel switch @@ -36,50 +52,64 @@ public unsafe class CustomLogging TraceLogLevel.LOG_NONE => ConsoleColor.White, _ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null) };*/ +#endif Console.WriteLine($"Custom " + message); // Console.ResetColor(); } + public void Init() + { + // First thing we do is setting our custom logger to ensure everything raylib logs + // will use our own logger instead of its internal one + SetTraceLogCallback(&LogCustom); + } + + public void Update() + { + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText("Check out the console output to see the custom logger in action!", 60, 200, 20, Color.LightGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { +#if !BROWSER + // Restore the default formatting logger on desktop. On WebAssembly we leave the (wasm-safe) + // custom logger in place — Logging.LogConsole formats via vsprintf, which traps on wasm. + SetTraceLogCallback(&Logging.LogConsole); +#endif + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - // First thing we do is setting our custom logger to ensure everything raylib logs - // will use our own logger instead of its internal one - SetTraceLogCallback(&LogCustom); - InitWindow(screenWidth, screenHeight, "raylib [core] example - custom logging"); - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new CustomLogging(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // TODO: Update your variables here - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("Check out the console output to see the custom logger in action!", 60, 200, 20, Color.LightGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); - SetTraceLogCallback(&Logging.LogConsole); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Core/DeltaTime.cs b/Examples/Core/DeltaTime.cs index 38dd077..f83ee51 100644 --- a/Examples/Core/DeltaTime.cs +++ b/Examples/Core/DeltaTime.cs @@ -21,108 +21,137 @@ namespace Examples.Core; using static Raylib_cs.Raylib; -public class DeltaTime +public partial class DeltaTime : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + // The speed applied to both circles + private const float speed = 10.0f; + private const float circleRadius = 32.0f; + + private int currentFps; + + // Store the position for the both of the circles + private Vector2 deltaCircle; + private Vector2 frameCircle; + + public string Name => "Core / Delta Time"; + + public string Title => "raylib [core] example - delta time"; + + public void Init() + { + currentFps = 60; + + // Store the position for the both of the circles + deltaCircle = new Vector2(0, (float)screenHeight / 3.0f); + frameCircle = new Vector2(0, (float)screenHeight * (2.0f / 3.0f)); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Adjust the FPS target based on the mouse wheel + var mouseWheel = GetMouseWheelMove(); + if (mouseWheel != 0) + { + currentFps += (int)mouseWheel; + if (currentFps < 0) + { + currentFps = 0; + } + SetTargetFPS(currentFps); + } + + // GetFrameTime() returns the time it took to draw the last frame, in seconds (usually called delta time) + // Uses the delta time to make the circle look like it's moving at a "consistent" speed regardless of FPS + + // Multiply by 6.0 (an arbitrary value) in order to make the speed + // visually closer to the other circle (at 60 fps), for comparison + deltaCircle.X += GetFrameTime() * 6.0f * speed; + // This circle can move faster or slower visually depending on the FPS + frameCircle.X += 0.1f * speed; + + // If either circle is off the screen, reset it back to the start + if (deltaCircle.X > screenWidth) + { + deltaCircle.X = 0; + } + + if (frameCircle.X > screenWidth) + { + frameCircle.X = 0; + } + + // Reset both circles positions + if (IsKeyPressed(KeyboardKey.R)) + { + deltaCircle.X = 0; + frameCircle.X = 0; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // Draw both circles to the screen + DrawCircleV(deltaCircle, circleRadius, Color.Red); + DrawCircleV(frameCircle, circleRadius, Color.Blue); + + // Draw the help text + // Determine what help text to show depending on the current FPS target + var fpsText = ""; + if (currentFps <= 0) + { + fpsText = $"FPS: unlimited ({GetFPS()})"; + } + else + { + fpsText = $"FPS: {GetFPS()} (target: {currentFps})"; + } + DrawText(fpsText, 10, 10, 20, Color.DarkGray); + DrawText($"Frame time: {GetFrameTime():F2} ms", 10, 30, 20, Color.DarkGray); + DrawText("Use the scroll wheel to change the fps limit, r to reset", 10, 50, 20, Color.DarkGray); + + // Draw the text above the circles + DrawText("FUNC: x += GetFrameTime()*speed", 10, 90, 20, Color.Red); + DrawText("FUNC: x += speed", 10, 240, 20, Color.Blue); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [core] example - delta time"); - int currentFps = 60; - - Vector2 deltaCircle = new Vector2(0, (float)screenHeight / 3.0f); - Vector2 frameCircle = new Vector2(0, (float)screenHeight * (2.0f / 3.0f)); - SetTargetFPS(60); //-------------------------------------------------------------------------------------- - const float speed = 10.0f; - const float circleRadius = 32.0f; + var game = new DeltaTime(); + game.Init(); // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // Adjust the FPS target based on the mouse wheel - float mouseWheel = GetMouseWheelMove(); - if (mouseWheel != 0) - { - currentFps += (int)mouseWheel; - if (currentFps < 0) - { - currentFps = 0; - } - SetTargetFPS(currentFps); - } - - // GetFrameTime() returns the time it took to draw the last frame, in seconds (usually called delta time) - // Uses the delta time to make the circle look like it's moving at a "consistent" speed regardless of FPS - - // Multiply by 6.0 (an arbitrary value) in order to make the speed - // visually closer to the other circle (at 60 fps), for comparison - deltaCircle.X += GetFrameTime() * 6.0f * speed; - // This circle can move faster or slower visually depending on the FPS - frameCircle.X += 0.1f * speed; - - // If either circle is off the screen, reset it back to the start - if (deltaCircle.X > screenWidth) - { - deltaCircle.X = 0; - } - - if (frameCircle.X > screenWidth) - { - frameCircle.X = 0; - } - - // Reset both circles positions - if (IsKeyPressed(KeyboardKey.R)) - { - deltaCircle.X = 0; - frameCircle.X = 0; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // Draw both circles to the screen - DrawCircleV(deltaCircle, circleRadius, Color.Red); - DrawCircleV(frameCircle, circleRadius, Color.Blue); - - // Draw the help text - // Determine what help text to show depending on the current FPS target - var fpsText = ""; - if (currentFps <= 0) - { - fpsText = $"FPS: unlimited ({GetFPS()})"; - } - else - { - fpsText = $"FPS: {GetFPS()} (target: {currentFps})"; - } - DrawText(fpsText, 10, 10, 20, Color.DarkGray); - DrawText($"Frame time: {GetFrameTime():F2} ms", 10, 30, 20, Color.DarkGray); - DrawText("Use the scroll wheel to change the fps limit, r to reset", 10, 50, 20, Color.DarkGray); - - // Draw the text above the circles - DrawText("FUNC: x += GetFrameTime()*speed", 10, 90, 20, Color.Red); - DrawText("FUNC: x += speed", 10, 240, 20, Color.Blue); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + 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 0d236e8..c4dcca6 100644 --- a/Examples/Core/DropFiles.cs +++ b/Examples/Core/DropFiles.cs @@ -1,83 +1,125 @@ /******************************************************************************************* * -* raylib [core] example - Windows drop files +* raylib [core] example - drop files * -* This example only works on platforms that support drag ref drop (Windows, Linux, OSX, Html5?) +* Example complexity rating: [★★☆☆] 2/4 * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* NOTE: This example only works on platforms that support drag & drop (Windows, Linux, OSX, Html5?) * -* Copyright (c) 2015 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.3, 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) 2015-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ +using System.Collections.Generic; using static Raylib_cs.Raylib; namespace Examples.Core; -public class DropFiles +[ExcludeFromBrowser] +public partial class DropFiles : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public const int MaxFilepathRecorded = 4096; + + public string Name => "Core / Drop Files"; + + public string Title => "raylib [core] example - drop files"; + + private List filePaths; + + public void Init() + { + // We will register a maximum of filepaths + filePaths = new(); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsFileDropped()) + { + var droppedFiles = GetDroppedFiles(); + + for (var i = 0; i < droppedFiles.Length; i++) + { + if (filePaths.Count < (MaxFilepathRecorded - 1)) + { + filePaths.Add(droppedFiles[i]); + } + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + if (filePaths.Count == 0) + { + DrawText("Drop your files to this window!", 100, 40, 20, Color.DarkGray); + } + else + { + DrawText("Dropped files:", 100, 40, 20, Color.DarkGray); + + for (var i = 0; i < filePaths.Count; i++) + { + if (i % 2 == 0) + { + DrawRectangle(0, 85 + 40 * i, screenWidth, 40, Fade(Color.LightGray, 0.5f)); + } + else + { + DrawRectangle(0, 85 + 40 * i, screenWidth, 40, Fade(Color.LightGray, 0.3f)); + } + + DrawText(filePaths[i], 120, 100 + 40 * i, 10, Color.Gray); + } + + DrawText("Drop new files...", 100, 110 + 40 * filePaths.Count, 20, Color.DarkGray); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [core] example - drop files"); - string[] files = new string[0]; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new DropFiles(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (IsFileDropped()) - { - files = Raylib.GetDroppedFiles(); - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - if (files.Length == 0) - { - DrawText("Drop your files to this window!", 100, 40, 20, Color.DarkGray); - } - else - { - DrawText("Dropped files:", 100, 40, 20, Color.DarkGray); - - for (int i = 0; i < files.Length; i++) - { - if (i % 2 == 0) - { - DrawRectangle(0, 85 + 40 * i, screenWidth, 40, ColorAlpha(Color.LightGray, 0.5f)); - } - else - { - DrawRectangle(0, 85 + 40 * i, screenWidth, 40, ColorAlpha(Color.LightGray, 0.3f)); - } - DrawText(files[i], 120, 100 + 40 * i, 10, Color.Gray); - } - - DrawText("Drop new files...", 100, 110 + 40 * files.Length, 20, Color.DarkGray); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; 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/InputGamepad.cs b/Examples/Core/InputGamepad.cs index efd2ffb..fc1b915 100644 --- a/Examples/Core/InputGamepad.cs +++ b/Examples/Core/InputGamepad.cs @@ -1,32 +1,34 @@ /******************************************************************************************* - * - * raylib [core] example - input gamepad - * - * Example complexity rating: [★☆☆☆] 1/4 - * - * NOTE: This example requires a Gamepad connected to the system - * raylib is configured to work with the following gamepads: - * - Xbox 360 Controller (Xbox 360, Xbox One) - * - PLAYSTATION(R)3 Controller - * Check raylib.h for buttons configuration - * - * Example originally created with raylib 1.1, 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) - * - ********************************************************************************************/ - +* +* raylib [core] example - input gamepad +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* NOTE: This example requires a Gamepad connected to the system +* raylib is configured to work with the following gamepads: +* - Xbox 360 Controller (Xbox 360, Xbox One) +* - PLAYSTATION(R)3 Controller +* Check raylib.h for buttons configuration +* +* Example originally created with raylib 1.1, 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.Numerics; using static Raylib_cs.Raylib; namespace Examples.Core; -public class InputGamepad +public partial class InputGamepad : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + // NOTE: Gamepad name ID depends on drivers and OS // These are some possible names the gamepads could have. public const string XBOX_ALIAS_1 = "xbox"; @@ -34,320 +36,503 @@ public class InputGamepad public const string PS_ALIAS_1 = "playstation"; public const string PS_ALIAS_2 = "sony"; + // Set axis deadzones + private const float leftStickDeadzoneX = 0.1f; + private const float leftStickDeadzoneY = 0.1f; + private const float rightStickDeadzoneX = 0.1f; + private const float rightStickDeadzoneY = 0.1f; + private const float leftTriggerDeadzone = -0.9f; + private const float rightTriggerDeadzone = -0.9f; + + private Texture2D texPs3Pad; + private Texture2D texXboxPad; + + private Rectangle vibrateButton; + + private int gamepad; // which gamepad to display + + public string Name => "Core / Input Gamepad"; + + public string Title => "raylib [core] example - input gamepad"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + public void Init() + { + texPs3Pad = LoadTexture("resources/ps3.png"); + texXboxPad = LoadTexture("resources/xbox.png"); + + vibrateButton = new Rectangle(); + + gamepad = 0; // which gamepad to display + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.Left) && gamepad > 0) + { + gamepad--; + } + + if (IsKeyPressed(KeyboardKey.Right)) + { + gamepad++; + } + + var mousePosition = GetMousePosition(); + + vibrateButton = new Rectangle(10, 70.0f + 20 * GetGamepadAxisCount(gamepad) + 20, 75, 24); + if (IsMouseButtonPressed(MouseButton.Left) && CheckCollisionPointRec(mousePosition, vibrateButton)) + { + SetGamepadVibration(gamepad, 1.0f, 1.0f, 1.0f); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + if (IsGamepadAvailable(gamepad)) + { + var gamepadName = GetGamepadName_(gamepad); + DrawText($"GP{gamepad}: {gamepadName}", 10, 10, 10, Color.Black); + + // Get axis values + var leftStickX = GetGamepadAxisMovement(gamepad, GamepadAxis.LeftX); + var leftStickY = GetGamepadAxisMovement(gamepad, GamepadAxis.LeftY); + var rightStickX = GetGamepadAxisMovement(gamepad, GamepadAxis.RightX); + var rightStickY = GetGamepadAxisMovement(gamepad, GamepadAxis.RightY); + var leftTrigger = GetGamepadAxisMovement(gamepad, GamepadAxis.LeftTrigger); + var rightTrigger = GetGamepadAxisMovement(gamepad, GamepadAxis.RightTrigger); + + // Calculate deadzones + if (leftStickX > -leftStickDeadzoneX && leftStickX < leftStickDeadzoneX) + { + leftStickX = 0.0f; + } + + if (leftStickY > -leftStickDeadzoneY && leftStickY < leftStickDeadzoneY) + { + leftStickY = 0.0f; + } + + if (rightStickX > -rightStickDeadzoneX && rightStickX < rightStickDeadzoneX) + { + rightStickX = 0.0f; + } + + if (rightStickY > -rightStickDeadzoneY && rightStickY < rightStickDeadzoneY) + { + rightStickY = 0.0f; + } + + if (leftTrigger < leftTriggerDeadzone) + { + leftTrigger = -1.0f; + } + + if (rightTrigger < rightTriggerDeadzone) + { + rightTrigger = -1.0f; + } + + if (gamepadName.Contains(XBOX_ALIAS_1, StringComparison.OrdinalIgnoreCase) || + gamepadName.Contains(XBOX_ALIAS_2, StringComparison.OrdinalIgnoreCase)) + { + DrawTexture(texXboxPad, 0, 0, Color.DarkGray); + + // Draw buttons: xbox home + if (IsGamepadButtonDown(gamepad, GamepadButton.Middle)) + { + DrawCircle(394, 89, 19, Color.Red); + } + + // Draw buttons: basic + if (IsGamepadButtonDown(gamepad, GamepadButton.MiddleRight)) + { + DrawCircle(436, 150, 9, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.MiddleLeft)) + { + DrawCircle(352, 150, 9, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceLeft)) + { + DrawCircle(501, 151, 15, Color.Blue); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceDown)) + { + DrawCircle(536, 187, 15, Color.Lime); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceRight)) + { + DrawCircle(572, 151, 15, Color.Maroon); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceUp)) + { + DrawCircle(536, 115, 15, Color.Gold); + } + + // Draw buttons: d-pad + DrawRectangle(317, 202, 19, 71, Color.Black); + DrawRectangle(293, 228, 69, 19, Color.Black); + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceUp)) + { + DrawRectangle(317, 202, 19, 26, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceDown)) + { + DrawRectangle(317, 202 + 45, 19, 26, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceLeft)) + { + DrawRectangle(292, 228, 25, 19, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceRight)) + { + DrawRectangle(292 + 44, 228, 26, 19, Color.Red); + } + + // Draw buttons: left-right back + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftTrigger1)) + { + DrawCircle(259, 61, 20, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.RightTrigger1)) + { + DrawCircle(536, 61, 20, Color.Red); + } + + // Draw axis: left joystick + var leftGamepadColor = Color.Black; + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftThumb)) + { + leftGamepadColor = Color.Red; + } + + DrawCircle(259, 152, 39, Color.Black); + DrawCircle(259, 152, 34, Color.LightGray); + DrawCircle(259 + (int)(leftStickX * 20), 152 + (int)(leftStickY * 20), 25, leftGamepadColor); + + // Draw axis: right joystick + var rightGamepadColor = Color.Black; + if (IsGamepadButtonDown(gamepad, GamepadButton.RightThumb)) + { + rightGamepadColor = Color.Red; + } + + DrawCircle(461, 237, 38, Color.Black); + DrawCircle(461, 237, 33, Color.LightGray); + DrawCircle(461 + (int)(rightStickX * 20), 237 + (int)(rightStickY * 20), 25, rightGamepadColor); + + // Draw axis: left-right triggers + DrawRectangle(170, 30, 15, 70, Color.Gray); + DrawRectangle(604, 30, 15, 70, Color.Gray); + DrawRectangle(170, 30, 15, (int)(((1 + leftTrigger) / 2) * 70), Color.Red); + DrawRectangle(604, 30, 15, (int)(((1 + rightTrigger) / 2) * 70), Color.Red); + } + else if (gamepadName.Contains(PS_ALIAS_1, StringComparison.OrdinalIgnoreCase) || gamepadName.Contains(PS_ALIAS_2, StringComparison.OrdinalIgnoreCase)) + { + DrawTexture(texPs3Pad, 0, 0, Color.DarkGray); + + // Draw buttons: ps + if (IsGamepadButtonDown(gamepad, GamepadButton.Middle)) + { + DrawCircle(396, 222, 13, Color.Red); + } + + // Draw buttons: basic + if (IsGamepadButtonDown(gamepad, GamepadButton.MiddleLeft)) + { + DrawRectangle(328, 170, 32, 13, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.MiddleRight)) + { + DrawTriangle( + new Vector2(436, 168), + new Vector2(436, 185), + new Vector2(464, 177), + Color.Red + ); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceUp)) + { + DrawCircle(557, 144, 13, Color.Lime); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceRight)) + { + DrawCircle(586, 173, 13, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceDown)) + { + DrawCircle(557, 203, 13, Color.Violet); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceLeft)) + { + DrawCircle(527, 173, 13, Color.Pink); + } + + // Draw buttons: d-pad + DrawRectangle(225, 132, 24, 84, Color.Black); + DrawRectangle(195, 161, 84, 25, Color.Black); + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceUp)) + { + DrawRectangle(225, 132, 24, 29, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceDown)) + { + DrawRectangle(225, 132 + 54, 24, 30, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceLeft)) + { + DrawRectangle(195, 161, 30, 25, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceRight)) + { + DrawRectangle(195 + 54, 161, 30, 25, Color.Red); + } + + // Draw buttons: left-right back buttons + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftTrigger1)) + { + DrawCircle(239, 82, 20, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.RightTrigger1)) + { + DrawCircle(557, 82, 20, Color.Red); + } + + // Draw axis: left joystick + var leftGamepadColor = Color.Black; + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftThumb)) + { + leftGamepadColor = Color.Red; + } + + DrawCircle(319, 255, 35, Color.Black); + DrawCircle(319, 255, 31, Color.LightGray); + DrawCircle(319 + (int)(leftStickX * 20), 255 + (int)(leftStickY * 20), 25, leftGamepadColor); + + // Draw axis: right joystick + var rightGamepadColor = Color.Black; + if (IsGamepadButtonDown(gamepad, GamepadButton.RightThumb)) + { + rightGamepadColor = Color.Red; + } + + DrawCircle(475, 255, 35, Color.Black); + DrawCircle(475, 255, 31, Color.LightGray); + DrawCircle(475 + (int)(rightStickX * 20), 255 + (int)(rightStickY * 20), 25, rightGamepadColor); + + // Draw axis: left-right triggers + DrawRectangle(169, 48, 15, 70, Color.Gray); + DrawRectangle(611, 48, 15, 70, Color.Gray); + DrawRectangle(169, 48, 15, (int)(((1 + leftTrigger) / 2) * 70), Color.Red); + DrawRectangle(611, 48, 15, (int)(((1 + rightTrigger) / 2) * 70), Color.Red); + } + else + { + // Draw background: generic + DrawRectangleRounded(new Rectangle(175, 110, 460, 220), 0.3f, 16, Color.DarkGray); + + // Draw buttons: basic + DrawCircle(365, 170, 12, Color.RayWhite); + DrawCircle(405, 170, 12, Color.RayWhite); + DrawCircle(445, 170, 12, Color.RayWhite); + DrawCircle(516, 191, 17, Color.RayWhite); + DrawCircle(551, 227, 17, Color.RayWhite); + DrawCircle(587, 191, 17, Color.RayWhite); + DrawCircle(551, 155, 17, Color.RayWhite); + if (IsGamepadButtonDown(gamepad, GamepadButton.MiddleLeft)) + { + DrawCircle(365, 170, 10, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.Middle)) + { + DrawCircle(405, 170, 10, Color.Green); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.MiddleRight)) + { + DrawCircle(445, 170, 10, Color.Blue); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceLeft)) + { + DrawCircle(516, 191, 15, Color.Gold); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceDown)) + { + DrawCircle(551, 227, 15, Color.Blue); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceRight)) + { + DrawCircle(587, 191, 15, Color.Green); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceUp)) + { + DrawCircle(551, 155, 15, Color.Red); + } + + // Draw buttons: d-pad + DrawRectangle(245, 145, 28, 88, Color.RayWhite); + DrawRectangle(215, 174, 88, 29, Color.RayWhite); + DrawRectangle(247, 147, 24, 84, Color.Black); + DrawRectangle(217, 176, 84, 25, Color.Black); + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceUp)) + { + DrawRectangle(247, 147, 24, 29, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceDown)) + { + DrawRectangle(247, 147 + 54, 24, 30, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceLeft)) + { + DrawRectangle(217, 176, 30, 25, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceRight)) + { + DrawRectangle(217 + 54, 176, 30, 25, Color.Red); + } + + // Draw buttons: left-right back + DrawRectangleRounded(new Rectangle(215, 98, 100, 10), 0.5f, 16, Color.DarkGray); + DrawRectangleRounded(new Rectangle(495, 98, 100, 10), 0.5f, 16, Color.DarkGray); + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftTrigger1)) + { + DrawRectangleRounded(new Rectangle(215, 98, 100, 10), 0.5f, 16, Color.Red); + } + + if (IsGamepadButtonDown(gamepad, GamepadButton.RightTrigger1)) + { + DrawRectangleRounded(new Rectangle(495, 98, 100, 10), 0.5f, 16, Color.Red); + } + + // Draw axis: left joystick + var leftGamepadColor = Color.Black; + if (IsGamepadButtonDown(gamepad, GamepadButton.LeftThumb)) + { + leftGamepadColor = Color.Red; + } + + DrawCircle(345, 260, 40, Color.Black); + DrawCircle(345, 260, 35, Color.LightGray); + DrawCircle(345 + (int)(leftStickX * 20), 260 + (int)(leftStickY * 20), 25, leftGamepadColor); + + // Draw axis: right joystick + var rightGamepadColor = Color.Black; + if (IsGamepadButtonDown(gamepad, GamepadButton.RightThumb)) + { + rightGamepadColor = Color.Red; + } + + DrawCircle(465, 260, 40, Color.Black); + DrawCircle(465, 260, 35, Color.LightGray); + DrawCircle(465 + (int)(rightStickX * 20), 260 + (int)(rightStickY * 20), 25, rightGamepadColor); + + // Draw axis: left-right triggers + DrawRectangle(151, 110, 15, 70, Color.Gray); + DrawRectangle(644, 110, 15, 70, Color.Gray); + DrawRectangle(151, 110, 15, (int)(((1 + leftTrigger) / 2) * 70), Color.Red); + DrawRectangle(644, 110, 15, (int)(((1 + rightTrigger) / 2) * 70), Color.Red); + } + + DrawText($"DETECTED AXIS [{GetGamepadAxisCount(gamepad)}]:", 10, 50, 10, Color.Maroon); + + for (var i = 0; i < GetGamepadAxisCount(gamepad); i++) + { + DrawText( + $"AXIS {i}: {GetGamepadAxisMovement(gamepad, (GamepadAxis)i):F2}", + 20, + 70 + 20 * i, + 10, + Color.DarkGray + ); + } + + // Draw vibrate button + DrawRectangleRec(vibrateButton, Color.SkyBlue); + DrawText("VIBRATE", (int)(vibrateButton.X + 14), (int)(vibrateButton.Y + 1), 10, Color.DarkGray); + + if (GetGamepadButtonPressed() != (int)GamepadButton.Unknown) + { + DrawText($"DETECTED BUTTON: {GetGamepadButtonPressed()}", 10, 430, 10, Color.Red); + } + else + { + DrawText("DETECTED BUTTON: NONE", 10, 430, 10, Color.Gray); + } + } + else + { + DrawText($"GP{gamepad}: NOT DETECTED", 10, 10, 10, Color.Gray); + DrawTexture(texXboxPad, 0, 0, Color.LightGray); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texPs3Pad); + UnloadTexture(texXboxPad); + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + SetConfigFlags(ConfigFlags.Msaa4xHint); // Set MSAA 4X hint before windows creation - // Set MSAA 4X hint before windows creation - SetConfigFlags(ConfigFlags.Msaa4xHint); - InitWindow(screenWidth, screenHeight, "raylib [core] example - gamepad input"); + InitWindow(screenWidth, screenHeight, "raylib [core] example - input gamepad"); - Texture2D texPs3Pad = LoadTexture("resources/ps3.png"); - Texture2D texXboxPad = LoadTexture("resources/xbox.png"); - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- - int gamepad = 0; - Rectangle vibrateButton = new Rectangle(); + var game = new InputGamepad(); + game.Init(); // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (IsKeyPressed(KeyboardKey.Left) && gamepad > 0) - { - gamepad--; - } - - if (IsKeyPressed(KeyboardKey.Right)) - { - gamepad++; - } - - Vector2 mousePosition = GetMousePosition(); - - vibrateButton = new Rectangle(10, 70.0f + 20 * GetGamepadAxisCount(gamepad) + 20, 75, 24); - if (IsMouseButtonPressed(MouseButton.Left) && CheckCollisionPointRec(mousePosition, vibrateButton)) - { - SetGamepadVibration(gamepad, 1.0f, 1.0f, 1.0f); - } - - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - if (IsGamepadAvailable(gamepad)) - { - string gamepadName = GetGamepadName_(gamepad); - DrawText($"GP{gamepad}: {gamepadName}", 10, 10, 10, Color.Black); - - if (gamepadName.Contains(XBOX_ALIAS_1, StringComparison.OrdinalIgnoreCase) || - gamepadName.Contains(XBOX_ALIAS_2, StringComparison.OrdinalIgnoreCase)) - { - DrawTexture(texXboxPad, 0, 0, Color.DarkGray); - - // Draw buttons: xbox home - if (IsGamepadButtonDown(gamepad, GamepadButton.Middle)) - { - DrawCircle(394, 89, 19, Color.Red); - } - - // Draw buttons: basic - if (IsGamepadButtonDown(gamepad, GamepadButton.MiddleRight)) - { - DrawCircle(436, 150, 9, Color.Red); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.MiddleLeft)) - { - DrawCircle(352, 150, 9, Color.Red); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceLeft)) - { - DrawCircle(501, 151, 15, Color.Blue); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceDown)) - { - DrawCircle(536, 187, 15, Color.Lime); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceRight)) - { - DrawCircle(572, 151, 15, Color.Maroon); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceUp)) - { - DrawCircle(536, 115, 15, Color.Gold); - } - - // Draw buttons: d-pad - DrawRectangle(317, 202, 19, 71, Color.Black); - DrawRectangle(293, 228, 69, 19, Color.Black); - if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceUp)) - { - DrawRectangle(317, 202, 19, 26, Color.Red); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceDown)) - { - DrawRectangle(317, 202 + 45, 19, 26, Color.Red); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceLeft)) - { - DrawRectangle(292, 228, 25, 19, Color.Red); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceRight)) - { - DrawRectangle(292 + 44, 228, 26, 19, Color.Red); - } - - // Draw buttons: left-right back - if (IsGamepadButtonDown(gamepad, GamepadButton.LeftTrigger1)) - { - DrawCircle(259, 61, 20, Color.Red); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.RightTrigger1)) - { - DrawCircle(536, 61, 20, Color.Red); - } - - // Draw axis: left joystick - DrawCircle(259, 152, 39, Color.Black); - DrawCircle(259, 152, 34, Color.LightGray); - DrawCircle( - 259 + (int)(GetGamepadAxisMovement(gamepad, GamepadAxis.LeftX) * 20), - 152 + (int)(GetGamepadAxisMovement(gamepad, GamepadAxis.LeftY) * 20), - 25, - Color.Black - ); - - // Draw axis: right joystick - DrawCircle(461, 237, 38, Color.Black); - DrawCircle(461, 237, 33, Color.LightGray); - DrawCircle( - 461 + (int)(GetGamepadAxisMovement(gamepad, GamepadAxis.RightX) * 20), - 237 + (int)(GetGamepadAxisMovement(gamepad, GamepadAxis.RightY) * 20), - 25, Color.Black - ); - - // Draw axis: left-right triggers - float leftTriggerX = GetGamepadAxisMovement(gamepad, GamepadAxis.LeftTrigger); - float rightTriggerX = GetGamepadAxisMovement(gamepad, GamepadAxis.RightTrigger); - DrawRectangle(170, 30, 15, 70, Color.Gray); - DrawRectangle(604, 30, 15, 70, Color.Gray); - DrawRectangle(170, 30, 15, (int)(((1.0f + leftTriggerX) / 2.0f) * 70), Color.Red); - DrawRectangle(604, 30, 15, (int)(((1.0f + rightTriggerX) / 2.0f) * 70), Color.Red); - } - else if (gamepadName.Contains(PS_ALIAS_1, StringComparison.OrdinalIgnoreCase) || gamepadName.Contains(PS_ALIAS_2, StringComparison.OrdinalIgnoreCase)) - { - DrawTexture(texPs3Pad, 0, 0, Color.DarkGray); - - // Draw buttons: ps - if (IsGamepadButtonDown(gamepad, GamepadButton.Middle)) - { - DrawCircle(396, 222, 13, Color.Red); - } - - // Draw buttons: basic - if (IsGamepadButtonDown(gamepad, GamepadButton.MiddleLeft)) - { - DrawRectangle(328, 170, 32, 13, Color.Red); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.MiddleRight)) - { - DrawTriangle( - new Vector2(436, 168), - new Vector2(436, 185), - new Vector2(464, 177), - Color.Red - ); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceUp)) - { - DrawCircle(557, 144, 13, Color.Lime); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceRight)) - { - DrawCircle(586, 173, 13, Color.Red); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceDown)) - { - DrawCircle(557, 203, 13, Color.Violet); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.RightFaceLeft)) - { - DrawCircle(527, 173, 13, Color.Pink); - } - - // Draw buttons: d-pad - DrawRectangle(225, 132, 24, 84, Color.Black); - DrawRectangle(195, 161, 84, 25, Color.Black); - if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceUp)) - { - DrawRectangle(225, 132, 24, 29, Color.Red); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceDown)) - { - DrawRectangle(225, 132 + 54, 24, 30, Color.Red); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceLeft)) - { - DrawRectangle(195, 161, 30, 25, Color.Red); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.LeftFaceRight)) - { - DrawRectangle(195 + 54, 161, 30, 25, Color.Red); - } - - // Draw buttons: left-right back buttons - if (IsGamepadButtonDown(gamepad, GamepadButton.LeftTrigger1)) - { - DrawCircle(239, 82, 20, Color.Red); - } - - if (IsGamepadButtonDown(gamepad, GamepadButton.RightTrigger1)) - { - DrawCircle(557, 82, 20, Color.Red); - } - - // Draw axis: left joystick - DrawCircle(319, 255, 35, Color.Black); - DrawCircle(319, 255, 31, Color.LightGray); - DrawCircle( - 319 + (int)(GetGamepadAxisMovement(gamepad, GamepadAxis.LeftX) * 20), - 255 + (int)(GetGamepadAxisMovement(gamepad, GamepadAxis.LeftY) * 20), - 25, - Color.Black - ); - - // Draw axis: right joystick - DrawCircle(475, 255, 35, Color.Black); - DrawCircle(475, 255, 31, Color.LightGray); - DrawCircle( - 475 + (int)(GetGamepadAxisMovement(gamepad, GamepadAxis.RightX) * 20), - 255 + (int)(GetGamepadAxisMovement(gamepad, GamepadAxis.RightY) * 20), - 25, - Color.Black - ); - - // Draw axis: left-right triggers - float leftTriggerX = GetGamepadAxisMovement(gamepad, GamepadAxis.LeftTrigger); - float rightTriggerX = GetGamepadAxisMovement(gamepad, GamepadAxis.RightTrigger); - DrawRectangle(169, 48, 15, 70, Color.Gray); - DrawRectangle(611, 48, 15, 70, Color.Gray); - DrawRectangle(169, 48, 15, (int)(((1.0f - leftTriggerX) / 2.0f) * 70), Color.Red); - DrawRectangle(611, 48, 15, (int)(((1.0f - rightTriggerX) / 2.0f) * 70), Color.Red); - } - else - { - DrawText("- GENERIC GAMEPAD -", 280, 180, 20, Color.Gray); - // TODO: Draw generic gamepad - } - - DrawText($"DETECTED AXIS [{GetGamepadAxisCount(gamepad)}]:", 10, 50, 10, Color.Maroon); - - for (int i = 0; i < GetGamepadAxisCount(gamepad); i++) - { - DrawText( - $"AXIS {i}: {GetGamepadAxisMovement(gamepad, (GamepadAxis)i)}", - 20, - 70 + 20 * i, - 10, - Color.DarkGray - ); - } - - DrawRectangleRec(vibrateButton, Color.SkyBlue); - DrawText("VIBRATE", (int)(vibrateButton.X + 14), (int)(vibrateButton.Y + 1), 10, Color.DarkGray); - - - if (GetGamepadButtonPressed() != (int)GamepadButton.Unknown) - { - DrawText($"DETECTED BUTTON: {GetGamepadButtonPressed()}", 10, 430, 10, Color.Red); - } - else - { - DrawText("DETECTED BUTTON: NONE", 10, 430, 10, Color.Gray); - } - } - else - { - DrawText($"GP{gamepad}: NOT DETECTED", 10, 10, 10, Color.Gray); - DrawTexture(texXboxPad, 0, 0, Color.LightGray); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(texPs3Pad); - UnloadTexture(texXboxPad); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Core/InputGestures.cs b/Examples/Core/InputGestures.cs index fda7d5c..50e7edf 100644 --- a/Examples/Core/InputGestures.cs +++ b/Examples/Core/InputGestures.cs @@ -18,145 +18,176 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class InputGestures +public partial class InputGestures : IExample { public const int MaxGestureStrings = 20; + private const int screenWidth = 800; + private const int screenHeight = 450; + + private Vector2 touchPosition; + private Rectangle touchArea; + + private int gesturesCount; + private string[] gestureStrings; + + private Gesture currentGesture; + private Gesture lastGesture; + + public string Name => "Core / Input Gestures"; + + public string Title => "raylib [core] example - input gestures"; + + public void Init() + { + touchPosition = new(0, 0); + touchArea = new(220, 10, screenWidth - 230, screenHeight - 20); + + gesturesCount = 0; + gestureStrings = new string[MaxGestureStrings]; + + currentGesture = Gesture.None; + lastGesture = Gesture.None; + + // SetGesturesEnabled(0b0000000000001001); // Enable only some gestures to be detected + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + lastGesture = currentGesture; + currentGesture = GetGestureDetected(); + touchPosition = GetTouchPosition(0); + + if (CheckCollisionPointRec(touchPosition, touchArea) && (currentGesture != Gesture.None)) + { + if (currentGesture != lastGesture) + { + // Store gesture string + switch ((Gesture)currentGesture) + { + case Gesture.Tap: + gestureStrings[gesturesCount] = "GESTURE TAP"; + break; + case Gesture.DoubleTap: + gestureStrings[gesturesCount] = "GESTURE DOUBLETAP"; + break; + case Gesture.Hold: + gestureStrings[gesturesCount] = "GESTURE HOLD"; + break; + case Gesture.Drag: + gestureStrings[gesturesCount] = "GESTURE DRAG"; + break; + case Gesture.SwipeRight: + gestureStrings[gesturesCount] = "GESTURE SWIPE RIGHT"; + break; + case Gesture.SwipeLeft: + gestureStrings[gesturesCount] = "GESTURE SWIPE LEFT"; + break; + case Gesture.SwipeUp: + gestureStrings[gesturesCount] = "GESTURE SWIPE UP"; + break; + case Gesture.SwipeDown: + gestureStrings[gesturesCount] = "GESTURE SWIPE DOWN"; + break; + case Gesture.PinchIn: + gestureStrings[gesturesCount] = "GESTURE PINCH IN"; + break; + case Gesture.PinchOut: + gestureStrings[gesturesCount] = "GESTURE PINCH OUT"; + break; + default: + break; + } + + gesturesCount++; + + // Reset gestures strings + if (gesturesCount >= MaxGestureStrings) + { + for (var i = 0; i < MaxGestureStrings; i++) + { + gestureStrings[i] = " "; + } + gesturesCount = 0; + } + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawRectangleRec(touchArea, Color.Gray); + DrawRectangle(225, 15, screenWidth - 240, screenHeight - 30, Color.RayWhite); + + DrawText("GESTURES TEST AREA", screenWidth - 270, screenHeight - 40, 20, Fade(Color.Gray, 0.5f)); + + for (var i = 0; i < gesturesCount; i++) + { + if (i % 2 == 0) + { + DrawRectangle(10, 30 + 20 * i, 200, 20, Fade(Color.LightGray, 0.5f)); + } + else + { + DrawRectangle(10, 30 + 20 * i, 200, 20, Fade(Color.LightGray, 0.3f)); + } + + if (i < gesturesCount - 1) + { + DrawText(gestureStrings[i], 35, 36 + 20 * i, 10, Color.DarkGray); + } + else + { + DrawText(gestureStrings[i], 35, 36 + 20 * i, 10, Color.Maroon); + } + } + + DrawRectangleLines(10, 29, 200, screenHeight - 50, Color.Gray); + DrawText("DETECTED GESTURES", 50, 15, 10, Color.Gray); + + if (currentGesture != Gesture.None) + { + DrawCircleV(touchPosition, 30, Color.Maroon); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [core] example - input gestures"); - Vector2 touchPosition = new(0, 0); - Rectangle touchArea = new(220, 10, screenWidth - 230, screenHeight - 20); - - int gesturesCount = 0; - string[] gestureStrings = new string[MaxGestureStrings]; - - Gesture currentGesture = Gesture.None; - Gesture lastGesture = Gesture.None; - - // SetGesturesEnabled(0b0000000000001001); // Enable only some gestures to be detected - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new InputGestures(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - lastGesture = currentGesture; - currentGesture = GetGestureDetected(); - touchPosition = GetTouchPosition(0); - - if (CheckCollisionPointRec(touchPosition, touchArea) && (currentGesture != Gesture.None)) - { - if (currentGesture != lastGesture) - { - // Store gesture string - switch ((Gesture)currentGesture) - { - case Gesture.Tap: - gestureStrings[gesturesCount] = "GESTURE TAP"; - break; - case Gesture.DoubleTap: - gestureStrings[gesturesCount] = "GESTURE DOUBLETAP"; - break; - case Gesture.Hold: - gestureStrings[gesturesCount] = "GESTURE HOLD"; - break; - case Gesture.Drag: - gestureStrings[gesturesCount] = "GESTURE DRAG"; - break; - case Gesture.SwipeRight: - gestureStrings[gesturesCount] = "GESTURE SWIPE RIGHT"; - break; - case Gesture.SwipeLeft: - gestureStrings[gesturesCount] = "GESTURE SWIPE LEFT"; - break; - case Gesture.SwipeUp: - gestureStrings[gesturesCount] = "GESTURE SWIPE UP"; - break; - case Gesture.SwipeDown: - gestureStrings[gesturesCount] = "GESTURE SWIPE DOWN"; - break; - case Gesture.PinchIn: - gestureStrings[gesturesCount] = "GESTURE PINCH IN"; - break; - case Gesture.PinchOut: - gestureStrings[gesturesCount] = "GESTURE PINCH OUT"; - break; - default: - break; - } - - gesturesCount++; - - // Reset gestures strings - if (gesturesCount >= MaxGestureStrings) - { - for (int i = 0; i < MaxGestureStrings; i++) - { - gestureStrings[i] = " "; - } - gesturesCount = 0; - } - } - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawRectangleRec(touchArea, Color.Gray); - DrawRectangle(225, 15, screenWidth - 240, screenHeight - 30, Color.RayWhite); - - DrawText("GESTURES TEST AREA", screenWidth - 270, screenHeight - 40, 20, ColorAlpha(Color.Gray, 0.5f)); - - for (int i = 0; i < gesturesCount; i++) - { - if (i % 2 == 0) - { - DrawRectangle(10, 30 + 20 * i, 200, 20, ColorAlpha(Color.LightGray, 0.5f)); - } - else - { - DrawRectangle(10, 30 + 20 * i, 200, 20, ColorAlpha(Color.LightGray, 0.3f)); - } - - if (i < gesturesCount - 1) - { - DrawText(gestureStrings[i], 35, 36 + 20 * i, 10, Color.DarkGray); - } - else - { - DrawText(gestureStrings[i], 35, 36 + 20 * i, 10, Color.Maroon); - } - } - - DrawRectangleLines(10, 29, 200, screenHeight - 50, Color.Gray); - DrawText("DETECTED GESTURES", 50, 15, 10, Color.Gray); - - if (currentGesture != Gesture.None) - { - DrawCircleV(touchPosition, 30, Color.Maroon); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- + return 0; } } diff --git a/Examples/Core/InputGesturesTestBed.cs b/Examples/Core/InputGesturesTestBed.cs index 893c5dd..ee3d883 100644 --- a/Examples/Core/InputGesturesTestBed.cs +++ b/Examples/Core/InputGesturesTestBed.cs @@ -1,406 +1,454 @@ /******************************************************************************************* - * - * raylib [core] example - input gestures testbed - * - * Example complexity rating: [★★★☆] 3/4 - * - * Example originally created with raylib 5.0, last time updated with raylib 6.0 - * - * Example contributed by ubkp (@ubkp) 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 ubkp (@ubkp) - * - ********************************************************************************************/ +* +* raylib [core] example - input gestures testbed +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 5.0, last time updated with raylib 6.0 +* +* Example contributed by ubkp (@ubkp) 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 ubkp (@ubkp) +* +********************************************************************************************/ +using System; using System.Numerics; +using static Raylib_cs.Raylib; namespace Examples.Core; -using static Raylib_cs.Raylib; - -public class InputGesturesTestBed +public partial class InputGesturesTestBed : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + public const int GESTURE_LOG_SIZE = 20; public const int MAX_TOUCH_COUNT = 32; - //------------------------------------------------------------------------------------ - // Program main entry point - //------------------------------------------------------------------------------------ + public string Name => "Core / Input Gestures Test Bed"; - public static int Main() + public string Title => "raylib [core] example - input gestures testbed"; + + private Vector2 messagePosition; + + // Last gesture variables definitions + private Gesture lastGesture; + private Vector2 lastGesturePosition; + + // Gesture log variables definitions + // NOTE: The gesture log uses an array (as an inverted circular queue) to store the performed gestures + private string[] gestureLog; + + // NOTE: The index for the inverted circular queue (moving from last to first direction, then looping around) + private int gestureLogIndex; + private Gesture previousGesture; + + // Log mode values: + // - 0 shows repeated events + // - 1 hides repeated events + // - 2 shows repeated events but hide hold events + // - 3 hides repeated events and hide hold events + private int logMode; + + private Color gestureColor; + private Rectangle logButton1; + private Rectangle logButton2; + private Vector2 gestureLogPosition; + + // Protractor variables definitions + private float angleLength; + private float currentAngleDegrees; + private Vector2 finalVector; + private Vector2 protractorPosition; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [core] example - input gestures testbed"); - - Vector2 messagePosition = new Vector2(160, 7); + messagePosition = new Vector2(160, 7); // Last gesture variables definitions - Gesture lastGesture = 0; - Vector2 lastGesturePosition = new Vector2(165, 130); + lastGesture = 0; + lastGesturePosition = new Vector2(165, 130); // Gesture log variables definitions // NOTE: The gesture log uses an array (as an inverted circular queue) to store the performed gestures - string[] gestureLog = new string[GESTURE_LOG_SIZE + 1]; - for (int i = 0; i < GESTURE_LOG_SIZE; i++) + gestureLog = new string[GESTURE_LOG_SIZE + 1]; + for (var i = 0; i < GESTURE_LOG_SIZE; i++) { gestureLog[i] = new string(new char[12]); } - ; + // NOTE: The index for the inverted circular queue (moving from last to first direction, then looping around) - int gestureLogIndex = GESTURE_LOG_SIZE; - Gesture previousGesture = 0; + gestureLogIndex = GESTURE_LOG_SIZE; + previousGesture = 0; // Log mode values: // - 0 shows repeated events // - 1 hides repeated events // - 2 shows repeated events but hide hold events // - 3 hides repeated events and hide hold events - int logMode = 1; + logMode = 1; - Color gestureColor = new Color(0, 0, 0, 255); - Rectangle logButton1 = new Rectangle(53, 7, 48, 26); - Rectangle logButton2 = new Rectangle(108, 7, 36, 26); - Vector2 gestureLogPosition = new Vector2(10, 10); + gestureColor = new Color(0, 0, 0, 255); + logButton1 = new Rectangle(53, 7, 48, 26); + logButton2 = new Rectangle(108, 7, 36, 26); + gestureLogPosition = new Vector2(10, 10); // Protractor variables definitions - float angleLength = 90.0f; - float currentAngleDegrees = 0.0f; - Vector2 finalVector = new Vector2(0.0f, 0.0f); - Vector2 protractorPosition = new Vector2(266.0f, 315.0f); + angleLength = 90.0f; + currentAngleDegrees = 0.0f; + finalVector = new Vector2(0.0f, 0.0f); + protractorPosition = new Vector2(266.0f, 315.0f); + } - SetTargetFPS(60); // Set our game to run at 60 frames-per-second + public void Update() + { + // Update //-------------------------------------------------------------------------------------- + // Handle common gestures data + int i, ii; // Iterators that will be reused by all for loops + var currentGesture = GetGestureDetected(); + var currentDragDegrees = GetGestureDragAngle(); + var currentPitchDegrees = GetGesturePinchAngle(); + var touchCount = GetTouchPointCount(); - // Main game loop - while (!WindowShouldClose()) // Detect window close button or ESC key + // Handle last gesture + if ((currentGesture != 0) && ((int)currentGesture != 4) && (currentGesture != previousGesture)) { - // Update - //-------------------------------------------------------------------------------------- - // Handle common gestures data - int i, ii; // Iterators that will be reused by all for loops - Gesture currentGesture = GetGestureDetected(); - float currentDragDegrees = GetGestureDragAngle(); - float currentPitchDegrees = GetGesturePinchAngle(); - int touchCount = GetTouchPointCount(); + lastGesture = currentGesture; // Filter the meaningful gestures (1, 2, 8 to 512) for the display + } - // Handle last gesture - if ((currentGesture != 0) && ((int)currentGesture != 4) && (currentGesture != previousGesture)) + // Handle gesture log + if (IsMouseButtonReleased(MouseButton.Left)) + { + if (CheckCollisionPointRec(GetMousePosition(), logButton1)) { - lastGesture = currentGesture; // Filter the meaningful gestures (1, 2, 8 to 512) for the display - } - - // Handle gesture log - if (IsMouseButtonReleased(MouseButton.Left)) - { - if (CheckCollisionPointRec(GetMousePosition(), logButton1)) + switch (logMode) { - switch (logMode) - { - case 3: - logMode = 2; - break; - case 2: - logMode = 3; - break; - case 1: - logMode = 0; - break; - default: - logMode = 1; - break; - } - } - else if (CheckCollisionPointRec(GetMousePosition(), logButton2)) - { - switch (logMode) - { - case 3: - logMode = 1; - break; - case 2: - logMode = 0; - break; - case 1: - logMode = 3; - break; - default: - logMode = 2; - break; - } + case 3: + logMode = 2; + break; + case 2: + logMode = 3; + break; + case 1: + logMode = 0; + break; + default: + logMode = 1; + break; } } - - int fillLog = 0; // Gate variable to be used to allow or not the gesture log to be filled - if (currentGesture != 0) + else if (CheckCollisionPointRec(GetMousePosition(), logButton2)) { - if (logMode == 3) // 3 hides repeated events and hide hold events + switch (logMode) { - if ((((int)currentGesture != 4) && (currentGesture != previousGesture)) || ((int)currentGesture < 3)) - { - fillLog = 1; - } + case 3: + logMode = 1; + break; + case 2: + logMode = 0; + break; + case 1: + logMode = 3; + break; + default: + logMode = 2; + break; } - else if (logMode == 2) // 2 shows repeated events but hide hold events - { - if ((int)currentGesture != 4) - { - fillLog = 1; - } - } - else if (logMode == 1) // 1 hides repeated events - { - if (currentGesture != previousGesture) - { - fillLog = 1; - } - } - else // 0 shows repeated events + } + } + + var fillLog = 0; // Gate variable to be used to allow or not the gesture log to be filled + if (currentGesture != 0) + { + if (logMode == 3) // 3 hides repeated events and hide hold events + { + if ((((int)currentGesture != 4) && (currentGesture != previousGesture)) || ((int)currentGesture < 3)) { fillLog = 1; } } - - if (fillLog > 0) // If one of the conditions from logMode was met, fill the gesture log + else if (logMode == 2) // 2 shows repeated events but hide hold events { - previousGesture = currentGesture; - gestureColor = GetGestureColor((int)currentGesture); - if (gestureLogIndex <= 0) + if ((int)currentGesture != 4) { - gestureLogIndex = GESTURE_LOG_SIZE; - } - gestureLogIndex--; - - // Copy the gesture respective name to the gesture log array - gestureLog[gestureLogIndex] = GetGestureName((int)currentGesture); - } - - // Handle protractor - if ((int)currentGesture > 255) - { - currentAngleDegrees = currentPitchDegrees; // Pinch In and Pinch Out - } - else if ((int)currentGesture > 15) - { - currentAngleDegrees = currentDragDegrees; // Swipe Right, Swipe Left, Swipe Up and Swipe Down - } - else if (currentGesture > 0) - { - currentAngleDegrees = 0.0f; // Tap, Doubletap, Hold and Grab - } - - float currentAngleRadians = - ((currentAngleDegrees + 90.0f) * MathF.PI / 180); // Convert the current angle to Radians - // Calculate the final vector for display - finalVector = new Vector2( - (angleLength * MathF.Sin(currentAngleRadians)) + protractorPosition.X, - (angleLength * MathF.Cos(currentAngleRadians)) + protractorPosition.Y - ) - ; - - // Handle touch and mouse pointer points - Vector2[] touchPosition = new Vector2[MAX_TOUCH_COUNT]; - - Vector2 mousePosition = Vector2.Zero; - if (currentGesture != Gesture.None) - { - if (touchCount != 0) - { - for (i = 0; i < touchCount; i++) - { - touchPosition[i] = GetTouchPosition(i); // Fill the touch positions - } - } - else - { - mousePosition = GetMousePosition(); + fillLog = 1; } } - //-------------------------------------------------------------------------------------- - - // Draw - //-------------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // Draw common elements - DrawText("*", (int)messagePosition.X + 5, (int)messagePosition.Y + 5, 10, Color.Black); - DrawText("Example optimized for Web/HTML5\non Smartphones with Touch Screen.", (int)messagePosition.X + 15, - (int)messagePosition.Y + 5, 10, Color.Black); - DrawText("*", (int)messagePosition.X + 5, (int)messagePosition.Y + 35, 10, Color.Black); - DrawText("While running on Desktop Web Browsers,\ninspect and turn on Touch Emulation.", - (int)messagePosition.X + 15, (int)messagePosition.Y + 35, 10, Color.Black); - - // Draw last gesture - DrawText("Last gesture", (int)lastGesturePosition.X + 33, (int)lastGesturePosition.Y - 47, 20, Color.Black); - DrawText("Swipe Tap Pinch Touch", (int)lastGesturePosition.X + 17, - (int)lastGesturePosition.Y - 18, 10, Color.Black); - DrawRectangle((int)lastGesturePosition.X + 20, (int)lastGesturePosition.Y, 20, 20, - lastGesture == Gesture.SwipeUp ? Color.Red : Color.LightGray); - DrawRectangle((int)lastGesturePosition.X, (int)lastGesturePosition.Y + 20, 20, 20, - lastGesture == Gesture.SwipeLeft ? Color.Red : Color.LightGray); - DrawRectangle((int)lastGesturePosition.X + 40, (int)lastGesturePosition.Y + 20, 20, 20, - lastGesture == Gesture.SwipeRight ? Color.Red : Color.LightGray); - DrawRectangle((int)lastGesturePosition.X + 20, (int)lastGesturePosition.Y + 40, 20, 20, - lastGesture == Gesture.SwipeDown ? Color.Red : Color.LightGray); - DrawCircle((int)lastGesturePosition.X + 80, (int)lastGesturePosition.Y + 16, 10, - lastGesture == Gesture.Tap ? Color.Blue : Color.LightGray); - DrawRing(new Vector2( - lastGesturePosition.X + 103, lastGesturePosition.Y + 16 - ), 6.0f, 11.0f, 0.0f, 360.0f, 0, lastGesture == Gesture.Drag ? Color.Lime : Color.LightGray); - DrawCircle((int)lastGesturePosition.X + 80, (int)lastGesturePosition.Y + 43, 10, - lastGesture == Gesture.DoubleTap ? Color.SkyBlue : Color.LightGray); - DrawCircle((int)lastGesturePosition.X + 103, (int)lastGesturePosition.Y + 43, 10, - lastGesture == Gesture.DoubleTap ? Color.SkyBlue : Color.LightGray); - DrawTriangle(new Vector2( - lastGesturePosition.X + 122, lastGesturePosition.Y + 16 - ), new Vector2( - lastGesturePosition.X + 137, lastGesturePosition.Y + 26 - ), new Vector2( - lastGesturePosition.X + 137, lastGesturePosition.Y + 6 - ), lastGesture == Gesture.PinchOut ? Color.Orange : Color.LightGray); - DrawTriangle(new Vector2( - lastGesturePosition.X + 147, lastGesturePosition.Y + 6 - ), new Vector2( - lastGesturePosition.X + 147, lastGesturePosition.Y + 26 - ), new Vector2( - lastGesturePosition.X + 162, lastGesturePosition.Y + 16 - ), lastGesture == Gesture.PinchOut ? Color.Orange : Color.Gray); - DrawTriangle(new Vector2( - lastGesturePosition.X + 125, lastGesturePosition.Y + 33 - ), new Vector2( - lastGesturePosition.X + 125, lastGesturePosition.Y + 53 - ), new Vector2( - lastGesturePosition.X + 140, lastGesturePosition.Y + 43 - ), lastGesture == Gesture.PinchIn ? Color.Violet : Color.LightGray); - DrawTriangle(new Vector2( - lastGesturePosition.X + 144, lastGesturePosition.Y + 43 - ), new Vector2( - lastGesturePosition.X + 159, lastGesturePosition.Y + 53 - ), new Vector2( - lastGesturePosition.X + 159, lastGesturePosition.Y + 33 - ), lastGesture == Gesture.PinchIn ? Color.Violet : Color.LightGray); - for (i = 0; i < 4; i++) + else if (logMode == 1) // 1 hides repeated events { - DrawCircle((int)lastGesturePosition.X + 180, (int)lastGesturePosition.Y + 7 + i * 15, 5, - touchCount <= i ? Color.LightGray : gestureColor); - } - - // Draw gesture log - DrawText("Log", (int)gestureLogPosition.X, (int)gestureLogPosition.Y, 20, Color.Black); - - // Loop in both directions to print the gesture log array in the inverted order (and looping around if the index started somewhere in the middle) - for (i = 0, ii = gestureLogIndex; i < GESTURE_LOG_SIZE; i++, ii = (ii + 1) % GESTURE_LOG_SIZE) - { - DrawText(gestureLog[ii], (int)gestureLogPosition.X, (int)gestureLogPosition.Y + 410 - i * 20, 20, - (i == 0 ? gestureColor : Color.LightGray)); - } - - Color logButton1Color, logButton2Color; - switch (logMode) - { - case 3: - logButton1Color = Color.Maroon; - logButton2Color = Color.Maroon; - break; - case 2: - logButton1Color = Color.Gray; - logButton2Color = Color.Maroon; - break; - case 1: - logButton1Color = Color.Maroon; - logButton2Color = Color.Gray; - break; - default: - logButton1Color = Color.Gray; - logButton2Color = Color.Gray; - break; - } - - DrawRectangleRec(logButton1, logButton1Color); - DrawText("Hide", (int)logButton1.X + 7, (int)logButton1.Y + 3, 10, Color.White); - DrawText("Repeat", (int)logButton1.X + 7, (int)logButton1.Y + 13, 10, Color.White); - DrawRectangleRec(logButton2, logButton2Color); - DrawText("Hide", (int)logButton1.X + 62, (int)logButton1.Y + 3, 10, Color.White); - DrawText("Hold", (int)logButton1.X + 62, (int)logButton1.Y + 13, 10, Color.White); - - // Draw protractor - DrawText("Angle", (int)protractorPosition.X + 55, (int)protractorPosition.Y + 76, 10, Color.Black); - - // Note: Official it's using raylibs functions for string manipulation. But in C# it will end up in an unsafe handling. - string angleString = currentAngleDegrees.ToString("F3"); - int angleStringDot = angleString.IndexOf('.'); - string angleStringTrim = angleString.Substring(0, angleStringDot + 3); - - DrawText(angleStringTrim, (int)protractorPosition.X + 55, (int)protractorPosition.Y + 92, 20, gestureColor); - DrawCircleV(protractorPosition, 80.0f, Color.White); - DrawLineEx(new Vector2( - protractorPosition.X - 90, protractorPosition.Y - ), new Vector2( - protractorPosition.X + 90, protractorPosition.Y - ), 3.0f, Color.LightGray); - DrawLineEx(new Vector2( - protractorPosition.X, protractorPosition.Y - 90 - ), new Vector2( - protractorPosition.X, protractorPosition.Y + 90 - ), 3.0f, Color.LightGray); - DrawLineEx(new Vector2( - protractorPosition.X - 80, protractorPosition.Y - 45 - ), new Vector2( - protractorPosition.X + 80, protractorPosition.Y + 45 - ), 3.0f, Color.Green); - DrawLineEx(new Vector2( - protractorPosition.X - 80, protractorPosition.Y + 45 - ), new Vector2( - protractorPosition.X + 80, protractorPosition.Y - 45 - ), 3.0f, Color.Green); - DrawText("0", (int)protractorPosition.X + 96, (int)protractorPosition.Y - 9, 20, Color.Black); - DrawText("30", (int)protractorPosition.X + 74, (int)protractorPosition.Y - 68, 20, Color.Black); - DrawText("90", (int)protractorPosition.X - 11, (int)protractorPosition.Y - 110, 20, Color.Black); - DrawText("150", (int)protractorPosition.X - 100, (int)protractorPosition.Y - 68, 20, Color.Black); - DrawText("180", (int)protractorPosition.X - 124, (int)protractorPosition.Y - 9, 20, Color.Black); - DrawText("210", (int)protractorPosition.X - 100, (int)protractorPosition.Y + 50, 20, Color.Black); - DrawText("270", (int)protractorPosition.X - 18, (int)protractorPosition.Y + 92, 20, Color.Black); - DrawText("330", (int)protractorPosition.X + 72, (int)protractorPosition.Y + 50, 20, Color.Black); - if (currentAngleDegrees != 0.0f) - { - DrawLineEx(protractorPosition, finalVector, 3.0f, gestureColor); - } - - // Draw touch and mouse pointer points - if (currentGesture != Gesture.None) - { - if (touchCount != 0) + if (currentGesture != previousGesture) { - for (i = 0; i < touchCount; i++) - { - DrawCircleV(touchPosition[i], 50.0f, Fade(gestureColor, 0.5f)); - DrawCircleV(touchPosition[i], 5.0f, gestureColor); - } - - if (touchCount == 2) - { - DrawLineEx(touchPosition[0], touchPosition[1], (((int)currentGesture == 512) ? 8.0f : 12.0f), - gestureColor); - } - } - else - { - DrawCircleV(mousePosition, 35.0f, Fade(gestureColor, 0.5f)); - DrawCircleV(mousePosition, 5.0f, gestureColor); + fillLog = 1; } } - - EndDrawing(); - //-------------------------------------------------------------------------------------- + else // 0 shows repeated events + { + fillLog = 1; + } } + if (fillLog > 0) // If one of the conditions from logMode was met, fill the gesture log + { + previousGesture = currentGesture; + gestureColor = GetGestureColor((int)currentGesture); + if (gestureLogIndex <= 0) + { + gestureLogIndex = GESTURE_LOG_SIZE; + } + gestureLogIndex--; + + // Copy the gesture respective name to the gesture log array + gestureLog[gestureLogIndex] = GetGestureName((int)currentGesture); + } + + // Handle protractor + if ((int)currentGesture > 255) + { + currentAngleDegrees = currentPitchDegrees; // Pinch In and Pinch Out + } + else if ((int)currentGesture > 15) + { + currentAngleDegrees = currentDragDegrees; // Swipe Right, Swipe Left, Swipe Up and Swipe Down + } + else if (currentGesture > 0) + { + currentAngleDegrees = 0.0f; // Tap, Doubletap, Hold and Grab + } + + var currentAngleRadians = + ((currentAngleDegrees + 90.0f) * MathF.PI / 180); // Convert the current angle to Radians + // Calculate the final vector for display + finalVector = new Vector2( + (angleLength * MathF.Sin(currentAngleRadians)) + protractorPosition.X, + (angleLength * MathF.Cos(currentAngleRadians)) + protractorPosition.Y + ); + + // Handle touch and mouse pointer points + var touchPosition = new Vector2[MAX_TOUCH_COUNT]; + + var mousePosition = Vector2.Zero; + if (currentGesture != Gesture.None) + { + if (touchCount != 0) + { + for (i = 0; i < touchCount; i++) + { + touchPosition[i] = GetTouchPosition(i); // Fill the touch positions + } + } + else + { + mousePosition = GetMousePosition(); + } + } + //-------------------------------------------------------------------------------------- + + // Draw + //-------------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // Draw common elements + DrawText("*", (int)messagePosition.X + 5, (int)messagePosition.Y + 5, 10, Color.Black); + DrawText("Example optimized for Web/HTML5\non Smartphones with Touch Screen.", (int)messagePosition.X + 15, + (int)messagePosition.Y + 5, 10, Color.Black); + DrawText("*", (int)messagePosition.X + 5, (int)messagePosition.Y + 35, 10, Color.Black); + DrawText("While running on Desktop Web Browsers,\ninspect and turn on Touch Emulation.", + (int)messagePosition.X + 15, (int)messagePosition.Y + 35, 10, Color.Black); + + // Draw last gesture + DrawText("Last gesture", (int)lastGesturePosition.X + 33, (int)lastGesturePosition.Y - 47, 20, Color.Black); + DrawText("Swipe Tap Pinch Touch", (int)lastGesturePosition.X + 17, + (int)lastGesturePosition.Y - 18, 10, Color.Black); + DrawRectangle((int)lastGesturePosition.X + 20, (int)lastGesturePosition.Y, 20, 20, + lastGesture == Gesture.SwipeUp ? Color.Red : Color.LightGray); + DrawRectangle((int)lastGesturePosition.X, (int)lastGesturePosition.Y + 20, 20, 20, + lastGesture == Gesture.SwipeLeft ? Color.Red : Color.LightGray); + DrawRectangle((int)lastGesturePosition.X + 40, (int)lastGesturePosition.Y + 20, 20, 20, + lastGesture == Gesture.SwipeRight ? Color.Red : Color.LightGray); + DrawRectangle((int)lastGesturePosition.X + 20, (int)lastGesturePosition.Y + 40, 20, 20, + lastGesture == Gesture.SwipeDown ? Color.Red : Color.LightGray); + DrawCircle((int)lastGesturePosition.X + 80, (int)lastGesturePosition.Y + 16, 10, + lastGesture == Gesture.Tap ? Color.Blue : Color.LightGray); + DrawRing(new Vector2( + lastGesturePosition.X + 103, lastGesturePosition.Y + 16 + ), 6.0f, 11.0f, 0.0f, 360.0f, 0, lastGesture == Gesture.Drag ? Color.Lime : Color.LightGray); + DrawCircle((int)lastGesturePosition.X + 80, (int)lastGesturePosition.Y + 43, 10, + lastGesture == Gesture.DoubleTap ? Color.SkyBlue : Color.LightGray); + DrawCircle((int)lastGesturePosition.X + 103, (int)lastGesturePosition.Y + 43, 10, + lastGesture == Gesture.DoubleTap ? Color.SkyBlue : Color.LightGray); + DrawTriangle(new Vector2( + lastGesturePosition.X + 122, lastGesturePosition.Y + 16 + ), new Vector2( + lastGesturePosition.X + 137, lastGesturePosition.Y + 26 + ), new Vector2( + lastGesturePosition.X + 137, lastGesturePosition.Y + 6 + ), lastGesture == Gesture.PinchOut ? Color.Orange : Color.LightGray); + DrawTriangle(new Vector2( + lastGesturePosition.X + 147, lastGesturePosition.Y + 6 + ), new Vector2( + lastGesturePosition.X + 147, lastGesturePosition.Y + 26 + ), new Vector2( + lastGesturePosition.X + 162, lastGesturePosition.Y + 16 + ), lastGesture == Gesture.PinchOut ? Color.Orange : Color.Gray); + DrawTriangle(new Vector2( + lastGesturePosition.X + 125, lastGesturePosition.Y + 33 + ), new Vector2( + lastGesturePosition.X + 125, lastGesturePosition.Y + 53 + ), new Vector2( + lastGesturePosition.X + 140, lastGesturePosition.Y + 43 + ), lastGesture == Gesture.PinchIn ? Color.Violet : Color.LightGray); + DrawTriangle(new Vector2( + lastGesturePosition.X + 144, lastGesturePosition.Y + 43 + ), new Vector2( + lastGesturePosition.X + 159, lastGesturePosition.Y + 53 + ), new Vector2( + lastGesturePosition.X + 159, lastGesturePosition.Y + 33 + ), lastGesture == Gesture.PinchIn ? Color.Violet : Color.LightGray); + for (i = 0; i < 4; i++) + { + DrawCircle((int)lastGesturePosition.X + 180, (int)lastGesturePosition.Y + 7 + i * 15, 5, + touchCount <= i ? Color.LightGray : gestureColor); + } + + // Draw gesture log + DrawText("Log", (int)gestureLogPosition.X, (int)gestureLogPosition.Y, 20, Color.Black); + + // Loop in both directions to print the gesture log array in the inverted order (and looping around if the index started somewhere in the middle) + for (i = 0, ii = gestureLogIndex; i < GESTURE_LOG_SIZE; i++, ii = (ii + 1) % GESTURE_LOG_SIZE) + { + DrawText(gestureLog[ii], (int)gestureLogPosition.X, (int)gestureLogPosition.Y + 410 - i * 20, 20, + (i == 0 ? gestureColor : Color.LightGray)); + } + + Color logButton1Color, logButton2Color; + switch (logMode) + { + case 3: + logButton1Color = Color.Maroon; + logButton2Color = Color.Maroon; + break; + case 2: + logButton1Color = Color.Gray; + logButton2Color = Color.Maroon; + break; + case 1: + logButton1Color = Color.Maroon; + logButton2Color = Color.Gray; + break; + default: + logButton1Color = Color.Gray; + logButton2Color = Color.Gray; + break; + } + + DrawRectangleRec(logButton1, logButton1Color); + DrawText("Hide", (int)logButton1.X + 7, (int)logButton1.Y + 3, 10, Color.White); + DrawText("Repeat", (int)logButton1.X + 7, (int)logButton1.Y + 13, 10, Color.White); + DrawRectangleRec(logButton2, logButton2Color); + DrawText("Hide", (int)logButton1.X + 62, (int)logButton1.Y + 3, 10, Color.White); + DrawText("Hold", (int)logButton1.X + 62, (int)logButton1.Y + 13, 10, Color.White); + + // Draw protractor + DrawText("Angle", (int)protractorPosition.X + 55, (int)protractorPosition.Y + 76, 10, Color.Black); + + // Note: Official it's using raylibs functions for string manipulation. But in C# it will end up in an unsafe handling. + var angleString = currentAngleDegrees.ToString("F3"); + var angleStringDot = angleString.IndexOf('.'); + var angleStringTrim = angleString[..(angleStringDot + 3)]; + + DrawText(angleStringTrim, (int)protractorPosition.X + 55, (int)protractorPosition.Y + 92, 20, gestureColor); + DrawCircleV(protractorPosition, 80.0f, Color.White); + DrawLineEx(new Vector2( + protractorPosition.X - 90, protractorPosition.Y + ), new Vector2( + protractorPosition.X + 90, protractorPosition.Y + ), 3.0f, Color.LightGray); + DrawLineEx(new Vector2( + protractorPosition.X, protractorPosition.Y - 90 + ), new Vector2( + protractorPosition.X, protractorPosition.Y + 90 + ), 3.0f, Color.LightGray); + DrawLineEx(new Vector2( + protractorPosition.X - 80, protractorPosition.Y - 45 + ), new Vector2( + protractorPosition.X + 80, protractorPosition.Y + 45 + ), 3.0f, Color.Green); + DrawLineEx(new Vector2( + protractorPosition.X - 80, protractorPosition.Y + 45 + ), new Vector2( + protractorPosition.X + 80, protractorPosition.Y - 45 + ), 3.0f, Color.Green); + DrawText("0", (int)protractorPosition.X + 96, (int)protractorPosition.Y - 9, 20, Color.Black); + DrawText("30", (int)protractorPosition.X + 74, (int)protractorPosition.Y - 68, 20, Color.Black); + DrawText("90", (int)protractorPosition.X - 11, (int)protractorPosition.Y - 110, 20, Color.Black); + DrawText("150", (int)protractorPosition.X - 100, (int)protractorPosition.Y - 68, 20, Color.Black); + DrawText("180", (int)protractorPosition.X - 124, (int)protractorPosition.Y - 9, 20, Color.Black); + DrawText("210", (int)protractorPosition.X - 100, (int)protractorPosition.Y + 50, 20, Color.Black); + DrawText("270", (int)protractorPosition.X - 18, (int)protractorPosition.Y + 92, 20, Color.Black); + DrawText("330", (int)protractorPosition.X + 72, (int)protractorPosition.Y + 50, 20, Color.Black); + if (currentAngleDegrees != 0.0f) + { + DrawLineEx(protractorPosition, finalVector, 3.0f, gestureColor); + } + + // Draw touch and mouse pointer points + if (currentGesture != Gesture.None) + { + if (touchCount != 0) + { + for (i = 0; i < touchCount; i++) + { + DrawCircleV(touchPosition[i], 50.0f, Fade(gestureColor, 0.5f)); + DrawCircleV(touchPosition[i], 5.0f, gestureColor); + } + + if (touchCount == 2) + { + DrawLineEx(touchPosition[0], touchPosition[1], (((int)currentGesture == 512) ? 8.0f : 12.0f), + gestureColor); + } + } + else + { + DrawCircleV(mousePosition, 35.0f, Fade(gestureColor, 0.5f)); + DrawCircleV(mousePosition, 5.0f, gestureColor); + } + } + + EndDrawing(); + //-------------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - input gestures testbed"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new InputGesturesTestBed(); + 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 @@ -409,7 +457,8 @@ public class InputGesturesTestBed return 0; } - static string GetGestureName(int gesture) + // Get text string for gesture value + private static string GetGestureName(int gesture) { switch (gesture) { @@ -441,7 +490,7 @@ public class InputGesturesTestBed } // Get color for gesture value - static Color GetGestureColor(int gesture) + private static Color GetGestureColor(int gesture) { switch (gesture) { diff --git a/Examples/Core/InputKeys.cs b/Examples/Core/InputKeys.cs index 56e7823..e15369b 100644 --- a/Examples/Core/InputKeys.cs +++ b/Examples/Core/InputKeys.cs @@ -18,64 +18,87 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class InputKeys +public partial class InputKeys : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Input Keys"; + + public string Title => "raylib [core] example - input keys"; + + private Vector2 ballPosition; + + public void Init() + { + ballPosition = new((float)screenWidth / 2, (float)screenHeight / 2); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyDown(KeyboardKey.Right)) + { + ballPosition.X += 2.0f; + } + + if (IsKeyDown(KeyboardKey.Left)) + { + ballPosition.X -= 2.0f; + } + + if (IsKeyDown(KeyboardKey.Up)) + { + ballPosition.Y -= 2.0f; + } + + if (IsKeyDown(KeyboardKey.Down)) + { + ballPosition.Y += 2.0f; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText("move the ball with arrow keys", 10, 10, 20, Color.DarkGray); + + DrawCircleV(ballPosition, 50, Color.Maroon); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [core] example - input keys"); - InitWindow(screenWidth, screenHeight, "raylib [core] example - keyboard input"); - - Vector2 ballPosition = new((float)screenWidth / 2, (float)screenHeight / 2); - - SetTargetFPS(60); // Set target frames-per-second + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new InputKeys(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (IsKeyDown(KeyboardKey.Right)) - { - ballPosition.X += 2.0f; - } - - if (IsKeyDown(KeyboardKey.Left)) - { - ballPosition.X -= 2.0f; - } - - if (IsKeyDown(KeyboardKey.Up)) - { - ballPosition.Y -= 2.0f; - } - - if (IsKeyDown(KeyboardKey.Down)) - { - ballPosition.Y += 2.0f; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("move the ball with arrow keys", 10, 10, 20, Color.DarkGray); - - DrawCircleV(ballPosition, 50, Color.Maroon); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Core/InputMouse.cs b/Examples/Core/InputMouse.cs index 6207883..a7b105a 100644 --- a/Examples/Core/InputMouse.cs +++ b/Examples/Core/InputMouse.cs @@ -19,95 +19,122 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class InputMouse +public partial class InputMouse : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Input Mouse"; + + public string Title => "raylib [core] example - input mouse"; + + private Vector2 ballPosition; + private Color ballColor; + + public void Init() + { + ballPosition = new(-100.0f, -100.0f); + ballColor = Color.DarkBlue; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.H)) + { + if (IsCursorHidden()) + { + ShowCursor(); + } + else + { + HideCursor(); + } + } + + ballPosition = GetMousePosition(); + + if (IsMouseButtonPressed(MouseButton.Left)) + { + ballColor = Color.Maroon; + } + else if (IsMouseButtonPressed(MouseButton.Middle)) + { + ballColor = Color.Lime; + } + else if (IsMouseButtonPressed(MouseButton.Right)) + { + ballColor = Color.DarkBlue; + } + else if (IsMouseButtonPressed(MouseButton.Side)) + { + ballColor = Color.Purple; + } + else if (IsMouseButtonPressed(MouseButton.Extra)) + { + ballColor = Color.Yellow; + } + else if (IsMouseButtonPressed(MouseButton.Forward)) + { + ballColor = Color.Orange; + } + else if (IsMouseButtonPressed(MouseButton.Back)) + { + ballColor = Color.Beige; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawCircleV(ballPosition, 40, ballColor); + + DrawText("move ball with mouse and click mouse button to change color", 10, 10, 20, Color.DarkGray); + DrawText("Press 'H' to toggle cursor visibility", 10, 30, 20, Color.DarkGray); + + if (IsCursorHidden()) + { + DrawText("CURSOR HIDDEN", 20, 60, 20, Color.Red); + } + else + { + DrawText("CURSOR VISIBLE", 20, 60, 20, Color.Lime); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [core] example - input mouse"); - InitWindow(screenWidth, screenHeight, "raylib [core] example - mouse input"); - - Vector2 ballPosition = new(-100.0f, -100.0f); - Color ballColor = Color.DarkBlue; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //--------------------------------------------------------------------------------------- + var game = new InputMouse(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - - if (IsKeyPressed(KeyboardKey.H)) - { - if (IsCursorHidden()) - { - ShowCursor(); - } - else - { - HideCursor(); - } - } - - ballPosition = GetMousePosition(); - - if (IsMouseButtonPressed(MouseButton.Left)) - { - ballColor = Color.Maroon; - } - else if (IsMouseButtonPressed(MouseButton.Middle)) - { - ballColor = Color.Lime; - } - else if (IsMouseButtonPressed(MouseButton.Right)) - { - ballColor = Color.DarkBlue; - } - else if (IsMouseButtonPressed(MouseButton.Extra)) - { - ballColor = Color.Yellow; - } - else if (IsMouseButtonPressed(MouseButton.Forward)) - { - ballColor = Color.Orange; - } - else if (IsMouseButtonPressed(MouseButton.Back)) - { - ballColor = Color.Beige; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawCircleV(ballPosition, 40, ballColor); - - DrawText("move ball with mouse and click mouse button to change color", 10, 10, 20, Color.DarkGray); - DrawText("Press 'H' to toggle cursor visibility", 10, 30, 20, Color.DarkGray); - - if (IsCursorHidden()) - { - DrawText("CURSOR HIDDEN", 20, 60, 20, Color.Red); - } - else - { - DrawText("CURSOR VISIBLE", 20, 60, 20, Color.Lime); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Core/InputMouseWheel.cs b/Examples/Core/InputMouseWheel.cs index 57900f4..92d21fe 100644 --- a/Examples/Core/InputMouseWheel.cs +++ b/Examples/Core/InputMouseWheel.cs @@ -17,48 +17,72 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class InputMouseWheel +public partial class InputMouseWheel : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Mouse Wheel"; + + public string Title => "raylib [core] example - input mouse wheel"; + + private int boxPositionY; + private int scrollSpeed; // Scrolling speed in pixels + + public void Init() + { + boxPositionY = screenHeight / 2 - 40; + scrollSpeed = 4; // Scrolling speed in pixels + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + boxPositionY -= (int)(GetMouseWheelMove() * scrollSpeed); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawRectangle(screenWidth / 2 - 40, boxPositionY, 80, 80, Color.Maroon); + + DrawText("Use mouse wheel to move the cube up and down!", 10, 10, 20, Color.Gray); + DrawText($"Box position Y: {boxPositionY:000}", 10, 40, 20, Color.LightGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [core] example - input mouse wheel"); - int boxPositionY = screenHeight / 2 - 40; - int scrollSpeed = 4; // Scrolling speed in pixels - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new InputMouseWheel(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - boxPositionY -= (int)(GetMouseWheelMove() * scrollSpeed); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawRectangle(screenWidth / 2 - 40, boxPositionY, 80, 80, Color.Maroon); - - DrawText("Use mouse wheel to move the cube up and down!", 10, 10, 20, Color.Gray); - DrawText($"Box position Y: {boxPositionY}", 10, 40, 20, Color.LightGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Core/InputMultitouch.cs b/Examples/Core/InputMultitouch.cs index f075bd8..ac93aea 100644 --- a/Examples/Core/InputMultitouch.cs +++ b/Examples/Core/InputMultitouch.cs @@ -20,74 +20,98 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class InputMultitouch +public partial class InputMultitouch : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int MaxTouchPoints = 10; + + public string Name => "Core / Input Multitouch"; + + public string Title => "raylib [core] example - input multitouch"; + + private Vector2[] touchPositions; + + public void Init() + { + touchPositions = new Vector2[MaxTouchPoints]; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Get the touch point count ( how many fingers are touching the screen ) + var tCount = GetTouchPointCount(); + + // Clamp touch points available ( set the maximum touch points allowed ) + if (tCount > MaxTouchPoints) + { + tCount = MaxTouchPoints; + } + + // Get touch points positions + for (var i = 0; i < tCount; i++) + { + touchPositions[i] = GetTouchPosition(i); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + for (var i = 0; i < tCount; i++) + { + // Make sure point is not (0, 0) as this means there is no touch for it + if ((touchPositions[i].X > 0) && (touchPositions[i].Y > 0)) + { + // Draw circle and touch index number + DrawCircleV(touchPositions[i], 34, Color.Orange); + DrawText(i.ToString(), + (int)touchPositions[i].X - 10, + (int)touchPositions[i].Y - 70, + 40, + Color.Black + ); + } + } + + DrawText("touch the screen at multiple locations to get multiple balls", 10, 10, 20, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [core] example - input multitouch"); - const int MaxTouchPoints = 10; - Vector2[] touchPositions = new Vector2[MaxTouchPoints]; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //--------------------------------------------------------------------------------------- + var game = new InputMultitouch(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // Get the touch point count (how many fingers are touching the screen ) - int tCount = GetTouchPointCount(); - - // Clamp touch points available (set the maximum touch points allowed ) - if (tCount > MaxTouchPoints) - { - tCount = MaxTouchPoints; - } - - // Get touch points positions - for (int i = 0; i < tCount; i++) - { - touchPositions[i] = GetTouchPosition(i); - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - for (int i = 0; i < tCount; i++) - { - // Make sure point is not (0, 0) as this means there is no touch for it - if ((touchPositions[i].X > 0) && (touchPositions[i].Y > 0)) - { - // Draw circle and touch index number - DrawCircleV(touchPositions[i], 34, Color.Orange); - DrawText(i.ToString(), - (int)touchPositions[i].X - 10, - (int)touchPositions[i].Y - 70, - 40, - Color.Black - ); - } - } - - DrawText("touch the screen at multiple locations to get multiple balls", 10, 10, 20, Color.DarkGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Core/InputVirtualControls.cs b/Examples/Core/InputVirtualControls.cs index 626d913..1477eb5 100644 --- a/Examples/Core/InputVirtualControls.cs +++ b/Examples/Core/InputVirtualControls.cs @@ -1,27 +1,27 @@ /******************************************************************************************* - * - * raylib [core] example - input virtual controls - * - * Example complexity rating: [★★☆☆] 2/4 - * - * Example originally created with raylib 5.0, last time updated with raylib 5.0 - * - * Example contributed by GreenSnakeLinux (@GreenSnakeLinux), - * reviewed by Ramon Santamaria (@raysan5), oblerion (@oblerion) and danilwhale (@danilwhale) - * - * 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 GreenSnakeLinux (@GreenSnakeLinux) and Ramon Santamaria (@raysan5) - * - ********************************************************************************************/ +* +* raylib [core] example - input virtual controls +* +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 5.0, last time updated with raylib 5.0 +* +* Example contributed by GreenSnakeLinux (@GreenSnakeLinux), +* reviewed by Ramon Santamaria (@raysan5), oblerion (@oblerion) and danilwhale (@danilwhale) +* +* 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 GreenSnakeLinux (@GreenSnakeLinux) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ +using System; using System.Numerics; +using static Raylib_cs.Raylib; namespace Examples.Core; -using static Raylib_cs.Raylib; - public enum PadButton { BUTTON_NONE = -1, @@ -32,21 +32,31 @@ public enum PadButton BUTTON_MAX } -public class InputVirtualControls +public partial class InputVirtualControls : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Input Virtual Controls"; + + public string Title => "raylib [core] example - input virtual controls"; + + private Vector2 padPosition; + private float buttonRadius; + private Vector2[] buttonPositions; + private Vector2[][] arrowTris; + private Color[] buttonLabelColors; + private int pressedButton; + private Vector2 inputPosition; + private Vector2 playerPosition; + private float playerSpeed; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + padPosition = new Vector2(100, 350); + buttonRadius = 30; - InitWindow(screenWidth, screenHeight, "raylib [core] example - input virtual controls"); - - Vector2 padPosition = new Vector2(100, 350); - float buttonRadius = 30; - - Vector2[] buttonPositions = + buttonPositions = [ new Vector2( padPosition.X,padPosition.Y - buttonRadius * 1.5f @@ -62,7 +72,7 @@ public class InputVirtualControls ) // Down ]; - Vector2[][] arrowTris = [ + arrowTris = [ // Up [ new Vector2( @@ -114,107 +124,126 @@ public class InputVirtualControls ] ; - Color[] buttonLabelColors = [ + buttonLabelColors = [ Color.Yellow, // Up Color.Blue, // Left Color.Red, // Right Color.Green // Down ]; - int pressedButton = (int)PadButton.BUTTON_NONE; - Vector2 inputPosition = new Vector2(0, 0); + pressedButton = (int)PadButton.BUTTON_NONE; + inputPosition = new Vector2(0, 0); - Vector2 playerPosition = new Vector2((float)screenWidth / 2, (float)screenHeight / 2); - float playerSpeed = 75f; + playerPosition = new Vector2((float)screenWidth / 2, (float)screenHeight / 2); + playerSpeed = 75f; + } + + public void Update() + { + // Update + //-------------------------------------------------------------------------- + if ((GetTouchPointCount() > 0)) + { + inputPosition = GetTouchPosition(0); // Use touch position + } + else + { + inputPosition = GetMousePosition(); // Use mouse position + } + + // Reset pressed button to none + pressedButton = (int)PadButton.BUTTON_NONE; + + // Make sure user is pressing left mouse button if they're from desktop + if ((GetTouchPointCount() > 0) || + ((GetTouchPointCount() == 0) && IsMouseButtonDown(MouseButton.Left))) + { + // Find nearest D-Pad button to the input position + for (var i = 0; i < (int)PadButton.BUTTON_MAX; i++) + { + var distX = MathF.Abs(buttonPositions[i].X - inputPosition.X); + var distY = MathF.Abs(buttonPositions[i].Y - inputPosition.Y); + + if ((distX + distY < buttonRadius)) + { + pressedButton = i; + break; + } + } + } + + // Move player according to pressed button + switch ((PadButton)pressedButton) + { + case PadButton.BUTTON_UP: + playerPosition.Y -= playerSpeed * GetFrameTime(); + break; + case PadButton.BUTTON_LEFT: + playerPosition.X -= playerSpeed * GetFrameTime(); + break; + case PadButton.BUTTON_RIGHT: + playerPosition.X += playerSpeed * GetFrameTime(); + break; + case PadButton.BUTTON_DOWN: + playerPosition.Y += playerSpeed * GetFrameTime(); + break; + default: + break; + } + //-------------------------------------------------------------------------- + + // Draw + //-------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + // Draw world + DrawCircleV(playerPosition, 50, Color.Maroon); + + // Draw GUI + for (var i = 0; i < (int)PadButton.BUTTON_MAX; i++) + { + DrawCircleV(buttonPositions[i], buttonRadius, (i == pressedButton) ? Color.DarkGray : Color.Black); + + DrawTriangle( + arrowTris[i][0], + arrowTris[i][1], + arrowTris[i][2], + buttonLabelColors[i] + ); + } + + DrawText("move the player with D-Pad buttons", 10, 10, 20, Color.DarkGray); + + EndDrawing(); + //-------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - input virtual controls"); SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new InputVirtualControls(); + game.Init(); + // Main game loop while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //-------------------------------------------------------------------------- - if ((GetTouchPointCount() > 0)) - { - inputPosition = GetTouchPosition(0); // Use touch position - } - else - { - inputPosition = GetMousePosition(); // Use mouse position - } - - // Reset pressed button to none - pressedButton = (int)PadButton.BUTTON_NONE; - - // Make sure user is pressing left mouse button if they're from desktop - if ((GetTouchPointCount() > 0) || - ((GetTouchPointCount() == 0) && IsMouseButtonDown(MouseButton.Left))) - { - // Find nearest D-Pad button to the input position - for (int i = 0; i < (int)PadButton.BUTTON_MAX; i++) - { - float distX = MathF.Abs(buttonPositions[i].X - inputPosition.X); - float distY = MathF.Abs(buttonPositions[i].Y - inputPosition.Y); - - if ((distX + distY < buttonRadius)) - { - pressedButton = i; - break; - } - } - } - - // Move player according to pressed button - switch ((PadButton)pressedButton) - { - case PadButton.BUTTON_UP: - playerPosition.Y -= playerSpeed * GetFrameTime(); - break; - case PadButton.BUTTON_LEFT: - playerPosition.X -= playerSpeed * GetFrameTime(); - break; - case PadButton.BUTTON_RIGHT: - playerPosition.X += playerSpeed * GetFrameTime(); - break; - case PadButton.BUTTON_DOWN: - playerPosition.Y += playerSpeed * GetFrameTime(); - break; - default: - break; - } - - ; - //-------------------------------------------------------------------------- - - // Draw - //-------------------------------------------------------------------------- - BeginDrawing(); - - ClearBackground(Color.RayWhite); - - // Draw world - DrawCircleV(playerPosition, 50, Color.Maroon); - - // Draw GUI - for (int i = 0; i < (int)PadButton.BUTTON_MAX; i++) - { - DrawCircleV(buttonPositions[i], buttonRadius, (i == pressedButton) ? Color.DarkGray : Color.Black); - - DrawTriangle( - arrowTris[i][0], - arrowTris[i][1], - arrowTris[i][2], - buttonLabelColors[i] - ); - } - - DrawText("move the player with D-Pad buttons", 10, 10, 20, Color.DarkGray); - - EndDrawing(); - //-------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- CloseWindow(); // Close window and OpenGL context 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 8d81b81..0371097 100644 --- a/Examples/Core/LoadingThread.cs +++ b/Examples/Core/LoadingThread.cs @@ -1,153 +1,185 @@ /******************************************************************************************* * -* raylib example - loading thread +* raylib [core] example - loading thread * -* This example has been created using raylib 2.5 (www.raylib.com) +* NOTE: raylib is NOT thread-safe: the loading thread only updates plain data +* (progress counter and loaded flag); all raylib calls happen on the main thread. +* +* Example originally created with raylib 2.5 (www.raylib.com) * raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) * -* Copyright (c) 2014-2019 Ramon Santamaria (@raysan5) +* Copyright (c) 2014-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ +using System.Diagnostics; using System.Threading; using static Raylib_cs.Raylib; -using static Raylib_cs.Color; -using static Raylib_cs.KeyboardKey; namespace Examples.Core; -enum State +[ExcludeFromBrowser("System.Threading.Thread is unsupported on single-threaded wasm")] +public partial class LoadingThread : IExample { - STATE_WAITING, - STATE_LOADING, - STATE_FINISHED -} + const int screenWidth = 800; + const int screenHeight = 450; -public class LoadingThread -{ - // C# bool is atomic. Used for synchronization - // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/variables#atomicity-of-variable-references - // Data Loaded completion indicator - static bool dataLoaded = false; + public string Name => "Core / Loading Thread"; - // Data progress accumulator - static int dataProgress = 0; + public string Title => "raylib [core] example - loading thread"; + + enum State + { + Waiting, + Loading, + Finished + } + + // Loading data thread; a Thread can only be started once, so a fresh one + // is created for every load + Thread loadingThread; + + // Data loaded completion indicator; volatile so the main thread sees the + // background thread's writes + volatile bool dataLoaded; + + // Data progress accumulator (0..500, the progress bar width in pixels) + volatile int dataProgress; + + State state; + int framesCounter; + + public void Init() + { + loadingThread = null; + dataLoaded = false; + dataProgress = 0; + + state = State.Waiting; + framesCounter = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + switch (state) + { + case State.Waiting: + if (IsKeyPressed(KeyboardKey.Enter)) + { + loadingThread = new Thread(LoadDataThread) { IsBackground = true }; + loadingThread.Start(); + TraceLog(TraceLogLevel.Info, "Loading thread initialized successfully"); + + state = State.Loading; + } + break; + + case State.Loading: + framesCounter++; + if (dataLoaded) + { + framesCounter = 0; + loadingThread.Join(); + TraceLog(TraceLogLevel.Info, "Loading thread terminated"); + + state = State.Finished; + } + break; + + case State.Finished: + if (IsKeyPressed(KeyboardKey.Enter)) + { + // Reset everything to launch again + dataLoaded = false; + dataProgress = 0; + + state = State.Waiting; + } + break; + + default: + break; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + switch (state) + { + case State.Waiting: + DrawText("PRESS ENTER to START LOADING DATA", 150, 170, 20, Color.DarkGray); + break; + + case State.Loading: + DrawRectangle(150, 200, dataProgress, 60, Color.SkyBlue); + if ((framesCounter / 15) % 2 == 0) + { + DrawText("LOADING DATA...", 240, 210, 40, Color.DarkBlue); + } + break; + + case State.Finished: + DrawRectangle(150, 200, 500, 60, Color.Lime); + DrawText("DATA LOADED!", 250, 210, 40, Color.Green); + break; + + default: + break; + } + + DrawRectangleLines(150, 200, 500, 60, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [core] example - loading thread"); - // Loading data thread id - Thread thread = new(new ThreadStart(LoadDataThread)); - - State state = State.STATE_WAITING; - int framesCounter = 0; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new LoadingThread(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) // Detect window close button or ESC key + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - switch (state) - { - case State.STATE_WAITING: - { - if (IsKeyPressed(KEY_ENTER)) - { - thread.Start(); - //int error = pthread_create(ref, NULL, ref, NULL); - //if (error != 0) TraceLog(TraceLogLevel.LOG_ERROR, "Error creating loading thread"); - //else TraceLog(TraceLogLevel.LOG_INFO, "Loading thread initialized successfully"); - - state = State.STATE_LOADING; - } - } - break; - case State.STATE_LOADING: - { - framesCounter++; - if (dataLoaded) - { - framesCounter = 0; - state = State.STATE_FINISHED; - } - } - break; - case State.STATE_FINISHED: - { - if (IsKeyPressed(KEY_ENTER)) - { - // Reset everything to launch again - // atomic_store(ref, false); - dataProgress = 0; - state = State.STATE_WAITING; - } - } - break; - default: break; - } - - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(RAYWHITE); - - switch (state) - { - case State.STATE_WAITING: - DrawText("PRESS ENTER to START LOADING DATA", 150, 170, 20, DARKGRAY); - break; - case State.STATE_LOADING: - { - DrawRectangle(150, 200, dataProgress, 60, SKYBLUE); - if ((framesCounter / 15) % 2 == 0) DrawText("LOADING DATA...", 240, 210, 40, DARKBLUE); - } - break; - case State.STATE_FINISHED: - { - DrawRectangle(150, 200, 500, 60, LIME); - DrawText("DATA LOADED!", 250, 210, 40, GREEN); - } - break; - default: break; - } - - DrawRectangleLines(150, 200, 500, 60, DARKGRAY); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); // Close window and OpenGL context + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } // Loading data thread function definition - static void LoadDataThread() + void LoadDataThread() { - int timeCounter = 0; // Time counted in ms - // clock_t prevTime = clock(); // Previous time + int timeCounter = 0; // Time counted in ms + var stopwatch = Stopwatch.StartNew(); // We simulate data loading with a time counter for 5 seconds while (timeCounter < 5000) { - //clock_t currentTime = clock() - prevTime; - //timeCounter = currentTime*1000/CLOCKS_PER_SEC; - timeCounter += 1; + timeCounter = (int)stopwatch.ElapsedMilliseconds; // We accumulate time over a global variable to be used in // main thread as a progress bar @@ -158,4 +190,3 @@ public class LoadingThread dataLoaded = true; } } - 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 41c2f86..d76948c 100644 --- a/Examples/Core/Picking3D.cs +++ b/Examples/Core/Picking3D.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [core] example - Picking in 3d mode +* raylib [core] example - 3d picking * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2015 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.3, 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) 2015-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,106 +18,148 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class Picking3d +public partial class Picking3d : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Picking 3D"; + + public string Title => "raylib [core] example - 3d picking"; + + private Camera3D camera; + private Vector3 cubePosition; + private Vector3 cubeSize; + private Ray ray; // Picking line ray + private RayCollision collision; // Ray collision hit info + + public void Init() + { + // Define the camera to look into our 3d world + camera = new Camera3D(); + 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 + + cubePosition = new(0.0f, 1.0f, 0.0f); + cubeSize = new(2.0f, 2.0f, 2.0f); + + ray = new(); // Picking line ray + collision = new(); // Ray collision hit info + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsCursorHidden()) + { + UpdateCamera(ref camera, CameraMode.FirstPerson); + } + + // Toggle camera controls + if (IsMouseButtonPressed(MouseButton.Right)) + { + if (IsCursorHidden()) + { + EnableCursor(); + } + else + { + DisableCursor(); + } + } + + if (IsMouseButtonPressed(MouseButton.Left)) + { + if (!collision.Hit) + { + ray = GetScreenToWorldRay(GetMousePosition(), camera); + + // Check collision between ray and box + BoundingBox box = new( + cubePosition - cubeSize / 2, + cubePosition + cubeSize / 2 + ); + collision = GetRayCollisionBox(ray, box); + } + else + { + collision.Hit = false; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + if (collision.Hit) + { + DrawCube(cubePosition, cubeSize.X, cubeSize.Y, cubeSize.Z, Color.Red); + DrawCubeWires(cubePosition, cubeSize.X, cubeSize.Y, cubeSize.Z, Color.Maroon); + + DrawCubeWires(cubePosition, cubeSize.X + 0.2f, cubeSize.Y + 0.2f, cubeSize.Z + 0.2f, Color.Green); + } + else + { + DrawCube(cubePosition, cubeSize.X, cubeSize.Y, cubeSize.Z, Color.Gray); + DrawCubeWires(cubePosition, cubeSize.X, cubeSize.Y, cubeSize.Z, Color.DarkGray); + } + + DrawRay(ray, Color.Maroon); + DrawGrid(10, 1.0f); + + EndMode3D(); + + DrawText("Try clicking on the box with your mouse!", 240, 10, 20, Color.DarkGray); + + if (collision.Hit) + { + var posX = (screenWidth - MeasureText("BOX SELECTED", 30)) / 2; + DrawText("BOX SELECTED", posX, (int)(screenHeight * 0.1f), 30, Color.Green); + } + + DrawText("Right click mouse to toggle camera controls", 10, 430, 10, Color.Gray); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [core] example - 3d picking"); - // Define the camera to look into our 3d world - Camera3D camera; - camera.Position = new Vector3(10.0f, 10.0f, 10.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; - - Vector3 cubePosition = new(0.0f, 1.0f, 0.0f); - Vector3 cubeSize = new(2.0f, 2.0f, 2.0f); - - // Picking line ray - Ray ray = new(new Vector3(0.0f, 0.0f, 0.0f), Vector3.Zero); - RayCollision collision = new(); - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new Picking3d(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Free); - - if (IsMouseButtonPressed(MouseButton.Left)) - { - if (!collision.Hit) - { - ray = GetScreenToWorldRay(GetMousePosition(), camera); - - // Check collision between ray and box - BoundingBox box = new( - cubePosition - cubeSize / 2, - cubePosition + cubeSize / 2 - ); - collision = GetRayCollisionBox(ray, box); - } - else - { - collision.Hit = false; - } - - ray = GetScreenToWorldRay(GetMousePosition(), camera); - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - if (collision.Hit) - { - DrawCube(cubePosition, cubeSize.X, cubeSize.Y, cubeSize.Z, Color.Red); - DrawCubeWires(cubePosition, cubeSize.X, cubeSize.Y, cubeSize.Z, Color.Maroon); - - DrawCubeWires(cubePosition, cubeSize.X + 0.2f, cubeSize.Y + 0.2f, cubeSize.Z + 0.2f, Color.Green); - } - else - { - DrawCube(cubePosition, cubeSize.X, cubeSize.Y, cubeSize.Z, Color.Gray); - DrawCubeWires(cubePosition, cubeSize.X, cubeSize.Y, cubeSize.Z, Color.DarkGray); - } - - DrawRay(ray, Color.Maroon); - DrawGrid(10, 1.0f); - - EndMode3D(); - - DrawText("Try selecting the box with mouse!", 240, 10, 20, Color.DarkGray); - - if (collision.Hit) - { - int posX = (screenWidth - MeasureText("BOX SELECTED", 30)) / 2; - DrawText("BOX SELECTED", posX, (int)(screenHeight * 0.1f), 30, Color.Green); - } - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; 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/RandomValues.cs b/Examples/Core/RandomValues.cs index 234a8b5..86152d2 100644 --- a/Examples/Core/RandomValues.cs +++ b/Examples/Core/RandomValues.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [core] example - Generate random values +* raylib [core] example - random values * -* This example has been created using raylib 1.1 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 * -* Copyright (c) 2014 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.1, last time updated with raylib 1.1 +* +* 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) * ********************************************************************************************/ @@ -13,57 +17,81 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class RandomValues +public partial class RandomValues : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Random Values"; + + public string Title => "raylib [core] example - random values"; + + private int randValue; // Get a random integer number between -8 and 5 (both included) + private int framesCounter; // Variable used to count frames + + public void Init() + { + // SetRandomSeed(0xaabbccff); // Set a custom random seed if desired, by default: "time(NULL)" + + randValue = GetRandomValue(-8, 5); // Get a random integer number between -8 and 5 (both included) + + framesCounter = 0; // Variable used to count frames + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + framesCounter++; + + // Every two seconds (120 frames) a new random value is generated + if (((framesCounter / 120) % 2) == 1) + { + randValue = GetRandomValue(-8, 5); + framesCounter = 0; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText("Every 2 seconds a new random value is generated:", 130, 100, 20, Color.Maroon); + + DrawText($"{randValue}", 360, 180, 80, Color.LightGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [core] example - random values"); - InitWindow(screenWidth, screenHeight, "raylib [core] example - generate random values"); - - // Variable used to count frames - int framesCounter = 0; - - // Get a random integer number between -8 and 5 (both included) - int randValue = GetRandomValue(-8, 5); - - SetTargetFPS(60); // Set our game to run at 60 frames-per-second + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new RandomValues(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - framesCounter++; - - // Every two seconds (120 frames) a new random value is generated - if (((framesCounter / 120) % 2) == 1) - { - randValue = GetRandomValue(-8, 5); - framesCounter = 0; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("Every 2 seconds a new random value is generated:", 130, 100, 20, Color.Maroon); - - DrawText($"{randValue}", 360, 180, 80, Color.LightGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + 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/ScissorTest.cs b/Examples/Core/ScissorTest.cs index 6a5074c..4902e69 100644 --- a/Examples/Core/ScissorTest.cs +++ b/Examples/Core/ScissorTest.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [core] example - Scissor test +* raylib [core] example - scissor test * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 +* +* Example originally created with raylib 2.5, last time updated with raylib 3.0 * * Example contributed by Chris Dill (@MysteriousSpace) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2019 Chris Dill (@MysteriousSpace) +* 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 Chris Dill (@MysteriousSpace) * ********************************************************************************************/ @@ -15,68 +19,92 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class ScissorTest +public partial class ScissorTest : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Scissor Test"; + + public string Title => "raylib [core] example - scissor test"; + + private Rectangle scissorArea; + private bool scissorMode; + + public void Init() + { + scissorArea = new(0, 0, 300, 300); + scissorMode = true; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.S)) + { + scissorMode = !scissorMode; + } + + // Centre the scissor area around the mouse position + scissorArea.X = GetMouseX() - scissorArea.Width / 2; + scissorArea.Y = GetMouseY() - scissorArea.Height / 2; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + if (scissorMode) + { + BeginScissorMode((int)scissorArea.X, (int)scissorArea.Y, (int)scissorArea.Width, (int)scissorArea.Height); + } + + // Draw full screen rectangle and some text + // NOTE: Only part defined by scissor area will be rendered + DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color.Red); + DrawText("Move the mouse around to reveal this text!", 190, 200, 20, Color.LightGray); + + if (scissorMode) + { + EndScissorMode(); + } + + DrawRectangleLinesEx(scissorArea, 1, Color.Black); + DrawText("Press S to toggle scissor test", 10, 10, 20, Color.Black); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [core] example - scissor test"); - Rectangle scissorArea = new(0, 0, 300, 300); - bool scissorMode = true; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new ScissorTest(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (IsKeyPressed(KeyboardKey.S)) - { - scissorMode = !scissorMode; - } - - // Centre the scissor area around the mouse position - scissorArea.X = GetMouseX() - scissorArea.Width / 2; - scissorArea.Y = GetMouseY() - scissorArea.Height / 2; - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - if (scissorMode) - { - BeginScissorMode((int)scissorArea.X, (int)scissorArea.Y, (int)scissorArea.Width, (int)scissorArea.Height); - } - - // Draw full screen rectangle and some text - // NOTE: Only part defined by scissor area will be rendered - DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Color.Red); - DrawText("Move the mouse around to reveal this text!", 190, 200, 20, Color.LightGray); - - if (scissorMode) - { - EndScissorMode(); - } - - DrawRectangleLinesEx(scissorArea, 1, Color.Black); - DrawText("Press S to toggle scissor test", 10, 10, 20, Color.Black); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + 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/SmoothPixelperfect.cs b/Examples/Core/SmoothPixelperfect.cs index 80e5dae..d161d46 100644 --- a/Examples/Core/SmoothPixelperfect.cs +++ b/Examples/Core/SmoothPixelperfect.cs @@ -1,14 +1,18 @@ /******************************************************************************************* * -* raylib [core] example - smooth pixel-perfect camera +* raylib [core] example - smooth pixelperfect * -* This example has been created using raylib 3.7 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 3.7, last time updated with raylib 4.0 * * Example contributed by Giancamillo Alessandroni (@NotManyIdeasDev) and * reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2021 Giancamillo Alessandroni (@NotManyIdeasDev) and 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 Giancamillo Alessandroni (@NotManyIdeasDev) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -18,117 +22,193 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public static class SmoothPixelPerfect +public partial class SmoothPixelPerfect : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int virtualScreenWidth = 160; + private const int virtualScreenHeight = 90; + + private const float virtualRatio = (float)screenWidth / (float)virtualScreenWidth; + + public string Name => "Core / Smooth Pixelperfect"; + + public string Title => "raylib [core] example - smooth pixelperfect"; + + private Camera2D worldSpaceCamera; // Game world camera + private Camera2D screenSpaceCamera; // Smoothing camera + private RenderTexture2D target; + + private Rectangle rec01; + private Rectangle rec02; + private Rectangle rec03; + + private Rectangle sourceRec; + private Rectangle destRec; + + private Vector2 origin; + private float rotation; + private float cameraX; + private float cameraY; + private bool smoothOn; + private bool overscan; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - const int virtualScreenWidth = 160; - const int virtualScreenHeight = 90; - - const float virtualRatio = (float)screenWidth / (float)virtualScreenWidth; - - InitWindow(screenWidth, screenHeight, "raylib [core] example - smooth pixel-perfect camera"); - - // Game world camera - Camera2D worldSpaceCamera = new(); + worldSpaceCamera = new(); // Game world camera worldSpaceCamera.Zoom = 1.0f; - // Smoothing camera - Camera2D screenSpaceCamera = new(); + screenSpaceCamera = new(); // Smoothing camera screenSpaceCamera.Zoom = 1.0f; - // This is where we'll draw all our objects. - RenderTexture2D target = LoadRenderTexture(virtualScreenWidth, virtualScreenHeight); + // Load render texture to draw all our objects + target = LoadRenderTexture(virtualScreenWidth, virtualScreenHeight); - Rectangle rec01 = new(70.0f, 35.0f, 20.0f, 20.0f); - Rectangle rec02 = new(90.0f, 55.0f, 30.0f, 10.0f); - Rectangle rec03 = new(80.0f, 65.0f, 15.0f, 25.0f); + rec01 = new(70.0f, 35.0f, 20.0f, 20.0f); + rec02 = new(90.0f, 55.0f, 30.0f, 10.0f); + rec03 = new(80.0f, 65.0f, 15.0f, 25.0f); // The target's height is flipped (in the source Rectangle), due to OpenGL reasons - Rectangle sourceRec = new( + sourceRec = new( 0.0f, 0.0f, (float)target.Texture.Width, -(float)target.Texture.Height ); - Rectangle destRec = new( - -virtualRatio, - -virtualRatio, - screenWidth + (virtualRatio * 2), - screenHeight + (virtualRatio * 2) + destRec = new( + (screenWidth - screenWidth / 1.25f) / 2.0f, + (screenHeight - screenHeight / 1.25f) / 2.0f, + screenWidth / 1.25f, + screenHeight / 1.25f ); - Vector2 origin = new(0.0f, 0.0f); + origin = new(0.0f, 0.0f); - float rotation = 0.0f; + rotation = 0.0f; - float cameraX = 0.0f; - float cameraY = 0.0f; + cameraX = 0.0f; + cameraY = 0.0f; + + smoothOn = true; + overscan = false; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + rotation += 60.0f * GetFrameTime(); // Rotate the rectangles, 60 degrees per second + + // Make the camera move to demonstrate the effect + cameraX = (MathF.Sin((float)GetTime()) * 50.0f) - 10.0f; + cameraY = MathF.Cos((float)GetTime()) * 30.0f; + + // Set the camera's target to the values computed above + screenSpaceCamera.Target = new Vector2(cameraX, cameraY); + + // Round worldSpace coordinates, keep decimals into screenSpace coordinates + worldSpaceCamera.Target.X = MathF.Truncate(screenSpaceCamera.Target.X); + screenSpaceCamera.Target.X -= worldSpaceCamera.Target.X; + screenSpaceCamera.Target.X *= virtualRatio; + + worldSpaceCamera.Target.Y = MathF.Truncate(screenSpaceCamera.Target.Y); + screenSpaceCamera.Target.Y -= worldSpaceCamera.Target.Y; + screenSpaceCamera.Target.Y *= virtualRatio; + + if (IsKeyPressed(KeyboardKey.S)) + { + smoothOn = !smoothOn; + } + + if (IsKeyPressed(KeyboardKey.O)) + { + overscan = !overscan; + } + + if (overscan) + { + destRec = new Rectangle( + -virtualRatio, + -virtualRatio, + screenWidth + (virtualRatio * 2), + screenHeight + (virtualRatio * 2) + ); + } + else + { + destRec = new Rectangle( + (screenWidth - screenWidth / 1.25f) / 2.0f, + (screenHeight - screenHeight / 1.25f) / 2.0f, + screenWidth / 1.25f, + screenHeight / 1.25f + ); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginTextureMode(target); + ClearBackground(Color.RayWhite); + + BeginMode2D(worldSpaceCamera); + DrawRectanglePro(rec01, origin, rotation, Color.Black); + DrawRectanglePro(rec02, origin, -rotation, Color.Red); + DrawRectanglePro(rec03, origin, rotation + 45.0f, Color.Blue); + EndMode2D(); + EndTextureMode(); + + BeginDrawing(); + ClearBackground(Color.LightGray); + + if (smoothOn) + { + BeginMode2D(screenSpaceCamera); + DrawTexturePro(target.Texture, sourceRec, destRec, origin, 0.0f, Color.White); + EndMode2D(); + } + else + { + DrawTexturePro(target.Texture, sourceRec, destRec, origin, 0.0f, Color.White); + } + + DrawText($"Screen resolution: {screenWidth}x{screenHeight}", 10, 10, 20, Color.DarkBlue); + DrawText($"World resolution: {virtualScreenWidth}x{virtualScreenHeight}", 10, 40, 20, Color.DarkGreen); + DrawText($"Smooth: {(smoothOn ? "ON" : "OFF")}", 10, screenHeight - 60, 20, Color.Red); + DrawText($"Overscan: {(overscan ? "ON" : "OFF")}", 10, screenHeight - 30, 20, Color.Red); + DrawFPS(GetScreenWidth() - 95, 10); + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadRenderTexture(target); // Unload render texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - smooth pixelperfect"); SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new SmoothPixelPerfect(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - rotation += 60.0f * GetFrameTime(); // Rotate the rectangles, 60 degrees per second - - // Make the camera move to demonstrate the effect - cameraX = (MathF.Sin((float)GetTime()) * 50.0f) - 10.0f; - cameraY = MathF.Cos((float)GetTime()) * 30.0f; - - // Set the camera's target to the values computed above - screenSpaceCamera.Target = new Vector2(cameraX, cameraY); - - // Round worldSpace coordinates, keep decimals into screenSpace coordinates - worldSpaceCamera.Target.X = (int)screenSpaceCamera.Target.X; - screenSpaceCamera.Target.X -= worldSpaceCamera.Target.X; - screenSpaceCamera.Target.X *= virtualRatio; - - worldSpaceCamera.Target.Y = (int)screenSpaceCamera.Target.Y; - screenSpaceCamera.Target.Y -= worldSpaceCamera.Target.Y; - screenSpaceCamera.Target.Y *= virtualRatio; - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginTextureMode(target); - ClearBackground(Color.RayWhite); - - BeginMode2D(worldSpaceCamera); - DrawRectanglePro(rec01, origin, rotation, Color.Black); - DrawRectanglePro(rec02, origin, -rotation, Color.Red); - DrawRectanglePro(rec03, origin, rotation + 45.0f, Color.Blue); - EndMode2D(); - - EndTextureMode(); - - BeginDrawing(); - ClearBackground(Color.Red); - - BeginMode2D(screenSpaceCamera); - DrawTexturePro(target.Texture, sourceRec, destRec, origin, 0.0f, Color.White); - EndMode2D(); - - DrawText($"Screen resolution: {screenWidth}x{screenHeight}", 10, 10, 20, Color.DarkBlue); - DrawText($"World resolution: {virtualScreenWidth}x{virtualScreenHeight}", 10, 40, 20, Color.DarkGreen); - DrawFPS(GetScreenWidth() - 95, 10); - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadRenderTexture(target); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Core/SplitScreen.cs b/Examples/Core/SplitScreen.cs index c28c7b6..828eac1 100644 --- a/Examples/Core/SplitScreen.cs +++ b/Examples/Core/SplitScreen.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [core] example - split screen +* raylib [core] example - 3d camera split screen * -* This example has been created using raylib 3.7 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 3.7, last time updated with raylib 4.0 * * Example contributed by Jeffery Myers (@JeffM2501) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2021 Jeffery Myers (@JeffM2501) +* 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 Jeffery Myers (@JeffM2501) * ********************************************************************************************/ @@ -16,25 +20,34 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public unsafe class SplitScreen +public partial class SplitScreen : IExample { - static Texture2D TextureGrid; - static Camera3D CameraPlayer1; - static Camera3D CameraPlayer2; + private const int screenWidth = 800; + private const int screenHeight = 450; + + private Camera3D CameraPlayer1; + private Camera3D CameraPlayer2; + + private RenderTexture2D screenPlayer1; + private RenderTexture2D screenPlayer2; + private Rectangle splitScreenRect; + + public string Name => "Core / Split Screen"; + + public string Title => "raylib [core] example - 3d camera split screen"; // Scene drawing - static void DrawScene() + private void DrawScene() { - int count = 5; + var count = 5; float spacing = 4; - // Grid of cube trees on a plane to make a "world" - // Simple world plane - DrawPlane(new Vector3(0, 0, 0), new Vector2(50, 50), Color.Beige); + // Draw scene: grid of cube trees on a plane to make a "world" + DrawPlane(new Vector3(0, 0, 0), new Vector2(50, 50), Color.Beige); // Simple world plane - for (float x = -count * spacing; x <= count * spacing; x += spacing) + for (var x = -count * spacing; x <= count * spacing; x += spacing) { - for (float z = -count * spacing; z <= count * spacing; z += spacing) + for (var z = -count * spacing; z <= count * spacing; z += spacing) { DrawCube(new Vector3(x, 1.5f, z), 1, 1, 1, Color.Lime); DrawCube(new Vector3(x, 0.5f, z), 0.25f, 1, 0.25f, Color.Brown); @@ -46,22 +59,8 @@ public unsafe class SplitScreen DrawCube(CameraPlayer2.Position, 1, 1, 1, Color.Blue); } - public static int Main() + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [core] example - split screen"); - - // Generate a simple texture to use for trees - Image img = GenImageChecked(256, 256, 32, 32, Color.DarkGray, Color.White); - TextureGrid = LoadTextureFromImage(img); - UnloadImage(img); - SetTextureFilter(TextureGrid, TextureFilter.Anisotropic16X); - SetTextureWrap(TextureGrid, TextureWrap.Clamp); - // Setup player 1 camera and screen CameraPlayer1.FovY = 45.0f; CameraPlayer1.Up.Y = 1.0f; @@ -69,7 +68,7 @@ public unsafe class SplitScreen CameraPlayer1.Position.Z = -3.0f; CameraPlayer1.Position.Y = 1.0f; - RenderTexture2D screenPlayer1 = LoadRenderTexture(screenWidth / 2, screenHeight); + screenPlayer1 = LoadRenderTexture(screenWidth / 2, screenHeight); // Setup player two camera and screen CameraPlayer2.FovY = 45.0f; @@ -78,97 +77,119 @@ public unsafe class SplitScreen CameraPlayer2.Position.X = -3.0f; CameraPlayer2.Position.Y = 3.0f; - RenderTexture2D screenPlayer2 = LoadRenderTexture(screenWidth / 2, screenHeight); + screenPlayer2 = LoadRenderTexture(screenWidth / 2, screenHeight); // Build a flipped rectangle the size of the split view to use for drawing later - Rectangle splitScreenRect = new( + splitScreenRect = new( 0.0f, 0.0f, (float)screenPlayer1.Texture.Width, (float)-screenPlayer1.Texture.Height ); + } - SetTargetFPS(60); + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // If anyone moves this frame, how far will they move based on the time since the last frame + // this moves things at 10 world units per second, regardless of the actual FPS + var offsetThisFrame = 10.0f * GetFrameTime(); + + // Move Player1 forward and backwards (no turning) + if (IsKeyDown(KeyboardKey.W)) + { + CameraPlayer1.Position.Z += offsetThisFrame; + CameraPlayer1.Target.Z += offsetThisFrame; + } + else if (IsKeyDown(KeyboardKey.S)) + { + CameraPlayer1.Position.Z -= offsetThisFrame; + CameraPlayer1.Target.Z -= offsetThisFrame; + } + + // Move Player2 forward and backwards (no turning) + if (IsKeyDown(KeyboardKey.Up)) + { + CameraPlayer2.Position.X += offsetThisFrame; + CameraPlayer2.Target.X += offsetThisFrame; + } + else if (IsKeyDown(KeyboardKey.Down)) + { + CameraPlayer2.Position.X -= offsetThisFrame; + CameraPlayer2.Target.X -= offsetThisFrame; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + // Draw Player1 view to the render texture + BeginTextureMode(screenPlayer1); + ClearBackground(Color.SkyBlue); + + BeginMode3D(CameraPlayer1); + DrawScene(); + EndMode3D(); + + DrawRectangle(0, 0, GetScreenWidth() / 2, 40, Fade(Color.RayWhite, 0.8f)); + DrawText("PLAYER1: W/S to move", 10, 10, 20, Color.Maroon); + EndTextureMode(); + + // Draw Player2 view to the render texture + BeginTextureMode(screenPlayer2); + ClearBackground(Color.SkyBlue); + + BeginMode3D(CameraPlayer2); + DrawScene(); + EndMode3D(); + + DrawRectangle(0, 0, GetScreenWidth() / 2, 40, Fade(Color.RayWhite, 0.8f)); + DrawText("PLAYER2: UP/DOWN to move", 10, 10, 20, Color.DarkBlue); + EndTextureMode(); + + // Draw both views render textures to the screen side by side + BeginDrawing(); + ClearBackground(Color.Black); + + DrawTextureRec(screenPlayer1.Texture, splitScreenRect, new Vector2(0, 0), Color.White); + DrawTextureRec(screenPlayer2.Texture, splitScreenRect, new Vector2(screenWidth / 2.0f, 0), Color.White); + + DrawRectangle(GetScreenWidth() / 2 - 2, 0, 4, GetScreenHeight(), Color.LightGray); + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadRenderTexture(screenPlayer1); // Unload render texture + UnloadRenderTexture(screenPlayer2); // Unload render texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [core] example - 3d camera split screen"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new SplitScreen(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // If anyone moves this frame, how far will they move based on the time since the last frame - // this moves thigns at 10 world units per second, regardless of the actual FPS - float offsetThisFrame = 10.0f * GetFrameTime(); - - // Move Player1 forward and backwards (no turning) - if (IsKeyDown(KeyboardKey.W)) - { - CameraPlayer1.Position.Z += offsetThisFrame; - CameraPlayer1.Target.Z += offsetThisFrame; - } - else if (IsKeyDown(KeyboardKey.S)) - { - CameraPlayer1.Position.Z -= offsetThisFrame; - CameraPlayer1.Target.Z -= offsetThisFrame; - } - - // Move Player2 forward and backwards (no turning) - if (IsKeyDown(KeyboardKey.Up)) - { - CameraPlayer2.Position.X += offsetThisFrame; - CameraPlayer2.Target.X += offsetThisFrame; - } - else if (IsKeyDown(KeyboardKey.Down)) - { - CameraPlayer2.Position.X -= offsetThisFrame; - CameraPlayer2.Target.X -= offsetThisFrame; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - // Draw Player1 view to the render texture - BeginTextureMode(screenPlayer1); - ClearBackground(Color.SkyBlue); - - BeginMode3D(CameraPlayer1); - DrawScene(); - EndMode3D(); - - DrawText("PLAYER 1 W/S to move", 10, 10, 20, Color.Red); - EndTextureMode(); - - // Draw Player2 view to the render texture - BeginTextureMode(screenPlayer2); - ClearBackground(Color.SkyBlue); - - BeginMode3D(CameraPlayer2); - DrawScene(); - EndMode3D(); - - DrawText("PLAYER 2 UP/DOWN to move", 10, 10, 20, Color.Blue); - EndTextureMode(); - - // Draw both views render textures to the screen side by side - BeginDrawing(); - ClearBackground(Color.Black); - - DrawTextureRec(screenPlayer1.Texture, splitScreenRect, new Vector2(0, 0), Color.White); - DrawTextureRec(screenPlayer2.Texture, splitScreenRect, new Vector2(screenWidth / 2.0f, 0), Color.White); - - EndDrawing(); + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadRenderTexture(screenPlayer1); - UnloadRenderTexture(screenPlayer2); - UnloadTexture(TextureGrid); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } } - 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/StorageValues.cs b/Examples/Core/StorageValues.cs index 5aee738..d64c358 100644 --- a/Examples/Core/StorageValues.cs +++ b/Examples/Core/StorageValues.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [core] example - Storage save/load values +* raylib [core] example - storage values * -* This example has been created using raylib 1.4 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2015 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.4, 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) 2015-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -13,79 +17,104 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class StorageValues +public partial class StorageValues : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + private const string storageDataFile = "storage.data"; + // NOTE: Storage positions must start with 0, directly related to file memory layout - enum StorageData + private enum StorageData { Score, HiScore } + public string Name => "Core / Storage Values"; + + public string Title => "raylib [core] example - storage values"; + + private int score = 0; + private int hiscore = 0; + private int framesCounter = 0; + + public void Init() + { + score = 0; + hiscore = 0; + framesCounter = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.R)) + { + score = GetRandomValue(1000, 2000); + hiscore = GetRandomValue(2000, 4000); + } + + if (IsKeyPressed(KeyboardKey.Enter)) + { + SaveStorageValue(storageDataFile, (int)StorageData.Score, score); + SaveStorageValue(storageDataFile, (int)StorageData.HiScore, hiscore); + } + else if (IsKeyPressed(KeyboardKey.Space)) + { + // NOTE: If requested position could not be found, value 0 is returned + score = LoadStorageValue(storageDataFile, (int)StorageData.Score); + hiscore = LoadStorageValue(storageDataFile, (int)StorageData.HiScore); + } + + framesCounter++; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText($"SCORE: {score}", 280, 130, 40, Color.Maroon); + DrawText($"HI-SCORE: {hiscore}", 210, 200, 50, Color.Black); + + DrawText($"frames: {framesCounter}", 10, 10, 20, Color.Lime); + + DrawText("Press R to generate random numbers", 220, 40, 20, Color.LightGray); + DrawText("Press ENTER to SAVE values", 250, 310, 20, Color.LightGray); + DrawText("Press SPACE to LOAD values", 252, 350, 20, Color.LightGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - const string storageDataFile = "storage.data"; + InitWindow(screenWidth, screenHeight, "raylib [core] example - storage values"); - InitWindow(screenWidth, screenHeight, "raylib [core] example - storage save/load values"); - - int score = 0; - int hiscore = 0; - int framesCounter = 0; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new StorageValues(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (IsKeyPressed(KeyboardKey.R)) - { - score = GetRandomValue(1000, 2000); - hiscore = GetRandomValue(2000, 4000); - } - - if (IsKeyPressed(KeyboardKey.Enter)) - { - SaveStorageValue(storageDataFile, (int)StorageData.Score, score); - SaveStorageValue(storageDataFile, (int)StorageData.HiScore, hiscore); - } - else if (IsKeyPressed(KeyboardKey.Space)) - { - // NOTE: If requested position could not be found, value 0 is returned - score = LoadStorageValue(storageDataFile, (int)StorageData.Score); - hiscore = LoadStorageValue(storageDataFile, (int)StorageData.HiScore); - } - - framesCounter++; - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText($"SCORE: {score}", 280, 130, 40, Color.Maroon); - DrawText($"HI-SCORE: {hiscore}", 210, 200, 50, Color.Black); - - DrawText($"frames: {framesCounter}", 10, 10, 20, Color.Lime); - - DrawText("Press R to generate random numbers", 220, 40, 20, Color.LightGray); - DrawText("Press ENTER to SAVE values", 250, 310, 20, Color.LightGray); - DrawText("Press SPACE to LOAD values", 252, 350, 20, Color.LightGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; @@ -97,11 +126,11 @@ public class StorageValues { using var fileNameBuffer = fileName.ToUtf8Buffer(); - bool success = false; - int dataSize = 0; - int newDataSize = 0; + var success = false; + var dataSize = 0; + var newDataSize = 0; - byte* fileData = LoadFileData(fileNameBuffer.AsPointer(), &dataSize); + var fileData = LoadFileData(fileNameBuffer.AsPointer(), &dataSize); byte* newFileData = null; if (fileData != null) @@ -115,13 +144,13 @@ public class StorageValues if (newFileData != null) { // RL_REALLOC succeded - int* dataPtr = (int*)newFileData; + var dataPtr = (int*)newFileData; dataPtr[position] = value; } else { // RL_REALLOC failed - int positionInBytes = position * sizeof(int); + var positionInBytes = position * sizeof(int); TraceLog( TraceLogLevel.Warning, @$"FILEIO: [{fileName}] Failed to realloc data ({dataSize}), @@ -140,7 +169,7 @@ public class StorageValues newDataSize = dataSize; // Replace value on selected position - int* dataPtr = (int*)newFileData; + var dataPtr = (int*)newFileData; dataPtr[position] = value; } @@ -155,7 +184,7 @@ public class StorageValues dataSize = (position + 1) * sizeof(int); fileData = (byte*)MemAlloc((uint)dataSize); - int* dataPtr = (int*)fileData; + var dataPtr = (int*)fileData; dataPtr[position] = value; success = SaveFileData(fileNameBuffer.AsPointer(), fileData, dataSize); @@ -173,9 +202,9 @@ public class StorageValues { using var fileNameBuffer = fileName.ToUtf8Buffer(); - int value = 0; - int dataSize = 0; - byte* fileData = LoadFileData(fileNameBuffer.AsPointer(), &dataSize); + var value = 0; + var dataSize = 0; + var fileData = LoadFileData(fileNameBuffer.AsPointer(), &dataSize); if (fileData != null) { @@ -188,7 +217,7 @@ public class StorageValues } else { - int* dataPtr = (int*)fileData; + var dataPtr = (int*)fileData; value = dataPtr[position]; } 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/VrSimulator.cs b/Examples/Core/VrSimulator.cs index f2b3894..fe27700 100644 --- a/Examples/Core/VrSimulator.cs +++ b/Examples/Core/VrSimulator.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [core] example - VR Simulator (Oculus Rift CV1 parameters) +* raylib [core] example - vr simulator * -* This example has been created using raylib 1.7 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 * -* Copyright (c) 2017 Ramon Santamaria (@raysan5) +* Example originally created with raylib 2.5, 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) 2017-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -15,51 +19,62 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class VrSimulator +public partial class VrSimulator : IExample { - public static int Main() + 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 => "Core / VR Simulator"; + + public string Title => "raylib [core] example - vr simulator"; + + public bool CursorDisabled => true; + + private VrStereoConfig config; + private Shader distortion; + private RenderTexture2D target; + private Rectangle sourceRec; + private Rectangle destRec; + private Camera3D camera; + private Vector3 cubePosition; + + public unsafe void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 1080; - const int screenHeight = 600; - - SetConfigFlags(ConfigFlags.Msaa4xHint); - InitWindow(screenWidth, screenHeight, "raylib [core] example - vr simulator"); - // VR device parameters definition - VrDeviceInfo device = new VrDeviceInfo + var device = new VrDeviceInfo { // Oculus Rift CV1 parameters for simulator - HResolution = 2160, - VResolution = 1200, - HScreenSize = 0.133793f, - VScreenSize = 0.0669f, - EyeToScreenDistance = 0.041f, - LensSeparationDistance = 0.07f, - InterpupillaryDistance = 0.07f, + HResolution = 2160, // Horizontal resolution in pixels + VResolution = 1200, // Vertical resolution in pixels + HScreenSize = 0.133793f, // Horizontal size in meters + VScreenSize = 0.0669f, // Vertical size in meters + EyeToScreenDistance = 0.041f, // Distance between eye and display in meters + LensSeparationDistance = 0.07f, // Lens separation distance in meters + InterpupillaryDistance = 0.07f, // IPD (distance between pupils) in meters }; - // NOTE: CV1 uses a Fresnel-hybrid-asymmetric lenses with specific distortion compute shaders. - // Following parameters are an approximation to distortion stereo rendering but results differ from actual - // device. - unsafe - { - device.LensDistortionValues[0] = 1.0f; - device.LensDistortionValues[1] = 0.22f; - device.LensDistortionValues[2] = 0.24f; - device.LensDistortionValues[3] = 0.0f; - device.ChromaAbCorrection[0] = 0.996f; - device.ChromaAbCorrection[1] = -0.004f; - device.ChromaAbCorrection[2] = 1.014f; - device.ChromaAbCorrection[3] = 0.0f; - } + // NOTE: CV1 uses fresnel-hybrid-asymmetric lenses with specific compute shaders + // Following parameters are just an approximation to CV1 distortion stereo rendering + device.LensDistortionValues[0] = 1.0f; // Lens distortion constant parameter 0 + device.LensDistortionValues[1] = 0.22f; // Lens distortion constant parameter 1 + device.LensDistortionValues[2] = 0.24f; // Lens distortion constant parameter 2 + device.LensDistortionValues[3] = 0.0f; // Lens distortion constant parameter 3 + device.ChromaAbCorrection[0] = 0.996f; // Chromatic aberration correction parameter 0 + device.ChromaAbCorrection[1] = -0.004f; // Chromatic aberration correction parameter 1 + device.ChromaAbCorrection[2] = 1.014f; // Chromatic aberration correction parameter 2 + device.ChromaAbCorrection[3] = 0.0f; // Chromatic aberration correction parameter 3 // Load VR stereo config for VR device parameteres (Oculus Rift CV1 parameters) - VrStereoConfig config = LoadVrStereoConfig(device); + config = LoadVrStereoConfig(device); // Distortion shader (uses device lens distortion and chroma) - Shader distortion = LoadShader(null, "resources/distortion330.fs"); + distortion = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/distortion.fs"); // Update distortion shader with lens and distortion-scale parameters Raylib.SetShaderValue( @@ -100,88 +115,104 @@ public class VrSimulator ShaderUniformDataType.Vec2 ); - unsafe - { - SetShaderValue( - distortion, - GetShaderLocation(distortion, "deviceWarpParam"), - device.LensDistortionValues, - ShaderUniformDataType.Vec4 - ); - SetShaderValue( - distortion, - GetShaderLocation(distortion, "chromaAbParam"), - device.ChromaAbCorrection, - ShaderUniformDataType.Vec4 - ); - } + SetShaderValue( + distortion, + GetShaderLocation(distortion, "deviceWarpParam"), + device.LensDistortionValues, + ShaderUniformDataType.Vec4 + ); + SetShaderValue( + distortion, + GetShaderLocation(distortion, "chromaAbParam"), + device.ChromaAbCorrection, + ShaderUniformDataType.Vec4 + ); // Initialize framebuffer for stereo rendering // NOTE: Screen size should match HMD aspect ratio - RenderTexture2D target = LoadRenderTexture(GetScreenWidth(), GetScreenHeight()); + target = LoadRenderTexture(device.HResolution, device.VResolution); + + // The target's height is flipped (in the source Rectangle), due to OpenGL reasons + sourceRec = new(0.0f, 0.0f, (float)target.Texture.Width, -(float)target.Texture.Height); + destRec = new(0.0f, 0.0f, (float)GetScreenWidth(), (float)GetScreenHeight()); // Define the camera to look into our 3d world - Camera3D camera; - camera.Position = new Vector3(5.0f, 2.0f, 5.0f); - camera.Target = new Vector3(0.0f, 2.0f, 0.0f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.FovY = 60.0f; - camera.Projection = CameraProjection.Perspective; + camera = new(); + camera.Position = new Vector3(5.0f, 2.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 + camera.FovY = 60.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type - Vector3 cubePosition = new(0.0f, 0.0f, 0.0f); + cubePosition = new(0.0f, 0.0f, 0.0f); + } - SetTargetFPS(90); // Set our game to run at 90 frames-per-second + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.FirstPerson); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginTextureMode(target); + ClearBackground(Color.RayWhite); + BeginVrStereoMode(config); + BeginMode3D(camera); + + DrawCube(cubePosition, 2.0f, 2.0f, 2.0f, Color.Red); + DrawCubeWires(cubePosition, 2.0f, 2.0f, 2.0f, Color.Maroon); + DrawGrid(40, 1.0f); + + EndMode3D(); + EndVrStereoMode(); + EndTextureMode(); + + BeginDrawing(); + ClearBackground(Color.RayWhite); + BeginShaderMode(distortion); + DrawTexturePro(target.Texture, sourceRec, destRec, new Vector2(0.0f, 0.0f), 0.0f, Color.White); + EndShaderMode(); + DrawFPS(10, 10); + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadVrStereoConfig(config); // Unload stereo config + + UnloadRenderTexture(target); // Unload stereo render fbo + UnloadShader(distortion); // Unload distortion shader + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + // NOTE: screenWidth/screenHeight should match VR device aspect ratio + InitWindow(screenWidth, screenHeight, "raylib [core] example - vr simulator"); + + DisableCursor(); // Limit cursor to relative movement inside the window + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new VrSimulator(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.FirstPerson); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginTextureMode(target); - ClearBackground(Color.RayWhite); - - BeginVrStereoMode(config); - BeginMode3D(camera); - - DrawCube(cubePosition, 2.0f, 2.0f, 2.0f, Color.Red); - DrawCubeWires(cubePosition, 2.0f, 2.0f, 2.0f, Color.Maroon); - DrawGrid(40, 1.0f); - - EndMode3D(); - EndVrStereoMode(); - EndTextureMode(); - - BeginShaderMode(distortion); - DrawTextureRec( - target.Texture, - new Rectangle(0, 0, (float)target.Texture.Width, (float)-target.Texture.Height), - new Vector2(0.0f, 0.0f), - Color.White - ); - EndShaderMode(); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadVrStereoConfig(config); - UnloadRenderTexture(target); - UnloadShader(distortion); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Core/WindowFlags.cs b/Examples/Core/WindowFlags.cs index b963a17..2f82111 100644 --- a/Examples/Core/WindowFlags.cs +++ b/Examples/Core/WindowFlags.cs @@ -2,10 +2,14 @@ * * raylib [core] example - window flags * -* This example has been created using raylib 3.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 * -* Copyright (c) 2020 Ramon Santamaria (@raysan5) +* Example originally created with raylib 3.5, last time updated with raylib 3.5 +* +* 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) 2020-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -15,15 +19,239 @@ using static Raylib_cs.ConfigFlags; namespace Examples.Core; -public class WindowFlags +[ExcludeFromBrowser("runtime window-state flags don't apply to the emscripten canvas")] +public partial class WindowFlags : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / Window Flags"; + + public string Title => "raylib [core] example - window flags"; + + private Vector2 ballPosition; + private Vector2 ballSpeed; + private float ballRadius; + + private int framesCounter = 0; + + public void Init() + { + ballPosition = new(GetScreenWidth() / 2.0f, GetScreenHeight() / 2.0f); + ballSpeed = new(5.0f, 4.0f); + ballRadius = 20; + + framesCounter = 0; + } + + public void Update() + { + // Update + //----------------------------------------------------- + if (IsKeyPressed(KeyboardKey.F)) + { + // modifies window size when scaling! + ToggleFullscreen(); + } + + if (IsKeyPressed(KeyboardKey.R)) + { + if (IsWindowState(ResizableWindow)) + { + ClearWindowState(ResizableWindow); + } + else + { + SetWindowState(ResizableWindow); + } + } + + if (IsKeyPressed(KeyboardKey.D)) + { + if (IsWindowState(UndecoratedWindow)) + { + ClearWindowState(UndecoratedWindow); + } + else + { + SetWindowState(UndecoratedWindow); + } + } + + if (IsKeyPressed(KeyboardKey.H)) + { + if (!IsWindowState(HiddenWindow)) + { + SetWindowState(HiddenWindow); + } + + framesCounter = 0; + } + + if (IsWindowState(HiddenWindow)) + { + framesCounter++; + if (framesCounter >= 240) + { + // Show window after 3 seconds + ClearWindowState(HiddenWindow); + } + } + + if (IsKeyPressed(KeyboardKey.N)) + { + if (!IsWindowState(MinimizedWindow)) + { + MinimizeWindow(); + } + + framesCounter = 0; + } + + if (IsWindowState(MinimizedWindow)) + { + framesCounter++; + if (framesCounter >= 240) + { + // Restore window after 3 seconds + RestoreWindow(); + } + } + + if (IsKeyPressed(KeyboardKey.M)) + { + // NOTE: Requires FLAG_WINDOW_RESIZABLE enabled! + if (IsWindowState(MaximizedWindow)) + { + RestoreWindow(); + } + else + { + MaximizeWindow(); + } + } + + if (IsKeyPressed(KeyboardKey.U)) + { + if (IsWindowState(UnfocusedWindow)) + { + ClearWindowState(UnfocusedWindow); + } + else + { + SetWindowState(UnfocusedWindow); + } + } + + if (IsKeyPressed(KeyboardKey.T)) + { + if (IsWindowState(TopmostWindow)) + { + ClearWindowState(TopmostWindow); + } + else + { + SetWindowState(TopmostWindow); + } + } + + if (IsKeyPressed(KeyboardKey.A)) + { + if (IsWindowState(AlwaysRunWindow)) + { + ClearWindowState(AlwaysRunWindow); + } + else + { + SetWindowState(AlwaysRunWindow); + } + } + + if (IsKeyPressed(KeyboardKey.V)) + { + if (IsWindowState(VSyncHint)) + { + ClearWindowState(VSyncHint); + } + else + { + SetWindowState(VSyncHint); + } + } + + if (IsKeyPressed(KeyboardKey.B)) + { + ToggleBorderlessWindowed(); + } + + // Bouncing ball logic + ballPosition.X += ballSpeed.X; + ballPosition.Y += ballSpeed.Y; + if ((ballPosition.X >= (GetScreenWidth() - ballRadius)) || (ballPosition.X <= ballRadius)) + { + ballSpeed.X *= -1.0f; + } + if ((ballPosition.Y >= (GetScreenHeight() - ballRadius)) || (ballPosition.Y <= ballRadius)) + { + ballSpeed.Y *= -1.0f; + } + //----------------------------------------------------- + + // Draw + //----------------------------------------------------- + BeginDrawing(); + + if (IsWindowState(TransparentWindow)) + { + ClearBackground(Color.Blank); + } + else + { + ClearBackground(Color.RayWhite); + } + + DrawCircleV(ballPosition, ballRadius, Color.Maroon); + DrawRectangleLinesEx(new Rectangle(0, 0, GetScreenWidth(), GetScreenHeight()), 4, Color.RayWhite); + + DrawCircleV(GetMousePosition(), 10, Color.DarkBlue); + + DrawFPS(10, 10); + + DrawText($"Screen Size: [{GetScreenWidth()}, {GetScreenHeight()}]", 10, 40, 10, Color.Green); + + // Draw window state info + DrawText("Following flags can be set after window creation:", 10, 60, 10, Color.Gray); + + DrawWindowState(FullscreenMode, "[F] FLAG_FULLSCREEN_MODE: ", 10, 80, 10); + DrawWindowState(ResizableWindow, "[R] FLAG_WINDOW_RESIZABLE: ", 10, 100, 10); + DrawWindowState(UndecoratedWindow, "[D] FLAG_WINDOW_UNDECORATED: ", 10, 120, 10); + DrawWindowState(HiddenWindow, "[H] FLAG_WINDOW_HIDDEN: ", 10, 140, 10); + DrawWindowState(MinimizedWindow, "[N] FLAG_WINDOW_MINIMIZED: ", 10, 160, 10); + DrawWindowState(MaximizedWindow, "[M] FLAG_WINDOW_MAXIMIZED: ", 10, 180, 10); + DrawWindowState(UnfocusedWindow, "[G] FLAG_WINDOW_UNFOCUSED: ", 10, 200, 10); + DrawWindowState(TopmostWindow, "[T] FLAG_WINDOW_TOPMOST: ", 10, 220, 10); + DrawWindowState(AlwaysRunWindow, "[A] FLAG_WINDOW_ALWAYS_RUN: ", 10, 240, 10); + DrawWindowState(VSyncHint, "[V] FLAG_VSYNC_HINT: ", 10, 260, 10); + DrawWindowState(BorderlessWindowMode, "[B] FLAG_BORDERLESS_WINDOWED_MODE: ", 10, 280, 10); + + DrawText("Following flags can only be set before window creation:", 10, 320, 10, Color.Gray); + + DrawWindowState(HighDpiWindow, "FLAG_WINDOW_HIGHDPI: ", 10, 340, 10); + DrawWindowState(TransparentWindow, "FLAG_WINDOW_TRANSPARENT: ", 10, 360, 10); + DrawWindowState(Msaa4xHint, "FLAG_MSAA_4X_HINT: ", 10, 380, 10); + + EndDrawing(); + //----------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //--------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - // Possible window flags /* FLAG_VSYNC_HINT @@ -42,235 +270,43 @@ public class WindowFlags */ // Set configuration flags for window creation - SetConfigFlags(VSyncHint | Msaa4xHint); + //SetConfigFlags(VSyncHint | Msaa4xHint | HighDpiWindow);// | TransparentWindow); InitWindow(screenWidth, screenHeight, "raylib [core] example - window flags"); - Vector2 ballPosition = new(GetScreenWidth() / 2, GetScreenHeight() / 2); - Vector2 ballSpeed = new(5.0f, 4.0f); - int ballRadius = 20; - - int framesCounter = 0; + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //---------------------------------------------------------- + var game = new WindowFlags(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //----------------------------------------------------- - if (IsKeyPressed(KeyboardKey.F)) - { - // modifies window size when scaling! - ToggleFullscreen(); - } - - if (IsKeyPressed(KeyboardKey.R)) - { - if (IsWindowState(ResizableWindow)) - { - ClearWindowState(ResizableWindow); - } - else - { - SetWindowState(ResizableWindow); - } - } - - if (IsKeyPressed(KeyboardKey.D)) - { - if (IsWindowState(UndecoratedWindow)) - { - ClearWindowState(UndecoratedWindow); - } - else - { - SetWindowState(UndecoratedWindow); - } - } - - if (IsKeyPressed(KeyboardKey.H)) - { - if (!IsWindowState(HiddenWindow)) - { - SetWindowState(HiddenWindow); - } - - framesCounter = 0; - } - - if (IsWindowState(HiddenWindow)) - { - framesCounter++; - if (framesCounter >= 240) - { - // Show window after 3 seconds - ClearWindowState(HiddenWindow); - } - } - - if (IsKeyPressed(KeyboardKey.N)) - { - if (!IsWindowState(MinimizedWindow)) - { - MinimizeWindow(); - } - - framesCounter = 0; - } - - if (IsWindowState(MinimizedWindow)) - { - framesCounter++; - if (framesCounter >= 240) - { - // Restore window after 3 seconds - RestoreWindow(); - } - } - - if (IsKeyPressed(KeyboardKey.M)) - { - // NOTE: Requires FLAG_WINDOW_RESIZABLE enabled! - if (IsWindowState(MaximizedWindow)) - { - RestoreWindow(); - } - else - { - MaximizeWindow(); - } - } - - if (IsKeyPressed(KeyboardKey.U)) - { - if (IsWindowState(UnfocusedWindow)) - { - ClearWindowState(UnfocusedWindow); - } - else - { - SetWindowState(UnfocusedWindow); - } - } - - if (IsKeyPressed(KeyboardKey.T)) - { - if (IsWindowState(TopmostWindow)) - { - ClearWindowState(TopmostWindow); - } - else - { - SetWindowState(TopmostWindow); - } - } - - if (IsKeyPressed(KeyboardKey.A)) - { - if (IsWindowState(AlwaysRunWindow)) - { - ClearWindowState(AlwaysRunWindow); - } - else - { - SetWindowState(AlwaysRunWindow); - } - } - - if (IsKeyPressed(KeyboardKey.V)) - { - if (IsWindowState(VSyncHint)) - { - ClearWindowState(VSyncHint); - } - else - { - SetWindowState(VSyncHint); - } - } - - // Bouncing ball logic - ballPosition.X += ballSpeed.X; - ballPosition.Y += ballSpeed.Y; - if ((ballPosition.X >= (GetScreenWidth() - ballRadius)) || (ballPosition.X <= ballRadius)) - { - ballSpeed.X *= -1.0f; - } - if ((ballPosition.Y >= (GetScreenHeight() - ballRadius)) || (ballPosition.Y <= ballRadius)) - { - ballSpeed.Y *= -1.0f; - } - //----------------------------------------------------- - - // Draw - //----------------------------------------------------- - BeginDrawing(); - - if (IsWindowState(TransparentWindow)) - { - ClearBackground(Color.Blank); - } - else - { - ClearBackground(Color.RayWhite); - } - - DrawCircleV(ballPosition, ballRadius, Color.Maroon); - DrawRectangleLinesEx(new Rectangle(0, 0, GetScreenWidth(), GetScreenHeight()), 4, Color.RayWhite); - - DrawCircleV(GetMousePosition(), 10, Color.DarkBlue); - - DrawFPS(10, 10); - - DrawText($"Screen Size: [{GetScreenWidth()}, {GetScreenHeight()}]", 10, 40, 10, Color.Green); - - // Draw window state info - Color on = Color.Lime; - Color off = Color.Maroon; - - DrawText("Following flags can be set after window creation:", 10, 60, 10, Color.Gray); - - DrawWindowState(FullscreenMode, "[F] FLAG_FULLSCREEN_MODE: ", 10, 80, 10); - DrawWindowState(ResizableWindow, "[R] FLAG_WINDOW_RESIZABLE: ", 10, 100, 10); - DrawWindowState(UndecoratedWindow, "[D] FLAG_WINDOW_UNDECORATED: ", 10, 120, 10); - DrawWindowState(HiddenWindow, "[H] FLAG_WINDOW_HIDDEN: ", 10, 140, 10); - DrawWindowState(MinimizedWindow, "[N] FLAG_WINDOW_MINIMIZED: ", 10, 160, 10); - DrawWindowState(MaximizedWindow, "[M] FLAG_WINDOW_MAXIMIZED: ", 10, 180, 10); - DrawWindowState(UnfocusedWindow, "[G] FLAG_WINDOW_UNFOCUSED: ", 10, 200, 10); - DrawWindowState(TopmostWindow, "[T] FLAG_WINDOW_TOPMOST: ", 10, 220, 10); - DrawWindowState(AlwaysRunWindow, "[A] FLAG_WINDOW_ALWAYS_RUN: ", 10, 240, 10); - DrawWindowState(VSyncHint, "[V] FLAG_VSYNC_HINT: ", 10, 260, 10); - - DrawText("Following flags can only be set before window creation:", 10, 300, 10, Color.Gray); - - DrawWindowState(HighDpiWindow, "[F] FLAG_WINDOW_HIGHDPI: ", 10, 320, 10); - DrawWindowState(TransparentWindow, "[F] FLAG_WINDOW_TRANSPARENT: ", 10, 340, 10); - DrawWindowState(Msaa4xHint, "[F] FLAG_MSAA_4X_HINT: ", 10, 360, 10); - - EndDrawing(); - //----------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //--------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //---------------------------------------------------------- return 0; } - static void DrawWindowState(ConfigFlags flag, string text, int posX, int posY, int fontSize) + private static void DrawWindowState(ConfigFlags flag, string text, int posX, int posY, int fontSize) { - Color onColor = Color.Lime; - Color offColor = Color.Maroon; + var onColor = Color.Lime; + var offColor = Color.Maroon; if (Raylib.IsWindowState(flag)) { - DrawText($"{text} on", posX, posY, fontSize, onColor); + DrawText($"{text}on", posX, posY, fontSize, onColor); } else { - DrawText($"{text} off", posX, posY, fontSize, offColor); + DrawText($"{text}off", posX, posY, fontSize, offColor); } } } - diff --git a/Examples/Core/WindowLetterbox.cs b/Examples/Core/WindowLetterbox.cs index 80b16f2..a9341de 100644 --- a/Examples/Core/WindowLetterbox.cs +++ b/Examples/Core/WindowLetterbox.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [core] example - window scale letterbox +* raylib [core] example - window letterbox * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 2.5, last time updated with raylib 4.0 * * Example contributed by Anata (@anatagawa) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2019 Anata (@anatagawa) and 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 Anata (@anatagawa) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -17,123 +21,153 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class WindowLetterbox +public partial class WindowLetterbox : IExample { - public static int Main() + private const int windowWidth = 800; + private const int windowHeight = 450; + + private const int gamescreenWidth = 640; + private const int gamescreenHeight = 480; + + public string Name => "Core / Window Letterbox"; + + public string Title => "raylib [core] example - window letterbox"; + + public ConfigFlags ConfigFlags => ConfigFlags.ResizableWindow | ConfigFlags.VSyncHint; + + private RenderTexture2D target; + private Color[] colors; + + public void Init() { - const int windowWidth = 800; - const int windowHeight = 450; - - // Enable config flags for resizable window and vertical synchro - SetConfigFlags(ConfigFlags.ResizableWindow | ConfigFlags.VSyncHint); - InitWindow(windowWidth, windowHeight, "raylib [core] example - window scale letterbox"); - SetWindowMinSize(320, 240); - - int gameScreenWidth = 640; - int gameScreenHeight = 480; - // Render texture initialization, used to hold the rendering result so we can easily resize it - RenderTexture2D target = LoadRenderTexture(gameScreenWidth, gameScreenHeight); - SetTextureFilter(target.Texture, TextureFilter.Bilinear); + target = LoadRenderTexture(gamescreenWidth, gamescreenHeight); + SetTextureFilter(target.Texture, TextureFilter.Bilinear); // Texture scale filter to use - Color[] colors = new Color[10]; - for (int i = 0; i < 10; i++) + colors = new Color[10]; + for (var i = 0; i < 10; i++) { colors[i] = new Color(GetRandomValue(100, 250), GetRandomValue(50, 150), GetRandomValue(10, 100), 255); } + } - SetTargetFPS(60); + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Compute required framebuffer scaling + var scale = MathF.Min( + (float)GetScreenWidth() / gamescreenWidth, + (float)GetScreenHeight() / gamescreenHeight + ); + + if (IsKeyPressed(KeyboardKey.Space)) + { + // Recalculate random colors for the bars + for (var i = 0; i < 10; i++) + { + colors[i] = new Color( + GetRandomValue(100, 250), + GetRandomValue(50, 150), + GetRandomValue(10, 100), + 255 + ); + } + } + + // Update virtual mouse (clamped mouse value behind game screen) + var mouse = GetMousePosition(); + var virtualMouse = Vector2.Zero; + virtualMouse.X = (mouse.X - (GetScreenWidth() - (gamescreenWidth * scale)) * 0.5f) / scale; + virtualMouse.Y = (mouse.Y - (GetScreenHeight() - (gamescreenHeight * scale)) * 0.5f) / scale; + + Vector2 max = new((float)gamescreenWidth, (float)gamescreenHeight); + virtualMouse = Vector2.Clamp(virtualMouse, Vector2.Zero, max); + + // Apply the same transformation as the virtual mouse to the real mouse (i.e. to work with raygui) + //SetMouseOffset(-(GetScreenWidth() - (gamescreenWidth*scale))*0.5f, -(GetScreenHeight() - (gamescreenHeight*scale))*0.5f); + //SetMouseScale(1/scale, 1/scale); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + // Draw everything in the render texture, note this will not be rendered on screen, yet + BeginTextureMode(target); + ClearBackground(Color.RayWhite); // Clear render texture background color + + for (var i = 0; i < 10; i++) + { + DrawRectangle(0, (gamescreenHeight / 10) * i, gamescreenWidth, gamescreenHeight / 10, colors[i]); + } + + DrawText( + "If executed inside a window,\nyou can resize the window,\nand see the screen scaling!", + 10, + 25, + 20, + Color.White + ); + + DrawText($"Default Mouse: [{(int)mouse.X} , {(int)mouse.Y}]", 350, 25, 20, Color.Green); + DrawText($"Virtual Mouse: [{(int)virtualMouse.X} , {(int)virtualMouse.Y}]", 350, 55, 20, Color.Yellow); + + EndTextureMode(); + + BeginDrawing(); + ClearBackground(Color.Black); // Clear screen background + + // Draw render texture to screen, properly scaled + Rectangle sourceRec = new( + 0.0f, + 0.0f, + (float)target.Texture.Width, + (float)-target.Texture.Height + ); + Rectangle destRec = new( + (GetScreenWidth() - ((float)gamescreenWidth * scale)) * 0.5f, + (GetScreenHeight() - ((float)gamescreenHeight * scale)) * 0.5f, + (float)gamescreenWidth * scale, + (float)gamescreenHeight * scale + ); + DrawTexturePro(target.Texture, sourceRec, destRec, new Vector2(0, 0), 0.0f, Color.White); + + EndDrawing(); + //-------------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadRenderTexture(target); // Unload render texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + // Enable config flags for resizable window and vertical synchro + SetConfigFlags(ConfigFlags.ResizableWindow | ConfigFlags.VSyncHint); + InitWindow(windowWidth, windowHeight, "raylib [core] example - window letterbox"); + SetWindowMinSize(320, 240); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new WindowLetterbox(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // Compute required framebuffer scaling - float scale = MathF.Min( - (float)GetScreenWidth() / gameScreenWidth, - (float)GetScreenHeight() / gameScreenHeight - ); - - if (IsKeyPressed(KeyboardKey.Space)) - { - // Recalculate random colors for the bars - for (int i = 0; i < 10; i++) - { - colors[i] = new Color( - GetRandomValue(100, 250), - GetRandomValue(50, 150), - GetRandomValue(10, 100), - 255 - ); - } - } - - // Update virtual mouse (clamped mouse value behind game screen) - Vector2 mouse = GetMousePosition(); - Vector2 virtualMouse = Vector2.Zero; - virtualMouse.X = (mouse.X - (GetScreenWidth() - (gameScreenWidth * scale)) * 0.5f) / scale; - virtualMouse.Y = (mouse.Y - (GetScreenHeight() - (gameScreenHeight * scale)) * 0.5f) / scale; - - Vector2 max = new((float)gameScreenWidth, (float)gameScreenHeight); - virtualMouse = Vector2.Clamp(virtualMouse, Vector2.Zero, max); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.Black); - - // Draw everything in the render texture, note this will not be rendered on screen, yet - BeginTextureMode(target); - ClearBackground(Color.RayWhite); - - for (int i = 0; i < 10; i++) - { - DrawRectangle(0, (gameScreenHeight / 10) * i, gameScreenWidth, gameScreenHeight / 10, colors[i]); - } - - DrawText( - "If executed inside a window,\nyou can resize the window,\nand see the screen scaling!", - 10, - 25, - 20, - Color.White - ); - - DrawText($"Default Mouse: [{(int)mouse.X} {(int)mouse.Y}]", 350, 25, 20, Color.Green); - DrawText($"Virtual Mouse: [{(int)virtualMouse.X}, {(int)virtualMouse.Y}]", 350, 55, 20, Color.Yellow); - - EndTextureMode(); - - // Draw RenderTexture2D to window, properly scaled - Rectangle sourceRec = new( - 0.0f, - 0.0f, - (float)target.Texture.Width, - (float)-target.Texture.Height - ); - Rectangle destRec = new( - (GetScreenWidth() - ((float)gameScreenWidth * scale)) * 0.5f, - (GetScreenHeight() - ((float)gameScreenHeight * scale)) * 0.5f, - (float)gameScreenWidth * scale, - (float)gameScreenHeight * scale - ); - DrawTexturePro(target.Texture, sourceRec, destRec, new Vector2(0, 0), 0.0f, Color.White); - - EndDrawing(); - //-------------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadRenderTexture(target); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } } - 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/Core/WorldScreen.cs b/Examples/Core/WorldScreen.cs index 72f9a20..57aa4e4 100644 --- a/Examples/Core/WorldScreen.cs +++ b/Examples/Core/WorldScreen.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [core] example - World to screen +* raylib [core] example - world screen * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2015 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.3, last time updated with raylib 1.4 +* +* 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 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,84 +18,113 @@ using static Raylib_cs.Raylib; namespace Examples.Core; -public class WorldScreen +public partial class WorldScreen : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Core / World to Screen"; + + public string Title => "raylib [core] example - world screen"; + + public bool CursorDisabled => true; + + private Camera3D camera; + private Vector3 cubePosition; + private Vector2 cubeScreenPosition = new(0.0f, 0.0f); + + public 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 + + cubePosition = new(0.0f, 0.0f, 0.0f); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.ThirdPerson); + + // Calculate cube screen space position (with a little offset to be in top) + cubeScreenPosition = GetWorldToScreen( + new Vector3(cubePosition.X, cubePosition.Y + 2.5f, cubePosition.Z), + camera + ); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawCube(cubePosition, 2.0f, 2.0f, 2.0f, Color.Red); + DrawCubeWires(cubePosition, 2.0f, 2.0f, 2.0f, Color.Maroon); + + DrawGrid(10, 1.0f); + + EndMode3D(); + + DrawText( + "Enemy: 100/100", + (int)cubeScreenPosition.X - MeasureText("Enemy: 100/100", 20) / 2, + (int)cubeScreenPosition.Y, + 20, + Color.Black + ); + + DrawText( + $"Cube position in screen space coordinates: [{(int)cubeScreenPosition.X}, {(int)cubeScreenPosition.Y}]", + 10, + 10, + 20, + Color.Lime + ); + DrawText("Text 2d should be always on top of the cube", 10, 40, 20, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [core] example - world screen"); - InitWindow(screenWidth, screenHeight, "raylib [core] example - 3d camera free"); + DisableCursor(); // Limit cursor to relative movement inside the window - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(10.0f, 10.0f, 10.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; - - Vector3 cubePosition = new(0.0f, 0.0f, 0.0f); - Vector2 cubeScreenPosition; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new WorldScreen(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Free); - - // Calculate cube screen space position (with a little offset to be in top) - cubeScreenPosition = GetWorldToScreen( - new Vector3(cubePosition.X, cubePosition.Y + 2.5f, cubePosition.Z), - camera - ); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawCube(cubePosition, 2.0f, 2.0f, 2.0f, Color.Red); - DrawCubeWires(cubePosition, 2.0f, 2.0f, 2.0f, Color.Maroon); - - DrawGrid(10, 1.0f); - - EndMode3D(); - - DrawText( - "Enemy: 100 / 100", - (int)cubeScreenPosition.X - MeasureText("Enemy: 100 / 100", 20) / 2, - (int)cubeScreenPosition.Y, - 20, - Color.Black - ); - DrawText( - "Text is always on top of the cube", - (screenWidth - MeasureText("Text is always on top of the cube", 20)) / 2, - 25, - 20, - Color.Gray - ); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } } - diff --git a/Examples/ExampleRegistry.cs b/Examples/ExampleRegistry.cs new file mode 100644 index 0000000..175a443 --- /dev/null +++ b/Examples/ExampleRegistry.cs @@ -0,0 +1,53 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Examples; + +/// +/// 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 +{ + /// Category order used for the browser dropdown and desktop run-all sequence. + private static readonly string[] CategoryOrder = + [ + "Core", + "Shapes", + "Models", + "Textures", + "Text", + "Audio", + "Shaders", + ]; + + private static readonly IExample[] AllExamples = DiscoverAll(); + + public static readonly IExample[] DesktopExamples = AllExamples; + + public static readonly IExample[] BrowserExamples = + 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.")] + [UnconditionalSuppressMessage("Trimming", "IL2067", + Justification = "Same as IL2026: all example types and their parameterless constructors are rooted.")] + private static IExample[] DiscoverAll() + { + return typeof(IExample).Assembly + .GetTypes() + .Where(t => typeof(IExample).IsAssignableFrom(t) && t.IsClass && !t.IsAbstract) + .Select(t => (IExample)Activator.CreateInstance(t)) + .OrderBy(e => Array.IndexOf(CategoryOrder, Category(e))) + .ThenBy(e => e.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + + // "Examples.Core" -> "Core" + private static string Category(IExample example) + { + var ns = example.GetType().Namespace ?? ""; + return ns[(ns.LastIndexOf('.') + 1)..]; + } +} diff --git a/Examples/Examples.csproj b/Examples/Examples.csproj index e4c3b6d..e0b6f89 100644 --- a/Examples/Examples.csproj +++ b/Examples/Examples.csproj @@ -3,28 +3,71 @@ Exe net8.0;net10.0 true - Examples.Program + Examples.Program + Examples.Web.Host $(MSBuildThisFileDirectory) 12 enable false + + true + Web/main.js + disable + + $(NoWarn);CA1416 + $(DefineConstants);BROWSER + + + true + true + true + $(EmccFlags) -sALLOW_MEMORY_GROWTH=1 -sINITIAL_MEMORY=64MB + + + -O0 + -O0 + false + 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 new file mode 100644 index 0000000..f2100d9 --- /dev/null +++ b/Examples/IExample.cs @@ -0,0 +1,55 @@ +namespace Examples; + +/// +/// A runnable raylib example. Loop-spanning state lives in instance fields, (re)initialized in +/// so re-selecting an example resets it. +/// +/// +/// 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 +{ + /// Display name shown in the navigation dropdown. + string Name { get; } + + /// Window title, matching the example's standalone Main(). + string Title { get; } + + /// 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 always applies it; the browser host paces its frame loop with it. + int TargetFps => 60; + + /// 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. Ignored in the browser. + bool CursorHidden => false; + + /// + /// 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. + void Update(); + + /// Free resources. + void Unload(); +} 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/BillboardDemo.cs b/Examples/Models/BillboardDemo.cs index 8b1359e..82c81a7 100644 --- a/Examples/Models/BillboardDemo.cs +++ b/Examples/Models/BillboardDemo.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [models] example - Drawing billboards +* raylib [models] example - billboard rendering * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 * -* Copyright (c) 2015 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.3, last time updated with raylib 3.5 +* +* 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 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,121 +18,131 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class BillboardDemo +public partial class BillboardDemo : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Billboard Demo"; + + public string Title => "raylib [models] example - billboard rendering"; + + private Camera3D camera; + private Texture2D bill; + private Vector3 billPositionStatic; + private Vector3 billPositionRotating; + private Rectangle source; + private Vector3 billUp; + private Vector2 size; + private Vector2 origin; + private float distanceStatic; + private float distanceRotating; + private float rotation; + + public void Init() + { + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(5.0f, 4.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 + + bill = LoadTexture("resources/billboard.png"); // Our billboard texture + billPositionStatic = new(0.0f, 2.0f, 0.0f); // Position of static billboard + billPositionRotating = new(1.0f, 2.0f, 1.0f); // Position of rotating billboard + + // Entire billboard texture, source is used to take a segment from a larger texture + source = new(0.0f, 0.0f, (float)bill.Width, (float)bill.Height); + + // NOTE: Billboard locked on axis-Y + billUp = new(0.0f, 1.0f, 0.0f); + + // Set the height of the rotating billboard to 1.0 with the aspect ratio fixed + size = new(source.Width / source.Height, 1.0f); + + // Rotate around origin + // Here we choose to rotate around the image center + origin = size * 0.5f; + + // Distance is needed for the correct billboard draw order + // Larger distance (further away from the camera) should be drawn prior to smaller distance + distanceStatic = 0.0f; + distanceRotating = 0.0f; + rotation = 0.0f; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + + rotation += 0.4f; + distanceStatic = Vector3.Distance(camera.Position, billPositionStatic); + distanceRotating = Vector3.Distance(camera.Position, billPositionRotating); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawGrid(10, 1.0f); // Draw a grid + + // Draw order matters! + if (distanceStatic > distanceRotating) + { + DrawBillboard(camera, bill, billPositionStatic, 2.0f, Color.White); + DrawBillboardPro(camera, bill, source, billPositionRotating, billUp, size, origin, rotation, Color.White); + } + else + { + DrawBillboardPro(camera, bill, source, billPositionRotating, billUp, size, origin, rotation, Color.White); + DrawBillboard(camera, bill, billPositionStatic, 2.0f, Color.White); + } + + EndMode3D(); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(bill); // Unload texture + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [models] example - billboard rendering"); - InitWindow(screenWidth, screenHeight, "raylib [models] example - drawing billboards"); - - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(5.0f, 4.0f, 5.0f); - camera.Target = new Vector3(0.0f, 2.0f, 0.0f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.FovY = 45.0f; - camera.Projection = CameraProjection.Perspective; - - // Our texture billboard - Texture2D bill = LoadTexture("resources/billboard.png"); - - // Position of billboard billboard - Vector3 billPositionStatic = new(0.0f, 2.0f, 0.0f); - Vector3 billPositionRotating = new(1.0f, 2.0f, 1.0f); - - // Entire billboard texture, source is used to take a segment from a larger texture. - Rectangle source = new(0.0f, 0.0f, (float)bill.Width, (float)bill.Height); - - // NOTE: Billboard locked on axis-Y - Vector3 billUp = new(0.0f, 1.0f, 0.0f); - - // Rotate around origin - // Here we choose to rotate around the image center - // NOTE: (-1, 1) is the range where origin.X, origin.Y is inside the texture - Vector2 rotateOrigin = Vector2.Zero; - - // Distance is needed for the correct billboard draw order - // Larger distance (further away from the camera) should be drawn prior to smaller distance. - float distanceStatic = 0.0f; - float distanceRotating = 0.0f; - - float rotation = 0.0f; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new BillboardDemo(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Orbital); - rotation += 0.4f; - distanceStatic = Vector3.Distance(camera.Position, billPositionStatic); - distanceRotating = Vector3.Distance(camera.Position, billPositionRotating); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawGrid(10, 1.0f); - - // Draw order matters! - if (distanceStatic > distanceRotating) - { - DrawBillboard(camera, bill, billPositionStatic, 2.0f, Color.White); - DrawBillboardPro( - camera, - bill, - source, - billPositionRotating, - billUp, - new Vector2(1.0f, 1.0f), - rotateOrigin, - rotation, - Color.White - ); - } - else - { - DrawBillboardPro( - camera, - bill, - source, - billPositionRotating, - billUp, - new Vector2(1.0f, 1.0f), - rotateOrigin, - rotation, - Color.White - ); - DrawBillboard(camera, bill, billPositionStatic, 2.0f, Color.White); - } - - EndMode3D(); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(bill); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- 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/BoxCollisions.cs b/Examples/Models/BoxCollisions.cs index 6d4436c..65bb5bc 100644 --- a/Examples/Models/BoxCollisions.cs +++ b/Examples/Models/BoxCollisions.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [models] example - Detect basic 3d collisions (box vs sphere vs box) +* raylib [models] example - box collisions * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 * -* Copyright (c) 2015 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.3, last time updated with raylib 3.5 +* +* 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 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,129 +18,165 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class BoxCollisions +public partial class BoxCollisions : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Box Collisions"; + + public string Title => "raylib [models] example - box collisions"; + + private Camera3D camera; + + private Vector3 playerPosition; + private Vector3 playerSize; + private Color playerColor; + + private Vector3 enemyBoxPos; + private Vector3 enemyBoxSize; + + private Vector3 enemySpherePos; + private float enemySphereSize; + + private bool collision; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [models] example - box collisions"); - // Define the camera to look into our 3d world - Camera3D camera = new(); + camera = new(); camera.Position = new Vector3(0.0f, 10.0f, 10.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; - Vector3 playerPosition = new(0.0f, 1.0f, 2.0f); - Vector3 playerSize = new(1.0f, 2.0f, 1.0f); - Color playerColor = Color.Green; + playerPosition = new(0.0f, 1.0f, 2.0f); + playerSize = new(1.0f, 2.0f, 1.0f); + playerColor = Color.Green; - Vector3 enemyBoxPos = new(-4.0f, 1.0f, 0.0f); - Vector3 enemyBoxSize = new(2.0f, 2.0f, 2.0f); + enemyBoxPos = new(-4.0f, 1.0f, 0.0f); + enemyBoxSize = new(2.0f, 2.0f, 2.0f); - Vector3 enemySpherePos = new(4.0f, 0.0f, 0.0f); - float enemySphereSize = 1.5f; + enemySpherePos = new(4.0f, 0.0f, 0.0f); + enemySphereSize = 1.5f; - bool collision = false; + collision = false; + } - SetTargetFPS(60); // Set our game to run at 60 frames-per-second + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + + // Move player + if (IsKeyDown(KeyboardKey.Right)) + { + playerPosition.X += 0.2f; + } + else if (IsKeyDown(KeyboardKey.Left)) + { + playerPosition.X -= 0.2f; + } + else if (IsKeyDown(KeyboardKey.Down)) + { + playerPosition.Z += 0.2f; + } + else if (IsKeyDown(KeyboardKey.Up)) + { + playerPosition.Z -= 0.2f; + } + + collision = false; + + // Check collisions player vs enemy-box + BoundingBox box1 = new( + playerPosition - (playerSize / 2), + playerPosition + (playerSize / 2) + ); + BoundingBox box2 = new( + enemyBoxPos - (enemyBoxSize / 2), + enemyBoxPos + (enemyBoxSize / 2) + ); + + if (CheckCollisionBoxes(box1, box2)) + { + collision = true; + } + + // Check collisions player vs enemy-sphere + if (CheckCollisionBoxSphere(box1, enemySpherePos, enemySphereSize)) + { + collision = true; + } + + if (collision) + { + playerColor = Color.Red; + } + else + { + playerColor = Color.Green; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + // Draw enemy-box + DrawCube(enemyBoxPos, enemyBoxSize.X, enemyBoxSize.Y, enemyBoxSize.Z, Color.Gray); + DrawCubeWires(enemyBoxPos, enemyBoxSize.X, enemyBoxSize.Y, enemyBoxSize.Z, Color.DarkGray); + + // Draw enemy-sphere + DrawSphere(enemySpherePos, enemySphereSize, Color.Gray); + DrawSphereWires(enemySpherePos, enemySphereSize, 16, 16, Color.DarkGray); + + // Draw player + DrawCubeV(playerPosition, playerSize, playerColor); + + DrawGrid(10, 1.0f); // Draw a grid + + EndMode3D(); + + DrawText("Move player with arrow keys to collide", 220, 40, 20, Color.Gray); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - box collisions"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new BoxCollisions(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - - // Move player - if (IsKeyDown(KeyboardKey.Right)) - { - playerPosition.X += 0.2f; - } - else if (IsKeyDown(KeyboardKey.Left)) - { - playerPosition.X -= 0.2f; - } - else if (IsKeyDown(KeyboardKey.Down)) - { - playerPosition.Z += 0.2f; - } - else if (IsKeyDown(KeyboardKey.Up)) - { - playerPosition.Z -= 0.2f; - } - - collision = false; - - // Check collisions player vs enemy-box - BoundingBox box1 = new( - playerPosition - (playerSize / 2), - playerPosition + (playerSize / 2) - ); - BoundingBox box2 = new( - enemyBoxPos - (enemyBoxSize / 2), - enemyBoxPos + (enemyBoxSize / 2) - ); - - if (CheckCollisionBoxes(box1, box2)) - { - collision = true; - } - - // Check collisions player vs enemy-sphere - if (CheckCollisionBoxSphere(box1, enemySpherePos, enemySphereSize)) - { - collision = true; - } - - if (collision) - { - playerColor = Color.Red; - } - else - { - playerColor = Color.Green; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - // Draw enemy-box - DrawCube(enemyBoxPos, enemyBoxSize.X, enemyBoxSize.Y, enemyBoxSize.Z, Color.Gray); - DrawCubeWires(enemyBoxPos, enemyBoxSize.X, enemyBoxSize.Y, enemyBoxSize.Z, Color.DarkGray); - - // Draw enemy-sphere - DrawSphere(enemySpherePos, enemySphereSize, Color.Gray); - DrawSphereWires(enemySpherePos, enemySphereSize, 16, 16, Color.DarkGray); - - // Draw player - DrawCubeV(playerPosition, playerSize, playerColor); - - DrawGrid(10, 1.0f); - - EndMode3D(); - - DrawText("Move player with cursors to collide", 220, 40, 20, Color.Gray); - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Models/CubicmapDemo.cs b/Examples/Models/CubicmapDemo.cs index 1ddaefd..cb4f5c8 100644 --- a/Examples/Models/CubicmapDemo.cs +++ b/Examples/Models/CubicmapDemo.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [models] example - Cubicmap loading and drawing +* raylib [models] example - cubicmap rendering * -* This example has been created using raylib 1.8 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2015 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.8, last time updated with raylib 3.5 +* +* 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 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,91 +18,128 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class CubicmapDemo +public partial class CubicmapDemo : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Cubicmap Demo"; + + public string Title => "raylib [models] example - cubicmap rendering"; + + private Camera3D camera; + private Texture2D cubicmap; + private Texture2D texture; + private Model model; + private Vector3 mapPosition; + private bool pause; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [models] example - cubesmap loading and drawing"); - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(16.0f, 14.0f, 16.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; + camera = new(); + camera.Position = new Vector3(16.0f, 14.0f, 16.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 - Image image = LoadImage("resources/cubicmap.png"); - Texture2D cubicmap = LoadTextureFromImage(image); + var image = LoadImage("resources/cubicmap.png"); // Load cubicmap image (RAM) + cubicmap = LoadTextureFromImage(image); // Convert image to texture to display (VRAM) - Mesh mesh = GenMeshCubicmap(image, new Vector3(1.0f, 1.0f, 1.0f)); - Model model = LoadModelFromMesh(mesh); + var mesh = GenMeshCubicmap(image, new Vector3(1.0f, 1.0f, 1.0f)); + model = LoadModelFromMesh(mesh); // NOTE: By default each cube is mapped to one part of texture atlas - Texture2D texture = LoadTexture("resources/cubicmap_atlas.png"); + texture = LoadTexture("resources/cubicmap_atlas.png"); // Load map texture // Set map diffuse texture Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.Albedo, ref texture); - Vector3 mapPosition = new(-16.0f, 0.0f, -8.0f); - UnloadImage(image); + mapPosition = new(-16.0f, 0.0f, -8.0f); // Set model position - SetTargetFPS(60); + UnloadImage(image); // Unload cubesmap image from RAM, already uploaded to VRAM + + pause = false; // Pause camera orbital rotation (and zoom) + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.P)) + { + pause = !pause; + } + + if (!pause) + { + UpdateCamera(ref camera, CameraMode.Orbital); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawModel(model, mapPosition, 1.0f, Color.White); + + EndMode3D(); + + Vector2 position = new(screenWidth - cubicmap.Width * 4 - 20, 20); + DrawTextureEx(cubicmap, position, 0.0f, 4.0f, Color.White); + DrawRectangleLines( + screenWidth - cubicmap.Width * 4 - 20, + 20, + cubicmap.Width * 4, + cubicmap.Height * 4, + Color.Green + ); + + DrawText("cubicmap image used to", 658, 90, 10, Color.Gray); + DrawText("generate map 3d model", 658, 104, 10, Color.Gray); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(cubicmap); // Unload cubicmap texture + UnloadTexture(texture); // Unload map texture + UnloadModel(model); // Unload map model + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - cubicmap rendering"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new CubicmapDemo(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Orbital); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawModel(model, mapPosition, 1.0f, Color.White); - - EndMode3D(); - - Vector2 position = new(screenWidth - cubicmap.Width * 4 - 20, 20); - DrawTextureEx(cubicmap, position, 0.0f, 4.0f, Color.White); - DrawRectangleLines( - screenWidth - cubicmap.Width * 4 - 20, - 20, - cubicmap.Width * 4, - cubicmap.Height * 4, - Color.Green - ); - - DrawText("cubicmap image used to", 658, 90, 10, Color.Gray); - DrawText("generate map 3d model", 658, 104, 10, Color.Gray); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(cubicmap); - UnloadTexture(texture); - UnloadModel(model); - - CloseWindow(); + 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/DynamicMesh.cs b/Examples/Models/DynamicMesh.cs index fff2c5e..e9dae9f 100644 --- a/Examples/Models/DynamicMesh.cs +++ b/Examples/Models/DynamicMesh.cs @@ -3,19 +3,28 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class DynamicMesh +public partial class DynamicMesh : IExample { - public unsafe static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int triangleRows = 48; + private const int vertexRows = triangleRows + 1; + + public string Name => "Models / Dynamic Mesh"; + + public string Title => "raylib [models] example - dynamic mesh"; + + private Camera3D camera; + private Mesh dynamicMesh; + private Texture2D texture; + private Color[] pixels; + private Material material; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [models] example - dynamic mesh"); - // Define the camera to look into our 3d world - Camera3D camera = new(); + camera = new(); camera.Position = Vector3.One * 1.5f; camera.Target = camera.Position + new Vector3(1f, -0.25f, 1f); camera.Up = Vector3.UnitY; @@ -23,18 +32,14 @@ public class DynamicMesh camera.Projection = CameraProjection.Perspective; // Generate a dynamic mesh using utils to allocate/access mesh attribute data - const int triangleRows = 48; - const int vertexRows = triangleRows + 1; - Mesh dynamicMesh = new(vertexRows * vertexRows, triangleRows * triangleRows * 2); + dynamicMesh = new(vertexRows * vertexRows, triangleRows * triangleRows * 2); dynamicMesh.AllocVertices(); dynamicMesh.AllocTexCoords(); dynamicMesh.AllocIndices(); - Span vertices = dynamicMesh.VerticesAs(); - Span texcoords = dynamicMesh.TexCoordsAs(); - Span indices = dynamicMesh.IndicesAs(); + var indices = dynamicMesh.IndicesAs(); for (int z = 0, i = 0; z < triangleRows; z++) { - for (int x = 0; x < triangleRows; x++, i += 6) + for (var x = 0; x < triangleRows; x++, i += 6) { indices[i + 0] = (ushort)(x + (z * vertexRows)); indices[i + 1] = (ushort)(indices[i] + vertexRows); @@ -47,73 +52,96 @@ public class DynamicMesh UploadMesh(ref dynamicMesh, true); // Allocate the texture - Image image = GenImageColor(triangleRows, triangleRows, Color.Blank); - Texture2D texture = LoadTextureFromImage(image); - Color[] pixels = new Color[texture.Width * texture.Height]; + var image = GenImageColor(triangleRows, triangleRows, Color.Blank); + texture = LoadTextureFromImage(image); + pixels = new Color[texture.Width * texture.Height]; UnloadImage(image); // Load the material - Material material = LoadMaterialDefault(); + material = LoadMaterialDefault(); SetMaterialTexture(ref material, MaterialMapIndex.Diffuse, texture); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + var time = (float)GetTime(); + Random random = new(42); + + var vertices = dynamicMesh.VerticesAs(); + var texcoords = dynamicMesh.TexCoordsAs(); + + for (int z = 0, i = 0; z < vertexRows; z++) + { + for (var x = 0; x < vertexRows; x++, i++) + { + var noiseX = SmoothNoise(time + random.Next(10000)); + var noiseZ = SmoothNoise(time + random.Next(10000)); + vertices[i].X = x + noiseX - .5f; + vertices[i].Y = (noiseX + noiseZ) / 2; + vertices[i].Z = z + noiseZ - .5f; + texcoords[i].X = (x - noiseZ) / triangleRows; + texcoords[i].Y = (z - noiseX) / triangleRows; + } + } + UpdateMeshBuffer(dynamicMesh, Mesh.VboIdIndexVertices, vertices, 0); + UpdateMeshBuffer(dynamicMesh, Mesh.VboIdIndexTexCoords, texcoords, 0); + + for (int y = 0, i = 0; y < texture.Height; y++) + { + for (var x = 0; x < texture.Width; x++, i++) + { + pixels[i] = new(32, 178, 170, 255); + pixels[i] = ColorBrightness(pixels[i], (SmoothNoise(time + random.Next(10000)) / 8) - (1 / 16f)); + pixels[i] = ColorAlpha(pixels[i], (triangleRows - new Vector2(x, y).Length()) / triangleRows); + } + } + UpdateTexture(texture, pixels); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + DrawMesh(dynamicMesh, material, Matrix4x4.Identity); + EndMode3D(); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadMaterial(material); + // Raylib.UnloadTexture(texture); <- No need to unload the texture. UnloadMaterial(Material) already unloaded it for us + UnloadMesh(dynamicMesh); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - dynamic mesh"); SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new DynamicMesh(); + game.Init(); + // Main game loop while (!WindowShouldClose()) { - // Update - //---------------------------------------------------------------------------------- - float time = (float)GetTime(); - Random random = new(42); - - for (int z = 0, i = 0; z < vertexRows; z++) - { - for (int x = 0; x < vertexRows; x++, i++) - { - float noiseX = SmoothNoise(time + random.Next(10000)); - float noiseZ = SmoothNoise(time + random.Next(10000)); - vertices[i].X = x + noiseX - .5f; - vertices[i].Y = (noiseX + noiseZ) / 2; - vertices[i].Z = z + noiseZ - .5f; - texcoords[i].X = (x - noiseZ) / triangleRows; - texcoords[i].Y = (z - noiseX) / triangleRows; - } - } - UpdateMeshBuffer(dynamicMesh, Mesh.VboIdIndexVertices, vertices, 0); - UpdateMeshBuffer(dynamicMesh, Mesh.VboIdIndexTexCoords, texcoords, 0); - - for (int y = 0, i = 0; y < texture.Height; y++) - { - for (int x = 0; x < texture.Width; x++, i++) - { - pixels[i] = new(32, 178, 170, 255); - pixels[i] = ColorBrightness(pixels[i], (SmoothNoise(time + random.Next(10000)) / 8) - (1 / 16f)); - pixels[i] = ColorAlpha(pixels[i], (triangleRows - new Vector2(x, y).Length()) / triangleRows); - } - } - UpdateTexture(texture, pixels); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - DrawMesh(dynamicMesh, material, Matrix4x4.Identity); - EndMode3D(); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadMaterial(material); - // Raylib.UnloadTexture(texture); <- No need to unload the texture. UnloadMaterial(Material) already unloaded it for us - UnloadMesh(dynamicMesh); - CloseWindow(); //-------------------------------------------------------------------------------------- diff --git a/Examples/Models/FirstPersonMaze.cs b/Examples/Models/FirstPersonMaze.cs index 1582b58..ac91600 100644 --- a/Examples/Models/FirstPersonMaze.cs +++ b/Examples/Models/FirstPersonMaze.cs @@ -2,10 +2,14 @@ * * raylib [models] example - first person maze * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2019 Ramon Santamaria (@raysan5) +* Example originally created with raylib 2.5, last time updated with raylib 3.5 +* +* 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 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,140 +18,163 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class FirstPersonMaze +public unsafe partial class FirstPersonMaze : IExample { - public unsafe static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / First Person Maze"; + + public string Title => "raylib [models] example - first person maze"; + + public bool CursorDisabled => true; + + private Camera3D camera; + private Texture2D cubicmap; + private Texture2D texture; + private Model model; + private Color* mapPixels; + private Vector3 mapPosition; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [models] example - first person maze"); - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(0.2f, 0.4f, 0.2f); - 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; + camera = new(); + camera.Position = new Vector3(0.2f, 0.4f, 0.2f); // Camera position + camera.Target = new Vector3(0.185f, 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 - Image imMap = LoadImage("resources/cubicmap.png"); - Texture2D cubicmap = LoadTextureFromImage(imMap); - Mesh mesh = GenMeshCubicmap(imMap, new Vector3(1.0f, 1.0f, 1.0f)); - Model model = LoadModelFromMesh(mesh); + var imMap = LoadImage("resources/cubicmap.png"); // Load cubicmap image (RAM) + cubicmap = LoadTextureFromImage(imMap); // Convert image to texture to display (VRAM) + var mesh = GenMeshCubicmap(imMap, new Vector3(1.0f, 1.0f, 1.0f)); + model = LoadModelFromMesh(mesh); // NOTE: By default each cube is mapped to one part of texture atlas - Texture2D texture = LoadTexture("resources/cubicmap_atlas.png"); + texture = LoadTexture("resources/cubicmap_atlas.png"); // Load map texture // Set map diffuse texture Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.Albedo, ref texture); // Get map image data to be used for collision detection - Color* mapPixels = LoadImageColors(imMap); - UnloadImage(imMap); + mapPixels = LoadImageColors(imMap); + UnloadImage(imMap); // Unload image from RAM - Vector3 mapPosition = new(-16.0f, 0.0f, -8.0f); - Vector3 playerPosition = camera.Position; + mapPosition = new(-16.0f, 0.0f, -8.0f); // Set model position + } - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + var oldCamPos = camera.Position; // Store old camera position - // Main game loop - while (!WindowShouldClose()) + UpdateCamera(ref camera, CameraMode.FirstPerson); + + // Check player collision (we simplify to 2D collision detection) + Vector2 playerPos = new(camera.Position.X, camera.Position.Z); + var playerRadius = 0.1f; // Collision radius (player is modelled as a cilinder for collision) + + var playerCellX = (int)(playerPos.X - mapPosition.X + 0.5f); + var playerCellY = (int)(playerPos.Y - mapPosition.Z + 0.5f); + + // Out-of-limits security check + if (playerCellX < 0) { - // Update - //---------------------------------------------------------------------------------- - Vector3 oldCamPos = camera.Position; + playerCellX = 0; + } + else if (playerCellX >= cubicmap.Width) + { + playerCellX = cubicmap.Width - 1; + } - UpdateCamera(ref camera, CameraMode.FirstPerson); + if (playerCellY < 0) + { + playerCellY = 0; + } + else if (playerCellY >= cubicmap.Height) + { + playerCellY = cubicmap.Height - 1; + } - // Check player collision (we simplify to 2D collision detection) - Vector2 playerPos = new(camera.Position.X, camera.Position.Z); - - // Collision radius (player is modelled as a cilinder for collision) - float playerRadius = 0.1f; - - int playerCellX = (int)(playerPos.X - mapPosition.X + 0.5f); - int playerCellY = (int)(playerPos.Y - mapPosition.Z + 0.5f); - - // Out-of-limits security check - if (playerCellX < 0) + // Check map collisions using image data and player position against surrounding cells only + for (var y = playerCellY - 1; y <= playerCellY + 1; y++) + { + // Avoid map accessing out of bounds + if ((y >= 0) && (y < cubicmap.Height)) { - playerCellX = 0; - } - else if (playerCellX >= cubicmap.Width) - { - playerCellX = cubicmap.Width - 1; - } - - if (playerCellY < 0) - { - playerCellY = 0; - } - else if (playerCellY >= cubicmap.Height) - { - playerCellY = cubicmap.Height - 1; - } - - // Check map collisions using image data and player position - // TODO: Improvement: Just check player surrounding cells for collision - for (int y = 0; y < cubicmap.Height; y++) - { - for (int x = 0; x < cubicmap.Width; x++) + for (var x = playerCellX - 1; x <= playerCellX + 1; x++) { - Color* mapPixelsData = mapPixels; - - // Collision: Color.white pixel, only check R channel - Rectangle rec = new( - mapPosition.X - 0.5f + x * 1.0f, - mapPosition.Z - 0.5f + y * 1.0f, - 1.0f, - 1.0f - ); - - bool collision = CheckCollisionCircleRec(playerPos, playerRadius, rec); - if ((mapPixelsData[y * cubicmap.Width + x].R == 255) && collision) + // NOTE: Collision: Only checking R channel for white pixel + if (((x >= 0) && (x < cubicmap.Width)) && + (mapPixels[y * cubicmap.Width + x].R == 255) && + (CheckCollisionCircleRec(playerPos, playerRadius, + new Rectangle(mapPosition.X - 0.5f + x * 1.0f, mapPosition.Z - 0.5f + y * 1.0f, 1.0f, 1.0f)))) { // Collision detected, reset camera position camera.Position = oldCamPos; } } } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // Draw maze map - BeginMode3D(camera); - DrawModel(model, mapPosition, 1.0f, Color.White); - EndMode3D(); - - DrawTextureEx(cubicmap, new Vector2(GetScreenWidth() - cubicmap.Width * 4 - 20, 20), 0.0f, 4.0f, Color.White); - DrawRectangleLines(GetScreenWidth() - cubicmap.Width * 4 - 20, 20, cubicmap.Width * 4, cubicmap.Height * 4, Color.Green); - - // Draw player position radar - DrawRectangle(GetScreenWidth() - cubicmap.Width * 4 - 20 + playerCellX * 4, 20 + playerCellY * 4, 4, 4, Color.Red); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + DrawModel(model, mapPosition, 1.0f, Color.White); // Draw maze map + EndMode3D(); + + DrawTextureEx(cubicmap, new Vector2(GetScreenWidth() - cubicmap.Width * 4 - 20, 20), 0.0f, 4.0f, Color.White); + DrawRectangleLines(GetScreenWidth() - cubicmap.Width * 4 - 20, 20, cubicmap.Width * 4, cubicmap.Height * 4, Color.Green); + + // Draw player position radar + DrawRectangle(GetScreenWidth() - cubicmap.Width * 4 - 20 + playerCellX * 4, 20 + playerCellY * 4, 4, 4, Color.Red); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadImageColors(mapPixels); // Unload color array + + UnloadTexture(cubicmap); // Unload cubicmap texture + UnloadTexture(texture); // Unload map texture + UnloadModel(model); // Unload map model + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - first person maze"); + + DisableCursor(); // Limit cursor to relative movement inside the window + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new FirstPersonMaze(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); // De-Initialization //-------------------------------------------------------------------------------------- - UnloadImageColors(mapPixels); - - UnloadTexture(cubicmap); - UnloadTexture(texture); - UnloadModel(model); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Models/GeometricShapes.cs b/Examples/Models/GeometricShapes.cs index 3a59c59..465264c 100644 --- a/Examples/Models/GeometricShapes.cs +++ b/Examples/Models/GeometricShapes.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [models] example - Draw some basic geometric shapes (cube, sphere, cylinder...) +* raylib [models] example - geometric shapes * -* This example has been created using raylib 1.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 * -* Copyright (c) 2014 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.0, last time updated with raylib 3.5 +* +* 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) * ********************************************************************************************/ @@ -14,70 +18,91 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class GeometricShapes +public partial class GeometricShapes : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Geometric Shapes"; + + public string Title => "raylib [models] example - geometric shapes"; + + private Camera3D camera; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [models] example - geometric shapes"); - // Define the camera to look into our 3d world - Camera3D camera = new(); + camera = new(); camera.Position = new Vector3(0.0f, 10.0f, 10.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; + } - SetTargetFPS(60); // Set our game to run at 60 frames-per-second + public void Update() + { + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawCube(new Vector3(-4.0f, 0.0f, 2.0f), 2.0f, 5.0f, 2.0f, Color.Red); + DrawCubeWires(new Vector3(-4.0f, 0.0f, 2.0f), 2.0f, 5.0f, 2.0f, Color.Gold); + DrawCubeWires(new Vector3(-4.0f, 0.0f, -2.0f), 3.0f, 6.0f, 2.0f, Color.Maroon); + + DrawSphere(new Vector3(-1.0f, 0.0f, -2.0f), 1.0f, Color.Green); + DrawSphereWires(new Vector3(1.0f, 0.0f, 2.0f), 2.0f, 16, 16, Color.Lime); + + DrawCylinder(new Vector3(4.0f, 0.0f, -2.0f), 1.0f, 2.0f, 3.0f, 4, Color.SkyBlue); + DrawCylinderWires(new Vector3(4.0f, 0.0f, -2.0f), 1.0f, 2.0f, 3.0f, 4, Color.DarkBlue); + DrawCylinderWires(new Vector3(4.5f, -1.0f, 2.0f), 1.0f, 1.0f, 2.0f, 6, Color.Brown); + + DrawCylinder(new Vector3(1.0f, 0.0f, -4.0f), 0.0f, 1.5f, 3.0f, 8, Color.Gold); + DrawCylinderWires(new Vector3(1.0f, 0.0f, -4.0f), 0.0f, 1.5f, 3.0f, 8, Color.Pink); + + DrawCapsule(new Vector3(-3.0f, 1.5f, -4.0f), new Vector3(-4.0f, -1.0f, -4.0f), 1.2f, 8, 8, Color.Violet); + DrawCapsuleWires(new Vector3(-3.0f, 1.5f, -4.0f), new Vector3(-4.0f, -1.0f, -4.0f), 1.2f, 8, 8, Color.Purple); + + DrawGrid(10, 1.0f); // Draw a grid + + EndMode3D(); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - geometric shapes"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new GeometricShapes(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // TODO: Update your variables here - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawCube(new Vector3(-4.0f, 0.0f, 2.0f), 2.0f, 5.0f, 2.0f, Color.Red); - DrawCubeWires(new Vector3(-4.0f, 0.0f, 2.0f), 2.0f, 5.0f, 2.0f, Color.Gold); - DrawCubeWires(new Vector3(-4.0f, 0.0f, -2.0f), 3.0f, 6.0f, 2.0f, Color.Maroon); - - DrawSphere(new Vector3(-1.0f, 0.0f, -2.0f), 1.0f, Color.Green); - DrawSphereWires(new Vector3(1.0f, 0.0f, 2.0f), 2.0f, 16, 16, Color.Lime); - - DrawCylinder(new Vector3(4.0f, 0.0f, -2.0f), 1.0f, 2.0f, 3.0f, 4, Color.SkyBlue); - DrawCylinderWires(new Vector3(4.0f, 0.0f, -2.0f), 1.0f, 2.0f, 3.0f, 4, Color.DarkBlue); - DrawCylinderWires(new Vector3(4.5f, -1.0f, 2.0f), 1.0f, 1.0f, 2.0f, 6, Color.Brown); - - DrawCylinder(new Vector3(1.0f, 0.0f, -4.0f), 0.0f, 1.5f, 3.0f, 8, Color.Gold); - DrawCylinderWires(new Vector3(1.0f, 0.0f, -4.0f), 0.0f, 1.5f, 3.0f, 8, Color.Pink); - - DrawGrid(10, 1.0f); - - EndMode3D(); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Models/HeightmapDemo.cs b/Examples/Models/HeightmapDemo.cs index d0471f5..74efd36 100644 --- a/Examples/Models/HeightmapDemo.cs +++ b/Examples/Models/HeightmapDemo.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [models] example - Heightmap loading and drawing +* raylib [models] example - heightmap rendering * -* This example has been created using raylib 1.8 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 * -* Copyright (c) 2015 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.8, last time updated with raylib 3.5 +* +* 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 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,77 +18,102 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class HeightmapDemo +public partial class HeightmapDemo : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Heightmap Demo"; + + public string Title => "raylib [models] example - heightmap rendering"; + + private Camera3D camera; + private Texture2D texture; + private Model model; + private Vector3 mapPosition; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [models] example - heightmap loading and drawing"); - // Define our custom camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(18.0f, 16.0f, 18.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; + camera = new(); + camera.Position = new Vector3(18.0f, 21.0f, 18.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 - Image image = LoadImage("resources/heightmap.png"); - Texture2D texture = LoadTextureFromImage(image); + var image = LoadImage("resources/heightmap.png"); // Load heightmap image (RAM) + texture = LoadTextureFromImage(image); // Convert image to texture (VRAM) - Mesh mesh = GenMeshHeightmap(image, new Vector3(16, 8, 16)); - Model model = LoadModelFromMesh(mesh); + var mesh = GenMeshHeightmap(image, new Vector3(16, 8, 16)); // Generate heightmap mesh (RAM and VRAM) + model = LoadModelFromMesh(mesh); // Load model from generated mesh // Set map diffuse texture Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.Albedo, ref texture); - Vector3 mapPosition = new(-8.0f, 0.0f, -8.0f); + mapPosition = new(-8.0f, 0.0f, -8.0f); // Define model position - UnloadImage(image); + UnloadImage(image); // Unload heightmap image from RAM, already uploaded to VRAM + } - SetTargetFPS(60); + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawModel(model, mapPosition, 1.0f, Color.Red); + + DrawGrid(20, 1.0f); + + EndMode3D(); + + DrawTexture(texture, screenWidth - texture.Width - 20, 20, Color.White); + DrawRectangleLines(screenWidth - texture.Width - 20, 20, texture.Width, texture.Height, Color.Green); + + 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 - heightmap rendering"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new HeightmapDemo(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Orbital); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawModel(model, mapPosition, 1.0f, Color.Red); - - DrawGrid(20, 1.0f); - - EndMode3D(); - - DrawTexture(texture, screenWidth - texture.Width - 20, 20, Color.White); - DrawRectangleLines(screenWidth - texture.Width - 20, 20, texture.Width, texture.Height, Color.Green); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(texture); - UnloadModel(model); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Models/LoadingGltf.cs b/Examples/Models/LoadingGltf.cs index 609bf41..865998a 100644 --- a/Examples/Models/LoadingGltf.cs +++ b/Examples/Models/LoadingGltf.cs @@ -25,93 +25,122 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class LoadingGltf +public partial class LoadingGltf : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Loading GLTF"; + + public string Title => "raylib [models] example - loading gltf"; + + 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(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(0.0f, 0.0f, 0.0f); // Set model world position + + // Load model animations + anims = LoadModelAnimations("resources/models/gltf/robot.glb", ref animCount); + + // Animation playing variables + animIndex = 0; // Current animation playing + animCurrentFrame = 0.0f; // 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], (float)animCurrentFrame); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawModel( + model, + position, + 1f, + 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 animations data + UnloadModel(model); // Unload model + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [models] example - loading gltf"); - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(6.0f, 6.0f, 6.0f); - camera.Target = new Vector3(0.0f, 2.0f, 0.0f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.FovY = 45.0f; - camera.Projection = CameraProjection.Perspective; - - Model model = LoadModel("resources/models/gltf/robot.glb"); - Vector3 position = new(0.0f, 0.0f, 0.0f); - - // Load animation data - var anims = LoadModelAnimations("resources/models/gltf/robot.glb"); - - // Animation playing variables - int animIndex = 0; - float animCurrentFrame = 0.0f; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new LoadingGltf(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Orbital); - - if (IsKeyPressed(KeyboardKey.Right)) - { - animIndex = (animIndex + 1) % anims.Length; - } - else if (IsKeyPressed(KeyboardKey.Left)) - { - animIndex = (animIndex + anims.Length - 1) % anims.Length; - } - - - // Update model animation - animCurrentFrame = (animCurrentFrame + 1) % anims[animIndex].KeyFrameCount; - UpdateModelAnimation(model, anims[animIndex], (float)animCurrentFrame); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawModel( - model, - position, - 1f, - 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(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadModelAnimations(anims); - UnloadModel(model); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } } - diff --git a/Examples/Models/LoadingIqm.cs b/Examples/Models/LoadingIqm.cs index 6905544..7ccfc12 100644 --- a/Examples/Models/LoadingIqm.cs +++ b/Examples/Models/LoadingIqm.cs @@ -25,93 +25,121 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class LoadingIqm +public partial class LoadingIqm : IExample { - public unsafe static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Loading IQM"; + + public string Title => "raylib [models] example - loading iqm"; + + private Camera3D camera; + private Model model; + private Texture2D texture; + 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(10.0f, 10.0f, 10.0f); // Camera position + camera.Target = new Vector3(0.0f, 4.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 + + model = LoadModel("resources/models/iqm/guy.iqm"); // Load the animated model mesh and basic data + texture = LoadTexture("resources/models/iqm/guytex.png"); // Load model texture and set material + Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.Diffuse, ref texture); // Set model material map texture + position = new(0.0f, 0.0f, 0.0f); // Set model position + + // Load animation data + anims = LoadModelAnimations("resources/models/iqm/guyanim.iqm", 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); + + // Play animation when spacebar is held down + animCurrentFrame += 1.0f; + UpdateModelAnimation(model, anims[0], animCurrentFrame); + + if (animCurrentFrame >= anims[0].KeyFrameCount) + { + animCurrentFrame = 0; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawModelEx( + model, + position, + new Vector3(1.0f, 0.0f, 0.0f), + -90.0f, + new Vector3(1.0f, 1.0f, 1.0f), + Color.White + ); + + DrawGrid(10, 1.0f); + + EndMode3D(); + DrawText($"Current animation: {anims[animIndex].NameToString()}", 10, 10, 20, Color.Maroon); + DrawText("(c) Guy IQM 3D model by @culacant", screenWidth - 200, screenHeight - 20, 10, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public unsafe void Unload() + { + UnloadTexture(texture); // Unload texture + UnloadModelAnimations(anims, animCount); // Unload model animations data + UnloadModel(model); // Unload model + } + + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [models] example - loading iqm"); - InitWindow(screenWidth, screenHeight, "raylib [models] example - model animation"); - - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(10.0f, 10.0f, 10.0f); - camera.Target = new Vector3(0.0f, 4.0f, 0.0f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.FovY = 45.0f; - camera.Projection = CameraProjection.Perspective; - - Model model = LoadModel("resources/models/iqm/guy.iqm"); - Texture2D texture = LoadTexture("resources/models/iqm/guytex.png"); - Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.Diffuse, ref texture); - Vector3 position = new(0.0f, 0.0f, 0.0f); - - // Load animation data - var anims = LoadModelAnimations("resources/models/iqm/guyanim.iqm"); - - // Animation playing variables - int animIndex = 0; - float animCurrentFrame = 0.0f; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new LoadingIqm(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Orbital); - - // Play animation when spacebar is held down - animCurrentFrame += 1.0f; - UpdateModelAnimation(model, anims[0], animCurrentFrame); - - if (animCurrentFrame >= anims[0].KeyFrameCount) - { - animCurrentFrame = 0; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawModelEx( - model, - position, - new Vector3(1.0f, 0.0f, 0.0f), - -90.0f, - new Vector3(1.0f, 1.0f, 1.0f), - Color.White - ); - - DrawGrid(10, 1.0f); - - EndMode3D(); - DrawText($"Current animation: {anims[animIndex].NameToString()}", 10, 10, 20, Color.Maroon); - DrawText("(c) Guy IQM 3D model by @culacant", screenWidth - 200, screenHeight - 20, 10, Color.Gray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(texture); - UnloadModelAnimations(anims); - UnloadModel(model); - - CloseWindow(); + 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/MeshDemo.cs b/Examples/Models/MeshDemo.cs index 9f2be23..98ef2f2 100644 --- a/Examples/Models/MeshDemo.cs +++ b/Examples/Models/MeshDemo.cs @@ -3,19 +3,24 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class MeshDemo +public partial class MeshDemo : IExample { - public unsafe static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Mesh Demo"; + + public string Title => "raylib [models] example - mesh demo"; + + private Camera3D camera; + private Model model; + private Texture2D texture; + private float rotationAngle; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [models] example - mesh demo"); - // Define the camera to look into our 3d world - Camera3D camera = new(); + camera = new(); camera.Position = Vector3.One * 1.5f; camera.Target = Vector3.Zero; camera.Up = Vector3.UnitY; @@ -28,10 +33,10 @@ public class MeshDemo tetrahedron.AllocTexCoords(); tetrahedron.AllocColors(); tetrahedron.AllocIndices(); - Span vertices = tetrahedron.VerticesAs(); - Span texcoords = tetrahedron.TexCoordsAs(); - Span colors = tetrahedron.ColorsAs(); - Span indices = tetrahedron.IndicesAs(); + var vertices = tetrahedron.VerticesAs(); + var texcoords = tetrahedron.TexCoordsAs(); + var colors = tetrahedron.ColorsAs(); + var indices = tetrahedron.IndicesAs(); // Coordinates for a regular tetrahedron vertices[0] = new(MathF.Sqrt(8f / 9f), 0f, -1f / 3f); @@ -65,49 +70,69 @@ public class MeshDemo indices[10] = 3; indices[11] = 2; - float rotationAngle = 0f; + rotationAngle = 0f; Raylib.UploadMesh(ref tetrahedron, false); - Model model = Raylib.LoadModelFromMesh(tetrahedron); + model = Raylib.LoadModelFromMesh(tetrahedron); - Image image = Raylib.GenImagePerlinNoise(16, 16, 0, 0, 1000f); + var image = Raylib.GenImagePerlinNoise(16, 16, 0, 0, 1000f); Raylib.ImageBlurGaussian(ref image, 2); Raylib.ImageColorBrightness(ref image, 100); Raylib.ImageDither(ref image, 4, 4, 4, 4); - Texture2D texture = Raylib.LoadTextureFromImage(image); + texture = Raylib.LoadTextureFromImage(image); Raylib.UnloadImage(image); Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.Diffuse, ref texture); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Free); + rotationAngle = Raymath.Wrap(rotationAngle += 1f, 0f, 360f); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + Raylib.DrawModelEx(model, Vector3.Zero, Vector3.UnitX, rotationAngle, Vector3.One, Color.White); + EndMode3D(); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texture); + UnloadModel(model); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - mesh demo"); SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new MeshDemo(); + game.Init(); + // Main game loop while (!WindowShouldClose()) { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Free); - rotationAngle = Raymath.Wrap(rotationAngle += 1f, 0f, 360f); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - Raylib.DrawModelEx(model, Vector3.Zero, Vector3.UnitX, rotationAngle, Vector3.One, Color.White); - EndMode3D(); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(texture); - UnloadModel(model); - CloseWindow(); //-------------------------------------------------------------------------------------- diff --git a/Examples/Models/MeshGeneration.cs b/Examples/Models/MeshGeneration.cs index e4b27c4..4415c90 100644 --- a/Examples/Models/MeshGeneration.cs +++ b/Examples/Models/MeshGeneration.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib example - procedural mesh generation +* raylib [models] example - mesh generation * -* This example has been created using raylib 1.8 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2017 Ramon Santamaria (Ray San) +* Example originally created with raylib 1.8, 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) 2017-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,25 +18,31 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class MeshGeneration +public partial class MeshGeneration : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Mesh Generation"; + + public string Title => "raylib [models] example - mesh generation"; + + private Texture2D texture; + private Model[] models; + private Camera3D camera; + private Vector3 position; + private int currentModel; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [models] example - mesh generation"); - - // We generate a isChecked image for texturing - Image isChecked = GenImageChecked(2, 2, 1, 1, Color.Red, Color.Green); - Texture2D texture = LoadTextureFromImage(isChecked); + // We generate a checked image for texturing + var isChecked = GenImageChecked(2, 2, 1, 1, Color.Red, Color.Green); + texture = LoadTextureFromImage(isChecked); UnloadImage(isChecked); - Model[] models = new Model[9]; + models = new Model[9]; - models[0] = LoadModelFromMesh(GenMeshPlane(2, 2, 5, 5)); + models[0] = LoadModelFromMesh(GenMeshPlane(2, 2, 4, 3)); models[1] = LoadModelFromMesh(GenMeshCube(2.0f, 1.0f, 2.0f)); models[2] = LoadModelFromMesh(GenMeshSphere(2, 32, 32)); models[3] = LoadModelFromMesh(GenMeshHemiSphere(2, 16, 16)); @@ -42,15 +52,17 @@ public class MeshGeneration models[7] = LoadModelFromMesh(GenMeshPoly(5, 2.0f)); models[8] = LoadModelFromMesh(GenMeshCustom()); - // Set isChecked texture as default diffuse component for all models material - for (int i = 0; i < models.Length; i++) + // NOTE: Generated meshes could be exported using ExportMesh() + + // Set checked texture as default diffuse component for all models material + for (var i = 0; i < models.Length; i++) { // Set map diffuse texture Raylib.SetMaterialTexture(ref models[i], 0, MaterialMapIndex.Albedo, ref texture); } // Define the camera to look into our 3d world - Camera3D camera = new(); + camera = new(); camera.Position = new Vector3(5.0f, 5.0f, 5.0f); camera.Target = new Vector3(0.0f, 0.0f, 0.0f); camera.Up = new Vector3(0.0f, 1.0f, 0.0f); @@ -58,92 +70,102 @@ public class MeshGeneration camera.Projection = CameraProjection.Perspective; // Model drawing position - Vector3 position = new(0.0f, 0.0f, 0.0f); + position = new(0.0f, 0.0f, 0.0f); - int currentModel = 0; + currentModel = 0; + } - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); - // Main game loop - while (!WindowShouldClose()) + if (IsMouseButtonPressed(MouseButton.Left)) { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Orbital); - - if (IsMouseButtonPressed(MouseButton.Left)) - { - // Cycle between the textures - currentModel = (currentModel + 1) % models.Length; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawModel(models[currentModel], position, 1.0f, Color.White); - - DrawGrid(10, 1.0f); - - EndMode3D(); - - DrawRectangle(30, 400, 310, 30, ColorAlpha(Color.SkyBlue, 0.5f)); - DrawRectangleLines(30, 400, 310, 30, ColorAlpha(Color.DarkBlue, 0.5f)); - DrawText("MOUSE LEFT BUTTON to CYCLE PROCEDURAL MODELS", 40, 410, 10, Color.Blue); - - switch (currentModel) - { - case 0: - DrawText("PLANE", 680, 10, 20, Color.DarkBlue); - break; - case 1: - DrawText("CUBE", 680, 10, 20, Color.DarkBlue); - break; - case 2: - DrawText("SPHERE", 680, 10, 20, Color.DarkBlue); - break; - case 3: - DrawText("HEMISPHERE", 640, 10, 20, Color.DarkBlue); - break; - case 4: - DrawText("CYLINDER", 680, 10, 20, Color.DarkBlue); - break; - case 5: - DrawText("TORUS", 680, 10, 20, Color.DarkBlue); - break; - case 6: - DrawText("KNOT", 680, 10, 20, Color.DarkBlue); - break; - case 7: - DrawText("POLY", 680, 10, 20, Color.DarkBlue); - break; - case 8: - DrawText("Custom (triagnle)", 580, 10, 20, Color.DarkBlue); - break; - default: - break; - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + currentModel = (currentModel + 1) % models.Length; // Cycle between the textures } - // De-Initialization - //-------------------------------------------------------------------------------------- - for (int i = 0; i < models.Length; i++) + if (IsKeyPressed(KeyboardKey.Right)) + { + currentModel++; + if (currentModel >= models.Length) + { + currentModel = 0; + } + } + else if (IsKeyPressed(KeyboardKey.Left)) + { + currentModel--; + if (currentModel < 0) + { + currentModel = models.Length - 1; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawModel(models[currentModel], position, 1.0f, Color.White); + DrawGrid(10, 1.0f); + + EndMode3D(); + + DrawRectangle(30, 400, 310, 30, Fade(Color.SkyBlue, 0.5f)); + DrawRectangleLines(30, 400, 310, 30, Fade(Color.DarkBlue, 0.5f)); + DrawText("MOUSE LEFT BUTTON to CYCLE PROCEDURAL MODELS", 40, 410, 10, Color.Blue); + + switch (currentModel) + { + case 0: + DrawText("PLANE", 680, 10, 20, Color.DarkBlue); + break; + case 1: + DrawText("CUBE", 680, 10, 20, Color.DarkBlue); + break; + case 2: + DrawText("SPHERE", 680, 10, 20, Color.DarkBlue); + break; + case 3: + DrawText("HEMISPHERE", 640, 10, 20, Color.DarkBlue); + break; + case 4: + DrawText("CYLINDER", 680, 10, 20, Color.DarkBlue); + break; + case 5: + DrawText("TORUS", 680, 10, 20, Color.DarkBlue); + break; + case 6: + DrawText("KNOT", 680, 10, 20, Color.DarkBlue); + break; + case 7: + DrawText("POLY", 680, 10, 20, Color.DarkBlue); + break; + case 8: + DrawText("Custom (triangle)", 580, 10, 20, Color.DarkBlue); + break; + default: + break; + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texture); // Unload texture + + // Unload models data (GPU VRAM) + for (var i = 0; i < models.Length; i++) { UnloadModel(models[i]); } - - CloseWindow(); - //-------------------------------------------------------------------------------------- - - return 0; } // Generate a simple triangle mesh from code @@ -153,9 +175,9 @@ public class MeshGeneration mesh.AllocVertices(); mesh.AllocTexCoords(); mesh.AllocNormals(); - Span vertices = mesh.VerticesAs(); - Span texcoords = mesh.TexCoordsAs(); - Span normals = mesh.NormalsAs(); + var vertices = mesh.VerticesAs(); + var texcoords = mesh.TexCoordsAs(); + var normals = mesh.NormalsAs(); // Vertex at (0, 0, 0) vertices[0] = new(0, 0, 0); @@ -177,4 +199,32 @@ public class MeshGeneration return mesh; } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - mesh generation"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new MeshGeneration(); + 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 e2dbceb..3f62595 100644 --- a/Examples/Models/MeshPicking.cs +++ b/Examples/Models/MeshPicking.cs @@ -1,12 +1,17 @@ /******************************************************************************************* * -* raylib [models] example - Mesh picking in 3d mode, ground plane, triangle, mesh +* raylib [models] example - mesh picking * -* This example has been created using raylib 1.7 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 * -* Copyright (c) 2015 Ramon Santamaria (@raysan5) -* Example contributed by Joel Davis (@joeld42) +* Example originally created with raylib 1.7, last time updated with raylib 4.0 +* +* Example contributed by Joel Davis (@joeld42) 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) 2017-2025 Joel Davis (@joeld42) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -16,19 +21,36 @@ using static Raylib_cs.Raymath; namespace Examples.Models; -public class MeshPicking +public partial class MeshPicking : IExample { - public unsafe static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Mesh Picking"; + + public string Title => "raylib [models] example - mesh picking"; + + private Camera3D camera; + private Ray ray; + private Model tower; + private Texture2D texture; + private Vector3 towerPos; + private BoundingBox towerBBox; + private Vector3 g0; + private Vector3 g1; + private Vector3 g2; + private Vector3 g3; + private Vector3 ta; + private Vector3 tb; + private Vector3 tc; + private Vector3 bary; + private Vector3 sp; + private float sr; + + public unsafe void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [models] example - mesh picking"); - // Define the camera to look into our 3d world - Camera3D camera; + camera = new(); camera.Position = new Vector3(20.0f, 20.0f, 20.0f); camera.Target = new Vector3(0.0f, 8.0f, 0.0f); camera.Up = new Vector3(0.0f, 1.6f, 0.0f); @@ -36,210 +58,232 @@ public class MeshPicking camera.Projection = CameraProjection.Perspective; // Picking ray - Ray ray = new(); + ray = new(); - Model tower = LoadModel("resources/models/obj/turret.obj"); - Texture2D texture = LoadTexture("resources/models/obj/turret_diffuse.png"); + tower = LoadModel("resources/models/obj/turret.obj"); + texture = LoadTexture("resources/models/obj/turret_diffuse.png"); Raylib.SetMaterialTexture(ref tower, 0, MaterialMapIndex.Albedo, ref texture); - Vector3 towerPos = new(0.0f, 0.0f, 0.0f); - BoundingBox towerBBox = GetMeshBoundingBox(tower.Meshes[0]); + towerPos = new(0.0f, 0.0f, 0.0f); + towerBBox = GetMeshBoundingBox(tower.Meshes[0]); // Ground quad - Vector3 g0 = new(-50.0f, 0.0f, -50.0f); - Vector3 g1 = new(-50.0f, 0.0f, 50.0f); - Vector3 g2 = new(50.0f, 0.0f, 50.0f); - Vector3 g3 = new(50.0f, 0.0f, -50.0f); + g0 = new(-50.0f, 0.0f, -50.0f); + g1 = new(-50.0f, 0.0f, 50.0f); + g2 = new(50.0f, 0.0f, 50.0f); + g3 = new(50.0f, 0.0f, -50.0f); // Test triangle - Vector3 ta = new(-25.0f, 0.5f, 0.0f); - Vector3 tb = new(-4.0f, 2.5f, 1.0f); - Vector3 tc = new(-8.0f, 6.5f, 0.0f); + ta = new(-25.0f, 0.5f, 0.0f); + tb = new(-4.0f, 2.5f, 1.0f); + tc = new(-8.0f, 6.5f, 0.0f); - Vector3 bary = new(0.0f, 0.0f, 0.0f); + bary = new(0.0f, 0.0f, 0.0f); // Test sphere - Vector3 sp = new(-30.0f, 5.0f, 5.0f); - float sr = 4.0f; + sp = new(-30.0f, 5.0f, 5.0f); + sr = 4.0f; + } + + public unsafe void Update() + { + //---------------------------------------------------------------------------------- + // Update + //---------------------------------------------------------------------------------- + if (IsCursorHidden()) + { + UpdateCamera(ref camera, CameraMode.FirstPerson); + } + + // Toggle camera controls + if (IsMouseButtonPressed(MouseButton.Right)) + { + if (IsCursorHidden()) + { + EnableCursor(); + } + else + { + DisableCursor(); + } + } + + // Display information about closest hit + RayCollision collision = new(); + var hitObjectName = "None"; + collision.Distance = float.MaxValue; + collision.Hit = false; + var cursorColor = Color.White; + + // Get ray and test against objects + ray = GetScreenToWorldRay(GetMousePosition(), camera); + + // Check ray collision against ground quad + var groundHitInfo = GetRayCollisionQuad(ray, g0, g1, g2, g3); + if (groundHitInfo.Hit && (groundHitInfo.Distance < collision.Distance)) + { + collision = groundHitInfo; + cursorColor = Color.Green; + hitObjectName = "Ground"; + } + + // Check ray collision against test triangle + var triHitInfo = GetRayCollisionTriangle(ray, ta, tb, tc); + if (triHitInfo.Hit && (triHitInfo.Distance < collision.Distance)) + { + collision = triHitInfo; + cursorColor = Color.Purple; + hitObjectName = "Triangle"; + + bary = Vector3Barycenter(collision.Point, ta, tb, tc); + } + + // Check ray collision against test sphere + var sphereHitInfo = GetRayCollisionSphere(ray, sp, sr); + if ((sphereHitInfo.Hit) && (sphereHitInfo.Distance < collision.Distance)) + { + collision = sphereHitInfo; + cursorColor = Color.Orange; + hitObjectName = "Sphere"; + } + + // Check ray collision against bounding box first, before trying the full ray-mesh test + var boxHitInfo = GetRayCollisionBox(ray, towerBBox); + if (boxHitInfo.Hit && boxHitInfo.Distance < collision.Distance) + { + collision = boxHitInfo; + cursorColor = Color.Orange; + hitObjectName = "Box"; + + // Check ray collision against model meshes + RayCollision meshHitInfo = new(); + for (var m = 0; m < tower.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, tower.Meshes[m], tower.Transform); + if (meshHitInfo.Hit) + { + // Save the closest hit mesh + if ((!collision.Hit) || (collision.Distance > meshHitInfo.Distance)) + { + collision = meshHitInfo; + } + break; + } + } + + if (meshHitInfo.Hit) + { + collision = meshHitInfo; + cursorColor = Color.Orange; + hitObjectName = "Mesh"; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + // Draw the tower + // WARNING: If scale is different than 1.0f, + // not considered by GetRayCollisionModel() + DrawModel(tower, towerPos, 1.0f, Color.White); + + // Draw the test triangle + DrawLine3D(ta, tb, Color.Purple); + DrawLine3D(tb, tc, Color.Purple); + DrawLine3D(tc, ta, Color.Purple); + + // Draw the test sphere + DrawSphereWires(sp, sr, 8, 8, Color.Purple); + + // Draw the mesh bbox if we hit it + if (boxHitInfo.Hit) + { + DrawBoundingBox(towerBBox, Color.Lime); + } + + // If we hit something, draw the cursor at the hit point + if (collision.Hit) + { + DrawCube(collision.Point, 0.3f, 0.3f, 0.3f, cursorColor); + DrawCubeWires(collision.Point, 0.3f, 0.3f, 0.3f, Color.Red); + + var normalEnd = collision.Point + collision.Normal; + DrawLine3D(collision.Point, normalEnd, Color.Red); + } + + DrawRay(ray, Color.Maroon); + + DrawGrid(10, 10.0f); + + EndMode3D(); + + // Draw some debug GUI text + DrawText($"Hit Object: {hitObjectName}", 10, 50, 10, Color.Black); + + if (collision.Hit) + { + var ypos = 70; + + DrawText($"Distance: {collision.Distance}", 10, ypos, 10, Color.Black); + + DrawText($"Hit Pos: {collision.Point}", 10, ypos + 15, 10, Color.Black); + + DrawText($"Hit Norm: {collision.Normal}", 10, ypos + 30, 10, Color.Black); + + if (triHitInfo.Hit && hitObjectName == "Triangle") + { + DrawText($"Barycenter: {bary}", 10, ypos + 45, 10, Color.Black); + } + } + + DrawText("Right click mouse to toggle camera controls", 10, 430, 10, Color.Gray); + + DrawText("(c) Turret 3D model by Alberto Cano", screenWidth - 200, screenHeight - 20, 10, Color.Gray); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadModel(tower); + UnloadTexture(texture); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - mesh picking"); SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new MeshPicking(); + game.Init(); + //---------------------------------------------------------------------------------- // Main game loop //-------------------------------------------------------------------------------------- while (!WindowShouldClose()) { - //---------------------------------------------------------------------------------- - // Update - //---------------------------------------------------------------------------------- - if (IsCursorHidden()) - { - UpdateCamera(ref camera, CameraMode.FirstPerson); - } - - // Toggle camera controls - if (IsMouseButtonPressed(MouseButton.Right)) - { - if (IsCursorHidden()) - { - EnableCursor(); - } - else - { - DisableCursor(); - } - } - - // Display information about closest hit - RayCollision collision = new(); - string hitObjectName = "None"; - collision.Distance = float.MaxValue; - collision.Hit = false; - Color cursorColor = Color.White; - - // Get ray and test against objects - ray = GetScreenToWorldRay(GetMousePosition(), camera); - - // Check ray collision aginst ground quad - RayCollision groundHitInfo = GetRayCollisionQuad(ray, g0, g1, g2, g3); - if (groundHitInfo.Hit && (groundHitInfo.Distance < collision.Distance)) - { - collision = groundHitInfo; - cursorColor = Color.Green; - hitObjectName = "Ground"; - } - - // Check ray collision against test triangle - RayCollision triHitInfo = GetRayCollisionTriangle(ray, ta, tb, tc); - if (triHitInfo.Hit && (triHitInfo.Distance < collision.Distance)) - { - collision = triHitInfo; - cursorColor = Color.Purple; - hitObjectName = "Triangle"; - - bary = Vector3Barycenter(collision.Point, ta, tb, tc); - } - - // Check ray collision against test sphere - RayCollision sphereHitInfo = GetRayCollisionSphere(ray, sp, sr); - if ((sphereHitInfo.Hit) && (sphereHitInfo.Distance < collision.Distance)) - { - collision = sphereHitInfo; - cursorColor = Color.Orange; - hitObjectName = "Sphere"; - } - - // Check ray collision against bounding box first, before trying the full ray-mesh test - RayCollision boxHitInfo = GetRayCollisionBox(ray, towerBBox); - if (boxHitInfo.Hit && boxHitInfo.Distance < collision.Distance) - { - collision = boxHitInfo; - cursorColor = Color.Orange; - hitObjectName = "Box"; - - // Check ray collision against model meshes - RayCollision meshHitInfo = new(); - for (int m = 0; m < tower.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, tower.Meshes[m], tower.Transform); - if (meshHitInfo.Hit) - { - // Save the closest hit mesh - if ((!collision.Hit) || (collision.Distance > meshHitInfo.Distance)) - { - collision = meshHitInfo; - } - break; - } - } - - if (meshHitInfo.Hit) - { - collision = meshHitInfo; - cursorColor = Color.Orange; - hitObjectName = "Mesh"; - } - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - // Draw the tower - DrawModel(tower, towerPos, 1.0f, Color.White); - - // Draw the test triangle - DrawLine3D(ta, tb, Color.Purple); - DrawLine3D(tb, tc, Color.Purple); - DrawLine3D(tc, ta, Color.Purple); - - // Draw the test sphere - DrawSphereWires(sp, sr, 8, 8, Color.Purple); - - // Draw the mesh bbox if we hit it - if (boxHitInfo.Hit) - { - DrawBoundingBox(towerBBox, Color.Lime); - } - - // If we hit something, draw the cursor at the hit point - if (collision.Hit) - { - DrawCube(collision.Point, 0.3f, 0.3f, 0.3f, cursorColor); - DrawCubeWires(collision.Point, 0.3f, 0.3f, 0.3f, Color.Red); - - Vector3 normalEnd = collision.Point + collision.Normal; - DrawLine3D(collision.Point, normalEnd, Color.Red); - } - - DrawRay(ray, Color.Maroon); - - DrawGrid(10, 10.0f); - - EndMode3D(); - - // Draw some debug GUI text - DrawText($"Hit Object: {hitObjectName}", 10, 50, 10, Color.Black); - - if (collision.Hit) - { - int ypos = 70; - - DrawText($"Distance: {collision.Distance}", 10, ypos, 10, Color.Black); - - DrawText($"Hit Pos: {collision.Point}", 10, ypos + 15, 10, Color.Black); - - DrawText($"Hit Norm: {collision.Normal}", 10, ypos + 30, 10, Color.Black); - - if (triHitInfo.Hit) - { - DrawText($"Barycenter: {bary}", 10, ypos + 45, 10, Color.Black); - } - } - - DrawText("Right click mouse to toggle camera controls", 10, 430, 10, Color.Gray); - - DrawText("(c) Turret 3D model by Alberto Cano", screenWidth - 200, screenHeight - 20, 10, Color.Gray); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadModel(tower); - UnloadTexture(texture); - CloseWindow(); //-------------------------------------------------------------------------------------- diff --git a/Examples/Models/ModelCubeTexture.cs b/Examples/Models/ModelCubeTexture.cs index 7af9219..c2c702f 100644 --- a/Examples/Models/ModelCubeTexture.cs +++ b/Examples/Models/ModelCubeTexture.cs @@ -1,13 +1,15 @@ /******************************************************************************************* * -* raylib [models] example - Draw textured cube +* raylib [models] example - textured cube +* +* Example complexity rating: [★★☆☆] 2/4 * * Example originally created with raylib 4.5, last time updated with raylib 4.5 * * 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-2023 Ramon Santamaria (@raysan5) +* Copyright (c) 2022-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -16,19 +18,22 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class ModelCubeTexture +public partial class ModelCubeTexture : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Model Cube Texture"; + + public string Title => "raylib [models] example - textured cube"; + + private Camera3D camera; + private Texture2D texture; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [models] example - draw cube texture"); - // Define the camera to look into our 3d world - Camera3D camera; + camera = new(); camera.Position = new Vector3(0.0f, 10.0f, 10.0f); camera.Target = new Vector3(0.0f, 0.0f, 0.0f); camera.Up = new Vector3(0.0f, 1.0f, 0.0f); @@ -36,60 +41,48 @@ public class ModelCubeTexture camera.Projection = CameraProjection.Perspective; // Load texture to be applied to the cubes sides - Texture2D texture = LoadTexture("resources/cubicmap_atlas.png"); + texture = LoadTexture("resources/cubicmap_atlas.png"); + } - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- + public void Update() + { + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); - // Main game loop - while (!WindowShouldClose()) - { - // Update - //---------------------------------------------------------------------------------- - //---------------------------------------------------------------------------------- + BeginMode3D(camera); - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); + // Draw cube with an applied texture + DrawCubeTexture(texture, new Vector3(-2.0f, 2.0f, 0.0f), 2.0f, 4.0f, 2.0f, Color.White); - BeginMode3D(camera); + // Draw cube with an applied texture, but only a defined rectangle piece of the texture + DrawCubeTextureRec( + texture, + new Rectangle(0, texture.Height / 2, texture.Width / 2, texture.Height / 2), + new Vector3(2.0f, 1.0f, 0.0f), + 2.0f, + 2.0f, + 2.0f, + Color.White + ); - // Draw cube with an applied texture - DrawCubeTexture(texture, new Vector3(-2.0f, 2.0f, 0.0f), 2.0f, 4.0f, 2.0f, Color.White); + DrawGrid(10, 1.0f); - // Draw cube with an applied texture, but only a defined rectangle piece of the texture - DrawCubeTextureRec( - texture, - new Rectangle(0, texture.Height / 2, texture.Width / 2, texture.Height / 2), - new Vector3(2.0f, 1.0f, 0.0f), - 2.0f, - 2.0f, - 2.0f, - Color.White - ); + EndMode3D(); - DrawGrid(10, 1.0f); + EndDrawing(); + //---------------------------------------------------------------------------------- + } - EndMode3D(); - - EndDrawing(); - //---------------------------------------------------------------------------------- - } - - // De-Initialization - //-------------------------------------------------------------------------------------- + public void Unload() + { UnloadTexture(texture); - - CloseWindow(); - //-------------------------------------------------------------------------------------- - - return 0; } // Draw cube textured // NOTE: Cube position is the center position - static void DrawCubeTexture( + private static void DrawCubeTexture( Texture2D texture, Vector3 position, float width, @@ -98,9 +91,9 @@ public class ModelCubeTexture Color color ) { - float x = position.X; - float y = position.Y; - float z = position.Z; + var x = position.X; + var y = position.Y; + var z = position.Z; // Set desired texture to be enabled while drawing following vertex data Rlgl.SetTexture(texture.Id); @@ -218,7 +211,7 @@ public class ModelCubeTexture } // Draw cube with texture piece applied to all faces - static void DrawCubeTextureRec( + private static void DrawCubeTextureRec( Texture2D texture, Rectangle source, Vector3 position, @@ -228,11 +221,11 @@ public class ModelCubeTexture Color color ) { - float x = position.X; - float y = position.Y; - float z = position.Z; - float texWidth = (float)texture.Width; - float texHeight = (float)texture.Height; + var x = position.X; + var y = position.Y; + var z = position.Z; + var texWidth = (float)texture.Width; + var texHeight = (float)texture.Height; // Set desired texture to be enabled while drawing following vertex data Rlgl.SetTexture(texture.Id); @@ -312,5 +305,32 @@ public class ModelCubeTexture Rlgl.SetTexture(0); } -} + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - textured cube"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new ModelCubeTexture(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); + //-------------------------------------------------------------------------------------- + + return 0; + } +} diff --git a/Examples/Models/ModelLoading.cs b/Examples/Models/ModelLoading.cs index 9a84d06..9198ecc 100644 --- a/Examples/Models/ModelLoading.cs +++ b/Examples/Models/ModelLoading.cs @@ -1,20 +1,28 @@ /******************************************************************************************* * -* raylib [models] example - Models loading +* raylib [models] example - loading * -* raylib supports multiple models file formats: +* Example complexity rating: [★☆☆☆] 1/4 * -* - OBJ > Text file, must include vertex position-texcoords-normals information, -* if files references some .mtl materials file, it will be loaded (or try to) -* - GLTF > Modern text/binary file format, includes lot of information and it could -* also reference external files, raylib will try loading mesh and materials data -* - IQM > Binary file format including mesh vertex data but also animation data, -* raylib can load .iqm animations. +* NOTE: raylib supports multiple models file formats: * -* This example has been created using raylib 2.6 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* - OBJ > Text file format. Must include vertex position-texcoords-normals information, +* if .obj references some .mtl materials file, it will be tried to be loaded +* - GLTF/GLB > Text/binary file formats. Includes lot of information and it could +* also reference external files, mesh and materials data will be tried to be loaded +* - IQM > Binary file format. Includes mesh vertex data but also animation data, +* meshes and animation data can be loaded +* - VOX > Binary file format. MagikaVoxel mesh format: +* https://github.com/ephtracy/voxel-model/blob/master/MagicaVoxel-file-format-vox.txt +* - M3D > Binary file format. Model 3D format: +* https://bztsrc.gitlab.io/model3d * -* Copyright (c) 2014-2019 Ramon Santamaria (@raysan5) +* Example originally created with raylib 2.0, 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) 2014-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -23,135 +31,170 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class ModelLoading +public partial class ModelLoading : IExample { - public unsafe static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Model Loading"; + + public string Title => "raylib [models] example - loading"; + + private Camera3D camera; + private Model model; + private Texture2D texture; + private Vector3 position; + private BoundingBox bounds; + private bool selected; + + public unsafe void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [models] example - models loading"); - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(50.0f, 50.0f, 50.0f); - camera.Target = new Vector3(0.0f, 10.0f, 0.0f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.FovY = 45.0f; - camera.Projection = CameraProjection.Perspective; + camera = new(); + camera.Position = new Vector3(50.0f, 50.0f, 50.0f); // Camera position + camera.Target = new Vector3(0.0f, 12.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 - Model model = LoadModel("resources/models/obj/castle.obj"); - Texture2D texture = LoadTexture("resources/models/obj/castle_diffuse.png"); + model = LoadModel("resources/models/obj/castle.obj"); // Load model + texture = LoadTexture("resources/models/obj/castle_diffuse.png"); // Load model texture // Set map diffuse texture Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.Albedo, ref texture); - Vector3 position = new(0.0f, 0.0f, 0.0f); - BoundingBox bounds = GetMeshBoundingBox(model.Meshes[0]); + position = new(0.0f, 0.0f, 0.0f); // Set model position + bounds = GetMeshBoundingBox(model.Meshes[0]); // Set model bounds // NOTE: bounds are calculated from the original size of the model, // if model is scaled on drawing, bounds must be also scaled - bool selected = false; + selected = false; + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + +#if BROWSER + // 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()) + { + var droppedFiles = Raylib.GetDroppedFiles(); + + if (droppedFiles.Length == 1) // Only support one file dropped + { + if (IsFileExtension(droppedFiles[0], ".obj") || + IsFileExtension(droppedFiles[0], ".gltf") || + IsFileExtension(droppedFiles[0], ".glb") || + IsFileExtension(droppedFiles[0], ".vox") || + IsFileExtension(droppedFiles[0], ".iqm") || + IsFileExtension(droppedFiles[0], ".m3d") // Model file formats supported + ) + { + UnloadModel(model); // Unload previous model + model = LoadModel(droppedFiles[0]); // Load new model + + // Set current map diffuse texture + Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.Albedo, ref texture); + + bounds = GetMeshBoundingBox(model.Meshes[0]); + + // Move camera position from target enough distance to visualize model properly + camera.Position.X = bounds.Max.X + 10.0f; + camera.Position.Y = bounds.Max.Y + 10.0f; + camera.Position.Z = bounds.Max.Z + 10.0f; + } + else if (IsFileExtension(droppedFiles[0], ".png")) // Texture file formats supported + { + // Unload current model texture and load new one + UnloadTexture(texture); + texture = LoadTexture(droppedFiles[0]); + Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.Albedo, ref texture); + } + } + } +#endif + + // Select model on mouse click + if (IsMouseButtonPressed(MouseButton.Left)) + { + // Check collision between ray and box + if (GetRayCollisionBox(GetScreenToWorldRay(GetMousePosition(), camera), bounds).Hit) + { + selected = !selected; + } + else + { + selected = false; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawModel(model, position, 1.0f, Color.White); // Draw 3d model with texture + + DrawGrid(20, 10.0f); // Draw a grid + + if (selected) + { + DrawBoundingBox(bounds, Color.Green); // Draw selection box + } + + EndMode3D(); + + DrawText("Drag & drop model to load mesh/texture.", 10, GetScreenHeight() - 20, 10, Color.DarkGray); + if (selected) + { + DrawText("MODEL SELECTED", GetScreenWidth() - 110, 10, 10, Color.Green); + } + + DrawText("(c) Castle 3D model by Alberto Cano", screenWidth - 200, screenHeight - 20, 10, Color.Gray); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texture); + UnloadModel(model); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - loading"); SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new ModelLoading(); + game.Init(); + // Main game loop while (!WindowShouldClose()) { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Orbital); - - if (IsFileDropped()) - { - string[] droppedFiles = Raylib.GetDroppedFiles(); - - if (droppedFiles.Length == 1) - { - if (IsFileExtension(droppedFiles[0], ".obj") || - IsFileExtension(droppedFiles[0], ".gltf") || - IsFileExtension(droppedFiles[0], ".glb") || - IsFileExtension(droppedFiles[0], ".vox") || - IsFileExtension(droppedFiles[0], ".iqm") || - IsFileExtension(droppedFiles[0], ".m3d") - ) - { - UnloadModel(model); - model = LoadModel(droppedFiles[0]); - - // Set current map diffuse texture - Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.Albedo, ref texture); - - bounds = GetMeshBoundingBox(model.Meshes[0]); - - // TODO: Move camera position from target enough distance to visualize model properly - } - else if (IsFileExtension(droppedFiles[0], ".png")) - { - // Unload model texture and load new one - UnloadTexture(texture); - texture = LoadTexture(droppedFiles[0]); - Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.Albedo, ref texture); - } - } - } - - // Select model on mouse click - if (IsMouseButtonPressed(MouseButton.Left)) - { - // Check collision between ray and box - if (GetRayCollisionBox(GetScreenToWorldRay(GetMousePosition(), camera), bounds).Hit) - { - selected = !selected; - } - else - { - selected = false; - } - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawModel(model, position, 1.0f, Color.White); - - DrawGrid(20, 10.0f); - - if (selected) - { - DrawBoundingBox(bounds, Color.Green); - } - - EndMode3D(); - - DrawText("Drag & drop model to load mesh/texture.", 10, GetScreenHeight() - 20, 10, Color.DarkGray); - if (selected) - { - DrawText("MODEL SELECTED", GetScreenWidth() - 110, 10, 10, Color.Green); - } - - DrawText("(c) Castle 3D model by Alberto Cano", screenWidth - 200, screenHeight - 20, 10, Color.Gray); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(texture); - UnloadModel(model); - CloseWindow(); //-------------------------------------------------------------------------------------- diff --git a/Examples/Models/OrthographicProjection.cs b/Examples/Models/OrthographicProjection.cs index bf9e302..1cde4b5 100644 --- a/Examples/Models/OrthographicProjection.cs +++ b/Examples/Models/OrthographicProjection.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [models] example - Show the difference between perspective and orthographic projection +* raylib [models] example - orthographic projection * -* This program is heavily based on the geometric objects example +* Example complexity rating: [★☆☆☆] 1/4 * -* This example has been created using raylib 1.9.7 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 2.0, last time updated with raylib 3.7 * -* Copyright (c) 2018 Max Danielsson ref Ramon Santamaria (@raysan5) +* Example contributed by Max Danielsson (@autious) 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) 2018-2025 Max Danielsson (@autious) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -16,96 +20,119 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class OrthographicProjection +public partial class OrthographicProjection : IExample { public const float FOVY_PERSPECTIVE = 45.0f; public const float WIDTH_ORTHOGRAPHIC = 10.0f; - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Orthographic Projection"; + + public string Title => "raylib [models] example - orthographic projection"; + + private Camera3D camera; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [models] example - geometric shapes"); - // Define the camera to look into our 3d world - Camera3D camera = new(); + camera = new(); camera.Position = new Vector3(0.0f, 10.0f, 10.0f); camera.Target = new Vector3(0.0f, 0.0f, 0.0f); camera.Up = new Vector3(0.0f, 1.0f, 0.0f); camera.FovY = FOVY_PERSPECTIVE; camera.Projection = CameraProjection.Perspective; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.Space)) + { + if (camera.Projection == CameraProjection.Perspective) + { + camera.FovY = WIDTH_ORTHOGRAPHIC; + camera.Projection = CameraProjection.Orthographic; + } + else + { + camera.FovY = FOVY_PERSPECTIVE; + camera.Projection = CameraProjection.Perspective; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawCube(new Vector3(-4.0f, 0.0f, 2.0f), 2.0f, 5.0f, 2.0f, Color.Red); + DrawCubeWires(new Vector3(-4.0f, 0.0f, 2.0f), 2.0f, 5.0f, 2.0f, Color.Gold); + DrawCubeWires(new Vector3(-4.0f, 0.0f, -2.0f), 3.0f, 6.0f, 2.0f, Color.Maroon); + + DrawSphere(new Vector3(-1.0f, 0.0f, -2.0f), 1.0f, Color.Green); + DrawSphereWires(new Vector3(1.0f, 0.0f, 2.0f), 2.0f, 16, 16, Color.Lime); + + DrawCylinder(new Vector3(4.0f, 0.0f, -2.0f), 1.0f, 2.0f, 3.0f, 4, Color.SkyBlue); + DrawCylinderWires(new Vector3(4.0f, 0.0f, -2.0f), 1.0f, 2.0f, 3.0f, 4, Color.DarkBlue); + DrawCylinderWires(new Vector3(4.5f, -1.0f, 2.0f), 1.0f, 1.0f, 2.0f, 6, Color.Brown); + + DrawCylinder(new Vector3(1.0f, 0.0f, -4.0f), 0.0f, 1.5f, 3.0f, 8, Color.Gold); + DrawCylinderWires(new Vector3(1.0f, 0.0f, -4.0f), 0.0f, 1.5f, 3.0f, 8, Color.Pink); + + DrawGrid(10, 1.0f); // Draw a grid + + EndMode3D(); + + DrawText("Press Spacebar to switch camera type", 10, GetScreenHeight() - 30, 20, Color.DarkGray); + + if (camera.Projection == CameraProjection.Orthographic) + { + DrawText("ORTHOGRAPHIC", 10, 40, 20, Color.Black); + } + else if (camera.Projection == CameraProjection.Perspective) + { + DrawText("PERSPECTIVE", 10, 40, 20, Color.Black); + } + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - orthographic projection"); SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new OrthographicProjection(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (IsKeyPressed(KeyboardKey.Space)) - { - if (camera.Projection == CameraProjection.Perspective) - { - camera.FovY = WIDTH_ORTHOGRAPHIC; - camera.Projection = CameraProjection.Orthographic; - } - else - { - camera.FovY = FOVY_PERSPECTIVE; - camera.Projection = CameraProjection.Perspective; - } - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawCube(new Vector3(-4.0f, 0.0f, 2.0f), 2.0f, 5.0f, 2.0f, Color.Red); - DrawCubeWires(new Vector3(-4.0f, 0.0f, 2.0f), 2.0f, 5.0f, 2.0f, Color.Gold); - DrawCubeWires(new Vector3(-4.0f, 0.0f, -2.0f), 3.0f, 6.0f, 2.0f, Color.Maroon); - - DrawSphere(new Vector3(-1.0f, 0.0f, -2.0f), 1.0f, Color.Green); - DrawSphereWires(new Vector3(1.0f, 0.0f, 2.0f), 2.0f, 16, 16, Color.Lime); - - DrawCylinder(new Vector3(4.0f, 0.0f, -2.0f), 1.0f, 2.0f, 3.0f, 4, Color.SkyBlue); - DrawCylinderWires(new Vector3(4.0f, 0.0f, -2.0f), 1.0f, 2.0f, 3.0f, 4, Color.DarkBlue); - DrawCylinderWires(new Vector3(4.5f, -1.0f, 2.0f), 1.0f, 1.0f, 2.0f, 6, Color.Brown); - - DrawCylinder(new Vector3(1.0f, 0.0f, -4.0f), 0.0f, 1.5f, 3.0f, 8, Color.Gold); - DrawCylinderWires(new Vector3(1.0f, 0.0f, -4.0f), 0.0f, 1.5f, 3.0f, 8, Color.Pink); - - DrawGrid(10, 1.0f); - - EndMode3D(); - - DrawText("Press Spacebar to switch camera type", 10, GetScreenHeight() - 30, 20, Color.DarkGray); - - if (camera.Projection == CameraProjection.Orthographic) - { - DrawText("ORTHOGRAPHIC", 10, 40, 20, Color.Black); - } - else if (camera.Projection == CameraProjection.Perspective) - { - DrawText("PERSPECTIVE", 10, 40, 20, Color.Black); - } - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; 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 4964af8..8238eae 100644 --- a/Examples/Models/SkyboxDemo.cs +++ b/Examples/Models/SkyboxDemo.cs @@ -1,32 +1,54 @@ /******************************************************************************************* * -* raylib [models] example - Skybox loading and drawing +* raylib [models] example - skybox rendering * -* This example has been created using raylib 1.8 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2017 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.8, 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) 2017-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ +using System; using System.Numerics; using static Raylib_cs.Raylib; namespace Examples.Models; -public class SkyboxDemo +[ExcludeFromBrowser("cubemap generation is too memory-heavy on web (upstream note)")] +public partial class SkyboxDemo : IExample { - public static int Main() + 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"; + + public string Title => "raylib [models] example - skybox rendering"; + + public bool CursorDisabled => true; + + private Camera3D camera; + private Model skybox; + private bool useHdr; + private Shader shdrCubemap; + private string skyboxFileName; + private Texture2D panorama; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [models] example - skybox loading and drawing"); - // Define the camera to look into our 3d world - Camera3D camera = new(); + camera = new(); camera.Position = new Vector3(1.0f, 1.0f, 1.0f); camera.Target = new Vector3(4.0f, 1.0f, 4.0f); camera.Up = new Vector3(0.0f, 1.0f, 0.0f); @@ -34,14 +56,19 @@ public class SkyboxDemo camera.Projection = CameraProjection.Perspective; // Load skybox model - Mesh cube = GenMeshCube(1.0f, 1.0f, 1.0f); - Model skybox = LoadModelFromMesh(cube); + var cube = GenMeshCube(1.0f, 1.0f, 1.0f); + skybox = LoadModelFromMesh(cube); - bool useHdr = false; + // Set this to true to use an HDR Texture + // NOTE: raylib must be built with HDR Support for this to work: SUPPORT_FILEFORMAT_HDR + useHdr = false; // Load skybox shader and set required locations // NOTE: Some locations are automatically set at shader loading - Shader shdrSkybox = LoadShader("resources/shaders/glsl330/skybox.vs", "resources/shaders/glsl330/skybox.fs"); + var shdrSkybox = LoadShader( + $"resources/shaders/glsl{GlslVersion}/skybox.vs", + $"resources/shaders/glsl{GlslVersion}/skybox.fs" + ); Raylib.SetShaderValue( shdrSkybox, @@ -67,9 +94,9 @@ public class SkyboxDemo Raylib.SetMaterialShader(ref skybox, 0, ref shdrSkybox); // Load cubemap shader and setup required shader locations - Shader shdrCubemap = LoadShader( - "resources/shaders/glsl330/cubemap.vs", - "resources/shaders/glsl330/cubemap.fs" + shdrCubemap = LoadShader( + $"resources/shaders/glsl{GlslVersion}/cubemap.vs", + $"resources/shaders/glsl{GlslVersion}/cubemap.fs" ); Raylib.SetShaderValue( shdrCubemap, @@ -79,14 +106,12 @@ public class SkyboxDemo ); // Load skybox - string skyboxFileName = "resources/dresden_square_2k.hdr"; - - Texture2D panorama; + skyboxFileName = "resources/dresden_square_2k.hdr"; if (useHdr) { panorama = LoadTexture(skyboxFileName); - Texture2D cubemap = GenTextureCubemap( + var cubemap = GenTextureCubemap( shdrCubemap, panorama, 1024, @@ -97,108 +122,130 @@ public class SkyboxDemo } else { - Image img = LoadImage("resources/skybox.png"); - Texture2D cubemap = LoadTextureCubemap(img, CubemapLayout.AutoDetect); + // TODO: WARNING: On PLATFORM_WEB it requires a big amount of memory to process input image + // and generate the required cubemap image to be passed to rlLoadTextureCubemap() + var img = LoadImage("resources/skybox.png"); + var cubemap = LoadTextureCubemap(img, CubemapLayout.AutoDetect); SetMaterialTexture(ref skybox, 0, MaterialMapIndex.Cubemap, ref cubemap); UnloadImage(img); } + } - DisableCursor(); + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.FirstPerson); - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- - - // Main game loop - while (!WindowShouldClose()) + // Load new cubemap texture on drag & drop + if (IsFileDropped()) { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.FirstPerson); + var files = Raylib.GetDroppedFiles(); - // Load new cubemap texture on drag & drop - if (IsFileDropped()) + if (files.Length == 1) { - string[] files = Raylib.GetDroppedFiles(); - - if (files.Length == 1) + if (IsFileExtension(files[0], ".png;.jpg;.hdr;.bmp;.tga")) { - if (IsFileExtension(files[0], ".png;.jpg;.hdr;.bmp;.tga")) + // Unload cubemap texture and load new one + UnloadTexture(Raylib.GetMaterialTexture(ref skybox, 0, MaterialMapIndex.Cubemap)); + + if (useHdr) { - // Unload cubemap texture and load new one - UnloadTexture(Raylib.GetMaterialTexture(ref skybox, 0, MaterialMapIndex.Cubemap)); - - if (useHdr) - { - panorama = LoadTexture(files[0]); - Texture2D cubemap = GenTextureCubemap( - shdrCubemap, - panorama, - 1024, - PixelFormat.UncompressedR8G8B8A8 - ); - SetMaterialTexture(ref skybox, 0, MaterialMapIndex.Cubemap, ref cubemap); - UnloadTexture(panorama); - } - else - { - Image img = LoadImage(files[0]); - Texture2D cubemap = LoadTextureCubemap(img, CubemapLayout.AutoDetect); - SetMaterialTexture(ref skybox, 0, MaterialMapIndex.Cubemap, ref cubemap); - UnloadImage(img); - } - - skyboxFileName = files[0]; + panorama = LoadTexture(files[0]); + var cubemap = GenTextureCubemap( + shdrCubemap, + panorama, + 1024, + PixelFormat.UncompressedR8G8B8A8 + ); + SetMaterialTexture(ref skybox, 0, MaterialMapIndex.Cubemap, ref cubemap); + UnloadTexture(panorama); } + else + { + var img = LoadImage(files[0]); + var cubemap = LoadTextureCubemap(img, CubemapLayout.AutoDetect); + SetMaterialTexture(ref skybox, 0, MaterialMapIndex.Cubemap, ref cubemap); + UnloadImage(img); + } + + skyboxFileName = files[0]; } } - //---------------------------------------------------------------------------------- + } + //---------------------------------------------------------------------------------- - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); - BeginMode3D(camera); + BeginMode3D(camera); - // We are inside the cube, we need to disable backface culling! - Rlgl.DisableBackfaceCulling(); - Rlgl.DisableDepthMask(); - DrawModel(skybox, Vector3.Zero, 1.0f, Color.White); - Rlgl.EnableBackfaceCulling(); - Rlgl.EnableDepthMask(); + // We are inside the cube, we need to disable backface culling! + Rlgl.DisableBackfaceCulling(); + Rlgl.DisableDepthMask(); + DrawModel(skybox, Vector3.Zero, 1.0f, Color.White); + Rlgl.EnableBackfaceCulling(); + Rlgl.EnableDepthMask(); - DrawGrid(10, 1.0f); + DrawGrid(10, 1.0f); - EndMode3D(); + EndMode3D(); - if (useHdr) - { - DrawText( - $"Panorama image from hdrihaven.com: {skyboxFileName}", - 10, - GetScreenHeight() - 20, - 10, - Color.Black - ); - } - else - { - DrawText($": {skyboxFileName}", 10, GetScreenHeight() - 20, 10, Color.Black); - } - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + if (useHdr) + { + DrawText( + $"Panorama image from hdrihaven.com: {skyboxFileName}", + 10, + GetScreenHeight() - 20, + 10, + Color.Black + ); + } + else + { + DrawText($": {skyboxFileName}", 10, GetScreenHeight() - 20, 10, Color.Black); } - // De-Initialization - //-------------------------------------------------------------------------------------- + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { UnloadShader(Raylib.GetMaterial(ref skybox, 0).Shader); UnloadTexture(Raylib.GetMaterialTexture(ref skybox, 0, MaterialMapIndex.Cubemap)); UnloadModel(skybox); + } + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - skybox rendering"); + + DisableCursor(); // Limit cursor to relative movement inside the window + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new SkyboxDemo(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- CloseWindow(); //-------------------------------------------------------------------------------------- @@ -215,10 +262,10 @@ public class SkyboxDemo // STEP 1: Setup framebuffer //------------------------------------------------------------------------------------------ - uint rbo = Rlgl.LoadTextureDepth(size, size, true); + var rbo = Rlgl.LoadTextureDepth(size, size, true); cubemap.Id = Rlgl.LoadTextureCubemap(null, size, format, 1); - uint fbo = Rlgl.LoadFramebuffer(); + var fbo = Rlgl.LoadFramebuffer(); Rlgl.FramebufferAttach( fbo, rbo, @@ -235,7 +282,7 @@ public class SkyboxDemo ); // Check if framebuffer is complete with attachments (valid) - if (Rlgl.FramebufferComplete(fbo)) + if (Rlgl.FramebufferComplete(fbo) != 0) { Console.WriteLine($"FBO: [ID {fbo}] Framebuffer object created successfully"); } @@ -247,7 +294,7 @@ public class SkyboxDemo Rlgl.EnableShader(shader.Id); // Define projection matrix and send it to shader - Matrix4x4 matFboProjection = Raymath.MatrixPerspective( + var matFboProjection = Raymath.MatrixPerspective( 90.0f * DEG2RAD, 1.0f, Rlgl.CULL_DISTANCE_NEAR, @@ -256,14 +303,14 @@ public class SkyboxDemo Rlgl.SetUniformMatrix(shader.Locs[(int)ShaderLocationIndex.MatrixProjection], matFboProjection); // Define view matrix for every side of the cubemap - Matrix4x4[] fboViews = new[] + var fboViews = new[] { - Raymath.MatrixLookAt(Vector3.Zero, new Vector3(-1.0f, 0.0f, 0.0f), new Vector3( 0.0f, -1.0f, 0.0f)), Raymath.MatrixLookAt(Vector3.Zero, new Vector3( 1.0f, 0.0f, 0.0f), new Vector3( 0.0f, -1.0f, 0.0f)), + Raymath.MatrixLookAt(Vector3.Zero, new Vector3(-1.0f, 0.0f, 0.0f), new Vector3( 0.0f, -1.0f, 0.0f)), Raymath.MatrixLookAt(Vector3.Zero, new Vector3( 0.0f, 1.0f, 0.0f), new Vector3( 0.0f, 0.0f, 1.0f)), Raymath.MatrixLookAt(Vector3.Zero, new Vector3( 0.0f, -1.0f, 0.0f), new Vector3( 0.0f, 0.0f, -1.0f)), - Raymath.MatrixLookAt(Vector3.Zero, new Vector3( 0.0f, 0.0f, -1.0f), new Vector3( 0.0f, -1.0f, 0.0f)), Raymath.MatrixLookAt(Vector3.Zero, new Vector3( 0.0f, 0.0f, 1.0f), new Vector3( 0.0f, -1.0f, 0.0f)), + Raymath.MatrixLookAt(Vector3.Zero, new Vector3( 0.0f, 0.0f, -1.0f), new Vector3( 0.0f, -1.0f, 0.0f)), }; // Set viewport to current fbo dimensions @@ -273,7 +320,7 @@ public class SkyboxDemo Rlgl.ActiveTextureSlot(0); Rlgl.EnableTexture(panorama.Id); - for (int i = 0; i < 6; i++) + for (var i = 0; i < 6; i++) { // Set the view matrix for the current cube face Rlgl.SetUniformMatrix(shader.Locs[(int)ShaderLocationIndex.MatrixView], fboViews[i]); diff --git a/Examples/Models/SolarSystem.cs b/Examples/Models/SolarSystem.cs index 8b5c294..36c989f 100644 --- a/Examples/Models/SolarSystem.cs +++ b/Examples/Models/SolarSystem.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [models] example - rlgl module usage with push/pop matrix transformations +* raylib [models] example - rlgl solar system * -* This example uses [rlgl] module funtionality (pseudo-OpenGL 1.1 style coding) +* Example complexity rating: [★★★★] 4/4 * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* NOTE: This example uses [rlgl] module functionality (pseudo-OpenGL 1.1 style coding) * -* Copyright (c) 2018 Ramon Santamaria (@raysan5) +* Example originally created with raylib 2.5, 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) 2018-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -17,145 +21,137 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class SolarSystem +public partial class SolarSystem : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const float sunRadius = 4.0f; + private const float earthRadius = 0.6f; + private const float earthOrbitRadius = 8.0f; + private const float moonRadius = 0.16f; + private const float moonOrbitRadius = 1.5f; + + public string Name => "Models / Solar System"; + + public string Title => "raylib [models] example - rlgl solar system"; + + private Camera3D camera; + + private float rotationSpeed; + + private float earthRotation; + private float earthOrbitRotation; + private float moonRotation; + private float moonOrbitRotation; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - const float sunRadius = 4.0f; - const float earthRadius = 0.6f; - const float earthOrbitRadius = 8.0f; - const float moonRadius = 0.16f; - const float moonOrbitRadius = 1.5f; - - InitWindow(screenWidth, screenHeight, "raylib [models] example - rlgl module usage with push/pop matrix transformations"); - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(16.0f, 16.0f, 16.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; + camera = new(); + camera.Position = new Vector3(16.0f, 16.0f, 16.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 - // General system rotation speed - float rotationSpeed = 0.2f; - // Rotation of earth around itself (days) in degrees - float earthRotation = 0.0f; - // Rotation of earth around the Sun (years) in degrees - float earthOrbitRotation = 0.0f; - // Rotation of moon around itself - float moonRotation = 0.0f; - // Rotation of moon around earth in degrees - float moonOrbitRotation = 0.0f; + rotationSpeed = 0.2f; // General system rotation speed - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- + earthRotation = 0.0f; // Rotation of earth around itself (days) in degrees + earthOrbitRotation = 0.0f; // Rotation of earth around the Sun (years) in degrees + moonRotation = 0.0f; // Rotation of moon around itself + moonOrbitRotation = 0.0f; // Rotation of moon around earth in degrees + } - // Main game loop - while (!WindowShouldClose()) - { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Free); + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + earthRotation += (5.0f * rotationSpeed); + earthOrbitRotation += (365 / 360.0f * (5.0f * rotationSpeed) * rotationSpeed); + moonRotation += (2.0f * rotationSpeed); + moonOrbitRotation += (8.0f * rotationSpeed); + //---------------------------------------------------------------------------------- - earthRotation += (5.0f * rotationSpeed); - earthOrbitRotation += (365 / 360.0f * (5.0f * rotationSpeed) * rotationSpeed); - moonRotation += (2.0f * rotationSpeed); - moonOrbitRotation += (8.0f * rotationSpeed); - //---------------------------------------------------------------------------------- + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); + BeginMode3D(camera); - BeginMode3D(camera); + Rlgl.PushMatrix(); + // Scale Sun + Rlgl.Scalef(sunRadius, sunRadius, sunRadius); + // Draw the Sun + DrawSphereBasic(Color.Gold); + Rlgl.PopMatrix(); - Rlgl.PushMatrix(); - // Scale Sun - Rlgl.Scalef(sunRadius, sunRadius, sunRadius); - // Draw the Sun - DrawSphereBasic(Color.Gold); - Rlgl.PopMatrix(); + Rlgl.PushMatrix(); + // Rotation for Earth orbit around Sun + Rlgl.Rotatef(earthOrbitRotation, 0.0f, 1.0f, 0.0f); + // Translation for Earth orbit + Rlgl.Translatef(earthOrbitRadius, 0.0f, 0.0f); - Rlgl.PushMatrix(); - // Rotation for Earth orbit around Sun - Rlgl.Rotatef(earthOrbitRotation, 0.0f, 1.0f, 0.0f); - // Translation for Earth orbit - Rlgl.Translatef(earthOrbitRadius, 0.0f, 0.0f); - // Rotation for Earth orbit around Sun inverted - Rlgl.Rotatef(-earthOrbitRotation, 0.0f, 1.0f, 0.0f); + Rlgl.PushMatrix(); + // Rotation for Earth itself + Rlgl.Rotatef(earthRotation, 0.25f, 1.0f, 0.0f); + // Scale Earth + Rlgl.Scalef(earthRadius, earthRadius, earthRadius); - Rlgl.PushMatrix(); - // Rotation for Earth itself - Rlgl.Rotatef(earthRotation, 0.25f, 1.0f, 0.0f); - // Scale Earth - Rlgl.Scalef(earthRadius, earthRadius, earthRadius); + // Draw the Earth + DrawSphereBasic(Color.Blue); + Rlgl.PopMatrix(); - // Draw the Earth - DrawSphereBasic(Color.Blue); - Rlgl.PopMatrix(); + // Rotation for Moon orbit around Earth + Rlgl.Rotatef(moonOrbitRotation, 0.0f, 1.0f, 0.0f); + // Translation for Moon orbit + Rlgl.Translatef(moonOrbitRadius, 0.0f, 0.0f); + // Rotation for Moon itself + Rlgl.Rotatef(moonRotation, 0.0f, 1.0f, 0.0f); + // Scale Moon + Rlgl.Scalef(moonRadius, moonRadius, moonRadius); - // Rotation for Moon orbit around Earth - Rlgl.Rotatef(moonOrbitRotation, 0.0f, 1.0f, 0.0f); - // Translation for Moon orbit - Rlgl.Translatef(moonOrbitRadius, 0.0f, 0.0f); - // Rotation for Moon orbit around Earth inverted - Rlgl.Rotatef(-moonOrbitRotation, 0.0f, 1.0f, 0.0f); - // Rotation for Moon itself - Rlgl.Rotatef(moonRotation, 0.0f, 1.0f, 0.0f); - // Scale Moon - Rlgl.Scalef(moonRadius, moonRadius, moonRadius); + // Draw the Moon + DrawSphereBasic(Color.LightGray); + Rlgl.PopMatrix(); - // Draw the Moon - DrawSphereBasic(Color.LightGray); - Rlgl.PopMatrix(); + // Some reference elements (not affected by previous matrix transformations) + DrawCircle3D( + new Vector3(0.0f, 0.0f, 0.0f), + earthOrbitRadius, + new Vector3(1, 0, 0), + 90.0f, + Fade(Color.Red, 0.5f) + ); + DrawGrid(20, 1.0f); - // Some reference elements (not affected by previous matrix transformations) - DrawCircle3D( - new Vector3(0.0f, 0.0f, 0.0f), - earthOrbitRadius, - new Vector3(1, 0, 0), - 90.0f, - ColorAlpha(Color.Red, 0.5f) - ); - DrawGrid(20, 1.0f); + EndMode3D(); - EndMode3D(); + DrawText("EARTH ORBITING AROUND THE SUN!", 400, 10, 20, Color.Maroon); + DrawFPS(10, 10); - DrawText("EARTH ORBITING AROUND THE SUN!", 400, 10, 20, Color.Maroon); - DrawFPS(10, 10); + EndDrawing(); + //---------------------------------------------------------------------------------- + } - EndDrawing(); - //---------------------------------------------------------------------------------- - } - - // De-Initialization - //-------------------------------------------------------------------------------------- - CloseWindow(); - //-------------------------------------------------------------------------------------- - - return 0; + public void Unload() + { } // Draw sphere without any matrix transformation // NOTE: Sphere is drawn in world position ( 0, 0, 0 ) with radius 1.0f - static void DrawSphereBasic(Color color) + private static void DrawSphereBasic(Color color) { - int rings = 16; - int slices = 16; + var rings = 16; + var slices = 16; Rlgl.Begin(DrawMode.Triangles); Rlgl.Color4ub(color.R, color.G, color.B, color.A); - for (int i = 0; i < (rings + 2); i++) + for (var i = 0; i < (rings + 2); i++) { - for (int j = 0; j < slices; j++) + for (var j = 0; j < slices; j++) { Rlgl.Vertex3f( MathF.Cos(DEG2RAD * (270 + (180 / (rings + 1)) * i)) * MathF.Sin(DEG2RAD * (j * 360 / slices)), @@ -192,5 +188,32 @@ public class SolarSystem } Rlgl.End(); } -} + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [models] example - rlgl solar system"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new SolarSystem(); + 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/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/Models/WavingCubes.cs b/Examples/Models/WavingCubes.cs index 27867d5..30a4442 100644 --- a/Examples/Models/WavingCubes.cs +++ b/Examples/Models/WavingCubes.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [models] example - Waving cubes +* raylib [models] example - waving cubes * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 2.5, last time updated with raylib 3.7 * * Example contributed by Codecat (@codecat) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2019 Codecat (@codecat) and 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 Codecat (@codecat) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -17,101 +21,125 @@ using static Raylib_cs.Raylib; namespace Examples.Models; -public class WavingCubes +public partial class WavingCubes : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + // Specify the amount of blocks in each direction + private const int numBlocks = 15; + + public string Name => "Models / Waving Cubes"; + + public string Title => "raylib [models] example - waving cubes"; + + private Camera3D camera; + + public void Init() + { + // Initialize the camera + camera = new(); + camera.Position = new Vector3(30.0f, 20.0f, 30.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 = 70.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + var time = GetTime(); + + // Calculate time scale for cube position and size + var scale = (2.0f + (float)Math.Sin(time)) * 0.7f; + + // Move camera around the scene + var cameraTime = time * 0.3; + camera.Position.X = (float)Math.Cos(cameraTime) * 40.0f; + camera.Position.Z = (float)Math.Sin(cameraTime) * 40.0f; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawGrid(10, 5.0f); + + for (var x = 0; x < numBlocks; x++) + { + for (var y = 0; y < numBlocks; y++) + { + for (var z = 0; z < numBlocks; z++) + { + // Scale of the blocks depends on x/y/z positions + var blockScale = (x + y + z) / 30.0f; + + // Scatter makes the waving effect by adding blockScale over time + var scatter = (float)Math.Sin(blockScale * 20.0f + (float)(time * 4.0f)); + + // Calculate the cube position + Vector3 cubePos = new( + (float)(x - numBlocks / 2) * (scale * 3.0f) + scatter, + (float)(y - numBlocks / 2) * (scale * 2.0f) + scatter, + (float)(z - numBlocks / 2) * (scale * 3.0f) + scatter + ); + + // Pick a color with a hue depending on cube position for the rainbow color effect + // NOTE: This function is quite costly to be done per cube and frame, + // pre-catching the results into a separate array could improve performance + var cubeColor = ColorFromHSV((float)(((x + y + z) * 18) % 360), 0.75f, 0.9f); + + // Calculate cube size + var cubeSize = (2.4f - scale) * blockScale; + + // And finally, draw the cube! + DrawCube(cubePos, cubeSize, cubeSize, cubeSize, cubeColor); + } + } + } + + EndMode3D(); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [models] example - waving cubes"); - // Initialize the camera - Camera3D camera = new(); - camera.Position = new Vector3(30.0f, 20.0f, 30.0f); - camera.Target = new Vector3(0.0f, 0.0f, 0.0f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.FovY = 70.0f; - camera.Projection = CameraProjection.Perspective; - - // Specify the amount of blocks in each direction - const int numBlocks = 15; - SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new WavingCubes(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - double time = GetTime(); - - // Calculate time scale for cube position and size - float scale = (2.0f + (float)Math.Sin(time)) * 0.7f; - - // Move camera around the scene - double cameraTime = time * 0.3; - camera.Position.X = (float)Math.Cos(cameraTime) * 40.0f; - camera.Position.Z = (float)Math.Sin(cameraTime) * 40.0f; - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawGrid(10, 5.0f); - - for (int x = 0; x < numBlocks; x++) - { - for (int y = 0; y < numBlocks; y++) - { - for (int z = 0; z < numBlocks; z++) - { - // Scale of the blocks depends on x/y/z positions - float blockScale = (x + y + z) / 30.0f; - - // Scatter makes the waving effect by adding blockScale over time - float scatter = (float)Math.Sin(blockScale * 20.0f + (float)(time * 4.0f)); - - // Calculate the cube position - Vector3 cubePos = new( - (float)(x - numBlocks / 2) * (scale * 3.0f) + scatter, - (float)(y - numBlocks / 2) * (scale * 2.0f) + scatter, - (float)(z - numBlocks / 2) * (scale * 3.0f) + scatter - ); - - // Pick a color with a hue depending on cube position for the rainbow color effect - Color cubeColor = ColorFromHSV((float)(((x + y + z) * 18) % 360), 0.75f, 0.9f); - - // Calculate cube size - float cubeSize = (2.4f - scale) * blockScale; - - // And finally, draw the cube! - DrawCube(cubePos, cubeSize, cubeSize, cubeSize, cubeColor); - } - } - } - - EndMode3D(); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } } - diff --git a/Examples/Models/YawPitchRoll.cs b/Examples/Models/YawPitchRoll.cs index 85e8092..3b26f34 100644 --- a/Examples/Models/YawPitchRoll.cs +++ b/Examples/Models/YawPitchRoll.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [models] example - Plane rotations (yaw, pitch, roll) +* raylib [models] example - yaw pitch roll * -* This example has been created using raylib 1.8 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 1.8, last time updated with raylib 4.0 * * Example contributed by Berni (@Berni8k) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2017-2021 Berni (@Berni8k) and 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) 2017-2025 Berni (@Berni8k) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -17,146 +21,178 @@ using static Raylib_cs.Raymath; namespace Examples.Models; -public class YawPitchRoll +public partial class YawPitchRoll : IExample { - public unsafe static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Models / Yaw Pitch Roll"; + + public string Title => "raylib [models] example - yaw pitch roll"; + + private Camera3D camera; + + private Model model; + private Texture2D texture; + + private float pitch; + private float roll; + private float yaw; + + public unsafe void Init() + { + camera = new(); + camera.Position = new Vector3(0.0f, 50.0f, -120.0f);// Camera position perspective + 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 = 30.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera type + + model = LoadModel("resources/models/obj/plane.obj"); // Load model + texture = LoadTexture("resources/models/obj/plane_diffuse.png"); // Load model texture + + SetTextureWrap(texture, TextureWrap.Repeat); // Force Repeat to avoid issue on Web version + + model.Materials[0].Maps[(int)MaterialMapIndex.Diffuse].Texture = texture; // Set map diffuse texture + + pitch = 0.0f; + roll = 0.0f; + yaw = 0.0f; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + + // Plane pitch (x-axis) controls + if (IsKeyDown(KeyboardKey.Down)) + { + pitch += 0.6f; + } + else if (IsKeyDown(KeyboardKey.Up)) + { + pitch -= 0.6f; + } + else + { + if (pitch > 0.3f) + { + pitch -= 0.3f; + } + else if (pitch < -0.3f) + { + pitch += 0.3f; + } + } + + // Plane yaw (y-axis) controls + if (IsKeyDown(KeyboardKey.S)) + { + yaw += 1.0f; + } + else if (IsKeyDown(KeyboardKey.A)) + { + yaw -= 1.0f; + } + else + { + if (yaw > 0.0f) + { + yaw -= 0.5f; + } + else if (yaw < 0.0f) + { + yaw += 0.5f; + } + } + + // Plane roll (z-axis) controls + if (IsKeyDown(KeyboardKey.Left)) + { + roll += 1.0f; + } + else if (IsKeyDown(KeyboardKey.Right)) + { + roll -= 1.0f; + } + else + { + if (roll > 0.0f) + { + roll -= 0.5f; + } + else if (roll < 0.0f) + { + roll += 0.5f; + } + } + + // Tranformation matrix for rotations + model.Transform = MatrixRotateXYZ(new Vector3(DEG2RAD * pitch, DEG2RAD * yaw, DEG2RAD * roll)); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // Draw 3D model (recomended to draw 3D always before 2D) + BeginMode3D(camera); + + // Draw 3d model with texture + DrawModel(model, new Vector3(0.0f, -8.0f, 0.0f), 1.0f, Color.White); + DrawGrid(10, 10.0f); + + EndMode3D(); + + // Draw controls info + DrawRectangle(30, 370, 260, 70, Fade(Color.Green, 0.5f)); + DrawRectangleLines(30, 370, 260, 70, Fade(Color.DarkGreen, 0.5f)); + DrawText("Pitch controlled with: KEY_UP / KEY_DOWN", 40, 380, 10, Color.DarkGray); + DrawText("Roll controlled with: KEY_LEFT / KEY_RIGHT", 40, 400, 10, Color.DarkGray); + DrawText("Yaw controlled with: KEY_A / KEY_S", 40, 420, 10, Color.DarkGray); + + DrawText( + "(c) WWI Plane Model created by GiaHanLam", + screenWidth - 240, + screenHeight - 20, + 10, + Color.DarkGray + ); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadModel(model); + } + + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [models] example - plane rotations (yaw, pitch, roll)"); - - Camera3D camera = new(); - camera.Position = new Vector3(0.0f, 50.0f, -120.0f); - camera.Target = new Vector3(0.0f, 0.0f, 0.0f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.FovY = 30.0f; - camera.Projection = CameraProjection.Perspective; - - // Model loading - Model model = LoadModel("resources/models/obj/plane.obj"); - Texture2D texture = LoadTexture("resources/models/obj/plane_diffuse.png"); - model.Materials[0].Maps[(int)MaterialMapIndex.Diffuse].Texture = texture; - - float pitch = 0.0f; - float roll = 0.0f; - float yaw = 0.0f; + InitWindow(screenWidth, screenHeight, "raylib [models] example - yaw pitch roll"); SetTargetFPS(60); //-------------------------------------------------------------------------------------- - while (!WindowShouldClose()) + var game = new YawPitchRoll(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - - // Plane roll (x-axis) controls - if (IsKeyDown(KeyboardKey.Down)) - { - pitch += 0.6f; - } - else if (IsKeyDown(KeyboardKey.Up)) - { - pitch -= 0.6f; - } - else - { - if (pitch > 0.3f) - { - pitch -= 0.3f; - } - else if (pitch < -0.3f) - { - pitch += 0.3f; - } - } - - // Plane yaw (y-axis) controls - if (IsKeyDown(KeyboardKey.S)) - { - yaw += 1.0f; - } - else if (IsKeyDown(KeyboardKey.A)) - { - yaw -= 1.0f; - } - else - { - if (yaw > 0.0f) - { - yaw -= 0.5f; - } - else if (yaw < 0.0f) - { - yaw += 0.5f; - } - } - - // Plane pitch (z-axis) controls - if (IsKeyDown(KeyboardKey.Left)) - { - roll += 1.0f; - } - else if (IsKeyDown(KeyboardKey.Right)) - { - roll -= 1.0f; - } - else - { - if (roll > 0.0f) - { - roll -= 0.5f; - } - else if (roll < 0.0f) - { - roll += 0.5f; - } - } - - // Tranformation matrix for rotations - model.Transform = MatrixRotateXYZ(new Vector3(DEG2RAD * pitch, DEG2RAD * yaw, DEG2RAD * roll)); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // Draw 3D model (recomended to draw 3D always before 2D) - BeginMode3D(camera); - - // Draw 3d model with texture - DrawModel(model, new Vector3(0.0f, -8.0f, 0.0f), 1.0f, Color.White); - DrawGrid(10, 10.0f); - - EndMode3D(); - - // Draw controls info - DrawRectangle(30, 370, 260, 70, Fade(Color.Green, 0.5f)); - DrawRectangleLines(30, 370, 260, 70, Fade(Color.DarkGreen, 0.5f)); - DrawText("Pitch controlled with: KEY_UP / KEY_DOWN", 40, 380, 10, Color.DarkGray); - DrawText("Roll controlled with: KEY_LEFT / KEY_RIGHT", 40, 400, 10, Color.DarkGray); - DrawText("Yaw controlled with: KEY_A / KEY_S", 40, 420, 10, Color.DarkGray); - - DrawText( - "(c) WWI Plane Model created by GiaHanLam", - screenWidth - 240, - screenHeight - 20, - 10, - Color.DarkGray - ); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadModel(model); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Program.cs b/Examples/Program.cs index ca374bf..a24b557 100644 --- a/Examples/Program.cs +++ b/Examples/Program.cs @@ -1,195 +1,88 @@ +#nullable enable using System.Diagnostics; -using Examples.Core; -using Examples.Shapes; -using Examples.Textures; -using Examples.Text; -using Examples.Models; -using Examples.Shaders; -using Examples.Audio; namespace Examples; -public class ExampleInfo +internal static class Program { - public ExampleInfo(string name, Func main) - { - this.Name = name; - this.Main = main; - } - - public string Name - { - get; set; - } - - public Func Main - { - get; set; - } -} - -public class ExampleList -{ - public static ExampleInfo[] AllExamples = new[] - { - // Core - new ExampleInfo("DeltaTime", DeltaTime.Main), - new ExampleInfo("InputGesturesTestBed", InputGesturesTestBed.Main), - new ExampleInfo("InputVirtualControls", InputVirtualControls.Main), - new ExampleInfo("Camera2dPlatformer", Camera2dPlatformer.Main), - new ExampleInfo("Camera2dDemo", Camera2dDemo.Main), - new ExampleInfo("Camera3dFirstPerson", Camera3dFirstPerson.Main), - new ExampleInfo("Camera3dFree", Camera3dFree.Main), - new ExampleInfo("Camera3dMode", Camera3dMode.Main), - new ExampleInfo("Picking3d", Picking3d.Main), - new ExampleInfo("BasicScreenManager", BasicScreenManager.Main), - new ExampleInfo("BasicWindow", BasicWindow.Main), - new ExampleInfo("CustomLogging", CustomLogging.Main), - new ExampleInfo("DropFiles", DropFiles.Main), - new ExampleInfo("InputGamepad", InputGamepad.Main), - new ExampleInfo("InputGestures", InputGestures.Main), - new ExampleInfo("InputKeys", InputKeys.Main), - new ExampleInfo("InputMouseWheel", InputMouseWheel.Main), - new ExampleInfo("InputMouse", InputMouse.Main), - new ExampleInfo("InputMultitouch", InputMultitouch.Main), - new ExampleInfo("RandomValues", RandomValues.Main), - new ExampleInfo("ScissorTest", ScissorTest.Main), - new ExampleInfo("SmoothPixelPerfect", SmoothPixelPerfect.Main), - new ExampleInfo("SplitScreen", SplitScreen.Main), - new ExampleInfo("StorageValues", StorageValues.Main), - new ExampleInfo("VrSimulator", VrSimulator.Main), - new ExampleInfo("WindowFlags", WindowFlags.Main), - new ExampleInfo("WindowLetterbox", WindowLetterbox.Main), - new ExampleInfo("WorldScreen", WorldScreen.Main), - - // Shapes - new ExampleInfo("BasicShapes", BasicShapes.Main), - new ExampleInfo("BouncingBall", BouncingBall.Main), - new ExampleInfo("CollisionArea", CollisionArea.Main), - new ExampleInfo("ColorsPalette", ColorsPalette.Main), - new ExampleInfo("EasingsBallAnim", EasingsBallAnim.Main), - new ExampleInfo("EasingsBoxAnim", EasingsBoxAnim.Main), - new ExampleInfo("EasingsRectangleArray", EasingsRectangleArray.Main), - new ExampleInfo("FollowingEyes", FollowingEyes.Main), - new ExampleInfo("LinesBezier", LinesBezier.Main), - new ExampleInfo("LogoRaylibAnim", LogoRaylibAnim.Main), - new ExampleInfo("LogoRaylibShape", LogoRaylibShape.Main), - new ExampleInfo("RectangleScaling", RectangleScaling.Main), - - // Textures - new ExampleInfo("BackgroundScrolling", BackgroundScrolling.Main), - new ExampleInfo("BlendModes", BlendModes.Main), - new ExampleInfo("Bunnymark", Bunnymark.Main), - new ExampleInfo("DrawTiled", DrawTiled.Main), - new ExampleInfo("ImageDrawing", ImageDrawing.Main), - new ExampleInfo("ImageGeneration", ImageGeneration.Main), - new ExampleInfo("ImageLoading", ImageLoading.Main), - new ExampleInfo("ImageProcessing", ImageProcessing.Main), - new ExampleInfo("ImageText", ImageText.Main), - new ExampleInfo("LogoRaylibTexture", LogoRaylibTexture.Main), - new ExampleInfo("MousePainting", MousePainting.Main), - new ExampleInfo("NpatchDrawing", NpatchDrawing.Main), - new ExampleInfo("ParticlesBlending", ParticlesBlending.Main), - new ExampleInfo("TexturedCurve", TexturedCurve.Main), - new ExampleInfo("Polygon", Polygon.Main), - new ExampleInfo("RawData", RawData.Main), - new ExampleInfo("SpriteAnim", SpriteAnim.Main), - new ExampleInfo("SpriteButton", SpriteButton.Main), - new ExampleInfo("SpriteExplosion", SpriteExplosion.Main), - new ExampleInfo("SrcRecDstRec", SrcRecDstRec.Main), - new ExampleInfo("ToImage", ToImage.Main), - - // Text - new ExampleInfo("CodepointsLoading", CodepointsLoading.Main), - new ExampleInfo("FontFilters", FontFilters.Main), - new ExampleInfo("FontLoading", FontLoading.Main), - new ExampleInfo("FontSdf", FontSdf.Main), - new ExampleInfo("FontSpritefont", FontSpritefont.Main), - new ExampleInfo("FormatText", FormatText.Main), - new ExampleInfo("InputBox", InputBox.Main), - new ExampleInfo("RaylibFonts", RaylibFonts.Main), - new ExampleInfo("RectangleBounds", RectangleBounds.Main), - new ExampleInfo("WritingAnim", WritingAnim.Main), - - // Models - new ExampleInfo("LoadingIqm", LoadingIqm.Main), - new ExampleInfo("LoadingGltf", LoadingGltf.Main), - new ExampleInfo("BillboardDemo", BillboardDemo.Main), - new ExampleInfo("BoxCollisions", BoxCollisions.Main), - new ExampleInfo("CubicmapDemo", CubicmapDemo.Main), - new ExampleInfo("ModelCubeTexture", ModelCubeTexture.Main), - new ExampleInfo("FirstPersonMaze", FirstPersonMaze.Main), - new ExampleInfo("GeometricShapes", GeometricShapes.Main), - new ExampleInfo("HeightmapDemo", HeightmapDemo.Main), - new ExampleInfo("MeshDemo", MeshDemo.Main), - new ExampleInfo("ModelLoading", ModelLoading.Main), - new ExampleInfo("MeshGeneration", MeshGeneration.Main), - new ExampleInfo("MeshPicking", MeshPicking.Main), - new ExampleInfo("OrthographicProjection", OrthographicProjection.Main), - new ExampleInfo("SolarSystem", SolarSystem.Main), - new ExampleInfo("SkyboxDemo", SkyboxDemo.Main), - new ExampleInfo("WavingCubes", WavingCubes.Main), - new ExampleInfo("YawPitchRoll", YawPitchRoll.Main), - new ExampleInfo("DynamicMesh", DynamicMesh.Main), - - // Shaders - new ExampleInfo("BasicLighting", BasicLighting.Main), - new ExampleInfo("BasicPbr", BasicPbr.Main), - new ExampleInfo("CustomUniform", CustomUniform.Main), - new ExampleInfo("Eratosthenes", Eratosthenes.Main), - new ExampleInfo("Fog", Fog.Main), - new ExampleInfo("HotReloading", HotReloading.Main), - new ExampleInfo("HybridRender", HybridRender.Main), - new ExampleInfo("JuliaSet", JuliaSet.Main), - new ExampleInfo("ModelShader", ModelShader.Main), - new ExampleInfo("MultiSample2d", MultiSample2d.Main), - new ExampleInfo("PaletteSwitch", PaletteSwitch.Main), - new ExampleInfo("PostProcessing", PostProcessing.Main), - new ExampleInfo("Raymarching", Raymarching.Main), - new ExampleInfo("MeshInstancing", MeshInstancing.Main), - new ExampleInfo("ShapesTextures", ShapesTextures.Main), - new ExampleInfo("SimpleMask", SimpleMask.Main), - new ExampleInfo("Spotlight", Spotlight.Main), - new ExampleInfo("TextureDrawing", TextureDrawing.Main), - new ExampleInfo("TextureOutline", TextureOutline.Main), - new ExampleInfo("TextureWaves", TextureWaves.Main), - new ExampleInfo("WriteDepth", WriteDepth.Main), - - // Audio - new ExampleInfo("ModulePlaying", ModulePlaying.Main), - new ExampleInfo("MusicStreamDemo", MusicStreamDemo.Main), - new ExampleInfo("SoundLoading", SoundLoading.Main), - }; - - public static ExampleInfo GetExample(string name) - { - var example = Array.Find(ExampleList.AllExamples, x => - x.Name.Equals(name, StringComparison.OrdinalIgnoreCase) - ); - return example; - } -} - -class Program -{ - static unsafe void Main(string[] args) + private static unsafe void Main(string[] args) { Raylib.SetTraceLogCallback(&Logging.LogConsole); if (args.Length > 0) { - var example = ExampleList.GetExample(args[0]); - example?.Main?.Invoke(); + var example = GetExample(args[0]); + if (example == null) + { + Console.WriteLine($"Unknown example: {args[0]}"); + return; + } + + RunExample(example); return; } - foreach (var example in ExampleList.AllExamples) + foreach (var example in ExampleRegistry.DesktopExamples) { - RunExampleProcess(Environment.ProcessPath, example.Name); + RunExampleProcess(Environment.ProcessPath!, example.GetType().Name); } } - static void RunExampleProcess( + private static IExample? GetExample(string name) + { + return Array.Find(ExampleRegistry.DesktopExamples, x => + x.GetType().Name.Equals(name, StringComparison.OrdinalIgnoreCase) || + x.Name.Equals(name, StringComparison.OrdinalIgnoreCase) + ); + } + + /// + /// Drives an example the same way its standalone Main() does: window setup from the + /// example's properties, then Init/Update/Unload around a blocking frame loop. + /// + private static void RunExample(IExample example) + { + if (example.ConfigFlags != 0) + { + SetConfigFlags(example.ConfigFlags); + } + + InitWindow(example.Width, example.Height, example.Title); + + if (example.CursorDisabled) + { + DisableCursor(); + } + else if (example.CursorHidden) + { + HideCursor(); + } + + SetTargetFPS(example.TargetFps); + + try + { + example.Init(); + + while (!example.ShouldClose) + { + example.Update(); + } + } + finally + { + try + { + example.Unload(); + } + finally + { + CloseWindow(); + } + } + } + + private static void RunExampleProcess( string fileName, string exampleName ) 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/BasicLighting.cs b/Examples/Shaders/BasicLighting.cs index dd98681..13ef4e0 100644 --- a/Examples/Shaders/BasicLighting.cs +++ b/Examples/Shaders/BasicLighting.cs @@ -2,26 +2,21 @@ * * raylib [shaders] example - basic lighting * +* 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. +* 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). +* NOTE: Shaders used in this example are #version 330 (OpenGL 3.3) * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 3.0, last time updated with raylib 4.2 * -* Example contributed by Chris Camacho (@codifies) and reviewed by Ramon Santamaria (@raysan5) +* Example contributed by Chris Camacho (@chriscamacho) and reviewed by Ramon Santamaria (@raysan5) * -* Chris Camacho (@codifies - http://bedroomcoders.co.uk/) notes: +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software * -* This is based on the PBR lighting example, but greatly simplified to aid learning... -* actually there is very little of the PBR example left! -* When I first looked at the bewildering complexity of the PBR example I feared -* I would never understand how I could do simple lighting with raylib however its -* a testement to the authors of raylib (including rlights.h) that the example -* came together fairly quickly. -* -* Copyright (c) 2019 Chris Camacho (@codifies) and Ramon Santamaria (@raysan5) +* Copyright (c) 2019-2025 Chris Camacho (@chriscamacho) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -31,52 +26,55 @@ using Examples.Shared; namespace Examples.Shaders; -public class BasicLighting +public class BasicLighting : IExample { - const int GLSL_VERSION = 330; + private const int screenWidth = 800; + private const int screenHeight = 450; - public unsafe static int Main() +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Basic Lighting"; + + public string Title => "raylib [shaders] example - basic lighting"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + private Camera3D camera; + private Shader shader; + private Light[] lights; + + public unsafe void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - // Enable Multi Sampling Anti Aliasing 4x (if available) - SetConfigFlags(ConfigFlags.Msaa4xHint); - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - basic lighting"); - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(2.0f, 4.0f, 6.0f); - camera.Target = new Vector3(0.0f, 0.5f, 0.0f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.FovY = 45.0f; - camera.Projection = CameraProjection.Perspective; + camera = new(); + camera.Position = new Vector3(2.0f, 4.0f, 6.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 plane model from a generated mesh - Model model = LoadModelFromMesh(GenMeshPlane(10.0f, 10.0f, 3, 3)); - Model cube = LoadModelFromMesh(GenMeshCube(2.0f, 4.0f, 2.0f)); - - Shader shader = LoadShader( - "resources/shaders/glsl330/lighting.vs", - "resources/shaders/glsl330/lighting.fs" + // Load basic lighting shader + shader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/lighting.vs", + $"resources/shaders/glsl{GlslVersion}/lighting.fs" ); - - // Get some required shader loactions + // 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 - int ambientLoc = GetShaderLocation(shader, "ambient"); - float[] ambient = new[] { 0.1f, 0.1f, 0.1f, 1.0f }; + // Ambient light level (some basic lighting) + var ambientLoc = GetShaderLocation(shader, "ambient"); + var ambient = new[] { 0.1f, 0.1f, 0.1f, 1.0f }; Raylib.SetShaderValue(shader, ambientLoc, ambient, ShaderUniformDataType.Vec4); - // Assign out lighting shader to model - model.Materials[0].Shader = shader; - cube.Materials[0].Shader = shader; - - // Using 4 point lights: Color.gold, Color.red, Color.green and Color.blue - Light[] lights = new Light[4]; + // Create lights + lights = new Light[4]; lights[0] = Rlights.CreateLight( 0, LightType.Point, @@ -109,114 +107,116 @@ public class BasicLighting Color.Blue, shader ); + } - SetTargetFPS(60); + 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( + shader, + shader.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; + } + + // Update light values (actually, only enable/disable them) + for (var i = 0; i < 4; i++) + { + Rlights.UpdateLightValues(shader, lights[i]); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + BeginShaderMode(shader); + + DrawPlane(Vector3.Zero, new Vector2(10.0f, 10.0f), Color.White); + DrawCube(Vector3.Zero, 2.0f, 4.0f, 2.0f, Color.White); + + EndShaderMode(); + + // Draw spheres to show where the lights are + for (var i = 0; i < 4; 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)); + } + } + + DrawGrid(10, 1.0f); + + EndMode3D(); + + DrawFPS(10, 10); + + DrawText("Use keys [Y][R][G][B] to toggle lights", 10, 40, 20, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); // Unload shader + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); // Enable Multi Sampling Anti Aliasing 4x (if available) + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - basic lighting"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new BasicLighting(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Orbital); - - 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; - } - - // Update light values (actually, only enable/disable them) - Rlights.UpdateLightValues(shader, lights[0]); - Rlights.UpdateLightValues(shader, lights[1]); - Rlights.UpdateLightValues(shader, lights[2]); - Rlights.UpdateLightValues(shader, lights[3]); - - // Update the light shader with the camera view position - Raylib.SetShaderValue( - shader, - shader.Locs[(int)ShaderLocationIndex.VectorView], - camera.Position, - ShaderUniformDataType.Vec3 - ); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawModel(model, Vector3.Zero, 1.0f, Color.White); - DrawModel(cube, Vector3.Zero, 1.0f, Color.White); - - // Draw markers to show where the lights are - if (lights[0].Enabled) - { - DrawSphereEx(lights[0].Position, 0.2f, 8, 8, Color.Yellow); - } - else - { - DrawSphereWires(lights[0].Position, 0.2f, 8, 8, ColorAlpha(Color.Yellow, 0.3f)); - } - - if (lights[1].Enabled) - { - DrawSphereEx(lights[1].Position, 0.2f, 8, 8, Color.Red); - } - else - { - DrawSphereWires(lights[1].Position, 0.2f, 8, 8, ColorAlpha(Color.Red, 0.3f)); - } - - if (lights[2].Enabled) - { - DrawSphereEx(lights[2].Position, 0.2f, 8, 8, Color.Green); - } - else - { - DrawSphereWires(lights[2].Position, 0.2f, 8, 8, ColorAlpha(Color.Green, 0.3f)); - } - - if (lights[3].Enabled) - { - DrawSphereEx(lights[3].Position, 0.2f, 8, 8, Color.Blue); - } - else - { - DrawSphereWires(lights[3].Position, 0.2f, 8, 8, ColorAlpha(Color.Blue, 0.3f)); - } - - DrawGrid(10, 1.0f); - - EndMode3D(); - - DrawFPS(10, 10); - DrawText("Use keys [Y][R][G][B] to toggle lights", 10, 40, 20, Color.DarkGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadModel(model); - UnloadModel(cube); - UnloadShader(shader); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Shaders/BasicPbr.cs b/Examples/Shaders/BasicPbr.cs index f551bc3..e29ce26 100644 --- a/Examples/Shaders/BasicPbr.cs +++ b/Examples/Shaders/BasicPbr.cs @@ -1,21 +1,23 @@ /******************************************************************************************* - * - * raylib [shaders] example - Basic PBR - * - * Example originally created with raylib 5.0, last time updated with raylib 5.1-dev - * - * Example contributed by Afan OLOVCIC (@_DevDad) 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-2024 Afan OLOVCIC (@_DevDad) - * - * Model: "Old Rusty Car" (https://skfb.ly/LxRy) by Renafox, - * licensed under Creative Commons Attribution-NonCommercial - * (http://creativecommons.org/licenses/by-nc/4.0/) - * - ********************************************************************************************/ +* +* raylib [shaders] example - basic pbr +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 5.0, last time updated with raylib 5.5 +* +* Example contributed by Afan OLOVCIC (@_DevDad) 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 Afan OLOVCIC (@_DevDad) +* +* Model: "Old Rusty Car" (https://skfb.ly/LxRy) by Renafox, +* licensed under Creative Commons Attribution-NonCommercial +* (http://creativecommons.org/licenses/by-nc/4.0/) +* +********************************************************************************************/ using System.Numerics; using Examples.Shared; @@ -23,31 +25,50 @@ using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class BasicPbr +public class BasicPbr : IExample { - private const int GLSL_VERSION = 330; +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif - public static unsafe int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shaders / Basic PBR"; + + public string Title => "raylib [shaders] example - basic pbr"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + private Camera3D camera; + private Shader shader; + private Model car; + private Model floor; + private PbrLight[] lights; + + private int metallicValueLoc; + private int roughnessValueLoc; + private int emissiveIntensityLoc; + private int emissiveColorLoc; + private int textureTilingLoc; + + private Vector2 carTextureTiling; + private Vector2 floorTextureTiling; + + public unsafe void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - // Enable Multi Sampling Anti Aliasing 4x (if available) - SetConfigFlags(ConfigFlags.Msaa4xHint); - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - basic pbr"); - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(2.0f, 4.0f, 6.0f); + camera = new(); + camera.Position = new Vector3(2.0f, 2.0f, 6.0f); camera.Target = new Vector3(0.0f, 0.5f, 0.0f); camera.Up = new Vector3(0.0f, 1.0f, 0.0f); camera.FovY = 45.0f; camera.Projection = CameraProjection.Perspective; // Load PBR shader and setup all required locations - var shader = LoadShader("resources/shaders/glsl330/pbr.vs", "resources/shaders/glsl330/pbr.fs"); + shader = LoadShader($"resources/shaders/glsl{GlslVersion}/pbr.vs", $"resources/shaders/glsl{GlslVersion}/pbr.fs"); shader.Locs[(int)ShaderLocationIndex.MapAlbedo] = GetShaderLocation(shader, "albedoMap"); // WARNING: Metalness, roughness, and ambient occlusion are all packed into a MRA texture @@ -75,23 +96,25 @@ public class BasicPbr SetShaderValue(shader, GetShaderLocation(shader, "ambient"), &ambientIntensity, ShaderUniformDataType.Float); // Get location for shader parameters that can be modified in real time - var emissiveIntensityLoc = GetShaderLocation(shader, "emissivePower"); - var emissiveColorLoc = GetShaderLocation(shader, "emissiveColor"); - var textureTilingLoc = GetShaderLocation(shader, "tiling"); + metallicValueLoc = GetShaderLocation(shader, "metallicValue"); + roughnessValueLoc = GetShaderLocation(shader, "roughnessValue"); + emissiveIntensityLoc = GetShaderLocation(shader, "emissivePower"); + emissiveColorLoc = GetShaderLocation(shader, "emissiveColor"); + textureTilingLoc = GetShaderLocation(shader, "tiling"); // Load old car model using PBR maps and shader // WARNING: We know this model consists of a single model.meshes[0] and // that model.materials[0] is by default assigned to that mesh // There could be more complex models consisting of multiple meshes and // multiple materials defined for those meshes... but always 1 mesh = 1 material - var car = LoadModel("resources/models/gltf/old_car_new.glb"); + car = LoadModel("resources/models/gltf/old_car_new.glb"); // Assign already setup PBR shader to model.materials[0], used by models.meshes[0] car.Materials[0].Shader = shader; // Setup materials[0].maps default parameters car.Materials[0].Maps[(int)MaterialMapIndex.Albedo].Color = Color.White; - car.Materials[0].Maps[(int)MaterialMapIndex.Metalness].Value = 0.0f; + car.Materials[0].Maps[(int)MaterialMapIndex.Metalness].Value = 1.0f; car.Materials[0].Maps[(int)MaterialMapIndex.Roughness].Value = 0.0f; car.Materials[0].Maps[(int)MaterialMapIndex.Occlusion].Value = 1.0f; car.Materials[0].Maps[(int)MaterialMapIndex.Emission].Color = new Color(255, 162, 0, 255); @@ -104,7 +127,7 @@ public class BasicPbr // Load floor model mesh and assign material parameters // NOTE: A basic plane shape can be generated instead of being loaded from a model file - var floor = LoadModel("resources/models/gltf/plane.glb"); + floor = LoadModel("resources/models/gltf/plane.glb"); //Mesh floorMesh = GenMeshPlane(10, 10, 10, 10); //GenMeshTangents(&floorMesh); // TODO: Review tangents generation //Model floor = LoadModelFromMesh(floorMesh); @@ -113,8 +136,8 @@ public class BasicPbr floor.Materials[0].Shader = shader; floor.Materials[0].Maps[(int)MaterialMapIndex.Albedo].Color = Color.White; - floor.Materials[0].Maps[(int)MaterialMapIndex.Metalness].Value = 0.0f; - floor.Materials[0].Maps[(int)MaterialMapIndex.Roughness].Value = 0.0f; + floor.Materials[0].Maps[(int)MaterialMapIndex.Metalness].Value = 0.8f; + floor.Materials[0].Maps[(int)MaterialMapIndex.Roughness].Value = 0.1f; floor.Materials[0].Maps[(int)MaterialMapIndex.Occlusion].Value = 1.0f; floor.Materials[0].Maps[(int)MaterialMapIndex.Emission].Color = Color.Black; @@ -124,11 +147,11 @@ public class BasicPbr // Models texture tiling parameter can be stored in the Material struct if required (CURRENTLY NOT USED) // NOTE: Material.params[4] are available for generic parameters storage (float) - var carTextureTiling = new Vector2(0.5f, 0.5f); - var floorTextureTiling = new Vector2(0.5f, 0.5f); + carTextureTiling = new Vector2(0.5f, 0.5f); + floorTextureTiling = new Vector2(0.5f, 0.5f); // Create some lights - var lights = new PbrLight[4]; + lights = new PbrLight[4]; lights[0] = PbrLights.CreateLight( 0, PbrLightType.Point, @@ -167,104 +190,113 @@ public class BasicPbr SetShaderValue(shader, GetShaderLocation(shader, "useTexNormal"), &usage, ShaderUniformDataType.Int); SetShaderValue(shader, GetShaderLocation(shader, "useTexMRA"), &usage, ShaderUniformDataType.Int); SetShaderValue(shader, GetShaderLocation(shader, "useTexEmissive"), &usage, ShaderUniformDataType.Int); + } - SetTargetFPS(60); // Set our game to run at 60 frames-per-second - //--------------------------------------------------------------------------------------- + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); - // Main game loop - while (!WindowShouldClose()) // Detect window close button or ESC key + // Update the shader with the camera view vector (points towards { 0.0f, 0.0f, 0.0f }) + var cameraPos = camera.Position; + SetShaderValue(shader, shader.Locs[(int)ShaderLocationIndex.VectorView], cameraPos, ShaderUniformDataType.Vec3); + + // Check key inputs to enable/disable lights + if (IsKeyPressed(KeyboardKey.One)) { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(&camera, CameraMode.Orbital); - - // Update the shader with the camera view vector (points towards { 0.0f, 0.0f, 0.0f }) - var cameraPos = camera.Position; - SetShaderValue(shader, shader.Locs[(int)ShaderLocationIndex.VectorView], cameraPos, ShaderUniformDataType.Vec3); - - // Check key inputs to enable/disable lights - if (IsKeyPressed(KeyboardKey.One)) - { - lights[2].Enabled = !lights[2].Enabled; - } - - if (IsKeyPressed(KeyboardKey.Two)) - { - lights[1].Enabled = !lights[1].Enabled; - } - - if (IsKeyPressed(KeyboardKey.Three)) - { - lights[3].Enabled = !lights[3].Enabled; - } - - if (IsKeyPressed(KeyboardKey.Four)) - { - lights[0].Enabled = !lights[0].Enabled; - } - - // Update light values on shader (actually, only enable/disable them) - for (var i = 0; i < 4; i++) - { - UpdateLight(shader, lights[i]); - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - - ClearBackground(Color.Black); - - BeginMode3D(camera); - - // Set floor model texture tiling and emissive color parameters on shader - SetShaderValue(shader, textureTilingLoc, &floorTextureTiling, ShaderUniformDataType.Vec2); - var floorEmissiveColor = ColorNormalize(floor.Materials[0].Maps[(int)MaterialMapIndex.Emission].Color); - SetShaderValue(shader, emissiveColorLoc, &floorEmissiveColor, ShaderUniformDataType.Vec4); - - DrawModel(floor, Vector3.Zero, 5.0f, Color.White); // Draw floor model - - // Set old car model texture tiling, emissive color and emissive intensity parameters on shader - SetShaderValue(shader, textureTilingLoc, &carTextureTiling, ShaderUniformDataType.Vec2); - var carEmissiveColor = ColorNormalize(car.Materials[0].Maps[(int)MaterialMapIndex.Emission].Color); - SetShaderValue(shader, emissiveColorLoc, &carEmissiveColor, ShaderUniformDataType.Vec4); - var emissiveIntensity = 0.01f; - SetShaderValue(shader, emissiveIntensityLoc, &emissiveIntensity, ShaderUniformDataType.Float); - - DrawModel(car, Vector3.Zero, 0.25f, Color.White); // Draw car model - - // Draw spheres to show the lights positions - for (var i = 0; i < 4; i++) - { - var color = lights[i].Color; - var lightColor = new Color((byte)(color.X * 255), (byte)(color.Y * 255), (byte)(color.Z * 255), - (byte)(color.W * 255)); - - if (lights[i].Enabled) - { - DrawSphereEx(lights[i].Position, 0.2f, 8, 8, lightColor); - } - else - { - DrawSphereWires(lights[i].Position, 0.2f, 8, 8, ColorAlpha(lightColor, 0.3f)); - } - } - - EndMode3D(); - - DrawText("Toggle lights: [1][2][3][4]", 10, 40, 20, Color.LightGray); - - DrawText("(c) Old Rusty Car model by Renafox (https://skfb.ly/LxRy)", screenWidth - 320, screenHeight - 20, 10, Color.LightGray); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + lights[2].Enabled = !lights[2].Enabled; } - // De-Initialization - //-------------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.Two)) + { + lights[1].Enabled = !lights[1].Enabled; + } + + if (IsKeyPressed(KeyboardKey.Three)) + { + lights[3].Enabled = !lights[3].Enabled; + } + + if (IsKeyPressed(KeyboardKey.Four)) + { + lights[0].Enabled = !lights[0].Enabled; + } + + // Update light values on shader (actually, only enable/disable them) + for (var i = 0; i < 4; i++) + { + UpdateLight(shader, lights[i]); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.Black); + + BeginMode3D(camera); + + // Set floor model texture tiling and emissive color parameters on shader + SetShaderValue(shader, textureTilingLoc, floorTextureTiling, ShaderUniformDataType.Vec2); + var floorEmissiveColor = ColorNormalize(floor.Materials[0].Maps[(int)MaterialMapIndex.Emission].Color); + SetShaderValue(shader, emissiveColorLoc, &floorEmissiveColor, ShaderUniformDataType.Vec4); + + // Set floor metallic and roughness values + var floorMetallicValue = floor.Materials[0].Maps[(int)MaterialMapIndex.Metalness].Value; + SetShaderValue(shader, metallicValueLoc, &floorMetallicValue, ShaderUniformDataType.Float); + var floorRoughnessValue = floor.Materials[0].Maps[(int)MaterialMapIndex.Roughness].Value; + SetShaderValue(shader, roughnessValueLoc, &floorRoughnessValue, ShaderUniformDataType.Float); + + DrawModel(floor, Vector3.Zero, 5.0f, Color.White); // Draw floor model + + // Set old car model texture tiling, emissive color and emissive intensity parameters on shader + SetShaderValue(shader, textureTilingLoc, carTextureTiling, ShaderUniformDataType.Vec2); + var carEmissiveColor = ColorNormalize(car.Materials[0].Maps[(int)MaterialMapIndex.Emission].Color); + SetShaderValue(shader, emissiveColorLoc, &carEmissiveColor, ShaderUniformDataType.Vec4); + var emissiveIntensity = 0.01f; + SetShaderValue(shader, emissiveIntensityLoc, &emissiveIntensity, ShaderUniformDataType.Float); + + // Set old car metallic and roughness values + var carMetallicValue = car.Materials[0].Maps[(int)MaterialMapIndex.Metalness].Value; + SetShaderValue(shader, metallicValueLoc, &carMetallicValue, ShaderUniformDataType.Float); + var carRoughnessValue = car.Materials[0].Maps[(int)MaterialMapIndex.Roughness].Value; + SetShaderValue(shader, roughnessValueLoc, &carRoughnessValue, ShaderUniformDataType.Float); + + DrawModel(car, Vector3.Zero, 0.25f, Color.White); // Draw car model + + // Draw spheres to show the lights positions + for (var i = 0; i < 4; i++) + { + var color = lights[i].Color; + var lightColor = new Color((byte)(color.X * 255), (byte)(color.Y * 255), (byte)(color.Z * 255), + (byte)(color.W * 255)); + + if (lights[i].Enabled) + { + DrawSphereEx(lights[i].Position, 0.2f, 8, 8, lightColor); + } + else + { + DrawSphereWires(lights[i].Position, 0.2f, 8, 8, ColorAlpha(lightColor, 0.3f)); + } + } + + EndMode3D(); + + DrawText("Toggle lights: [1][2][3][4]", 10, 40, 20, Color.LightGray); + + DrawText("(c) Old Rusty Car model by Renafox (https://skfb.ly/LxRy)", screenWidth - 320, screenHeight - 20, 10, Color.LightGray); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public unsafe void Unload() + { // Unbind (disconnect) shader from car.material[0] // to avoid UnloadMaterial() trying to unload it automatically car.Materials[0].Shader = new(); @@ -278,7 +310,32 @@ public class BasicPbr UnloadModel(floor); UnloadShader(shader); + } + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + // Enable Multi Sampling Anti Aliasing 4x (if available) + SetConfigFlags(ConfigFlags.Msaa4xHint); + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - basic pbr"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //--------------------------------------------------------------------------------------- + + var game = new BasicPbr(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- CloseWindow(); //-------------------------------------------------------------------------------------- @@ -287,8 +344,8 @@ public class BasicPbr private static void UpdateLight(Shader shader, PbrLight light) { - SetShaderValue(shader, light.EnabledLoc, light.Enabled, ShaderUniformDataType.Int); - SetShaderValue(shader, light.TypeLoc, light.Type, ShaderUniformDataType.Int); + SetShaderValue(shader, light.EnabledLoc, light.Enabled ? 1 : 0, ShaderUniformDataType.Int); + SetShaderValue(shader, light.TypeLoc, (int)light.Type, ShaderUniformDataType.Int); // Send to shader light position values SetShaderValue(shader, light.PositionLoc, light.Position, ShaderUniformDataType.Vec3); 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/CustomUniform.cs b/Examples/Shaders/CustomUniform.cs index 3149df1..44b3159 100644 --- a/Examples/Shaders/CustomUniform.cs +++ b/Examples/Shaders/CustomUniform.cs @@ -1,18 +1,22 @@ /******************************************************************************************* * -* raylib [shaders] example - Apply a postprocessing shader and connect a custom uniform variable +* raylib [shaders] example - custom uniform +* +* 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. +* 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), to test this example * on OpenGL ES 2.0 platforms (Android, Raspberry Pi, HTML5), use #version 100 shaders * raylib comes with shaders ready for both versions, check raylib/shaders install folder * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.3, last time updated with raylib 4.0 * -* Copyright (c) 2015 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) 2015-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -21,123 +25,156 @@ using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class CustomUniform +public class CustomUniform : IExample { - public static int Main() +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shaders / Custom Uniform"; + + public string Title => "raylib [shaders] example - custom uniform"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + private Camera3D camera; + private Model model; + private Texture2D texture; + private Vector3 position; + private Shader shader; + private int swirlCenterLoc; + private float[] swirlCenter; + private RenderTexture2D target; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - // Enable Multi Sampling Anti Aliasing 4x (if available) - SetConfigFlags(ConfigFlags.Msaa4xHint); - - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - custom uniform variable"); - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(8.0f, 8.0f, 8.0f); - camera.Target = new Vector3(0.0f, 1.5f, 0.0f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.FovY = 45.0f; - camera.Projection = CameraProjection.Perspective; + camera = new(); + camera.Position = new Vector3(8.0f, 8.0f, 8.0f); // Camera position + camera.Target = new Vector3(0.0f, 1.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 - Model model = LoadModel("resources/models/obj/barracks.obj"); - Texture2D texture = LoadTexture("resources/models/obj/barracks_diffuse.png"); + model = LoadModel("resources/models/obj/barracks.obj"); // Load OBJ model + texture = LoadTexture("resources/models/obj/barracks_diffuse.png"); // Load model texture (diffuse map) // Set model diffuse texture Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.Albedo, ref texture); - Vector3 position = new(0.0f, 0.0f, 0.0f); + position = new(0.0f, 0.0f, 0.0f); // Set model position - // Load postpro shader - Shader shader = LoadShader("resources/shaders/glsl330/base.vs", - "resources/shaders/glsl330/swirl.fs"); + // Load postprocessing shader + // NOTE: Defining 0 (NULL) for vertex shader forces usage of internal default vertex shader + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/swirl.fs"); // Get variable (uniform) location on the shader to connect with the program // NOTE: If uniform variable could not be found in the shader, function returns -1 - int swirlCenterLoc = GetShaderLocation(shader, "center"); + swirlCenterLoc = GetShaderLocation(shader, "center"); - float[] swirlCenter = new float[2] { (float)screenWidth / 2, (float)screenHeight / 2 }; + swirlCenter = new float[2] { (float)screenWidth / 2, (float)screenHeight / 2 }; // Create a RenderTexture2D to be used for render to texture - RenderTexture2D target = LoadRenderTexture(screenWidth, screenHeight); + target = LoadRenderTexture(screenWidth, screenHeight); + } - SetTargetFPS(60); + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + + var mousePosition = GetMousePosition(); + + swirlCenter[0] = mousePosition.X; + swirlCenter[1] = screenHeight - mousePosition.Y; + + // Send new value to the shader to be used on drawing + Raylib.SetShaderValue(shader, swirlCenterLoc, swirlCenter, ShaderUniformDataType.Vec2); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginTextureMode(target); // Enable drawing to texture + ClearBackground(Color.RayWhite); // Clear texture background + + BeginMode3D(camera); // Begin 3d mode drawing + DrawModel(model, position, 0.5f, Color.White); // Draw 3d model with texture + DrawGrid(10, 1.0f); // Draw a grid + EndMode3D(); // End 3d mode drawing, returns to orthographic 2d mode + + DrawText("TEXT DRAWN IN RENDER TEXTURE", 200, 10, 30, Color.Red); + EndTextureMode(); // End drawing to texture (now we have a texture available for next passes) + + BeginDrawing(); + ClearBackground(Color.RayWhite); // Clear screen background + + // Enable shader using the custom uniform + BeginShaderMode(shader); + + // NOTE: Render texture must be y-flipped due to default OpenGL coordinates (left-bottom) + DrawTextureRec( + target.Texture, + new Rectangle(0, 0, target.Texture.Width, -target.Texture.Height), + new Vector2(0, 0), + Color.White + ); + + EndShaderMode(); + + // Draw some 2d text over drawn texture + DrawText( + "(c) Barracks 3D model by Alberto Cano", + screenWidth - 220, + screenHeight - 20, + 10, + Color.Gray + ); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); // Unload shader + UnloadTexture(texture); // Unload texture + UnloadModel(model); // Unload model + UnloadRenderTexture(target); // Unload render texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); // Enable Multi Sampling Anti Aliasing 4x (if available) + + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - custom uniform"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new CustomUniform(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - Vector2 mousePosition = GetMousePosition(); - - swirlCenter[0] = mousePosition.X; - swirlCenter[1] = screenHeight - mousePosition.Y; - - // Send new value to the shader to be used on drawing - Raylib.SetShaderValue(shader, swirlCenterLoc, swirlCenter, ShaderUniformDataType.Vec2); - - UpdateCamera(ref camera, CameraMode.Orbital); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // Enable drawing to texture - BeginTextureMode(target); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawModel(model, position, 0.5f, Color.White); - DrawGrid(10, 1.0f); - - EndMode3D(); - - DrawText("TEXT DRAWN IN RENDER TEXTURE", 200, 10, 30, Color.Red); - - // End drawing to texture (now we have a texture available for next passes) - EndTextureMode(); - - BeginShaderMode(shader); - - // NOTE: Render texture must be y-flipped due to default OpenGL coordinates (left-bottom) - DrawTextureRec( - target.Texture, - new Rectangle(0, 0, target.Texture.Width, -target.Texture.Height), - new Vector2(0, 0), - Color.White - ); - - EndShaderMode(); - - DrawText( - "(c) Barracks 3D model by Alberto Cano", - screenWidth - 220, - screenHeight - 20, - 10, - Color.Gray - ); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadShader(shader); - UnloadTexture(texture); - UnloadModel(model); - UnloadRenderTexture(target); - - CloseWindow(); + 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/Eratosthenes.cs b/Examples/Shaders/Eratosthenes.cs index 73d1cf4..d97ed6d 100644 --- a/Examples/Shaders/Eratosthenes.cs +++ b/Examples/Shaders/Eratosthenes.cs @@ -1,25 +1,29 @@ /******************************************************************************************* * -* raylib [shaders] example - Sieve of Eratosthenes +* raylib [shaders] example - eratosthenes sieve * -* Sieve of Eratosthenes, the earliest known (ancient Greek) prime number sieve. +* Example complexity rating: [★★★☆] 3/4 * -* "Sift the twos and sift the threes, -* The Sieve of Eratosthenes. -* When the multiples sublime, -* the numbers that are left are prime." +* NOTE: Sieve of Eratosthenes, the earliest known (ancient Greek) prime number sieve +* +* "Sift the twos and sift the threes, +* The Sieve of Eratosthenes. +* When the multiples sublime, +* the numbers that are left are prime." * * 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. +* 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). +* NOTE: Shaders used in this example are #version 330 (OpenGL 3.3) * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 2.5, last time updated with raylib 4.0 * -* Example contributed by ProfJski and reviewed by Ramon Santamaria (@raysan5) +* Example contributed by ProfJski (@ProfJski) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2019 ProfJski and 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 ProfJski (@ProfJski) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -28,73 +32,97 @@ using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class Eratosthenes +public class Eratosthenes : IExample { - const int GlslVersion = 330; +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shaders / Eratosthenes"; + + public string Title => "raylib [shaders] example - eratosthenes sieve"; + + private RenderTexture2D target; + private Shader shader; + + public void Init() + { + target = LoadRenderTexture(screenWidth, screenHeight); + + // Load Eratosthenes shader + // NOTE: Defining 0 (NULL) for vertex shader forces usage of internal default vertex shader + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/eratosthenes.fs"); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // Nothing to do here, everything is happening in the shader + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + 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(); // End drawing to texture (now we have a blank texture available for the shader) + + BeginDrawing(); + ClearBackground(Color.RayWhite); // Clear screen background + + BeginShaderMode(shader); + // NOTE: Render texture must be y-flipped due to default OpenGL coordinates (left-bottom) + DrawTextureRec( + target.Texture, + new Rectangle(0, 0, target.Texture.Width, -target.Texture.Height), + new Vector2(0.0f, 0.0f), + Color.White + ); + EndShaderMode(); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); + UnloadRenderTexture(target); + } public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - Sieve of Eratosthenes"); - - RenderTexture2D target = LoadRenderTexture(screenWidth, screenHeight); - - // Load Eratosthenes shader - // NOTE: Defining 0 (NULL) for vertex shader forces usage of internal default vertex shader - Shader shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/eratosthenes.fs"); + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - eratosthenes sieve"); SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new Eratosthenes(); + game.Init(); + // Main game loop while (!WindowShouldClose()) { - // Update - //---------------------------------------------------------------------------------- - // Nothing to do here, everything is happening in the shader - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // Enable drawing to texture - BeginTextureMode(target); - ClearBackground(Color.Black); - - // 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); - - // End drawing to texture (now we have a blank texture available for the shader) - EndTextureMode(); - - BeginShaderMode(shader); - // NOTE: Render texture must be y-flipped due to default OpenGL coordinates (left-bottom) - DrawTextureRec( - target.Texture, - new Rectangle(0, 0, target.Texture.Width, -target.Texture.Height), - new Vector2(0.0f, 0.0f), - Color.White - ); - EndShaderMode(); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadShader(shader); - UnloadRenderTexture(target); - CloseWindow(); //-------------------------------------------------------------------------------------- diff --git a/Examples/Shaders/Fog.cs b/Examples/Shaders/Fog.cs index c94ec58..b10f3f5 100644 --- a/Examples/Shaders/Fog.cs +++ b/Examples/Shaders/Fog.cs @@ -1,27 +1,22 @@ /******************************************************************************************* * -* raylib [shaders] example - fog +* raylib [shaders] example - fog 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. +* 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). +* NOTE: Shaders used in this example are #version 330 (OpenGL 3.3) * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 2.5, last time updated with raylib 3.7 * -* Example contributed by Chris Camacho (@codifies) and reviewed by Ramon Santamaria (@raysan5) +* Example contributed by Chris Camacho (@chriscamacho) and reviewed by Ramon Santamaria (@raysan5) * -* Chris Camacho (@codifies - http://bedroomcoders.co.uk/) notes: +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software * -* This is based on the PBR lighting example, but greatly simplified to aid learning... -* actually there is very little of the PBR example left! -* When I first looked at the bewildering complexity of the PBR example I feared -* I would never understand how I could do simple lighting with raylib however its -* a testement to the authors of raylib (including rlights.h) that the example -* came together fairly quickly. -* -* Copyright (c) 2019 Chris Camacho (@codifies) and Ramon Santamaria (@raysan5) +* Copyright (c) 2019-2025 Chris Camacho (@chriscamacho) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -32,21 +27,36 @@ using Examples.Shared; namespace Examples.Shaders; -public class Fog +public class Fog : IExample { - public unsafe static int Main() +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shaders / Fog"; + + public string Title => "raylib [shaders] example - fog rendering"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + private Camera3D camera; + private Model modelA; + private Model modelB; + private Model modelC; + private Texture2D texture; + private Shader shader; + private int fogDensityLoc; + private float fogDensity; + + public unsafe void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - // Enable Multi Sampling Anti Aliasing 4x (if available) - SetConfigFlags(ConfigFlags.Msaa4xHint); - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - fog"); - // Define the camera to look into our 3d world - Camera3D camera = new(); + camera = new(); camera.Position = new Vector3(2.0f, 2.0f, 6.0f); camera.Target = new Vector3(0.0f, 0.5f, 0.0f); camera.Up = new Vector3(0.0f, 1.0f, 0.0f); @@ -54,10 +64,10 @@ public class Fog camera.Projection = CameraProjection.Perspective; // Load models and texture - Model modelA = LoadModelFromMesh(GenMeshTorus(0.4f, 1.0f, 16, 32)); - Model modelB = LoadModelFromMesh(GenMeshCube(1.0f, 1.0f, 1.0f)); - Model modelC = LoadModelFromMesh(GenMeshSphere(0.5f, 32, 32)); - Texture2D texture = LoadTexture("resources/texel_checker.png"); + modelA = LoadModelFromMesh(GenMeshTorus(0.4f, 1.0f, 16, 32)); + modelB = LoadModelFromMesh(GenMeshCube(1.0f, 1.0f, 1.0f)); + modelC = LoadModelFromMesh(GenMeshSphere(0.5f, 32, 32)); + texture = LoadTexture("resources/texel_checker.png"); // Assign texture to default model material Raylib.SetMaterialTexture(ref modelA, 0, MaterialMapIndex.Albedo, ref texture); @@ -65,12 +75,15 @@ public class Fog Raylib.SetMaterialTexture(ref modelC, 0, MaterialMapIndex.Albedo, ref texture); // Load shader and set up some uniforms - Shader shader = LoadShader("resources/shaders/glsl330/lighting.vs", "resources/shaders/glsl330/fog.fs"); + shader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/lighting.vs", + $"resources/shaders/glsl{GlslVersion}/fog.fs" + ); shader.Locs[(int)ShaderLocationIndex.MatrixModel] = GetShaderLocation(shader, "matModel"); shader.Locs[(int)ShaderLocationIndex.VectorView] = GetShaderLocation(shader, "viewPos"); // Ambient light level - int ambientLoc = GetShaderLocation(shader, "ambient"); + var ambientLoc = GetShaderLocation(shader, "ambient"); Raylib.SetShaderValue( shader, ambientLoc, @@ -78,8 +91,12 @@ public class Fog ShaderUniformDataType.Vec4 ); - float fogDensity = 0.15f; - int fogDensityLoc = GetShaderLocation(shader, "fogDensity"); + var fogColor = ColorNormalize(Color.Gray); + var fogColorLoc = GetShaderLocation(shader, "fogColor"); + Raylib.SetShaderValue(shader, fogColorLoc, fogColor, ShaderUniformDataType.Vec4); + + fogDensity = 0.15f; + fogDensityLoc = GetShaderLocation(shader, "fogDensity"); Raylib.SetShaderValue(shader, fogDensityLoc, fogDensity, ShaderUniformDataType.Float); // NOTE: All models share the same shader @@ -89,90 +106,112 @@ public class Fog // Using just 1 point lights Rlights.CreateLight(0, LightType.Point, new Vector3(0, 2, 6), Vector3.Zero, Color.White, shader); + } - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); - // Main game loop - while (!WindowShouldClose()) + if (IsKeyDown(KeyboardKey.Up)) { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Orbital); - - if (IsKeyDown(KeyboardKey.Up)) + fogDensity += 0.001f; + if (fogDensity > 1.0f) { - fogDensity += 0.001f; - if (fogDensity > 1.0f) - { - fogDensity = 1.0f; - } + fogDensity = 1.0f; } - - if (IsKeyDown(KeyboardKey.Down)) - { - fogDensity -= 0.001f; - if (fogDensity < 0.0f) - { - fogDensity = 0.0f; - } - } - - Raylib.SetShaderValue(shader, fogDensityLoc, fogDensity, ShaderUniformDataType.Float); - - // Rotate the torus - modelA.Transform = MatrixMultiply(modelA.Transform, MatrixRotateX(-0.025f)); - modelA.Transform = MatrixMultiply(modelA.Transform, MatrixRotateZ(0.012f)); - - // Update the light shader with the camera view position - Raylib.SetShaderValue( - shader, - shader.Locs[(int)ShaderLocationIndex.VectorView], - camera.Position, - ShaderUniformDataType.Vec3 - ); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.Gray); - - BeginMode3D(camera); - - // Draw the three models - DrawModel(modelA, Vector3.Zero, 1.0f, Color.White); - DrawModel(modelB, new Vector3(-2.6f, 0, 0), 1.0f, Color.White); - DrawModel(modelC, new Vector3(2.6f, 0, 0), 1.0f, Color.White); - - for (int i = -20; i < 20; i += 2) - { - DrawModel(modelA, new Vector3(i, 0, 2), 1.0f, Color.White); - } - - EndMode3D(); - - DrawText( - $"Use up/down to change fog density [{fogDensity:F2}]", - 10, - 10, - 20, - Color.RayWhite - ); - - EndDrawing(); - //---------------------------------------------------------------------------------- } - // De-Initialization - //-------------------------------------------------------------------------------------- + if (IsKeyDown(KeyboardKey.Down)) + { + fogDensity -= 0.001f; + if (fogDensity < 0.0f) + { + fogDensity = 0.0f; + } + } + + Raylib.SetShaderValue(shader, fogDensityLoc, fogDensity, ShaderUniformDataType.Float); + + // Rotate the torus + modelA.Transform = MatrixMultiply(modelA.Transform, MatrixRotateX(-0.025f)); + modelA.Transform = MatrixMultiply(modelA.Transform, MatrixRotateZ(0.012f)); + + // Update the light shader with the camera view position + Raylib.SetShaderValue( + shader, + shader.Locs[(int)ShaderLocationIndex.VectorView], + camera.Position, + ShaderUniformDataType.Vec3 + ); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.Gray); + + BeginMode3D(camera); + + // Draw the three models + DrawModel(modelA, Vector3.Zero, 1.0f, Color.White); + DrawModel(modelB, new Vector3(-2.6f, 0, 0), 1.0f, Color.White); + DrawModel(modelC, new Vector3(2.6f, 0, 0), 1.0f, Color.White); + + for (var i = -20; i < 20; i += 2) + { + DrawModel(modelA, new Vector3(i, 0, 2), 1.0f, Color.White); + } + + EndMode3D(); + + DrawText( + $"Use KEY_UP/KEY_DOWN to change fog density [{fogDensity:F2}]", + 10, + 10, + 20, + Color.RayWhite + ); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { UnloadModel(modelA); UnloadModel(modelB); UnloadModel(modelC); UnloadTexture(texture); UnloadShader(shader); + } + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + // Enable Multi Sampling Anti Aliasing 4x (if available) + SetConfigFlags(ConfigFlags.Msaa4xHint); + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - fog rendering"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new Fog(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- CloseWindow(); //-------------------------------------------------------------------------------------- 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/HotReloading.cs b/Examples/Shaders/HotReloading.cs index f3d5d29..6390e68 100644 --- a/Examples/Shaders/HotReloading.cs +++ b/Examples/Shaders/HotReloading.cs @@ -1,135 +1,189 @@ /******************************************************************************************* * -* raylib [shaders] example - Hot reloading +* raylib [shaders] example - hot reloading +* +* Example complexity rating: [★★★☆] 3/4 * * NOTE: This example requires raylib OpenGL 3.3 for shaders support and only #version 330 -* is currently supported. OpenGL ES 2.0 platforms are not supported at the moment. +* is currently supported. OpenGL ES 2.0 platforms are not supported at the moment * -* This example has been created using raylib 3.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 3.0, last time updated with raylib 3.5 * -* Copyright (c) 2020 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) 2020-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ +using System; using System.Numerics; using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class HotReloading +public class HotReloading : IExample { +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shaders / Hot Reloading"; + + public string Title => "raylib [shaders] example - hot reloading"; + + private string fragShaderFileName; + private Shader shader; + private int resolutionLoc; + private int mouseLoc; + private int timeLoc; + private float[] resolution; + private float totalTime; +#if !BROWSER + private long fragShaderFileModTime; + private bool shaderAutoReloading; +#endif + + public void Init() + { + fragShaderFileName = $"resources/shaders/glsl{GlslVersion}/reload.fs"; +#if !BROWSER + fragShaderFileModTime = GetFileModTime(fragShaderFileName); +#endif + + // Load raymarching shader + // NOTE: Defining 0 (NULL) for vertex shader forces usage of internal default vertex shader + shader = LoadShader(null, fragShaderFileName); + + // Get shader locations for required uniforms + resolutionLoc = GetShaderLocation(shader, "resolution"); + mouseLoc = GetShaderLocation(shader, "mouse"); + timeLoc = GetShaderLocation(shader, "time"); + + resolution = new[] { (float)screenWidth, (float)screenHeight }; + Raylib.SetShaderValue(shader, resolutionLoc, resolution, ShaderUniformDataType.Vec2); + + totalTime = 0.0f; +#if !BROWSER + shaderAutoReloading = false; +#endif + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + totalTime += GetFrameTime(); + var mouse = GetMousePosition(); + var mousePos = new[] { mouse.X, mouse.Y }; + + // Set shader required uniform values + Raylib.SetShaderValue(shader, timeLoc, totalTime, ShaderUniformDataType.Float); + Raylib.SetShaderValue(shader, mouseLoc, mousePos, ShaderUniformDataType.Vec2); + +#if !BROWSER + // Hot shader reloading + if (shaderAutoReloading || (IsMouseButtonPressed(MouseButton.Left))) + { + var currentFragShaderModTime = GetFileModTime(fragShaderFileName); + + // Check if shader file has been modified + if (currentFragShaderModTime != fragShaderFileModTime) + { + // Try reloading updated shader + var updatedShader = LoadShader(null, fragShaderFileName); + + // It was correctly loaded + if (updatedShader.Id != Rlgl.GetShaderIdDefault()) + { + UnloadShader(shader); + shader = updatedShader; + + // Get shader locations for required uniforms + resolutionLoc = GetShaderLocation(shader, "resolution"); + mouseLoc = GetShaderLocation(shader, "mouse"); + timeLoc = GetShaderLocation(shader, "time"); + + // Reset required uniforms + Raylib.SetShaderValue( + shader, + resolutionLoc, + resolution, + ShaderUniformDataType.Vec2 + ); + } + + fragShaderFileModTime = currentFragShaderModTime; + } + } + + if (IsKeyPressed(KeyboardKey.A)) + { + shaderAutoReloading = !shaderAutoReloading; + } +#endif + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // We only draw a white full-screen rectangle, frame is generated in shader + BeginShaderMode(shader); + DrawRectangle(0, 0, screenWidth, screenHeight, Color.White); + EndShaderMode(); + +#if BROWSER + DrawText("Shader generates the frame in real time", 10, 10, 10, Color.Black); +#else + var info = $"PRESS [A] to TOGGLE SHADER AUTOLOADING: {(shaderAutoReloading ? "AUTO" : "MANUAL")}"; + DrawText(info, 10, 10, 10, shaderAutoReloading ? Color.Red : Color.Black); + if (!shaderAutoReloading) + { + DrawText("MOUSE CLICK to SHADER RE-LOADING", 10, 30, 10, Color.Black); + } + + var lastModification = DateTimeOffset.FromUnixTimeSeconds(fragShaderFileModTime).LocalDateTime.ToString(); + DrawText($"Shader last modification: {lastModification}", 10, 430, 10, Color.Black); +#endif + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - int screenWidth = 800; - int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - hot reloading"); - string fragShaderFileName = "resources/shaders/glsl330/reload.fs"; - long fragShaderFileModTime = GetFileModTime(fragShaderFileName); - - // Load raymarching shader - // NOTE: Defining 0 (NULL) for vertex shader forces usage of internal default vertex shader - Shader shader = LoadShader(null, fragShaderFileName); - - // Get shader locations for required uniforms - int resolutionLoc = GetShaderLocation(shader, "resolution"); - int mouseLoc = GetShaderLocation(shader, "mouse"); - int timeLoc = GetShaderLocation(shader, "time"); - - float[] resolution = new[] { (float)screenWidth, (float)screenHeight }; - Raylib.SetShaderValue(shader, resolutionLoc, resolution, ShaderUniformDataType.Vec2); - - float totalTime = 0.0f; - bool shaderAutoReloading = false; - SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new HotReloading(); + game.Init(); + // Main game loop while (!WindowShouldClose()) { - // Update - //---------------------------------------------------------------------------------- - totalTime += GetFrameTime(); - Vector2 mouse = GetMousePosition(); - float[] mousePos = new[] { mouse.X, mouse.Y }; - - // Set shader required uniform values - Raylib.SetShaderValue(shader, timeLoc, totalTime, ShaderUniformDataType.Float); - Raylib.SetShaderValue(shader, mouseLoc, mousePos, ShaderUniformDataType.Vec2); - - // Hot shader reloading - if (shaderAutoReloading || (IsMouseButtonPressed(MouseButton.Left))) - { - long currentFragShaderModTime = GetFileModTime(fragShaderFileName); - - // Check if shader file has been modified - if (currentFragShaderModTime != fragShaderFileModTime) - { - // Try reloading updated shader - Shader updatedShader = LoadShader(null, fragShaderFileName); - - // It was correctly loaded - if (updatedShader.Id != 0) //rlGetShaderIdDefault()) - { - UnloadShader(shader); - shader = updatedShader; - - // Get shader locations for required uniforms - resolutionLoc = GetShaderLocation(shader, "resolution"); - mouseLoc = GetShaderLocation(shader, "mouse"); - timeLoc = GetShaderLocation(shader, "time"); - - // Reset required uniforms - Raylib.SetShaderValue( - shader, - resolutionLoc, - resolution, - ShaderUniformDataType.Vec2 - ); - } - - fragShaderFileModTime = currentFragShaderModTime; - } - } - - if (IsKeyPressed(KeyboardKey.A)) - { - shaderAutoReloading = !shaderAutoReloading; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // We only draw a white full-screen rectangle, frame is generated in shader - BeginShaderMode(shader); - DrawRectangle(0, 0, screenWidth, screenHeight, Color.White); - EndShaderMode(); - - string info = $"PRESS [A] to TOGGLE SHADER AUTOLOADING: {(shaderAutoReloading ? "AUTO" : "MANUAL")}"; - DrawText(info, 10, 10, 10, shaderAutoReloading ? Color.Red : Color.Black); - if (!shaderAutoReloading) - { - DrawText("MOUSE CLICK to SHADER RE-LOADING", 10, 30, 10, Color.Black); - } - - // DrawText($"Shader last modification: ", 10, 430, 10, Color.BLACK); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadShader(shader); - CloseWindow(); //-------------------------------------------------------------------------------------- diff --git a/Examples/Shaders/HybridRender.cs b/Examples/Shaders/HybridRender.cs index e610327..4b3ed79 100644 --- a/Examples/Shaders/HybridRender.cs +++ b/Examples/Shaders/HybridRender.cs @@ -1,6 +1,8 @@ /******************************************************************************************* * -* raylib [shaders] example - Hybrid Rendering +* raylib [shaders] example - hybrid rendering +* +* Example complexity rating: [★★★★] 4/4 * * Example originally created with raylib 4.2, last time updated with raylib 4.2 * @@ -9,7 +11,7 @@ * 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-2023 Buğra Alptekin Sarı (@BugraAlptekinSari) +* Copyright (c) 2022-2025 Buğra Alptekin Sarı (@BugraAlptekinSari) * ********************************************************************************************/ @@ -19,43 +21,54 @@ using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class HybridRender +public class HybridRender : IExample { - struct RayLocs + private struct RayLocs { public int CamPos; public int CamDir; public int ScreenCenter; } - const int GLSL_VERSION = 330; +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shaders / Hybrid Render"; + + public string Title => "raylib [shaders] example - hybrid rendering"; + + private Shader shdrRaymarch; + private Shader shdrRaster; + private RayLocs marchLocs; + private RenderTexture2D target; + private Camera3D camera; + private float camDist; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - hybrid render"); - - // This shader calculates pixel depth and color using raymarch - Shader shdrRaymarch = LoadShader(null, $"resources/shaders/glsl{GLSL_VERSION}/hybrid_raymarch.fs"); + // This Shader calculates pixel depth and color using raymarch + shdrRaymarch = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/hybrid_raymarch.fs"); // This Shader is a standard rasterization fragment shader with the addition of depth writing // You are required to write depth for all shaders if one shader does it - Shader shdrRaster = LoadShader(null, $"resources/shaders/glsl{GLSL_VERSION}/hybrid_raster.fs"); + shdrRaster = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/hybrid_raster.fs"); - // Declare struct used to store camera locs - RayLocs marchLocs = new(); + // Declare Struct used to store camera locs + marchLocs = new(); - // Fill the struct with shader locs. + // Fill the struct with shader locs marchLocs.CamPos = GetShaderLocation(shdrRaymarch, "camPos"); marchLocs.CamDir = GetShaderLocation(shdrRaymarch, "camDir"); marchLocs.ScreenCenter = GetShaderLocation(shdrRaymarch, "screenCenter"); - // Transfer screenCenter position to shader. Which is used to calculate ray direction. - Vector2 screenCenter = new(screenWidth / 2, screenHeight / 2); + // Transfer screenCenter position to shader. Which is used to calculate ray direction + Vector2 screenCenter = new(screenWidth / 2.0f, screenHeight / 2.0f); SetShaderValue( shdrRaymarch, marchLocs.ScreenCenter, @@ -63,91 +76,111 @@ public class HybridRender ShaderUniformDataType.Vec2 ); - // Use customized function to create writable depth texture buffer - RenderTexture2D target = LoadRenderTextureDepthTex(screenWidth, screenHeight); + // Use Customized function to create writable depth texture buffer + target = LoadRenderTextureDepthTex(screenWidth, screenHeight); // Define the camera to look into our 3d world - Camera3D camera; - camera.Position = new Vector3(0.5f, 1.0f, 1.5f); - camera.Target = new Vector3(0.0f, 0.5f, 0.0f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.FovY = 45.0f; - camera.Projection = CameraProjection.Perspective; + camera = new(); + camera.Position = new Vector3(0.5f, 1.0f, 1.5f); // 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 - // Camera FOV is pre-calculated in the camera Distance. - float camDist = 1.0f / (MathF.Tan(camera.FovY * 0.5f * Raylib.DEG2RAD)); + // Camera FOV is pre-calculated in the camera distance + camDist = 1.0f / (MathF.Tan(camera.FovY * 0.5f * Raylib.DEG2RAD)); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + + // Update Camera Postion in the ray march shader + SetShaderValue( + shdrRaymarch, + marchLocs.CamPos, + camera.Position, + ShaderUniformDataType.Vec3 + ); + + // Update Camera Looking Vector. Vector length determines FOV + var camDir = Vector3.Normalize(camera.Target - camera.Position) * camDist; + SetShaderValue(shdrRaymarch, marchLocs.CamDir, camDir, ShaderUniformDataType.Vec3); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + // Draw into our custom render texture (framebuffer) + BeginTextureMode(target); + ClearBackground(Color.White); + + // Raymarch Scene + // Manually enable Depth Test to handle multiple rendering methods + Rlgl.EnableDepthTest(); + BeginShaderMode(shdrRaymarch); + DrawRectangleRec(new Rectangle(0, 0, screenWidth, screenHeight), Color.White); + EndShaderMode(); + + // Rasterize Scene + BeginMode3D(camera); + BeginShaderMode(shdrRaster); + DrawCubeWiresV(new Vector3(0.0f, 0.5f, 1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.Red); + DrawCubeV(new Vector3(0.0f, 0.5f, 1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.Purple); + DrawCubeWiresV(new Vector3(0.0f, 0.5f, -1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.DarkGreen); + DrawCubeV(new Vector3(0.0f, 0.5f, -1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.Yellow); + DrawGrid(10, 1.0f); + EndShaderMode(); + EndMode3D(); + + EndTextureMode(); + + // Draw custom render texture + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawTextureRec( + target.Texture, + new Rectangle(0, 0, screenWidth, -screenHeight), + Vector2.Zero, + Color.White + ); + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadRenderTextureDepthTex(target); + UnloadShader(shdrRaymarch); + UnloadShader(shdrRaster); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - hybrid rendering"); SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new HybridRender(); + game.Init(); + // Main game loop while (!WindowShouldClose()) { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Orbital); - - // Update Camera Postion in the ray march shader. - SetShaderValue( - shdrRaymarch, - marchLocs.CamPos, - camera.Position, - ShaderUniformDataType.Vec3 - ); - - // Update Camera Looking Vector. Vector length determines FOV. - Vector3 camDir = Vector3.Normalize(camera.Target - camera.Position) * camDist; - SetShaderValue(shdrRaymarch, marchLocs.CamDir, camDir, ShaderUniformDataType.Vec3); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - // Draw into our custom render texture (framebuffer) - BeginTextureMode(target); - ClearBackground(Color.White); - - // Raymarch Scene - // Manually enable Depth Test to handle multiple rendering methods. - Rlgl.EnableDepthTest(); - BeginShaderMode(shdrRaymarch); - DrawRectangleRec(new Rectangle(0, 0, screenWidth, screenHeight), Color.White); - EndShaderMode(); - - // Rasterize Scene - BeginMode3D(camera); - BeginShaderMode(shdrRaster); - DrawCubeWiresV(new Vector3(0.0f, 0.5f, 1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.Red); - DrawCubeV(new Vector3(0.0f, 0.5f, 1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.Purple); - DrawCubeWiresV(new Vector3(0.0f, 0.5f, -1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.DarkGreen); - DrawCubeV(new Vector3(0.0f, 0.5f, -1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.Yellow); - DrawGrid(10, 1.0f); - EndShaderMode(); - EndMode3D(); - - EndTextureMode(); - - // Draw custom render texture - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawTextureRec( - target.Texture, - new Rectangle(0, 0, screenWidth, -screenHeight), - Vector2.Zero, - Color.White - ); - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadRenderTextureDepthTex(target); - UnloadShader(shdrRaymarch); - UnloadShader(shdrRaster); - CloseWindow(); //-------------------------------------------------------------------------------------- @@ -203,7 +236,7 @@ public class HybridRender ); // Check if fbo is complete with attachments (valid) - if (Rlgl.FramebufferComplete(target.Id)) + if (Rlgl.FramebufferComplete(target.Id) != 0) { TraceLog(TraceLogLevel.Info, $"FBO: [ID {target.Id}] Framebuffer object created successfully"); } diff --git a/Examples/Shaders/JuliaSet.cs b/Examples/Shaders/JuliaSet.cs index 5a1339a..8e146e9 100644 --- a/Examples/Shaders/JuliaSet.cs +++ b/Examples/Shaders/JuliaSet.cs @@ -1,18 +1,22 @@ /******************************************************************************************* * -* raylib [shaders] example - julia sets +* raylib [shaders] example - julia 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. +* 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). +* NOTE: Shaders used in this example are #version 330 (OpenGL 3.3) * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 2.5, last time updated with raylib 4.0 * -* Example contributed by eggmund (@eggmund) and reviewed by Ramon Santamaria (@raysan5) +* Example contributed by Josh Colclough (@joshcol9232) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2019 eggmund (@eggmund) and 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 Josh Colclough (@joshcol9232) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -21,12 +25,27 @@ using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class JuliaSet +public class JuliaSet : IExample { - const int GlslVersion = 330; + private const int screenWidth = 800; + private const int screenHeight = 450; + private const float zoomSpeed = 1.01f; + private const float offsetSpeedMul = 2.0f; + + private const float startingZoom = 0.75f; + +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Julia Set"; + + public string Title => "raylib [shaders] example - julia set"; // A few good julia sets - static float[][] PointsOfInterest = new float[][] { + private float[][] PointsOfInterest = new float[][] { new float[] { -0.348827f, 0.607167f }, new float[] { -0.786268f, 0.169728f }, new float[] { -0.8f, 0.156f }, @@ -35,38 +54,38 @@ public class JuliaSet new float[] { -0.70176f, -0.3842f }, }; - public static int Main() + private Shader shader; + private RenderTexture2D target; + private float[] c; + private float[] offset; + private float zoom; + private int cLoc; + private int zoomLoc; + private int offsetLoc; + private int incrementSpeed; + private bool showControls; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - const float zoomSpeed = 1.01f; - const float offsetSpeedMul = 2.0f; - - const float startingZoom = 0.75f; - - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - julia sets"); - // Load julia set shader // NOTE: Defining 0 (NULL) for vertex shader forces usage of internal default vertex shader - Shader shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/julia_set.fs"); + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/julia_set.fs"); // Create a RenderTexture2D to be used for render to texture - RenderTexture2D target = LoadRenderTexture(screenWidth, screenHeight); + target = LoadRenderTexture(screenWidth, screenHeight); // c constant to use in z^2 + c - float[] c = { PointsOfInterest[0][0], PointsOfInterest[0][1] }; + c = new float[] { PointsOfInterest[0][0], PointsOfInterest[0][1] }; // Offset and zoom to draw the julia set at. (centered on screen and default size) - float[] offset = { 0, 0 }; - float zoom = startingZoom; + offset = new float[] { 0, 0 }; + zoom = startingZoom; // 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 - int cLoc = GetShaderLocation(shader, "c"); - int zoomLoc = GetShaderLocation(shader, "zoom"); - int offsetLoc = GetShaderLocation(shader, "offset"); + cLoc = GetShaderLocation(shader, "c"); + zoomLoc = GetShaderLocation(shader, "zoom"); + offsetLoc = GetShaderLocation(shader, "offset"); // Upload the shader uniform values! Raylib.SetShaderValue(shader, cLoc, c, ShaderUniformDataType.Vec2); @@ -74,166 +93,189 @@ public class JuliaSet Raylib.SetShaderValue(shader, offsetLoc, offset, ShaderUniformDataType.Vec2); // Multiplier of speed to change c value - int incrementSpeed = 0; + incrementSpeed = 0; // Show controls - bool showControls = true; + showControls = true; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // 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)) + { + + if (IsKeyPressed(KeyboardKey.One)) + { + c[0] = PointsOfInterest[0][0]; + c[1] = PointsOfInterest[0][1]; + } + else if (IsKeyPressed(KeyboardKey.Two)) + { + c[0] = PointsOfInterest[1][0]; + c[1] = PointsOfInterest[1][1]; + } + else if (IsKeyPressed(KeyboardKey.Three)) + { + c[0] = PointsOfInterest[2][0]; + c[1] = PointsOfInterest[2][1]; + } + else if (IsKeyPressed(KeyboardKey.Four)) + { + c[0] = PointsOfInterest[3][0]; + c[1] = PointsOfInterest[3][1]; + } + else if (IsKeyPressed(KeyboardKey.Five)) + { + c[0] = PointsOfInterest[4][0]; + c[1] = PointsOfInterest[4][1]; + } + else if (IsKeyPressed(KeyboardKey.Six)) + { + c[0] = PointsOfInterest[5][0]; + c[1] = PointsOfInterest[5][1]; + } + Raylib.SetShaderValue(shader, cLoc, c, ShaderUniformDataType.Vec2); + } + + // If "R" is pressed, reset zoom and offset + if (IsKeyPressed(KeyboardKey.R)) + { + zoom = startingZoom; + offset[0] = 0.0f; + offset[1] = 0.0f; + Raylib.SetShaderValue(shader, zoomLoc, zoom, ShaderUniformDataType.Float); + Raylib.SetShaderValue(shader, offsetLoc, offset, ShaderUniformDataType.Vec2); + } + + // Pause animation (c change) + if (IsKeyPressed(KeyboardKey.Space)) + { + incrementSpeed = 0; + } + + // Toggle whether or not to show controls + if (IsKeyPressed(KeyboardKey.F1)) + { + showControls = !showControls; + } + + if (IsKeyPressed(KeyboardKey.Right)) + { + incrementSpeed++; + } + else if (IsKeyPressed(KeyboardKey.Left)) + { + incrementSpeed--; + } + + // If either left or right button is pressed, zoom in/out + if (IsMouseButtonDown(MouseButton.Left) || IsMouseButtonDown(MouseButton.Right)) + { + if (IsMouseButtonDown(MouseButton.Left)) + { + zoom *= zoomSpeed; + } + + if (IsMouseButtonDown(MouseButton.Right)) + { + zoom *= 1.0f / zoomSpeed; + } + + var mousePos = GetMousePosition(); + var offsetVelocity = Vector2.Zero; + + offsetVelocity.X = (mousePos.X / screenWidth - 0.5f) * offsetSpeedMul / zoom; + offsetVelocity.Y = (mousePos.Y / screenHeight - 0.5f) * offsetSpeedMul / zoom; + + // Apply move velocity to camera + offset[0] += GetFrameTime() * offsetVelocity.X; + offset[1] += GetFrameTime() * offsetVelocity.Y; + + Raylib.SetShaderValue(shader, zoomLoc, zoom, ShaderUniformDataType.Float); + Raylib.SetShaderValue(shader, offsetLoc, offset, ShaderUniformDataType.Vec2); + } + + // Increment c value with time + var dc = GetFrameTime() * incrementSpeed * 0.0005f; + c[0] += dc; + c[1] += dc; + + Raylib.SetShaderValue(shader, cLoc, c, ShaderUniformDataType.Vec2); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + // Using a render texture to draw Julia set + BeginTextureMode(target); + ClearBackground(Color.Black); + + // 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); + + // Draw the saved texture and rendered julia 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 KEY_F1 to toggle these controls", 10, 30, 10, Color.RayWhite); + DrawText("Press KEYS [1 - 6] to change point of interest", 10, 45, 10, Color.RayWhite); + DrawText("Press KEY_LEFT | KEY_RIGHT to change speed", 10, 60, 10, Color.RayWhite); + DrawText("Press KEY_SPACE to stop movement animation", 10, 75, 10, Color.RayWhite); + DrawText("Press KEY_R to recenter the camera", 10, 90, 10, Color.RayWhite); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); + UnloadRenderTexture(target); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - julia set"); SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new JuliaSet(); + game.Init(); + // Main game loop while (!WindowShouldClose()) { - // Update - //---------------------------------------------------------------------------------- - // 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)) - { - - if (IsKeyPressed(KeyboardKey.One)) - { - c[0] = PointsOfInterest[0][0]; - c[1] = PointsOfInterest[0][1]; - } - else if (IsKeyPressed(KeyboardKey.Two)) - { - c[0] = PointsOfInterest[1][0]; - c[1] = PointsOfInterest[1][1]; - } - else if (IsKeyPressed(KeyboardKey.Three)) - { - c[0] = PointsOfInterest[2][0]; - c[1] = PointsOfInterest[2][1]; - } - else if (IsKeyPressed(KeyboardKey.Four)) - { - c[0] = PointsOfInterest[3][0]; - c[1] = PointsOfInterest[3][1]; - } - else if (IsKeyPressed(KeyboardKey.Five)) - { - c[0] = PointsOfInterest[4][0]; - c[1] = PointsOfInterest[4][1]; - } - else if (IsKeyPressed(KeyboardKey.Six)) - { - c[0] = PointsOfInterest[5][0]; - c[1] = PointsOfInterest[5][1]; - } - Raylib.SetShaderValue(shader, cLoc, c, ShaderUniformDataType.Vec2); - } - - if (IsKeyPressed(KeyboardKey.R)) - { - zoom = startingZoom; - offset[0] = 1f; - offset[1] = 1f; - Raylib.SetShaderValue(shader, zoomLoc, zoom, ShaderUniformDataType.Float); - Raylib.SetShaderValue(shader, offsetLoc, offset, ShaderUniformDataType.Vec2); - } - - // Pause animation (c change) - if (IsKeyPressed(KeyboardKey.Space)) - { - incrementSpeed = 0; - } - - // Toggle whether or not to show controls - if (IsKeyPressed(KeyboardKey.F1)) - { - showControls = !showControls; - } - - if (IsKeyPressed(KeyboardKey.Right)) - { - incrementSpeed++; - } - else if (IsKeyPressed(KeyboardKey.Left)) - { - incrementSpeed--; - } - - // If either left or right button is pressed, zoom in/out - if (IsMouseButtonDown(MouseButton.Left) || IsMouseButtonDown(MouseButton.Right)) - { - if (IsMouseButtonDown(MouseButton.Left)) - { - zoom *= zoomSpeed; - } - - if (IsMouseButtonDown(MouseButton.Right)) - { - zoom *= 1.0f / zoomSpeed; - } - - Vector2 mousePos = GetMousePosition(); - Vector2 offsetVelocity = Vector2.Zero; - - offsetVelocity.X = (mousePos.X / screenWidth - 0.5f) * offsetSpeedMul / zoom; - offsetVelocity.Y = (mousePos.Y / screenHeight - 0.5f) * offsetSpeedMul / zoom; - - // Apply move velocity to camera - offset[0] += GetFrameTime() * offsetVelocity.X; - offset[1] += GetFrameTime() * offsetVelocity.Y; - - Raylib.SetShaderValue(shader, zoomLoc, zoom, ShaderUniformDataType.Float); - Raylib.SetShaderValue(shader, offsetLoc, offset, ShaderUniformDataType.Vec2); - } - - // Increment c value with time - float amount = GetFrameTime() * incrementSpeed * 0.0005f; - c[0] += amount; - c[1] += amount; - - Raylib.SetShaderValue(shader, cLoc, c, ShaderUniformDataType.Vec2); - - // Using a render texture to draw Julia set - // Enable drawing to texture - BeginTextureMode(target); - ClearBackground(Color.Black); - - // Draw a rectangle in shader mode to be used as shader canvas - // NOTE: Rectangle uses font Color.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(); - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.Black); - - // Draw the saved texture and rendered julia set with shader - // NOTE: We do not invert texture on Y, already considered inside shader - BeginShaderMode(shader); - DrawTexture(target.Texture, 0, 0, Color.White); - EndShaderMode(); - - if (showControls) - { - DrawText("Press Mouse buttons right/left to zoom in/out and move", 10, 15, 10, Color.RayWhite); - DrawText("Press KEY_F1 to toggle these controls", 10, 30, 10, Color.RayWhite); - DrawText("Press KEYS [1 - 6] to change point of interest", 10, 45, 10, Color.RayWhite); - DrawText("Press KEY_LEFT | KEY_RIGHT to change speed", 10, 60, 10, Color.RayWhite); - DrawText("Press KEY_SPACE to pause movement animation", 10, 75, 10, Color.RayWhite); - DrawText("Press KEY_R to recenter the camera", 10, 90, 10, Color.RayWhite); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadShader(shader); - UnloadRenderTexture(target); - CloseWindow(); //-------------------------------------------------------------------------------------- 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/MeshInstancing.cs b/Examples/Shaders/MeshInstancing.cs index 20d3870..a639530 100644 --- a/Examples/Shaders/MeshInstancing.cs +++ b/Examples/Shaders/MeshInstancing.cs @@ -1,112 +1,97 @@ /******************************************************************************************* * -* raylib [shaders] example - rlgl module usage for instanced meshes +* raylib [shaders] example - mesh instancing * -* This example uses [rlgl] module funtionality (pseudo-OpenGL 1.1 style coding) +* Example complexity rating: [★★★★] 4/4 * -* This example has been created using raylib 3.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 3.7, last time updated with raylib 4.2 * -* Example contributed by @seanpringle and reviewed by Ramon Santamaria (@raysan5) +* Example contributed by seanpringle (@seanpringle) and reviewed by Max (@moliad) and Ramon Santamaria (@raysan5) * -* Copyright (c) 2020 @seanpringle +* 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) 2020-2025 seanpringle (@seanpringle), Max (@moliad) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ +using System; using System.Numerics; using static Raylib_cs.Raylib; using Examples.Shared; namespace Examples.Shaders; -public class MeshInstancing +public class MeshInstancing : IExample { - public static int Main() + 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 MaxInstances = 10000; + + public string Name => "Shaders / Mesh Instancing"; + + public string Title => "raylib [shaders] example - mesh instancing"; + + private Camera3D camera; + private Mesh cube; + private Matrix4x4[] transforms; + private Shader shader; + private Material matInstances; + private Material matDefault; + + public unsafe void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - const int fps = 60; - - // Enable Multi Sampling Anti Aliasing 4x (if available) - SetConfigFlags(ConfigFlags.Msaa4xHint); - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - rlgl mesh instanced"); - - // Speed of jump animation - int speed = 30; - // Count of separate groups jumping around - int groups = 2; - // Maximum amplitude of jump - float amp = 10; - // Global variance in jump height - float variance = 0.8f; - // Individual cube's computed loop timer - float loop = 0.0f; - - // Used for various 3D coordinate & vector ops - float x = 0.0f; - float y = 0.0f; - float z = 0.0f; - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(-125.0f, 125.0f, -125.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; + camera = new(); + camera.Position = new Vector3(-125.0f, 125.0f, -125.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 - // Number of instances to display - const int instances = 10000; - Mesh cube = GenMeshCube(1.0f, 1.0f, 1.0f); + // Define mesh to be instanced + cube = GenMeshCube(1.0f, 1.0f, 1.0f); - // Rotation state of instances - Matrix4x4[] rotations = new Matrix4x4[instances]; - // Per-frame rotation animation of instances - Matrix4x4[] rotationsInc = new Matrix4x4[instances]; - // Locations of instances - Matrix4x4[] translations = new Matrix4x4[instances]; + // Define transforms to be uploaded to GPU for instances + transforms = new Matrix4x4[MaxInstances]; // Pre-multiplied transformations passed to rlgl - // Scatter random cubes around - for (int i = 0; i < instances; i++) + // Translate and rotate cubes randomly + for (var i = 0; i < MaxInstances; i++) { - x = GetRandomValue(-50, 50); - y = GetRandomValue(-50, 50); - z = GetRandomValue(-50, 50); - translations[i] = Matrix4x4.CreateTranslation(x, y, z); - - x = GetRandomValue(0, 360); - y = GetRandomValue(0, 360); - z = GetRandomValue(0, 360); - Vector3 axis = Vector3.Normalize(new Vector3(x, y, z)); - float angle = (float)GetRandomValue(0, 10) * DEG2RAD; - - rotationsInc[i] = Matrix4x4.CreateFromAxisAngle(axis, angle); - rotations[i] = Matrix4x4.Identity; - } - - // Pre-multiplied transformations passed to rlgl - Matrix4x4[] transforms = new Matrix4x4[instances]; - Shader shader = LoadShader( - "resources/shaders/glsl330/lighting_instancing.vs", - "resources/shaders/glsl330/lighting.fs" - ); - - // Get some shader loactions - unsafe - { - int* locs = (int*)shader.Locs; - locs[(int)ShaderLocationIndex.MatrixMvp] = GetShaderLocation(shader, "mvp"); - locs[(int)ShaderLocationIndex.VectorView] = GetShaderLocation(shader, "viewPos"); - locs[(int)ShaderLocationIndex.MatrixModel] = GetShaderLocationAttrib( - shader, - "instanceTransform" + var translation = Matrix4x4.CreateTranslation( + GetRandomValue(-50, 50), + GetRandomValue(-50, 50), + GetRandomValue(-50, 50) ); + var axis = Vector3.Normalize(new Vector3( + GetRandomValue(0, 360), + GetRandomValue(0, 360), + GetRandomValue(0, 360) + )); + var angle = GetRandomValue(0, 180) * DEG2RAD; + var rotation = Matrix4x4.CreateFromAxisAngle(axis, angle); + + transforms[i] = Matrix4x4.Transpose(Matrix4x4.Multiply(rotation, translation)); } - // Ambient light level - int ambientLoc = GetShaderLocation(shader, "ambient"); + // Load lighting shader + shader = LoadShader( + $"resources/shaders/glsl{GlslVersion}/lighting_instancing.vs", + $"resources/shaders/glsl{GlslVersion}/lighting.fs" + ); + // Get shader locations + shader.Locs[(int)ShaderLocationIndex.MatrixMvp] = GetShaderLocation(shader, "mvp"); + shader.Locs[(int)ShaderLocationIndex.VectorView] = GetShaderLocation(shader, "viewPos"); + + // Set shader value: ambient light level + var ambientLoc = GetShaderLocation(shader, "ambient"); Raylib.SetShaderValue( shader, ambientLoc, @@ -114,211 +99,105 @@ public class MeshInstancing ShaderUniformDataType.Vec4 ); + // Create one light Rlights.CreateLight( 0, LightType.Directorional, - new Vector3(50, 50, 0), + new Vector3(50.0f, 50.0f, 0.0f), Vector3.Zero, Color.White, shader ); - Material material = LoadMaterialDefault(); - material.Shader = shader; - unsafe - { - material.Maps[(int)MaterialMapIndex.Diffuse].Color = Color.Red; - } + // NOTE: We are assigning the intancing shader to material.shader + // to be used on mesh drawing with DrawMeshInstanced() + matInstances = LoadMaterialDefault(); + matInstances.Shader = shader; + matInstances.Maps[(int)MaterialMapIndex.Diffuse].Color = Color.Red; - int textPositionY = 300; + // Load default material (using raylib intenral default shader) for non-instanced mesh drawing + // WARNING: Default shader enables vertex color attribute BUT GenMeshCube() does not generate vertex colors, so, + // when drawing the color attribute is disabled and a default color value is provided as input for thevertex attribute + matDefault = LoadMaterialDefault(); + matDefault.Maps[(int)MaterialMapIndex.Diffuse].Color = Color.Blue; + } - // Simple frames counter to manage animation - int framesCounter = 0; + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); - SetTargetFPS(fps); + // Update the light shader with the camera view position + float[] cameraPos = { camera.Position.X, camera.Position.Y, camera.Position.Z }; + Raylib.SetShaderValue( + shader, + shader.Locs[(int)ShaderLocationIndex.VectorView], + cameraPos, + ShaderUniformDataType.Vec3 + ); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + // Draw cube mesh with default material (BLUE) + DrawMesh(cube, matDefault, Matrix4x4.Transpose(Matrix4x4.CreateTranslation(-10.0f, 0.0f, 0.0f))); + + // Draw meshes instanced using material containing instancing shader (RED + lighting), + // transforms[] for the instances should be provided, they are dynamically + // updated in GPU every frame, so we can animate the different mesh instances + DrawMeshInstanced(cube, matInstances, transforms, MaxInstances); + + // Draw cube mesh with default material (BLUE) + DrawMesh(cube, matDefault, Matrix4x4.Transpose(Matrix4x4.CreateTranslation(10.0f, 0.0f, 0.0f))); + + EndMode3D(); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + // Detach shader so UnloadMaterial does not also unload it, then free everything. + matInstances.Shader = new(); + UnloadMaterial(matInstances); + UnloadMaterial(matDefault); + UnloadMesh(cube); + UnloadShader(shader); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - mesh instancing"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new MeshInstancing(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Free); - - textPositionY = 300; - framesCounter += 1; - - if (IsKeyDown(KeyboardKey.Up)) - { - amp += 0.5f; - } - - if (IsKeyDown(KeyboardKey.Down)) - { - amp = (amp <= 1) ? 1.0f : (amp - 1.0f); - } - - if (IsKeyDown(KeyboardKey.Left)) - { - variance = (variance <= 0.0f) ? 0.0f : (variance - 0.01f); - } - - if (IsKeyDown(KeyboardKey.Right)) - { - variance = (variance >= 1.0f) ? 1.0f : (variance + 0.01f); - } - - if (IsKeyDown(KeyboardKey.One)) - { - groups = 1; - } - - if (IsKeyDown(KeyboardKey.Two)) - { - groups = 2; - } - - if (IsKeyDown(KeyboardKey.Three)) - { - groups = 3; - } - - if (IsKeyDown(KeyboardKey.Four)) - { - groups = 4; - } - - if (IsKeyDown(KeyboardKey.Five)) - { - groups = 5; - } - - if (IsKeyDown(KeyboardKey.Six)) - { - groups = 6; - } - - if (IsKeyDown(KeyboardKey.Seven)) - { - groups = 7; - } - - if (IsKeyDown(KeyboardKey.Eight)) - { - groups = 8; - } - - if (IsKeyDown(KeyboardKey.Nine)) - { - groups = 9; - } - - if (IsKeyDown(KeyboardKey.W)) - { - groups = 7; - amp = 25; - speed = 18; - variance = 0.70f; - } - - if (IsKeyDown(KeyboardKey.Equal)) - { - speed = (speed <= (int)(fps * 0.25f)) ? (int)(fps * 0.25f) : (int)(speed * 0.95f); - } - - if (IsKeyDown(KeyboardKey.KpAdd)) - { - speed = (speed <= (int)(fps * 0.25f)) ? (int)(fps * 0.25f) : (int)(speed * 0.95f); - } - - if (IsKeyDown(KeyboardKey.Minus)) - { - speed = (int)MathF.Max(speed * 1.02f, speed + 1); - } - - if (IsKeyDown(KeyboardKey.KpSubtract)) - { - speed = (int)MathF.Max(speed * 1.02f, speed + 1); - } - - // Update the light shader with the camera view position - float[] cameraPos = { camera.Position.X, camera.Position.Y, camera.Position.Z }; - Raylib.SetShaderValue( - shader, - (int)ShaderLocationIndex.VectorView, - cameraPos, - ShaderUniformDataType.Vec3 - ); - - // Apply per-instance transformations - for (int i = 0; i < instances; i++) - { - rotations[i] = Matrix4x4.Multiply(rotations[i], rotationsInc[i]); - transforms[i] = Matrix4x4.Multiply(rotations[i], translations[i]); - - // Get the animation cycle's framesCounter for this instance - loop = (float)((framesCounter + (int)(((float)(i % groups) / groups) * speed)) % speed) / speed; - - // Calculate the y according to loop cycle - y = (MathF.Sin(loop * MathF.PI * 2)) * amp * ((1 - variance) + (variance * (float)(i % (groups * 10)) / (groups * 10))); - - // Clamp to floor - y = (y < 0) ? 0.0f : y; - - transforms[i] = Matrix4x4.Multiply(transforms[i], Matrix4x4.CreateTranslation(0.0f, y, 0.0f)); - transforms[i] = Matrix4x4.Transpose(transforms[i]); - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - DrawMeshInstanced(cube, material, transforms, instances); - EndMode3D(); - - DrawText("A CUBE OF DANCING CUBES!", 490, 10, 20, Color.Maroon); - DrawText("PRESS KEYS:", 10, textPositionY, 20, Color.Black); - - DrawText("1 - 9", 10, textPositionY += 25, 10, Color.Black); - DrawText(": Number of groups", 50, textPositionY, 10, Color.Black); - DrawText($": {groups}", 160, textPositionY, 10, Color.Black); - - DrawText("UP", 10, textPositionY += 15, 10, Color.Black); - DrawText(": increase amplitude", 50, textPositionY, 10, Color.Black); - DrawText($": {amp}%.2f", 160, textPositionY, 10, Color.Black); - - DrawText("DOWN", 10, textPositionY += 15, 10, Color.Black); - DrawText(": decrease amplitude", 50, textPositionY, 10, Color.Black); - - DrawText("LEFT", 10, textPositionY += 15, 10, Color.Black); - DrawText(": decrease variance", 50, textPositionY, 10, Color.Black); - DrawText($": {variance}.2f", 160, textPositionY, 10, Color.Black); - - DrawText("RIGHT", 10, textPositionY += 15, 10, Color.Black); - DrawText(": increase variance", 50, textPositionY, 10, Color.Black); - - DrawText("+/=", 10, textPositionY += 15, 10, Color.Black); - DrawText(": increase speed", 50, textPositionY, 10, Color.Black); - DrawText($": {speed} = {((float)fps / speed)} loops/sec", 160, textPositionY, 10, Color.Black); - - DrawText("-", 10, textPositionY += 15, 10, Color.Black); - DrawText(": decrease speed", 50, textPositionY, 10, Color.Black); - - DrawText("W", 10, textPositionY += 15, 10, Color.Black); - DrawText(": Wild setup!", 50, textPositionY, 10, Color.Black); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Shaders/ModelShader.cs b/Examples/Shaders/ModelShader.cs index 1d78b96..033b530 100644 --- a/Examples/Shaders/ModelShader.cs +++ b/Examples/Shaders/ModelShader.cs @@ -1,18 +1,22 @@ /******************************************************************************************* * -* raylib [shaders] example - Apply a shader to a 3d model +* raylib [shaders] example - model shader +* +* 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. +* 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), to test this example * on OpenGL ES 2.0 platforms (Android, Raspberry Pi, HTML5), use #version 100 shaders * raylib comes with shaders ready for both versions, check raylib/shaders install folder * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.3, last time updated with raylib 3.7 * -* Copyright (c) 2014 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) 2014-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -21,86 +25,122 @@ using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class ModelShader +public class ModelShader : 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 / Model Shader"; + + public string Title => "raylib [shaders] example - model shader"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + public bool CursorDisabled => true; + + private Camera3D camera; + private Model model; + private Texture2D texture; + private Shader shader; + private Vector3 position; + + public 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, -1.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 + + model = LoadModel("resources/models/obj/watermill.obj"); // Load OBJ model + texture = LoadTexture("resources/models/obj/watermill_diffuse.png"); // Load model texture + + // Load shader for model + // NOTE: Defining 0 (NULL) for vertex shader forces usage of internal default vertex shader + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/grayscale.fs"); + + Raylib.SetMaterialShader(ref model, 0, ref shader); // Set shader effect to 3d model + Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.Albedo, ref texture); // Bind texture to model + + position = new(0.0f, 0.0f, 0.0f); // Set model position + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Free); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawModel(model, position, 0.2f, Color.White); // Draw 3d model with texture + + DrawGrid(10, 1.0f); // Draw a grid + + EndMode3D(); + + DrawText( + "(c) Watermill 3D model by Alberto Cano", + screenWidth - 210, + screenHeight - 20, + 10, + Color.Gray + ); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); // Unload shader + UnloadTexture(texture); // Unload texture + UnloadModel(model); // Unload model + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - // Enable Multi Sampling Anti Aliasing 4x (if available) - SetConfigFlags(ConfigFlags.Msaa4xHint); + SetConfigFlags(ConfigFlags.Msaa4xHint); // Enable Multi Sampling Anti Aliasing 4x (if available) InitWindow(screenWidth, screenHeight, "raylib [shaders] example - model shader"); - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(4.0f, 4.0f, 4.0f); - camera.Target = new Vector3(0.0f, 1.0f, -1.0f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.FovY = 45.0f; - camera.Projection = CameraProjection.Perspective; - - Model model = LoadModel("resources/models/obj/watermill.obj"); - Texture2D texture = LoadTexture("resources/models/obj/watermill_diffuse.png"); - Shader shader = LoadShader("resources/shaders/glsl330/base.vs", - "resources/shaders/glsl330/grayscale.fs"); - - Raylib.SetMaterialShader(ref model, 0, ref shader); - Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.Albedo, ref texture); - - Vector3 position = new(0.0f, 0.0f, 0.0f); - - SetTargetFPS(60); + DisableCursor(); // Limit cursor to relative movement inside the window + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new ModelShader(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Free); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawModel(model, position, 0.2f, Color.White); - - DrawGrid(10, 1.0f); - - EndMode3D(); - - DrawText( - "(c) Watermill 3D model by Alberto Cano", - screenWidth - 210, - screenHeight - 20, - 10, - Color.Gray - ); - - DrawText($"Camera3D position: ({camera.Position})", 600, 20, 10, Color.Black); - DrawText($"Camera3D target: ({camera.Position})", 600, 40, 10, Color.Gray); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadShader(shader); - UnloadTexture(texture); - UnloadModel(model); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Shaders/MultiSample2d.cs b/Examples/Shaders/MultiSample2d.cs index ba318fc..5375cf2 100644 --- a/Examples/Shaders/MultiSample2d.cs +++ b/Examples/Shaders/MultiSample2d.cs @@ -1,18 +1,22 @@ /******************************************************************************************* * -* raylib [shaders] example - Multiple sample2D with default batch system +* raylib [shaders] example - multi sample2d +* +* 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. +* 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), to test this example * on OpenGL ES 2.0 platforms (Android, Raspberry Pi, HTML5), use #version 100 shaders * raylib comes with shaders ready for both versions, check raylib/shaders install folder * -* This example has been created using raylib 3.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 3.5, last time updated with raylib 3.5 * -* Copyright (c) 2020 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) 2020-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -20,94 +24,129 @@ using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class MultiSample2d +public class MultiSample2d : 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 / Multi Sample 2D"; + + public string Title => "raylib [shaders] example - multi sample2d"; + + private Texture2D texRed; + private Texture2D texBlue; + private Shader shader; + private int texBlueLoc; + private int dividerLoc; + private float dividerValue; + + public void Init() + { + var imRed = GenImageColor(800, 450, new Color(255, 0, 0, 255)); + texRed = LoadTextureFromImage(imRed); + UnloadImage(imRed); + + var imBlue = GenImageColor(800, 450, new Color(0, 0, 255, 255)); + texBlue = LoadTextureFromImage(imBlue); + UnloadImage(imBlue); + + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/color_mix.fs"); + + // Get an additional sampler2D location to be enabled on drawing + texBlueLoc = GetShaderLocation(shader, "texture1"); + + // Get shader uniform for divider + dividerLoc = GetShaderLocation(shader, "divider"); + dividerValue = 0.5f; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyDown(KeyboardKey.Right)) + { + dividerValue += 0.01f; + } + else if (IsKeyDown(KeyboardKey.Left)) + { + dividerValue -= 0.01f; + } + + if (dividerValue < 0.0f) + { + dividerValue = 0.0f; + } + else if (dividerValue > 1.0f) + { + dividerValue = 1.0f; + } + + Raylib.SetShaderValue(shader, dividerLoc, dividerValue, ShaderUniformDataType.Float); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginShaderMode(shader); + + // WARNING: Additional textures (sampler2D) are enabled for ALL draw calls in the batch, + // but EndShaderMode() forces batch drawing and resets active textures, this way + // other textures (sampler2D) can be activated on consequent drawings (if required) + // The downside of this approach is that SetShaderValue() must be called inside the loop, + // to be set again after every EndShaderMode() reset + SetShaderValueTexture(shader, texBlueLoc, texBlue); + + // We are drawing texRed using default [sampler2D texture0] but + // an additional texture units is enabled for texBlue [sampler2D texture1] + DrawTexture(texRed, 0, 0, Color.White); + + EndShaderMode(); // Texture sampler2D is reseted, needs to be set again for next frame + + var y = GetScreenHeight() - 40; + DrawText("Use KEY_LEFT/KEY_RIGHT to move texture mixing in shader!", 80, y, 20, Color.RayWhite); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); + UnloadTexture(texRed); + UnloadTexture(texBlue); + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib - multiple sample2D"); - - Image imRed = GenImageColor(800, 450, new Color(255, 0, 0, 255)); - Texture2D texRed = LoadTextureFromImage(imRed); - UnloadImage(imRed); - - Image imBlue = GenImageColor(800, 450, new Color(0, 0, 255, 255)); - Texture2D texBlue = LoadTextureFromImage(imBlue); - UnloadImage(imBlue); - - Shader shader = LoadShader(null, "resources/shaders/glsl330/color_mix.fs"); - - // Get an additional sampler2D location to be enabled on drawing - int texBlueLoc = GetShaderLocation(shader, "texture1"); - - // Get shader uniform for divider - int dividerLoc = GetShaderLocation(shader, "divider"); - float dividerValue = 0.5f; + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - multi sample2d"); SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new MultiSample2d(); + game.Init(); + // Main game loop while (!WindowShouldClose()) { - // Update - //---------------------------------------------------------------------------------- - if (IsKeyDown(KeyboardKey.Right)) - { - dividerValue += 0.01f; - } - else if (IsKeyDown(KeyboardKey.Left)) - { - dividerValue -= 0.01f; - } - - if (dividerValue < 0.0f) - { - dividerValue = 0.0f; - } - else if (dividerValue > 1.0f) - { - dividerValue = 1.0f; - } - - Raylib.SetShaderValue(shader, dividerLoc, dividerValue, ShaderUniformDataType.Float); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginShaderMode(shader); - - // WARNING: Additional samplers are enabled for all draw calls in the batch, - // EndShaderMode() forces batch drawing and consequently resets active textures - // to let other sampler2D to be activated on consequent drawings (if required) - SetShaderValueTexture(shader, texBlueLoc, texBlue); - - // We are drawing texRed using default sampler2D texture0 but - // an additional texture units is enabled for texBlue (sampler2D texture1) - DrawTexture(texRed, 0, 0, Color.White); - - EndShaderMode(); - - int y = GetScreenHeight() - 40; - DrawText("Use KEY_LEFT/KEY_RIGHT to move texture mixing in shader!", 80, y, 20, Color.RayWhite); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadShader(shader); - UnloadTexture(texRed); - UnloadTexture(texBlue); - CloseWindow(); //-------------------------------------------------------------------------------------- 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/PaletteSwitch.cs b/Examples/Shaders/PaletteSwitch.cs index b14b189..bed75e4 100644 --- a/Examples/Shaders/PaletteSwitch.cs +++ b/Examples/Shaders/PaletteSwitch.cs @@ -1,20 +1,24 @@ /******************************************************************************************* * -* raylib [shaders] example - Color palette switch +* raylib [shaders] example - palette switch +* +* 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. +* 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), to test this example * on OpenGL ES 2.0 platforms (Android, Raspberry Pi, HTML5), use #version 100 shaders * raylib comes with shaders ready for both versions, check raylib/shaders install folder * -* This example has been created using raylib 2.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 2.5, last time updated with raylib 3.7 * * Example contributed by Marco Lizza (@MarcoLizza) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2019 Marco Lizza (@MarcoLizza) and 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 Marco Lizza (@MarcoLizza) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -22,13 +26,24 @@ using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class PaletteSwitch +public class PaletteSwitch : IExample { - const int GlslVersion = 330; - const int ColorsPerPalette = 8; - const int VALUES_PER_COLOR = 3; + private const int screenWidth = 800; + private const int screenHeight = 450; - static int[][] Palettes = new int[][] { +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + private const int ColorsPerPalette = 8; + private const int VALUES_PER_COLOR = 3; + + public string Name => "Shaders / Palette Switch"; + + public string Title => "raylib [shaders] example - palette switch"; + + private int[][] Palettes = new int[][] { // 3-BIT RGB new int[] { 0, 0, 0, @@ -64,101 +79,119 @@ public class PaletteSwitch } }; - static string[] PaletteText = new string[] { + private string[] PaletteText = new string[] { "3-BIT RGB", "AMMO-8 (GameBoy-like)", "RKBV (2-strip film)" }; + private Shader shader; + private int paletteLoc; + private int currentPalette; + private int lineHeight; + + public void Init() + { + // Load shader to be used on some parts drawing + // NOTE 1: Using GLSL 330 shader version, on OpenGL ES 2.0 use GLSL 100 shader version + // NOTE 2: Defining 0 (NULL) for vertex shader forces usage of internal default vertex shader + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/palette_switch.fs"); + + // Get variable (uniform) location on the shader to connect with the program + // NOTE: If uniform variable could not be found in the shader, function returns -1 + paletteLoc = GetShaderLocation(shader, "palette"); + + currentPalette = 0; + lineHeight = screenHeight / ColorsPerPalette; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.Right)) + { + currentPalette++; + } + else if (IsKeyPressed(KeyboardKey.Left)) + { + currentPalette--; + } + + if (currentPalette >= Palettes.Length) + { + currentPalette = 0; + } + else if (currentPalette < 0) + { + currentPalette = Palettes.Length - 1; + } + + // Send palette data to the shader to be used on drawing + // NOTE: We are sending RGB triplets w/o the alpha channel + Raylib.SetShaderValueV( + shader, + paletteLoc, + Palettes[currentPalette], + ShaderUniformDataType.IVec3, + ColorsPerPalette + ); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginShaderMode(shader); + + for (var i = 0; i < ColorsPerPalette; i++) + { + // Draw horizontal screen-wide rectangles with increasing "palette index" + // The used palette index is encoded in the RGB components of the pixel + DrawRectangle(0, lineHeight * i, GetScreenWidth(), lineHeight, new Color(i, i, i, 255)); + } + + EndShaderMode(); + + DrawText("< >", 10, 10, 30, Color.DarkBlue); + DrawText("CURRENT PALETTE:", 60, 15, 20, Color.RayWhite); + DrawText(PaletteText[currentPalette], 300, 15, 20, Color.Red); + + DrawFPS(700, 15); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); // Unload shader + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - palette switch"); - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - color palette switch"); - - // Load shader to be used on some parts drawing - // NOTE 1: Using GLSL 330 shader version, on OpenGL ES 2.0 use GLSL 100 shader version - // NOTE 2: Defining 0 (NULL) for vertex shader forces usage of internal default vertex shader - Shader shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/palette_switch.fs"); - - // Get variable (uniform) location on the shader to connect with the program - // NOTE: If uniform variable could not be found in the shader, function returns -1 - int paletteLoc = GetShaderLocation(shader, "palette"); - - int currentPalette = 0; - int lineHeight = screenHeight / ColorsPerPalette; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new PaletteSwitch(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (IsKeyPressed(KeyboardKey.Right)) - { - currentPalette++; - } - else if (IsKeyPressed(KeyboardKey.Left)) - { - currentPalette--; - } - - if (currentPalette >= Palettes.Length) - { - currentPalette = 0; - } - else if (currentPalette < 0) - { - currentPalette = Palettes.Length - 1; - } - - // Send new value to the shader to be used on drawing. - // NOTE: We are sending RGB triplets w/o the alpha channel - Raylib.SetShaderValueV( - shader, - paletteLoc, - Palettes[currentPalette], - ShaderUniformDataType.IVec3, - ColorsPerPalette - ); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginShaderMode(shader); - - for (int i = 0; i < ColorsPerPalette; i++) - { - // Draw horizontal screen-wide rectangles with increasing "palette index" - // The used palette index is encoded in the RGB components of the pixel - DrawRectangle(0, lineHeight * i, GetScreenWidth(), lineHeight, new Color(i, i, i, 255)); - } - - EndShaderMode(); - - DrawText("< >", 10, 10, 30, Color.DarkBlue); - DrawText("CURRENT PALETTE:", 60, 15, 20, Color.RayWhite); - DrawText(PaletteText[currentPalette], 300, 15, 20, Color.Red); - - DrawFPS(700, 15); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadShader(shader); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Shaders/PostProcessing.cs b/Examples/Shaders/PostProcessing.cs index b7dd2a5..9df3515 100644 --- a/Examples/Shaders/PostProcessing.cs +++ b/Examples/Shaders/PostProcessing.cs @@ -1,18 +1,22 @@ /******************************************************************************************* * -* raylib [shaders] example - Apply a postprocessing shader to a scene +* raylib [shaders] example - postprocessing +* +* 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. +* 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), to test this example * on OpenGL ES 2.0 platforms (Android, Raspberry Pi, HTML5), use #version 100 shaders * raylib comes with shaders ready for both versions, check raylib/shaders install folder * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.3, last time updated with raylib 4.0 * -* Copyright (c) 2015 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) 2015-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -21,11 +25,24 @@ using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class PostProcessing +public class PostProcessing : IExample { - public const int GLSL_VERSION = 330; + private const int screenWidth = 800; + private const int screenHeight = 450; - enum PostproShader +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Post Processing"; + + public string Title => "raylib [shaders] example - postprocessing"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + private enum PostproShader { FxGrayScale = 0, FxPosterization, @@ -43,7 +60,7 @@ public class PostProcessing Max } - static string[] postproShaderText = new string[] { + private string[] postproShaderText = new string[] { "GRAYSCALE", "POSTERIZATION", "DREAM_VISION", @@ -59,40 +76,39 @@ public class PostProcessing //"FXAA" }; - public static int Main() + private Camera3D camera; + private Model model; + private Texture2D texture; + private Vector3 position; + private Shader[] shaders; + private int currentShader; + private RenderTexture2D target; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - // Enable Multi Sampling Anti Aliasing 4x (if available) - SetConfigFlags(ConfigFlags.Msaa4xHint); - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - postprocessing shader"); - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(2.0f, 3.0f, 2.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; + camera = new(); + camera.Position = new Vector3(2.0f, 3.0f, 2.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 - Model model = LoadModel("resources/models/obj/church.obj"); - Texture2D texture = LoadTexture("resources/models/obj/church_diffuse.png"); + model = LoadModel("resources/models/church.obj"); // Load OBJ model + texture = LoadTexture("resources/models/church_diffuse.png"); // Load model texture (diffuse map) // Set model diffuse texture Raylib.SetMaterialTexture(ref model, 0, MaterialMapIndex.Albedo, ref texture); - Vector3 position = new(0.0f, 0.0f, 0.0f); + position = new(0.0f, 0.0f, 0.0f); // Set model position // Load all postpro shaders // NOTE 1: All postpro shader use the base vertex shader (DEFAULT_VERTEX_SHADER) // NOTE 2: We load the correct shader depending on GLSL version - Shader[] shaders = new Shader[(int)PostproShader.Max]; + shaders = new Shader[(int)PostproShader.Max]; // NOTE: Defining null (NULL) for vertex shader forces usage of internal default vertex shader - string shaderPath = "resources/shaders/glsl330"; + var shaderPath = $"resources/shaders/glsl{GlslVersion}"; shaders[(int)PostproShader.FxGrayScale] = LoadShader(null, $"{shaderPath}/grayscale.fs"); shaders[(int)PostproShader.FxPosterization] = LoadShader(null, $"{shaderPath}/posterization.fs"); shaders[(int)PostproShader.FxDreamVision] = LoadShader(null, $"{shaderPath}/dream_vision.fs"); @@ -106,99 +122,122 @@ public class PostProcessing shaders[(int)PostproShader.FxBloom] = LoadShader(null, $"{shaderPath}/bloom.fs"); shaders[(int)PostproShader.FxBlur] = LoadShader(null, $"{shaderPath}/blur.fs"); - int currentShader = (int)PostproShader.FxGrayScale; + currentShader = (int)PostproShader.FxGrayScale; // Create a RenderTexture2D to be used for render to texture - RenderTexture2D target = LoadRenderTexture(screenWidth, screenHeight); + target = LoadRenderTexture(screenWidth, screenHeight); + } - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); - // Main game loop - while (!WindowShouldClose()) + if (IsKeyPressed(KeyboardKey.Right)) { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Orbital); - - if (IsKeyPressed(KeyboardKey.Right)) - { - currentShader++; - } - else if (IsKeyPressed(KeyboardKey.Left)) - { - currentShader--; - } - - if (currentShader >= (int)PostproShader.Max) - { - currentShader = 0; - } - else if (currentShader < 0) - { - currentShader = (int)PostproShader.Max - 1; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // Enable drawing to texture - BeginTextureMode(target); - ClearBackground(Color.RayWhite); - - BeginMode3D(camera); - - DrawModel(model, position, 0.1f, Color.White); - - DrawGrid(10, 1.0f); - - EndMode3D(); - - // End drawing to texture (now we have a texture available for next passes) - EndTextureMode(); - - // Render previously generated texture using selected postpro shader - BeginShaderMode(shaders[currentShader]); - - // NOTE: Render texture must be y-flipped due to default OpenGL coordinates (left-bottom) - DrawTextureRec( - target.Texture, - new Rectangle(0, 0, target.Texture.Width, -target.Texture.Height), - new Vector2(0, 0), - Color.White - ); - - EndShaderMode(); - - DrawRectangle(0, 9, 580, 30, ColorAlpha(Color.LightGray, 0.7f)); - - DrawText("(c) Church 3D model by Alberto Cano", screenWidth - 200, screenHeight - 20, 10, Color.Gray); - - DrawText("CURRENT POSTPRO SHADER:", 10, 15, 20, Color.Black); - DrawText(postproShaderText[currentShader], 330, 15, 20, Color.Red); - DrawText("< >", 540, 10, 30, Color.DarkBlue); - - DrawFPS(700, 15); - - EndDrawing(); - //---------------------------------------------------------------------------------- + currentShader++; + } + else if (IsKeyPressed(KeyboardKey.Left)) + { + currentShader--; } - // De-Initialization - //-------------------------------------------------------------------------------------- - for (int i = 0; i < (int)PostproShader.Max; i++) + if (currentShader >= (int)PostproShader.Max) + { + currentShader = 0; + } + else if (currentShader < 0) + { + currentShader = (int)PostproShader.Max - 1; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // Enable drawing to texture + BeginTextureMode(target); + ClearBackground(Color.RayWhite); + + BeginMode3D(camera); + + DrawModel(model, position, 0.1f, Color.White); + + DrawGrid(10, 1.0f); + + EndMode3D(); + + // End drawing to texture (now we have a texture available for next passes) + EndTextureMode(); + + // Render generated texture using selected postprocessing shader + BeginShaderMode(shaders[currentShader]); + + // NOTE: Render texture must be y-flipped due to default OpenGL coordinates (left-bottom) + DrawTextureRec( + target.Texture, + new Rectangle(0, 0, target.Texture.Width, -target.Texture.Height), + new Vector2(0, 0), + Color.White + ); + + EndShaderMode(); + + DrawRectangle(0, 9, 580, 30, Fade(Color.LightGray, 0.7f)); + + DrawText("(c) Church 3D model by Alberto Cano", screenWidth - 200, screenHeight - 20, 10, Color.Gray); + + DrawText("CURRENT POSTPRO SHADER:", 10, 15, 20, Color.Black); + DrawText(postproShaderText[currentShader], 330, 15, 20, Color.Red); + DrawText("< >", 540, 10, 30, Color.DarkBlue); + + DrawFPS(700, 15); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + // Unload all postpro shaders + for (var i = 0; i < (int)PostproShader.Max; i++) { UnloadShader(shaders[i]); } - UnloadTexture(texture); - UnloadModel(model); - UnloadRenderTexture(target); + UnloadTexture(texture); // Unload texture + UnloadModel(model); // Unload model + UnloadRenderTexture(target); // Unload render texture + } - CloseWindow(); + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.Msaa4xHint); // Enable Multi Sampling Anti Aliasing 4x (if available) + + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - postprocessing"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new PostProcessing(); + 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/Raymarching.cs b/Examples/Shaders/Raymarching.cs index b40c032..e521b6d 100644 --- a/Examples/Shaders/Raymarching.cs +++ b/Examples/Shaders/Raymarching.cs @@ -1,18 +1,18 @@ /******************************************************************************************* * -* raylib [shaders] example - Raymarching shapes generation +* raylib [shaders] example - raymarching rendering * -* 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 complexity rating: [★★★★] 4/4 * -* NOTE: Shaders used in this example are #version 330 (OpenGL 3.3), to test this example -* on OpenGL ES 2.0 platforms (Android, Raspberry Pi, HTML5), use #version 100 shaders -* raylib comes with shaders ready for both versions, check raylib/shaders install folder +* NOTE: This example requires raylib OpenGL 3.3 for shaders support and only #version 330 +* is currently supported. OpenGL ES 2.0 platforms are not supported at the moment * -* This example has been created using raylib 2.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 2.0, last time updated with raylib 4.2 * -* Copyright (c) 2018 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) 2018-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -22,98 +22,138 @@ using static Raylib_cs.ConfigFlags; namespace Examples.Shaders; -public class Raymarching +public class Raymarching : IExample { +#if BROWSER + public const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else public const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Raymarching"; + + public string Title => "raylib [shaders] example - raymarching rendering"; + + public ConfigFlags ConfigFlags => ConfigFlags.ResizableWindow; + + public bool CursorDisabled => true; + + private int screenWidth; + private int screenHeight; + + private Camera3D camera; + private Shader shader; + private int viewEyeLoc; + private int viewCenterLoc; + private int runTimeLoc; + private int resolutionLoc; + private float runTime; + + public void Init() + { + screenWidth = GetScreenWidth(); + screenHeight = GetScreenHeight(); + + camera = new(); + camera.Position = new Vector3(2.5f, 2.5f, 3.0f); // Camera position + camera.Target = new Vector3(0.0f, 0.0f, 0.7f); // Camera looking at point + camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target) + camera.FovY = 65.0f; // Camera field-of-view Y + camera.Projection = CameraProjection.Perspective; // Camera projection type + + // Load raymarching shader + // NOTE: Defining 0 (NULL) for vertex shader forces usage of internal default vertex shader + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/raymarching.fs"); + + // Get shader locations for required uniforms + viewEyeLoc = GetShaderLocation(shader, "viewEye"); + viewCenterLoc = GetShaderLocation(shader, "viewCenter"); + runTimeLoc = GetShaderLocation(shader, "runTime"); + resolutionLoc = GetShaderLocation(shader, "resolution"); + + float[] resolution = { (float)screenWidth, (float)screenHeight }; + Raylib.SetShaderValue(shader, resolutionLoc, resolution, ShaderUniformDataType.Vec2); + + runTime = 0.0f; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.FirstPerson); + + var deltaTime = GetFrameTime(); + runTime += deltaTime; + + // Set shader required uniform values + Raylib.SetShaderValue(shader, viewEyeLoc, camera.Position, ShaderUniformDataType.Vec3); + Raylib.SetShaderValue(shader, viewCenterLoc, camera.Target, ShaderUniformDataType.Vec3); + Raylib.SetShaderValue(shader, runTimeLoc, runTime, ShaderUniformDataType.Float); + + // Check if screen is resized + if (IsWindowResized()) + { + screenWidth = GetScreenWidth(); + screenHeight = GetScreenHeight(); + var resolution = new float[] { (float)screenWidth, (float)screenHeight }; + Raylib.SetShaderValue(shader, resolutionLoc, resolution, ShaderUniformDataType.Vec2); + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // We only draw a white full-screen rectangle, + // frame is generated in shader using raymarching + BeginShaderMode(shader); + DrawRectangle(0, 0, screenWidth, screenHeight, Color.White); + EndShaderMode(); + + DrawText( + "(c) Raymarching shader by Iñigo Quilez. MIT License.", + screenWidth - 280, + screenHeight - 20, + 10, + Color.Black + ); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); // Unload shader + } public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - int screenWidth = 800; - int screenHeight = 450; - SetConfigFlags(ResizableWindow); - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - raymarching shapes"); + InitWindow(800, 450, "raylib [shaders] example - raymarching rendering"); - Camera3D camera = new(); - camera.Position = new Vector3(2.5f, 2.5f, 3.0f); - camera.Target = new Vector3(0.0f, 0.0f, 0.7f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.FovY = 65.0f; - - // Load raymarching shader - // NOTE: Defining 0 (NULL) for vertex shader forces usage of internal default vertex shader - Shader shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/raymarching.fs"); - - // Get shader locations for required uniforms - int viewEyeLoc = GetShaderLocation(shader, "viewEye"); - int viewCenterLoc = GetShaderLocation(shader, "viewCenter"); - int runTimeLoc = GetShaderLocation(shader, "runTime"); - int resolutionLoc = GetShaderLocation(shader, "resolution"); - - float[] resolution = { (float)screenWidth, (float)screenHeight }; - Raylib.SetShaderValue(shader, resolutionLoc, resolution, ShaderUniformDataType.Vec2); - - float runTime = 0.0f; - - SetTargetFPS(60); + DisableCursor(); // Limit cursor to relative movement inside the window + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new Raymarching(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Check if screen is resized - //---------------------------------------------------------------------------------- - if (IsWindowResized()) - { - screenWidth = GetScreenWidth(); - screenHeight = GetScreenHeight(); - resolution = new float[] { (float)screenWidth, (float)screenHeight }; - Raylib.SetShaderValue(shader, resolutionLoc, resolution, ShaderUniformDataType.Vec2); - } - - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Free); - - float deltaTime = GetFrameTime(); - runTime += deltaTime; - - // Set shader required uniform values - Raylib.SetShaderValue(shader, viewEyeLoc, camera.Position, ShaderUniformDataType.Vec3); - Raylib.SetShaderValue(shader, viewCenterLoc, camera.Target, ShaderUniformDataType.Vec3); - Raylib.SetShaderValue(shader, runTimeLoc, runTime, ShaderUniformDataType.Float); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // We only draw a white full-screen rectangle, - // frame is generated in shader using raymarching - BeginShaderMode(shader); - DrawRectangle(0, 0, screenWidth, screenHeight, Color.White); - EndShaderMode(); - - DrawText( - "(c) Raymarching shader by Iñigo Quilez. MIT License.", - screenWidth - 280, - screenHeight - 20, - 10, - Color.Black - ); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadShader(shader); - - CloseWindow(); + 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/ShapesTextures.cs b/Examples/Shaders/ShapesTextures.cs index 2def789..c0a5eef 100644 --- a/Examples/Shaders/ShapesTextures.cs +++ b/Examples/Shaders/ShapesTextures.cs @@ -1,18 +1,22 @@ /******************************************************************************************* * -* raylib [shaders] example - Apply a shader to some shape or texture +* raylib [shaders] example - shapes textures +* +* 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. +* 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), to test this example * on OpenGL ES 2.0 platforms (Android, Raspberry Pi, HTML5), use #version 100 shaders * raylib comes with shaders ready for both versions, check raylib/shaders install folder * -* This example has been created using raylib 1.7 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.7, last time updated with raylib 3.7 * -* Copyright (c) 2015 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) 2015-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -21,101 +25,123 @@ using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class ShapesTextures +public class ShapesTextures : 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 / Shapes Textures"; + + public string Title => "raylib [shaders] example - shapes textures"; + + private Texture2D fudesumi; + private Shader shader; + + public void Init() + { + fudesumi = LoadTexture("resources/fudesumi.png"); + + // Load shader to be used on some parts drawing + // NOTE 1: Using GLSL 330 shader version, on OpenGL ES 2.0 use GLSL 100 shader version + // NOTE 2: Defining null (NULL) for vertex shader forces usage of internal default vertex shader + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/grayscale.fs"); + } + + public void Update() + { + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // Start drawing with default shader + DrawText("USING DEFAULT SHADER", 20, 40, 10, Color.Red); + + DrawCircle(80, 120, 35, Color.DarkBlue); + DrawCircleGradient(new Vector2(80, 220), 60, Color.Green, Color.SkyBlue); + DrawCircleLines(80, 340, 80, Color.DarkBlue); + + + // Activate our custom shader to be applied on next shapes/textures drawings + BeginShaderMode(shader); + + DrawText("USING CUSTOM SHADER", 190, 40, 10, Color.Red); + + DrawRectangle(250 - 60, 90, 120, 60, Color.Red); + DrawRectangleGradientH(250 - 90, 170, 180, 130, Color.Maroon, Color.Gold); + DrawRectangleLines(250 - 40, 320, 80, 60, Color.Orange); + + // Activate our default shader for next drawings + EndShaderMode(); + + DrawText("USING DEFAULT SHADER", 370, 40, 10, Color.Red); + + DrawTriangle( + new Vector2(430, 80), + new Vector2(430 - 60, 150), + new Vector2(430 + 60, 150), Color.Violet + ); + + DrawTriangleLines( + new Vector2(430, 160), + new Vector2(430 - 20, 230), + new Vector2(430 + 20, 230), Color.DarkBlue + ); + + DrawPoly(new Vector2(430, 320), 6, 80, 0, Color.Brown); + + // Activate our custom shader to be applied on next shapes/textures drawings + BeginShaderMode(shader); + + // Using custom shader + DrawTexture(fudesumi, 500, -30, Color.White); + + // Activate our default shader for next drawings + EndShaderMode(); + + DrawText("(c) Fudesumi sprite by Eiden Marsal", 380, screenHeight - 20, 10, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); // Unload shader + UnloadTexture(fudesumi); // Unload texture + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - shapes textures"); - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - shapes and texture shaders"); - - Texture2D fudesumi = LoadTexture("resources/fudesumi.png"); - - // NOTE: Using GLSL 330 shader version, on OpenGL ES 2.0 use GLSL 100 shader version - Shader shader = LoadShader( - "resources/shaders/glsl330/base.vs", - "resources/shaders/glsl330/grayscale.fs" - ); - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new ShapesTextures(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // TODO: Update your variables here - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // Start drawing with default shader - DrawText("USING DEFAULT SHADER", 20, 40, 10, Color.Red); - - DrawCircle(80, 120, 35, Color.DarkBlue); - DrawCircleGradient(new Vector2(80, 220), 60, Color.Green, Color.SkyBlue); - DrawCircleLines(80, 340, 80, Color.DarkBlue); - - - // Activate our custom shader to be applied on next shapes/textures drawings - BeginShaderMode(shader); - - DrawText("USING CUSTOM SHADER", 190, 40, 10, Color.Red); - - DrawRectangle(250 - 60, 90, 120, 60, Color.Red); - DrawRectangleGradientH(250 - 90, 170, 180, 130, Color.Maroon, Color.Gold); - DrawRectangleLines(250 - 40, 320, 80, 60, Color.Orange); - - // Activate our default shader for next drawings - EndShaderMode(); - - DrawText("USING DEFAULT SHADER", 370, 40, 10, Color.Red); - - DrawTriangle( - new Vector2(430, 80), - new Vector2(430 - 60, 150), - new Vector2(430 + 60, 150), Color.Violet - ); - - DrawTriangleLines( - new Vector2(430, 160), - new Vector2(430 - 20, 230), - new Vector2(430 + 20, 230), Color.DarkBlue - ); - - DrawPoly(new Vector2(430, 320), 6, 80, 0, Color.Brown); - - // Activate our custom shader to be applied on next shapes/textures drawings - BeginShaderMode(shader); - - // Using custom shader - DrawTexture(fudesumi, 500, -30, Color.White); - - // Activate our default shader for next drawings - EndShaderMode(); - - DrawText("(c) Fudesumi sprite by Eiden Marsal", 380, screenHeight - 20, 10, Color.Gray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadShader(shader); - UnloadTexture(fudesumi); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } - } diff --git a/Examples/Shaders/SimpleMask.cs b/Examples/Shaders/SimpleMask.cs index ce43c2f..032aa90 100644 --- a/Examples/Shaders/SimpleMask.cs +++ b/Examples/Shaders/SimpleMask.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [shaders] example - Simple shader mask +* raylib [shaders] example - simple mask * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Example contributed by Chris Camacho (@codifies) and reviewed by Ramon Santamaria (@raysan5) +* Example originally created with raylib 2.5, last time updated with raylib 3.7 * -* Copyright (c) 2019 Chris Camacho (@codifies) and Ramon Santamaria (@raysan5) +* Example contributed by Chris Camacho (@chriscamacho) 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 Chris Camacho (@chriscamacho) and Ramon Santamaria (@raysan5) * ******************************************************************************************** * @@ -24,54 +28,72 @@ using static Raylib_cs.Raymath; namespace Examples.Shaders; -public class SimpleMask +public class SimpleMask : IExample { - public unsafe static int Main() + 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 / Simple Mask"; + + public string Title => "raylib [shaders] example - simple mask"; + + public bool CursorDisabled => true; + + private Camera3D camera; + private Model model1; + private Model model2; + private Model model3; + private Shader shader; + private Texture2D texDiffuse; + private Texture2D texMask; + private int shaderFrame; + private int framesCounter; + private Vector3 rotation; + + public unsafe void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib - simple shader mask"); - // Define the camera to look into our 3d world - Camera3D camera = new(); - camera.Position = new Vector3(0.0f, 1.0f, 2.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; + camera = new(); + camera.Position = new Vector3(0.0f, 1.0f, 2.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 // Define our three models to show the shader on - Mesh torus = GenMeshTorus(.3f, 1, 16, 32); - Model model1 = LoadModelFromMesh(torus); + var torus = GenMeshTorus(.3f, 1, 16, 32); + model1 = LoadModelFromMesh(torus); - Mesh cube = GenMeshCube(.8f, .8f, .8f); - Model model2 = LoadModelFromMesh(cube); + var cube = GenMeshCube(.8f, .8f, .8f); + model2 = LoadModelFromMesh(cube); // Generate model to be shaded just to see the gaps in the other two - Mesh sphere = GenMeshSphere(1, 16, 16); - Model model3 = LoadModelFromMesh(sphere); + var sphere = GenMeshSphere(1, 16, 16); + model3 = LoadModelFromMesh(sphere); // Load the shader - Shader shader = LoadShader("resources/shaders/glsl330/mask.vs", "resources/shaders/glsl330/mask.fs"); + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/mask.fs"); // Load and apply the diffuse texture (colour map) - Texture2D texDiffuse = LoadTexture("resources/plasma.png"); + texDiffuse = LoadTexture("resources/plasma.png"); - Material* materials = model1.Materials; - MaterialMap* maps = materials[0].Maps; + var materials = model1.Materials; + var maps = materials[0].Maps; model1.Materials[0].Maps[(int)MaterialMapIndex.Albedo].Texture = texDiffuse; materials = model2.Materials; maps = materials[0].Maps; maps[(int)MaterialMapIndex.Albedo].Texture = texDiffuse; - // Using MAP_EMISSION as a spare slot to use for 2nd texture - // NOTE: Don't use MAP_IRRADIANCE, MAP_PREFILTER or MAP_CUBEMAP - // as they are bound as cube maps - Texture2D texMask = LoadTexture("resources/mask.png"); + // Using MATERIAL_MAP_EMISSION as a spare slot to use for 2nd texture + // NOTE: Don't use MATERIAL_MAP_IRRADIANCE, MATERIAL_MAP_PREFILTER or MATERIAL_MAP_CUBEMAP as they are bound as cube maps + texMask = LoadTexture("resources/mask.png"); materials = model1.Materials; maps = (MaterialMap*)materials[0].Maps; @@ -81,11 +103,11 @@ public class SimpleMask maps = (MaterialMap*)materials[0].Maps; maps[(int)MaterialMapIndex.Emission].Texture = texMask; - int* locs = shader.Locs; + var locs = shader.Locs; locs[(int)ShaderLocationIndex.MapEmission] = GetShaderLocation(shader, "mask"); // Frame is incremented each frame to animate the shader - int shaderFrame = GetShaderLocation(shader, "framesCounter"); + shaderFrame = GetShaderLocation(shader, "frame"); // Apply the shader to the two models materials = model1.Materials; @@ -94,69 +116,88 @@ public class SimpleMask materials = (Material*)model2.Materials; materials[0].Shader = shader; - int framesCounter = 0; + framesCounter = 0; + rotation = new(0, 0, 0); // Model rotation angles + } - // Model rotation angles - Vector3 rotation = new(0, 0, 0); + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.FirstPerson); - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- + framesCounter++; + rotation.X += 0.01f; + rotation.Y += 0.005f; + rotation.Z -= 0.0025f; - // Main game loop - while (!WindowShouldClose()) - { - // Update - //---------------------------------------------------------------------------------- - framesCounter++; - rotation.X += 0.01f; - rotation.Y += 0.005f; - rotation.Z -= 0.0025f; + // Send frames counter to shader for animation + Raylib.SetShaderValue(shader, shaderFrame, framesCounter, ShaderUniformDataType.Int); - // Send frames counter to shader for animation - Raylib.SetShaderValue(shader, shaderFrame, framesCounter, ShaderUniformDataType.Int); + // Rotate one of the models + model1.Transform = MatrixRotateXYZ(rotation); + //---------------------------------------------------------------------------------- - // Rotate one of the models - model1.Transform = MatrixRotateXYZ(rotation); + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.DarkBlue); - UpdateCamera(ref camera, CameraMode.Custom); - //---------------------------------------------------------------------------------- + BeginMode3D(camera); - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.DarkBlue); + DrawModel(model1, new Vector3(0.5f, 0, 0), 1, Color.White); + DrawModelEx(model2, new Vector3(-.5f, 0, 0), new Vector3(1, 1, 0), 50, new Vector3(1, 1, 1), Color.White); + DrawModel(model3, new Vector3(0, 0, -1.5f), 1, Color.White); + DrawGrid(10, 1.0f); // Draw a grid - BeginMode3D(camera); + EndMode3D(); - DrawModel(model1, new Vector3(0.5f, 0, 0), 1, Color.White); - DrawModelEx(model2, new Vector3(-.5f, 0, 0), new Vector3(1, 1, 0), 50, new Vector3(1, 1, 1), Color.White); - DrawModel(model3, new Vector3(0, 0, -1.5f), 1, Color.White); - DrawGrid(10, 1.0f); + var frameText = $"Frame: {framesCounter}"; + DrawRectangle(16, 698, MeasureText(frameText, 20) + 8, 42, Color.Blue); + DrawText(frameText, 20, 700, 20, Color.White); - EndMode3D(); + DrawFPS(10, 10); - string frameText = $"Frame: {framesCounter}"; - DrawRectangle(16, 698, MeasureText(frameText, 20) + 8, 42, Color.Blue); - DrawText(frameText, 20, 700, 20, Color.White); + EndDrawing(); + //---------------------------------------------------------------------------------- + } - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- - } - - // De-Initialization - //-------------------------------------------------------------------------------------- + public void Unload() + { UnloadModel(model1); UnloadModel(model2); UnloadModel(model3); - UnloadTexture(texDiffuse); - UnloadTexture(texMask); + UnloadTexture(texDiffuse); // Unload default diffuse texture + UnloadTexture(texMask); // Unload texture mask - UnloadShader(shader); + UnloadShader(shader); // Unload shader + } - CloseWindow(); + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - simple mask"); + + DisableCursor(); // Limit cursor to relative movement inside the window + SetTargetFPS(60); // Set to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new SimpleMask(); + 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/Spotlight.cs b/Examples/Shaders/Spotlight.cs index cf86e3a..b8e37c4 100644 --- a/Examples/Shaders/Spotlight.cs +++ b/Examples/Shaders/Spotlight.cs @@ -1,29 +1,32 @@ /******************************************************************************************* * -* raylib [shaders] example - Simple shader mask +* raylib [shaders] example - spotlight rendering * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Example contributed by Chris Camacho (@chriscamacho - http://bedroomcoders.co.uk/) -* and reviewed by Ramon Santamaria (@raysan5) +* Example originally created with raylib 2.5, last time updated with raylib 3.7 * -* Copyright (c) 2019 Chris Camacho (@chriscamacho) and Ramon Santamaria (@raysan5) +* Example contributed by Chris Camacho (@chriscamacho) 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 Chris Camacho (@chriscamacho) and Ramon Santamaria (@raysan5) * ******************************************************************************************** * -* The shader makes alpha holes in the forground to give the apearance of a top +* The shader makes alpha holes in the forground to give the appearance of a top * down look at a spotlight casting a pool of light... * * The right hand side of the screen there is just enough light to see whats * going on without the spot light, great for a stealth type game where you -* have to avoid the spotlights. +* have to avoid the spotlights * -* The left hand side of the screen is in pitch dark except for where the spotlights are. +* The left hand side of the screen is in pitch dark except for where the spotlights are * * Although this example doesn't scale like the letterbox example, you could integrate * the two techniques, but by scaling the actual colour of the render texture rather -* than using alpha as a mask. +* than using alpha as a mask * ********************************************************************************************/ @@ -33,14 +36,29 @@ using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class Spotlight +public class Spotlight : 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 + // NOTE: It must be the same as define in shader - const int MaxSpots = 3; - const int MaxStars = 400; + private const int MaxSpots = 3; + private const int MaxStars = 400; + + public string Name => "Shaders / Spotlight"; + + public string Title => "raylib [shaders] example - spotlight rendering"; + + public bool CursorHidden => true; // Spot data - struct Spot + private struct Spot { public Vector2 pos; public Vector2 vel; @@ -54,53 +72,51 @@ public class Spotlight } // Stars in the star field have a position and velocity - struct Star + private struct Star { public Vector2 pos; public Vector2 vel; } - public static int Main() + private Texture2D texRay; + private Star[] stars; + private int frameCounter; + private Shader shdrSpot; + private Spot[] spots; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + texRay = LoadTexture("resources/raysan.png"); - InitWindow(screenWidth, screenHeight, "raylib - shader spotlight"); - HideCursor(); + stars = new Star[MaxStars]; - Texture2D texRay = LoadTexture("resources/raysan.png"); - - Star[] stars = new Star[MaxStars]; - - for (int n = 0; n < MaxStars; n++) + for (var n = 0; n < MaxStars; n++) { ResetStar(ref stars[n]); } // Progress all the stars on, so they don't all start in the centre - for (int m = 0; m < screenWidth / 2.0; m++) + for (var m = 0; m < screenWidth / 2.0; m++) { - for (int n = 0; n < MaxStars; n++) + for (var n = 0; n < MaxStars; n++) { UpdateStar(ref stars[n]); } } - int frameCounter = 0; + frameCounter = 0; // Use default vert shader - Shader shdrSpot = LoadShader(null, "resources/shaders/glsl330/spotlight.fs"); + shdrSpot = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/spotlight.fs"); // Get the locations of spots in the shader - Spot[] spots = new Spot[MaxSpots]; + spots = new Spot[MaxSpots]; - for (int i = 0; i < MaxSpots; i++) + for (var i = 0; i < MaxSpots; i++) { - string posName = $"spots[{i}].pos"; - string innerName = $"spots[{i}].inner"; - string radiusName = $"spots[{i}].radius"; + var posName = $"spots[{i}].pos"; + var innerName = $"spots[{i}].inner"; + var radiusName = $"spots[{i}].radius"; spots[i].posLoc = GetShaderLocation(shdrSpot, posName); spots[i].innerLoc = GetShaderLocation(shdrSpot, innerName); @@ -108,14 +124,14 @@ public class Spotlight } // Tell the shader how wide the screen is so we can have - // a pitch Color.black half and a dimly lit half. - int wLoc = GetShaderLocation(shdrSpot, "screenWidth"); - float sw = (float)GetScreenWidth(); + // a pitch black half and a dimly lit half + var wLoc = GetShaderLocation(shdrSpot, "screenWidth"); + var sw = (float)GetScreenWidth(); Raylib.SetShaderValue(shdrSpot, wLoc, sw, ShaderUniformDataType.Float); - // Randomise the locations and velocities of the spotlights - // and initialise the shader locations - for (int i = 0; i < MaxSpots; i++) + // Randomize the locations and velocities of the spotlights + // and initialize the shader locations + for (var i = 0; i < MaxSpots; i++) { spots[i].pos.X = GetRandomValue(64, screenWidth - 64); spots[i].pos.Y = GetRandomValue(64, screenHeight - 64); @@ -123,8 +139,8 @@ public class Spotlight while ((MathF.Abs(spots[i].vel.X) + MathF.Abs(spots[i].vel.Y)) < 2) { - spots[i].vel.X = GetRandomValue(-40, 40) / 10.0f; - spots[i].vel.Y = GetRandomValue(-40, 40) / 10.0f; + spots[i].vel.X = GetRandomValue(-400, 40) / 25.0f; + spots[i].vel.Y = GetRandomValue(-400, 40) / 25.0f; } spots[i].inner = 28.0f * (i + 1); @@ -149,118 +165,110 @@ public class Spotlight ShaderUniformDataType.Float ); } - - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- - - // Main game loop - while (!WindowShouldClose()) - { - // Update - //---------------------------------------------------------------------------------- - frameCounter++; - - // Move the stars, resetting them if the go offscreen - for (int n = 0; n < MaxStars; n++) - { - UpdateStar(ref stars[n]); - } - - // Update the spots, send them to the shader - for (int i = 0; i < MaxSpots; i++) - { - if (i == 0) - { - Vector2 mp = GetMousePosition(); - spots[i].pos.X = mp.X; - spots[i].pos.Y = screenHeight - mp.Y; - } - else - { - spots[i].pos.X += spots[i].vel.X; - spots[i].pos.Y += spots[i].vel.Y; - - if (spots[i].pos.X < 64) - { - spots[i].vel.X = -spots[i].vel.X; - } - - if (spots[i].pos.X > (screenWidth - 64)) - { - spots[i].vel.X = -spots[i].vel.X; - } - - if (spots[i].pos.Y < 64) - { - spots[i].vel.Y = -spots[i].vel.Y; - } - - if (spots[i].pos.Y > (screenHeight - 64)) - { - spots[i].vel.Y = -spots[i].vel.Y; - } - } - - Raylib.SetShaderValue( - shdrSpot, - spots[i].posLoc, - spots[i].pos, - ShaderUniformDataType.Vec2 - ); - } - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.DarkBlue); - - // Draw stars and bobs - for (int n = 0; n < MaxStars; n++) - { - // MathF.Single pixel is just too small these days! - DrawRectangle((int)stars[n].pos.X, (int)stars[n].pos.Y, 2, 2, Color.White); - } - - for (int i = 0; i < 16; i++) - { - DrawTexture( - texRay, - (int)((screenWidth / 2.0) + MathF.Cos((frameCounter + i * 8) / 51.45f) * (screenWidth / 2.2) - 32), - (int)((screenHeight / 2.0) + MathF.Sin((frameCounter + i * 8) / 17.87f) * (screenHeight / 4.2)), - Color.White - ); - } - - // Draw spot lights - BeginShaderMode(shdrSpot); - - // Instead of a blank rectangle you could render a render texture of the full screen used to do screen - // scaling (slight adjustment to shader would be required to actually pay attention to the colour!) - DrawRectangle(0, 0, screenWidth, screenHeight, Color.White); - EndShaderMode(); - - DrawFPS(10, 10); - - DrawText("Move the mouse!", 10, 30, 20, Color.Green); - DrawText("Pitch Color.Black", (int)(screenWidth * 0.2f), screenHeight / 2, 20, Color.Green); - DrawText("Dark", (int)(screenWidth * 0.66f), screenHeight / 2, 20, Color.Green); - - EndDrawing(); - //---------------------------------------------------------------------------------- - } - - // De-Initialization - //-------------------------------------------------------------------------------------- - UnloadTexture(texRay); - UnloadShader(shdrSpot); - - CloseWindow(); - //-------------------------------------------------------------------------------------- - - return 0; } - static void ResetStar(ref Star s) + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + frameCounter++; + + // Move the stars, resetting them if the go offscreen + for (var n = 0; n < MaxStars; n++) + { + UpdateStar(ref stars[n]); + } + + // Update the spots, send them to the shader + for (var i = 0; i < MaxSpots; i++) + { + if (i == 0) + { + var mp = GetMousePosition(); + spots[i].pos.X = mp.X; + spots[i].pos.Y = screenHeight - mp.Y; + } + else + { + spots[i].pos.X += spots[i].vel.X; + spots[i].pos.Y += spots[i].vel.Y; + + if (spots[i].pos.X < 64) + { + spots[i].vel.X = -spots[i].vel.X; + } + + if (spots[i].pos.X > (screenWidth - 64)) + { + spots[i].vel.X = -spots[i].vel.X; + } + + if (spots[i].pos.Y < 64) + { + spots[i].vel.Y = -spots[i].vel.Y; + } + + if (spots[i].pos.Y > (screenHeight - 64)) + { + spots[i].vel.Y = -spots[i].vel.Y; + } + } + + Raylib.SetShaderValue( + shdrSpot, + spots[i].posLoc, + spots[i].pos, + ShaderUniformDataType.Vec2 + ); + } + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.DarkBlue); + + // Draw stars and bobs + for (var n = 0; n < MaxStars; n++) + { + // Single pixel is just too small these days! + DrawRectangle((int)stars[n].pos.X, (int)stars[n].pos.Y, 2, 2, Color.White); + } + + for (var i = 0; i < 16; i++) + { + DrawTexture( + texRay, + (int)((screenWidth / 2.0) + MathF.Cos((frameCounter + i * 8) / 51.45f) * (screenWidth / 2.2) - 32), + (int)((screenHeight / 2.0) + MathF.Sin((frameCounter + i * 8) / 17.87f) * (screenHeight / 4.2)), + Color.White + ); + } + + // Draw spot lights + BeginShaderMode(shdrSpot); + + // Instead of a blank rectangle you could render a render texture of the full screen used to do screen + // scaling (slight adjustment to shader would be required to actually pay attention to the colour!) + DrawRectangle(0, 0, screenWidth, screenHeight, Color.White); + EndShaderMode(); + + DrawFPS(10, 10); + + DrawText("Move the mouse!", 10, 30, 20, Color.Green); + DrawText("Pitch Black", (int)(screenWidth * 0.2f), screenHeight / 2, 20, Color.Green); + DrawText("Dark", (int)(screenWidth * 0.66f), screenHeight / 2, 20, Color.Green); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texRay); + UnloadShader(shdrSpot); + } + + private static void ResetStar(ref Star s) { s.pos = new Vector2(GetScreenWidth() / 2.0f, GetScreenHeight() / 2.0f); @@ -270,10 +278,10 @@ public class Spotlight s.vel.Y = (float)GetRandomValue(-1000, 1000) / 100.0f; } while (!((MathF.Abs(s.vel.X) + (MathF.Abs(s.vel.Y)) > 1))); - s.pos += s.pos + (s.vel * new Vector2(8.0f, 8.0f)); + s.pos += s.vel * new Vector2(8.0f, 8.0f); } - static void UpdateStar(ref Star s) + private static void UpdateStar(ref Star s) { s.pos += s.vel; @@ -283,4 +291,33 @@ public class Spotlight ResetStar(ref s); } } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - spotlight rendering"); + HideCursor(); + + SetTargetFPS(60); // Set to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new Spotlight(); + 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/Shaders/TextureDrawing.cs b/Examples/Shaders/TextureDrawing.cs index 001e900..1d70b32 100644 --- a/Examples/Shaders/TextureDrawing.cs +++ b/Examples/Shaders/TextureDrawing.cs @@ -1,15 +1,17 @@ /******************************************************************************************* * -* raylib [textures] example - Texture drawing +* raylib [shaders] example - texture rendering * -* This example illustrates how to draw on a blank texture using a shader +* Example complexity rating: [★★☆☆] 2/4 * -* This example has been created using raylib 2.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 2.0, last time updated with raylib 3.7 * -* Example contributed by Michał Ciesielski and reviewed by Ramon Santamaria (@raysan5) +* Example contributed by Michał Ciesielski (@ciessielski) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2019 Michał Ciesielski and 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 Michał Ciesielski (@ciessielski) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -17,68 +19,92 @@ using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class TextureDrawing +public class TextureDrawing : IExample { - const int GlslVersion = 330; + 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 Drawing"; + + public string Title => "raylib [shaders] example - texture rendering"; + + private Texture2D texture; + private Shader shader; + private float time; + private int timeLoc; + + public void Init() + { + var imBlank = GenImageColor(1024, 1024, Color.Blank); + texture = LoadTextureFromImage(imBlank); // Load blank texture to fill on shader + UnloadImage(imBlank); + + // NOTE: Using GLSL 330 shader version, on OpenGL ES 2.0 use GLSL 100 shader version + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/cubes_panning.fs"); + + time = 0.0f; + timeLoc = GetShaderLocation(shader, "uTime"); + Raylib.SetShaderValue(shader, timeLoc, time, ShaderUniformDataType.Float); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + time = (float)GetTime(); + Raylib.SetShaderValue(shader, timeLoc, time, ShaderUniformDataType.Float); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginShaderMode(shader); // Enable our custom shader for next shapes/textures drawings + DrawTexture(texture, 0, 0, Color.White); // Drawing BLANK texture, all rendering magic happens on shader + EndShaderMode(); // Disable our custom shader, return to default shader + + DrawText("BACKGROUND is PAINTED and ANIMATED on SHADER!", 10, 10, 20, Color.Maroon); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); + UnloadTexture(texture); + } public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - texture rendering"); - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - texture drawing"); - - // Load blank texture to fill on shader - Image imBlank = GenImageColor(1024, 1024, Color.Blank); - Texture2D texture = LoadTextureFromImage(imBlank); - UnloadImage(imBlank); - - // NOTE: Using GLSL 330 shader version, on OpenGL ES 2.0 use GLSL 100 shader version - Shader shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/cubes_panning.fs"); - - float time = 0.0f; - int timeLoc = GetShaderLocation(shader, "uTime"); - Raylib.SetShaderValue(shader, timeLoc, time, ShaderUniformDataType.Float); - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new TextureDrawing(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - time = (float)GetTime(); - Raylib.SetShaderValue(shader, timeLoc, time, ShaderUniformDataType.Float); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // Enable our custom shader for next shapes/textures drawings - BeginShaderMode(shader); - - // Drawing blank texture, all magic happens on shader - DrawTexture(texture, 0, 0, Color.White); - - // Disable our custom shader, return to default shader - EndShaderMode(); - - DrawText("BACKGROUND is PAINTED and ANIMATED on SHADER!", 10, 10, 20, Color.Maroon); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadShader(shader); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Shaders/TextureOutline.cs b/Examples/Shaders/TextureOutline.cs index 4023d5c..63cf304 100644 --- a/Examples/Shaders/TextureOutline.cs +++ b/Examples/Shaders/TextureOutline.cs @@ -1,15 +1,20 @@ /******************************************************************************************* * -* raylib [textures] example - Texture drawing +* raylib [shaders] example - texture outline * -* This example illustrates how to draw on a blank texture using a shader +* Example complexity rating: [★★★☆] 3/4 * -* This example has been created using raylib 2.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* 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 contributed by Michał Ciesielski and reviewed by Ramon Santamaria (@raysan5) +* Example originally created with raylib 4.0, last time updated with raylib 4.0 * -* Copyright (c) 2019 Michał Ciesielski and Ramon Santamaria (@raysan5) +* Example contributed by Serenity Skiff (@GoldenThumbs) 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 Serenity Skiff (@GoldenThumbs) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -17,32 +22,41 @@ using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class TextureOutline +public class TextureOutline : IExample { - const int GLSL_VERSION = 330; + private const int screenWidth = 800; + private const int screenHeight = 450; - public static int Main() +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Texture Outline"; + + public string Title => "raylib [shaders] example - texture outline"; + + private Texture2D texture; + private Shader shdrOutline; + private float outlineSize; + private int outlineSizeLoc; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + texture = LoadTexture("resources/fudesumi.png"); + shdrOutline = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/outline.fs"); - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - Apply an outline to a texture"); + outlineSize = 2.0f; - Texture2D texture = LoadTexture("resources/fudesumi.png"); - Shader shdrOutline = LoadShader(null, $"resources/shaders/glsl{GLSL_VERSION}/outline.fs"); - - float outlineSize = 2.0f; - - // Normalized red color - float[] outlineColor = new[] { 1.0f, 0.0f, 0.0f, 1.0f }; + // Normalized RED color + var outlineColor = new[] { 1.0f, 0.0f, 0.0f, 1.0f }; float[] textureSize = { (float)texture.Width, (float)texture.Height }; // Get shader locations - int outlineSizeLoc = GetShaderLocation(shdrOutline, "outlineSize"); - int outlineColorLoc = GetShaderLocation(shdrOutline, "outlineColor"); - int textureSizeLoc = GetShaderLocation(shdrOutline, "textureSize"); + outlineSizeLoc = GetShaderLocation(shdrOutline, "outlineSize"); + var outlineColorLoc = GetShaderLocation(shdrOutline, "outlineColor"); + var textureSizeLoc = GetShaderLocation(shdrOutline, "textureSize"); // Set shader values (they can be changed later) Raylib.SetShaderValue( @@ -63,55 +77,75 @@ public class TextureOutline textureSize, ShaderUniformDataType.Vec2 ); + } - SetTargetFPS(60); + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + outlineSize += GetMouseWheelMove(); + if (outlineSize < 1.0f) + { + outlineSize = 1.0f; + } + + Raylib.SetShaderValue( + shdrOutline, + outlineSizeLoc, + outlineSize, + ShaderUniformDataType.Float + ); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(Color.RayWhite); + + BeginShaderMode(shdrOutline); + DrawTexture(texture, GetScreenWidth() / 2 - texture.Width / 2, -30, Color.White); + EndShaderMode(); + + DrawText("Shader-based\ntexture\noutline", 10, 10, 20, Color.Gray); + DrawText("Scroll mouse wheel to\nchange outline size", 10, 72, 20, Color.Gray); + DrawText($"Outline size: {(int)outlineSize} px", 10, 120, 20, Color.Maroon); + + DrawFPS(710, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texture); + UnloadShader(shdrOutline); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - texture outline"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new TextureOutline(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - outlineSize += GetMouseWheelMove(); - if (outlineSize < 1.0f) - { - outlineSize = 1.0f; - } - - Raylib.SetShaderValue( - shdrOutline, - outlineSizeLoc, - outlineSize, - ShaderUniformDataType.Float - ); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - - ClearBackground(Color.RayWhite); - - BeginShaderMode(shdrOutline); - DrawTexture(texture, GetScreenWidth() / 2 - texture.Width / 2, -30, Color.White); - EndShaderMode(); - - DrawText("Shader-based\ntexture\noutline", 10, 10, 20, Color.Gray); - - DrawText($"Outline size: {outlineSize} px", 10, 120, 20, Color.Maroon); - - DrawFPS(710, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(texture); - UnloadShader(shdrOutline); - - CloseWindow(); + 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/TextureWaves.cs b/Examples/Shaders/TextureWaves.cs index 8446a92..bb49258 100644 --- a/Examples/Shaders/TextureWaves.cs +++ b/Examples/Shaders/TextureWaves.cs @@ -1,20 +1,24 @@ /******************************************************************************************* * -* raylib [shaders] example - Texture Waves +* raylib [shaders] example - texture waves +* +* 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. +* 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), to test this example * on OpenGL ES 2.0 platforms (Android, Raspberry Pi, HTML5), use #version 100 shaders * raylib comes with shaders ready for both versions, check raylib/shaders install folder * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 2.5, last time updated with raylib 3.7 * * Example contributed by Anata (@anatagawa) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2019 Anata (@anatagawa) and 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 Anata (@anatagawa) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -22,40 +26,49 @@ using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class TextureWaves +public class TextureWaves : IExample { - const int GlslVersion = 330; + private const int screenWidth = 800; + private const int screenHeight = 450; - public static int Main() +#if BROWSER + const int GlslVersion = 100; // WebGL1 needs GLSL ES 100 +#else + private const int GlslVersion = 330; +#endif + + public string Name => "Shaders / Texture Waves"; + + public string Title => "raylib [shaders] example - texture waves"; + + private Texture2D texture; + private Shader shader; + private int secondsLoc; + private float seconds; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - texture waves"); - // Load texture texture to apply shaders - Texture2D texture = LoadTexture("resources/space.png"); + texture = LoadTexture("resources/space.png"); // Load shader and setup location points and values - Shader shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/wave.fs"); + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/wave.fs"); - int secondsLoc = GetShaderLocation(shader, "secondes"); - int freqXLoc = GetShaderLocation(shader, "freqX"); - int freqYLoc = GetShaderLocation(shader, "freqY"); - int ampXLoc = GetShaderLocation(shader, "ampX"); - int ampYLoc = GetShaderLocation(shader, "ampY"); - int speedXLoc = GetShaderLocation(shader, "speedX"); - int speedYLoc = GetShaderLocation(shader, "speedY"); + secondsLoc = GetShaderLocation(shader, "seconds"); + var freqXLoc = GetShaderLocation(shader, "freqX"); + var freqYLoc = GetShaderLocation(shader, "freqY"); + var ampXLoc = GetShaderLocation(shader, "ampX"); + var ampYLoc = GetShaderLocation(shader, "ampY"); + var speedXLoc = GetShaderLocation(shader, "speedX"); + var speedYLoc = GetShaderLocation(shader, "speedY"); // Shader uniform values that can be updated at any time - float freqX = 25.0f; - float freqY = 25.0f; - float ampX = 5.0f; - float ampY = 5.0f; - float speedX = 8.0f; - float speedY = 8.0f; + var freqX = 25.0f; + var freqY = 25.0f; + var ampX = 5.0f; + var ampY = 5.0f; + var speedX = 8.0f; + var speedY = 8.0f; float[] screenSize = { (float)GetScreenWidth(), (float)GetScreenHeight() }; Raylib.SetShaderValue( @@ -71,43 +84,63 @@ public class TextureWaves Raylib.SetShaderValue(shader, speedXLoc, speedX, ShaderUniformDataType.Float); Raylib.SetShaderValue(shader, speedYLoc, speedY, ShaderUniformDataType.Float); - float seconds = 0.0f; + seconds = 0.0f; + } - SetTargetFPS(60); + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + seconds += GetFrameTime(); + + Raylib.SetShaderValue(shader, secondsLoc, seconds, ShaderUniformDataType.Float); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + BeginShaderMode(shader); + + DrawTexture(texture, 0, 0, Color.White); + DrawTexture(texture, texture.Width, 0, Color.White); + + EndShaderMode(); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadShader(shader); // Unload shader + UnloadTexture(texture); // Unload texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - texture waves"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new TextureWaves(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - seconds += GetFrameTime(); - - Raylib.SetShaderValue(shader, secondsLoc, seconds, ShaderUniformDataType.Float); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - BeginShaderMode(shader); - - DrawTexture(texture, 0, 0, Color.White); - DrawTexture(texture, texture.Width, 0, Color.White); - - EndShaderMode(); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadShader(shader); - UnloadTexture(texture); - - CloseWindow(); + 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/Shaders/WriteDepth.cs b/Examples/Shaders/WriteDepth.cs index 7b6a6ac..e2c917d 100644 --- a/Examples/Shaders/WriteDepth.cs +++ b/Examples/Shaders/WriteDepth.cs @@ -1,6 +1,8 @@ /******************************************************************************************* * -* raylib [shaders] example - Depth buffer writing +* raylib [shaders] example - depth writing +* +* Example complexity rating: [★★☆☆] 2/4 * * Example originally created with raylib 4.2, last time updated with raylib 4.2 * @@ -9,7 +11,7 @@ * 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-2023 Buğra Alptekin Sarı (@BugraAlptekinSari) +* Copyright (c) 2022-2025 Buğra Alptekin Sarı (@BugraAlptekinSari) * ********************************************************************************************/ @@ -18,86 +20,115 @@ using static Raylib_cs.Raylib; namespace Examples.Shaders; -public class WriteDepth +public class WriteDepth : IExample { - const int GLSL_VERSION = 330; + 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 / Write Depth"; + + public string Title => "raylib [shaders] example - depth writing"; + + private Camera3D camera; + private RenderTexture2D target; + private Shader shader; + + public void Init() + { + // Define the camera to look into our 3d world + camera = new(); + camera.Position = new Vector3(2.0f, 2.0f, 3.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 custom render texture with writable depth texture buffer + target = LoadRenderTextureDepthTex(screenWidth, screenHeight); + + // Load depth writing shader + // NOTE: The shader inverts the depth buffer by writing into it by `gl_FragDepth = 1 - gl_FragCoord.z;` + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/depth_write.fs"); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCamera(ref camera, CameraMode.Orbital); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + + // Draw into our custom render texture + BeginTextureMode(target); + ClearBackground(Color.White); + + BeginMode3D(camera); + BeginShaderMode(shader); + + DrawCubeWiresV(new Vector3(0.0f, 0.5f, 1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.Red); + DrawCubeV(new Vector3(0.0f, 0.5f, 1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.Purple); + DrawCubeWiresV(new Vector3(0.0f, 0.5f, -1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.DarkGreen); + DrawCubeV(new Vector3(0.0f, 0.5f, -1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.Yellow); + DrawGrid(10, 1.0f); + + EndShaderMode(); + EndMode3D(); + EndTextureMode(); + + // Draw into screen our custom render texture + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawTextureRec( + target.Texture, + new Rectangle(0, 0, screenWidth, -screenHeight), + Vector2.Zero, + Color.White + ); + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadRenderTextureDepthTex(target); + UnloadShader(shader); + } public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [shaders] example - depth writing"); - InitWindow(screenWidth, screenHeight, "raylib [shaders] example - write depth buffer"); - - // The shader inverts the depth buffer by writing into it by `gl_FragDepth = 1 - gl_FragCoord.z;` - Shader shader = LoadShader(null, $"resources/shaders/glsl{GLSL_VERSION}/write_depth.fs"); - - // Use customized function to create writable depth texture buffer - RenderTexture2D target = LoadRenderTextureDepthTex(screenWidth, screenHeight); - - // Define the camera to look into our 3d world - Camera3D camera; - camera.Position = new Vector3(2.0f, 2.0f, 3.0f); - camera.Target = new Vector3(0.0f, 0.5f, 0.0f); - camera.Up = new Vector3(0.0f, 1.0f, 0.0f); - camera.FovY = 45.0f; - camera.Projection = CameraProjection.Perspective; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new WriteDepth(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - UpdateCamera(ref camera, CameraMode.Orbital); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - - // Draw into our custom render texture (framebuffer) - BeginTextureMode(target); - ClearBackground(Color.White); - - BeginMode3D(camera); - BeginShaderMode(shader); - - DrawCubeWiresV(new Vector3(0.0f, 0.5f, 1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.Red); - DrawCubeV(new Vector3(0.0f, 0.5f, 1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.Purple); - DrawCubeWiresV(new Vector3(0.0f, 0.5f, -1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.DarkGreen); - DrawCubeV(new Vector3(0.0f, 0.5f, -1.0f), new Vector3(1.0f, 1.0f, 1.0f), Color.Yellow); - DrawGrid(10, 1.0f); - - EndShaderMode(); - EndMode3D(); - EndTextureMode(); - - // Draw custom render texture - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawTextureRec( - target.Texture, - new Rectangle(0, 0, screenWidth, -screenHeight), - Vector2.Zero, - Color.White - ); - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadRenderTextureDepthTex(target); - UnloadShader(shader); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; @@ -152,7 +183,7 @@ public class WriteDepth ); // Check if fbo is complete with attachments (valid) - if (Rlgl.FramebufferComplete(target.Id)) + if (Rlgl.FramebufferComplete(target.Id) != 0) { TraceLog(TraceLogLevel.Info, $"FBO: [ID {target.Id}] Framebuffer object created successfully"); } 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/BasicShapes.cs b/Examples/Shapes/BasicShapes.cs index 6846847..57f425c 100644 --- a/Examples/Shapes/BasicShapes.cs +++ b/Examples/Shapes/BasicShapes.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [shapes] example - Draw basic shapes 2d (rectangle, circle, line...) +* raylib [shapes] example - basic shapes * -* This example has been created using raylib 1.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 * -* Copyright (c) 2014 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.0, 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) 2014-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,66 +18,101 @@ using static Raylib_cs.Raylib; namespace Examples.Shapes; -public class BasicShapes +public partial class BasicShapes : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Basic Shapes"; + + public string Title => "raylib [shapes] example - basic shapes"; + + private float rotation; + + public void Init() + { + rotation = 0.0f; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + rotation += 0.2f; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText("some basic shapes available on raylib", 20, 20, 20, Color.DarkGray); + + // Circle shapes and lines + DrawCircle(screenWidth / 5, 120, 35, Color.DarkBlue); + DrawCircleGradient(new Vector2(screenWidth / 5.0f, 220.0f), 60, Color.Green, Color.SkyBlue); + DrawCircleLines(screenWidth / 5, 340, 80, Color.DarkBlue); + DrawEllipse(screenWidth / 5, 120, 25, 20, Color.Yellow); + DrawEllipseLines(screenWidth / 5, 120, 30, 25, Color.Yellow); + + // Rectangle shapes and lines + DrawRectangle(screenWidth / 4 * 2 - 60, 100, 120, 60, Color.Red); + DrawRectangleGradientH(screenWidth / 4 * 2 - 90, 170, 180, 130, Color.Maroon, Color.Gold); + DrawRectangleLines(screenWidth / 4 * 2 - 40, 320, 80, 60, Color.Orange); // NOTE: Uses QUADS internally, not lines + + // Triangle shapes and lines + DrawTriangle( + new Vector2(screenWidth / 4.0f * 3.0f, 80.0f), + new Vector2(screenWidth / 4.0f * 3.0f - 60.0f, 150.0f), + new Vector2(screenWidth / 4.0f * 3.0f + 60.0f, 150.0f), Color.Violet + ); + + DrawTriangleLines( + new Vector2(screenWidth / 4.0f * 3.0f, 160.0f), + new Vector2(screenWidth / 4.0f * 3.0f - 20.0f, 230.0f), + new Vector2(screenWidth / 4.0f * 3.0f + 20.0f, 230.0f), Color.DarkBlue + ); + + // Polygon shapes and lines + DrawPoly(new Vector2(screenWidth / 4.0f * 3, 330), 6, 80, rotation, Color.Brown); + DrawPolyLines(new Vector2(screenWidth / 4.0f * 3, 330), 6, 90, rotation, Color.Brown); + DrawPolyLinesEx(new Vector2(screenWidth / 4.0f * 3, 330), 6, 85, rotation, 6, Color.Beige); + + // NOTE: We draw all LINES based shapes together to optimize internal drawing, + // this way, all LINES are rendered in a single draw pass + DrawLine(18, 42, screenWidth - 18, 42, Color.Black); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - basic shapes"); - InitWindow(screenWidth, screenHeight, "raylib [shapes] example - basic shapes drawing"); - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new BasicShapes(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // TODO: Update your variables here - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("some basic shapes available on raylib", 20, 20, 20, Color.DarkGray); - - DrawLine(18, 42, screenWidth - 18, 42, Color.Black); - - DrawCircle(screenWidth / 4, 120, 35, Color.DarkBlue); - DrawCircleGradient(new Vector2(screenWidth / 4, 220), 60, Color.Green, Color.SkyBlue); - DrawCircleLines(screenWidth / 4, 340, 80, Color.DarkBlue); - - DrawRectangle(screenWidth / 4 * 2 - 60, 100, 120, 60, Color.Red); - DrawRectangleGradientH(screenWidth / 4 * 2 - 90, 170, 180, 130, Color.Maroon, Color.Gold); - DrawRectangleLines(screenWidth / 4 * 2 - 40, 320, 80, 60, Color.Orange); - - DrawTriangle( - new Vector2(screenWidth / 4 * 3, 80), - new Vector2(screenWidth / 4 * 3 - 60, 150), - new Vector2(screenWidth / 4 * 3 + 60, 150), Color.Violet - ); - - DrawTriangleLines( - new Vector2(screenWidth / 4 * 3, 160), - new Vector2(screenWidth / 4 * 3 - 20, 230), - new Vector2(screenWidth / 4 * 3 + 20, 230), Color.DarkBlue - ); - - DrawPoly(new Vector2(screenWidth / 4 * 3, 320), 6, 80, 0, Color.Brown); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Shapes/BouncingBall.cs b/Examples/Shapes/BouncingBall.cs index 0771ef1..eca2249 100644 --- a/Examples/Shapes/BouncingBall.cs +++ b/Examples/Shapes/BouncingBall.cs @@ -2,10 +2,16 @@ * * raylib [shapes] example - bouncing ball * -* This example has been created using raylib 1.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 * -* Copyright (c) 2013 Ramon Santamaria (@raysan5) +* Example originally created with raylib 2.5, last time updated with raylib 2.5 +* +* Example contributed by Ramon Santamaria (@raysan5), reviewed 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) 2013-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,83 +20,136 @@ using static Raylib_cs.Raylib; namespace Examples.Shapes; -public class BouncingBall +public partial class BouncingBall : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Bouncing Ball"; + + public string Title => "raylib [shapes] example - bouncing ball"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + private Vector2 ballPosition; + private Vector2 ballSpeed; + private int ballRadius; + private float gravity; + + private bool useGravity; + private bool pause; + private int framesCounter; + + public void Init() + { + ballPosition = new(GetScreenWidth() / 2.0f, GetScreenHeight() / 2.0f); + ballSpeed = new(5.0f, 4.0f); + ballRadius = 20; + gravity = 0.2f; + + useGravity = true; + pause = false; + framesCounter = 0; + } + + public void Update() + { + // Update + //----------------------------------------------------- + if (IsKeyPressed(KeyboardKey.G)) + { + useGravity = !useGravity; + } + if (IsKeyPressed(KeyboardKey.Space)) + { + pause = !pause; + } + + if (!pause) + { + ballPosition.X += ballSpeed.X; + ballPosition.Y += ballSpeed.Y; + + if (useGravity) + { + ballSpeed.Y += gravity; + } + + // Check walls collision for bouncing + if ((ballPosition.X >= (GetScreenWidth() - ballRadius)) || (ballPosition.X <= ballRadius)) + { + ballSpeed.X *= -1.0f; + } + if ((ballPosition.Y >= (GetScreenHeight() - ballRadius)) || (ballPosition.Y <= ballRadius)) + { + ballSpeed.Y *= -0.95f; + } + } + else + { + framesCounter++; + } + //----------------------------------------------------- + + // Draw + //----------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawCircleV(ballPosition, (float)ballRadius, Color.Maroon); + DrawText("PRESS SPACE to PAUSE BALL MOVEMENT", 10, GetScreenHeight() - 25, 20, Color.LightGray); + + if (useGravity) + { + DrawText("GRAVITY: ON (Press G to disable)", 10, GetScreenHeight() - 50, 20, Color.DarkGreen); + } + else + { + DrawText("GRAVITY: OFF (Press G to enable)", 10, GetScreenHeight() - 50, 20, Color.Red); + } + + // On pause, we draw a blinking message + if (pause && ((framesCounter / 30) % 2) != 0) + { + DrawText("PAUSED", 350, 200, 30, Color.Gray); + } + + DrawFPS(10, 10); + + EndDrawing(); + //----------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //--------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - + SetConfigFlags(ConfigFlags.Msaa4xHint); InitWindow(screenWidth, screenHeight, "raylib [shapes] example - bouncing ball"); - Vector2 ballPosition = new(GetScreenWidth() / 2, GetScreenHeight() / 2); - Vector2 ballSpeed = new(5.0f, 4.0f); - int ballRadius = 20; - - bool pause = false; - int framesCounter = 0; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //---------------------------------------------------------- + var game = new BouncingBall(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //----------------------------------------------------- - if (IsKeyPressed(KeyboardKey.Space)) - { - pause = !pause; - } - - if (!pause) - { - ballPosition.X += ballSpeed.X; - ballPosition.Y += ballSpeed.Y; - - // Check walls collision for bouncing - if ((ballPosition.X >= (GetScreenWidth() - ballRadius)) || (ballPosition.X <= ballRadius)) - { - ballSpeed.X *= -1.0f; - } - if ((ballPosition.Y >= (GetScreenHeight() - ballRadius)) || (ballPosition.Y <= ballRadius)) - { - ballSpeed.Y *= -1.0f; - } - } - else - { - framesCounter += 1; - } - //----------------------------------------------------- - - // Draw - //----------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawCircleV(ballPosition, ballRadius, Color.Maroon); - DrawText("PRESS SPACE to PAUSE BALL MOVEMENT", 10, GetScreenHeight() - 25, 20, Color.LightGray); - - // On pause, we draw a blinking message - if (pause && ((framesCounter / 30) % 2) == 0) - { - DrawText("PAUSED", 350, 200, 30, Color.Gray); - } - DrawFPS(10, 10); - - EndDrawing(); - //----------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //--------------------------------------------------------- - CloseWindow(); + 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/CollisionArea.cs b/Examples/Shapes/CollisionArea.cs index 05ba2f0..6b01cd5 100644 --- a/Examples/Shapes/CollisionArea.cs +++ b/Examples/Shapes/CollisionArea.cs @@ -2,10 +2,14 @@ * * raylib [shapes] example - collision area * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2013-2019 Ramon Santamaria (@raysan5) +* Example originally created with raylib 2.5, last time updated with raylib 2.5 +* +* 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) * ********************************************************************************************/ @@ -13,124 +17,156 @@ using static Raylib_cs.Raylib; namespace Examples.Shapes; -public class CollisionArea +public partial class CollisionArea : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Collision Area"; + + public string Title => "raylib [shapes] example - collision area"; + + private Rectangle boxA; + private int boxASpeedX; + private Rectangle boxB; + private Rectangle boxCollision; + private int screenUpperLimit; + private bool pause; + private bool collision; + + public void Init() + { + // Box A: Moving box + boxA = new(10, GetScreenHeight() / 2.0f - 50, 200, 100); + boxASpeedX = 4; + + // Box B: Mouse moved box + boxB = new(GetScreenWidth() / 2.0f - 30, GetScreenHeight() / 2.0f - 30, 60, 60); + + boxCollision = new(); // Collision rectangle + + screenUpperLimit = 40; // Top menu limits + + pause = false; // Movement pause + collision = false; // Collision detection + } + + public void Update() + { + // Update + //----------------------------------------------------- + // Move box if not paused + if (!pause) + { + boxA.X += boxASpeedX; + } + + // Bounce box on x screen limits + if (((boxA.X + boxA.Width) >= GetScreenWidth()) || (boxA.X <= 0)) + { + boxASpeedX *= -1; + } + + // Update player-controlled-box (box02) + boxB.X = GetMouseX() - boxB.Width / 2; + boxB.Y = GetMouseY() - boxB.Height / 2; + + // Make sure Box B does not go out of move area limits + if ((boxB.X + boxB.Width) >= GetScreenWidth()) + { + boxB.X = GetScreenWidth() - boxB.Width; + } + else if (boxB.X <= 0) + { + boxB.X = 0; + } + + if ((boxB.Y + boxB.Height) >= GetScreenHeight()) + { + boxB.Y = GetScreenHeight() - boxB.Height; + } + else if (boxB.Y <= screenUpperLimit) + { + boxB.Y = screenUpperLimit; + } + + // Check boxes collision + collision = CheckCollisionRecs(boxA, boxB); + + // Get collision rectangle (only on collision) + if (collision) + { + boxCollision = GetCollisionRec(boxA, boxB); + } + + // Pause Box A movement + if (IsKeyPressed(KeyboardKey.Space)) + { + pause = !pause; + } + //----------------------------------------------------- + + // Draw + //----------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawRectangle(0, 0, screenWidth, screenUpperLimit, collision ? Color.Red : Color.Black); + + DrawRectangleRec(boxA, Color.Gold); + DrawRectangleRec(boxB, Color.Blue); + + if (collision) + { + // Draw collision area + DrawRectangleRec(boxCollision, Color.Lime); + + // Draw collision message + var cx = GetScreenWidth() / 2 - MeasureText("COLLISION!", 20) / 2; + var cy = screenUpperLimit / 2 - 10; + DrawText("COLLISION!", cx, cy, 20, Color.Black); + + // Draw collision area + var text = $"Collision Area: {(int)boxCollision.Width * (int)boxCollision.Height}"; + DrawText(text, GetScreenWidth() / 2 - 100, screenUpperLimit + 10, 20, Color.Black); + } + + // Draw help instructions + DrawText("Press SPACE to PAUSE/RESUME", 20, screenHeight - 35, 20, Color.LightGray); + + DrawFPS(10, 10); + + EndDrawing(); + //----------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //--------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [shapes] example - collision area"); - // Box A: Moving box - Rectangle boxA = new(10, GetScreenHeight() / 2 - 50, 200, 100); - int boxASpeedX = 4; - - // Box B: Mouse moved box - Rectangle boxB = new(GetScreenWidth() / 2 - 30, GetScreenHeight() / 2 - 30, 60, 60); - Rectangle boxCollision = new(); - - int screenUpperLimit = 40; - - // Movement pause - bool pause = false; - bool collision = false; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //---------------------------------------------------------- + var game = new CollisionArea(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //----------------------------------------------------- - // Move box if not paused - if (!pause) - { - boxA.X += boxASpeedX; - } - - // Bounce box on x screen limits - if (((boxA.X + boxA.Width) >= GetScreenWidth()) || (boxA.X <= 0)) - { - boxASpeedX *= -1; - } - - // Update player-controlled-box (box02) - boxB.X = GetMouseX() - boxB.Width / 2; - boxB.Y = GetMouseY() - boxB.Height / 2; - - // Make sure Box B does not go out of move area limits - if ((boxB.X + boxB.Width) >= GetScreenWidth()) - { - boxB.X = GetScreenWidth() - boxB.Width; - } - else if (boxB.X <= 0) - { - boxB.X = 0; - } - - if ((boxB.Y + boxB.Height) >= GetScreenHeight()) - { - boxB.Y = GetScreenHeight() - boxB.Height; - } - else if (boxB.Y <= screenUpperLimit) - { - boxB.Y = screenUpperLimit; - } - - // Check boxes collision - collision = CheckCollisionRecs(boxA, boxB); - - // Get collision rectangle (only on collision) - if (collision) - { - boxCollision = GetCollisionRec(boxA, boxB); - } - - // Pause Box A movement - if (IsKeyPressed(KeyboardKey.Space)) - { - pause = !pause; - } - //----------------------------------------------------- - - // Draw - //----------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawRectangle(0, 0, screenWidth, screenUpperLimit, collision ? Color.Red : Color.Black); - - DrawRectangleRec(boxA, Color.Gold); - DrawRectangleRec(boxB, Color.Blue); - - if (collision) - { - // Draw collision area - DrawRectangleRec(boxCollision, Color.Lime); - - // Draw collision message - int cx = GetScreenWidth() / 2 - MeasureText("COLLISION!", 20) / 2; - int cy = screenUpperLimit / 2 - 10; - DrawText("COLLISION!", cx, cy, 20, Color.Black); - - // Draw collision area - string text = $"Collision Area: {(int)boxCollision.Width * (int)boxCollision.Height}"; - DrawText(text, GetScreenWidth() / 2 - 100, screenUpperLimit + 10, 20, Color.Black); - } - - DrawFPS(10, 10); - - EndDrawing(); - //----------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //--------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //---------------------------------------------------------- return 0; diff --git a/Examples/Shapes/ColorsPalette.cs b/Examples/Shapes/ColorsPalette.cs index bb3a9c2..9ada68a 100644 --- a/Examples/Shapes/ColorsPalette.cs +++ b/Examples/Shapes/ColorsPalette.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [shapes] example - Colors palette +* raylib [shapes] example - colors palette * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2014-2019 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.0, last time updated with raylib 2.5 +* +* 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) * ********************************************************************************************/ @@ -14,72 +18,80 @@ using static Raylib_cs.Raylib; namespace Examples.Shapes; -public class ColorsPalette +public partial class ColorsPalette : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public const int MaxColorsCount = 21; // Number of colors available + + public string Name => "Shapes / Colors Palette"; + + public string Title => "raylib [shapes] example - colors palette"; + + private Color[] colors; + private string[] colorNames; + private Rectangle[] colorsRecs; + private int[] colorState; + private Vector2 mousePoint; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [shapes] example - colors palette"); - - Color[] colors = new[] + colors = new[] { - Color.DarkGray, - Color.Maroon, - Color.Orange, - Color.DarkGreen, - Color.DarkBlue, - Color.DarkPurple, - Color.DarkBrown, - Color.Gray, - Color.Red, - Color.Gold, - Color.Lime, - Color.Blue, - Color.Violet, - Color.Brown, - Color.LightGray, - Color.Pink, - Color.Yellow, - Color.Green, - Color.SkyBlue, - Color.Purple, - Color.Beige - }; + Color.DarkGray, + Color.Maroon, + Color.Orange, + Color.DarkGreen, + Color.DarkBlue, + Color.DarkPurple, + Color.DarkBrown, + Color.Gray, + Color.Red, + Color.Gold, + Color.Lime, + Color.Blue, + Color.Violet, + Color.Brown, + Color.LightGray, + Color.Pink, + Color.Yellow, + Color.Green, + Color.SkyBlue, + Color.Purple, + Color.Beige + }; - string[] colorNames = new[] + colorNames = new[] { - "DARKGRAY", - "MAROON", - "ORANGE", - "DARKGREEN", - "DARKBLUE", - "DARKPURPLE", - "DARKBROWN", - "GRAY", - "RED", - "GOLD", - "LIME", - "BLUE", - "VIOLET", - "BROWN", - "LIGHTGRAY", - "PINK", - "YELLOW", - "GREEN", - "SKYBLUE", - "PURPLE", - "BEIGE" - }; + "DARKGRAY", + "MAROON", + "ORANGE", + "DARKGREEN", + "DARKBLUE", + "DARKPURPLE", + "DARKBROWN", + "GRAY", + "RED", + "GOLD", + "LIME", + "BLUE", + "VIOLET", + "BROWN", + "LIGHTGRAY", + "PINK", + "YELLOW", + "GREEN", + "SKYBLUE", + "PURPLE", + "BEIGE" + }; // Rectangles array - Rectangle[] colorsRecs = new Rectangle[colors.Length]; + colorsRecs = new Rectangle[colors.Length]; // Fills colorsRecs data (for every rectangle) - for (int i = 0; i < colorsRecs.Length; i++) + for (var i = 0; i < colorsRecs.Length; i++) { colorsRecs[i].X = 20 + 100 * (i % 7) + 10 * (i % 7); colorsRecs[i].Y = 80 + 100 * (i / 7) + 10 * (i / 7); @@ -88,82 +100,101 @@ public class ColorsPalette } // Color state: 0-DEFAULT, 1-MOUSE_HOVER - int[] colorState = new int[colors.Length]; + colorState = new int[colors.Length]; - Vector2 mousePoint = new(0.0f, 0.0f); + mousePoint = new(0.0f, 0.0f); + } - SetTargetFPS(60); + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + mousePoint = GetMousePosition(); + + for (var i = 0; i < colors.Length; i++) + { + if (CheckCollisionPointRec(mousePoint, colorsRecs[i])) + { + colorState[i] = 1; + } + else + { + colorState[i] = 0; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText("raylib colors palette", 28, 42, 20, Color.Black); + DrawText( + "press SPACE to see all colors", + GetScreenWidth() - 180, + GetScreenHeight() - 40, + 10, + Color.Gray + ); + + for (var i = 0; i < colorsRecs.Length; i++) // Draw all rectangles + { + DrawRectangleRec(colorsRecs[i], Fade(colors[i], colorState[i] != 0 ? 0.6f : 1.0f)); + + if (IsKeyDown(KeyboardKey.Space) || colorState[i] != 0) + { + DrawRectangle( + (int)colorsRecs[i].X, + (int)(colorsRecs[i].Y + colorsRecs[i].Height - 26), + (int)colorsRecs[i].Width, + 20, + Color.Black + ); + DrawRectangleLinesEx(colorsRecs[i], 6, Fade(Color.Black, 0.3f)); + DrawText( + colorNames[i], + (int)(colorsRecs[i].X + colorsRecs[i].Width - MeasureText(colorNames[i], 10) - 12), + (int)(colorsRecs[i].Y + colorsRecs[i].Height - 20), + 10, + colors[i] + ); + } + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - colors palette"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new ColorsPalette(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - mousePoint = GetMousePosition(); - - for (int i = 0; i < colors.Length; i++) - { - if (CheckCollisionPointRec(mousePoint, colorsRecs[i])) - { - colorState[i] = 1; - } - else - { - colorState[i] = 0; - } - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("raylib colors palette", 28, 42, 20, Color.Black); - DrawText( - "press SPACE to see all colors", - GetScreenWidth() - 180, - GetScreenHeight() - 40, - 10, - Color.Gray - ); - - // Draw all rectangles - for (int i = 0; i < colorsRecs.Length; i++) - { - DrawRectangleRec(colorsRecs[i], ColorAlpha(colors[i], colorState[i] != 0 ? 0.6f : 1.0f)); - - if (IsKeyDown(KeyboardKey.Space) || colorState[i] != 0) - { - DrawRectangle( - (int)colorsRecs[i].X, - (int)(colorsRecs[i].Y + colorsRecs[i].Height - 26), - (int)colorsRecs[i].Width, - 20, - Color.Black - ); - DrawRectangleLinesEx(colorsRecs[i], 6, ColorAlpha(Color.Black, 0.3f)); - DrawText( - colorNames[i], - (int)(colorsRecs[i].X + colorsRecs[i].Width - MeasureText(colorNames[i], 10) - 12), - (int)(colorsRecs[i].Y + colorsRecs[i].Height - 20), - 10, - colors[i] - ); - } - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + 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 7178fc3..2f593a8 100644 --- a/Examples/Shapes/DrawCircleSector.cs +++ b/Examples/Shapes/DrawCircleSector.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [shapes] example - draw circle sector (with gui options) +* raylib [shapes] example - circle sector drawing * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 2.5, last time updated with raylib 2.5 * * Example contributed by Vlad Adrian (@demizdor) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2018 Vlad Adrian (@demizdor) and 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) 2018-2025 Vlad Adrian (@demizdor) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -17,79 +21,128 @@ using static Raylib_cs.Raylib; namespace Examples.Shapes; -public class DrawCircleSector +public partial class DrawCircleSector : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Draw Circle Sector"; + + public string Title => "raylib [shapes] example - circle sector drawing"; + + private Vector2 center; + private float outerRadius; + private float startAngle; + private float endAngle; + private float segments; + private float minSegments; + + public void Init() + { + center = new((GetScreenWidth() - 300) / 2.0f, GetScreenHeight() / 2.0f); + + outerRadius = 180.0f; + startAngle = 0.0f; + endAngle = 180.0f; + segments = 10.0f; + minSegments = 4; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // NOTE: All variables update happens inside GUI control functions + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawLine(500, 0, 500, GetScreenHeight(), Fade(Color.LightGray, 0.6f)); + DrawRectangle(500, 0, GetScreenWidth() - 500, GetScreenHeight(), Fade(Color.LightGray, 0.3f)); + + DrawCircleSector(center, outerRadius, startAngle, endAngle, (int)segments, Fade(Color.Maroon, 0.3f)); + DrawCircleSectorLines( + center, + outerRadius, + startAngle, + endAngle, + (int)segments, + Fade(Color.Maroon, 0.6f) + ); + + // Draw GUI controls + //------------------------------------------------------------------------------ + 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", $"{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)); + var color = (segments >= minSegments) ? Color.Maroon : Color.DarkGray; + DrawText($"MODE: {((segments >= minSegments) ? "MANUAL" : "AUTO")}", 600, 200, 10, color); + + 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 (!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 //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - circle sector drawing"); - InitWindow(screenWidth, screenHeight, "raylib [shapes] example - draw circle sector"); - - Vector2 center = new((GetScreenWidth() - 300) / 2, GetScreenHeight() / 2); - - float outerRadius = 180.0f; - int startAngle = 0; - int endAngle = 180; - int segments = 0; - int minSegments = 4; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new DrawCircleSector(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // NOTE: All variables update happens inside GUI control functions - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawLine(500, 0, 500, GetScreenHeight(), ColorAlpha(Color.LightGray, 0.6f)); - DrawRectangle(500, 0, GetScreenWidth() - 500, GetScreenHeight(), ColorAlpha(Color.LightGray, 0.3f)); - - DrawCircleSector(center, outerRadius, startAngle, endAngle, segments, ColorAlpha(Color.Maroon, 0.3f)); - DrawCircleSectorLines( - center, - outerRadius, - startAngle, - endAngle, - segments, - ColorAlpha(Color.Maroon, 0.6f) - ); - - // Draw GUI controls - //------------------------------------------------------------------------------ - /*startAngle = GuiSliderBar(new Rectangle( 600, 40, 120, 20), "StartAngle", startAngle, 0, 720, true ); - endAngle = GuiSliderBar(new Rectangle( 600, 70, 120, 20), "EndAngle", endAngle, 0, 720, true); - - outerRadius = GuiSliderBar(new Rectangle( 600, 140, 120, 20), "Radius", outerRadius, 0, 200, true); - segments = GuiSliderBar(new Rectangle( 600, 170, 120, 20), "Segments", segments, 0, 100, true);*/ - //------------------------------------------------------------------------------ - - minSegments = (int)MathF.Ceiling((endAngle - startAngle) / 90); - Color color = (segments >= minSegments) ? Color.Maroon : Color.DarkGray; - DrawText($"MODE: {((segments >= minSegments) ? "MANUAL" : "AUTO")}", 600, 270, 10, color); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } } - diff --git a/Examples/Shapes/DrawRectangleRounded.cs b/Examples/Shapes/DrawRectangleRounded.cs index 5190358..780c00e 100644 --- a/Examples/Shapes/DrawRectangleRounded.cs +++ b/Examples/Shapes/DrawRectangleRounded.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [shapes] example - draw rectangle rounded (with gui options) +* raylib [shapes] example - rounded rectangle drawing * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 2.5, last time updated with raylib 2.5 * * Example contributed by Vlad Adrian (@demizdor) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2018 Vlad Adrian (@demizdor) and 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) 2018-2025 Vlad Adrian (@demizdor) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -15,91 +19,154 @@ using static Raylib_cs.Raylib; namespace Examples.Shapes; -public class DrawRectangleRounded +public partial class DrawRectangleRounded : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Draw Rectangle Rounded"; + + public string Title => "raylib [shapes] example - rounded rectangle drawing"; + + private float roundness; + private float width; + private float height; + private float segments; + private float lineThick; + + private bool drawRect; + private bool drawRoundedRect; + private bool drawRoundedLines; + + public void Init() + { + roundness = 0.2f; + width = 200.0f; + height = 100.0f; + segments = 0.0f; + lineThick = 1.0f; + + drawRect = false; + drawRoundedRect = true; + drawRoundedLines = false; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + Rectangle rec = new( + ((float)GetScreenWidth() - width - 250) / 2, + (GetScreenHeight() - height) / 2.0f, + (float)width, + (float)height + ); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawLine(560, 0, 560, GetScreenHeight(), Fade(Color.LightGray, 0.6f)); + DrawRectangle(560, 0, GetScreenWidth() - 500, GetScreenHeight(), Fade(Color.LightGray, 0.3f)); + + if (drawRect) + { + DrawRectangleRec(rec, Fade(Color.Gold, 0.6f)); + } + if (drawRoundedRect) + { + DrawRectangleRounded(rec, roundness, (int)segments, Fade(Color.Maroon, 0.2f)); + } + if (drawRoundedLines) + { + DrawRectangleRoundedLinesEx(rec, roundness, (int)segments, lineThick, Fade(Color.Maroon, 0.4f)); + } + + // Draw GUI controls + //------------------------------------------------------------------------------ + 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); + //------------------------------------------------------------------------------ + + var text = $"MODE: {((segments >= 4) ? "MANUAL" : "AUTO")}"; + DrawText(text, 640, 280, 10, (segments >= 4) ? Color.Maroon : Color.DarkGray); + 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 (!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 //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - rounded rectangle drawing"); - InitWindow(screenWidth, screenHeight, "raylib [shapes] example - draw rectangle rounded"); - - float roundness = 0.2f; - int width = 400; - int height = 200; - int segments = 0; - int lineThick = 10; - - bool drawRect = false; - bool drawRoundedRect = false; - bool drawRoundedLines = true; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new DrawRectangleRounded(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - Rectangle rec = new( - (GetScreenWidth() - width - 250) / 2.0f, - (GetScreenHeight() - height) / 2.0f, - (float)width, - (float)height - ); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawLine(560, 0, 560, GetScreenHeight(), ColorAlpha(Color.LightGray, 0.6f)); - DrawRectangle(560, 0, GetScreenWidth() - 500, GetScreenHeight(), ColorAlpha(Color.LightGray, 0.3f)); - - if (drawRect) - { - DrawRectangleRec(rec, ColorAlpha(Color.Gold, 0.6f)); - } - if (drawRoundedRect) - { - DrawRectangleRounded(rec, roundness, segments, ColorAlpha(Color.Maroon, 0.2f)); - } - if (drawRoundedLines) - { - DrawRectangleRoundedLinesEx(rec, roundness, segments, (float)lineThick, ColorAlpha(Color.Maroon, 0.4f)); - } - - // Draw GUI controls - //------------------------------------------------------------------------------ - /*width = GuiSliderBar(new Rectangle( 640, 40, 105, 20 ), "Width", width, 0, GetScreenWidth() - 300, true ); - height = GuiSliderBar(new Rectangle( 640, 70, 105, 20 ), "Height", height, 0, GetScreenHeight() - 50, true); - roundness = GuiSliderBar(new Rectangle( 640, 140, 105, 20 ), "Roundness", roundness, 0.0f, 1.0f, true); - lineThick = GuiSliderBar(new Rectangle( 640, 170, 105, 20 ), "Thickness", lineThick, 0, 20, true); - segments = GuiSliderBar(new Rectangle( 640, 240, 105, 20), "Segments", segments, 0, 60, true); - - drawRoundedRect = GuiCheckBox(new Rectangle( 640, 320, 20, 20 ), "DrawRoundedRect", drawRoundedRect); - drawRoundedLines = GuiCheckBox(new Rectangle( 640, 350, 20, 20 ), "DrawRoundedLines", drawRoundedLines); - drawRect = GuiCheckBox(new Rectangle( 640, 380, 20, 20), "DrawRect", drawRect);*/ - //------------------------------------------------------------------------------ - - string text = $"MODE: {((segments >= 4) ? "MANUAL" : "AUTO")}"; - DrawText(text, 640, 280, 10, (segments >= 4) ? Color.Maroon : Color.DarkGray); - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } } - diff --git a/Examples/Shapes/DrawRing.cs b/Examples/Shapes/DrawRing.cs index 2a53ce7..cb4f5a3 100644 --- a/Examples/Shapes/DrawRing.cs +++ b/Examples/Shapes/DrawRing.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [shapes] example - draw ring (with gui options) +* raylib [shapes] example - ring drawing * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 2.5, last time updated with raylib 2.5 * * Example contributed by Vlad Adrian (@demizdor) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2018 Vlad Adrian (@demizdor) and 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) 2018-2025 Vlad Adrian (@demizdor) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -17,114 +21,180 @@ using static Raylib_cs.Raylib; namespace Examples.Shapes; -public class DrawRing +public partial class DrawRing : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Draw Ring"; + + public string Title => "raylib [shapes] example - ring drawing"; + + private Vector2 center; + + private float innerRadius; + private float outerRadius; + + private float startAngle; + private float endAngle; + private float segments; + + private bool drawRing; + private bool drawRingLines; + private bool drawCircleLines; + + public void Init() + { + center = new((GetScreenWidth() - 300) / 2.0f, GetScreenHeight() / 2.0f); + + innerRadius = 80.0f; + outerRadius = 190.0f; + + startAngle = 0.0f; + endAngle = 360.0f; + segments = 0.0f; + + drawRing = true; + drawRingLines = false; + drawCircleLines = false; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // NOTE: All variables update happens inside GUI control functions + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawLine(500, 0, 500, GetScreenHeight(), Fade(Color.LightGray, 0.6f)); + DrawRectangle(500, 0, GetScreenWidth() - 500, GetScreenHeight(), Fade(Color.LightGray, 0.3f)); + + if (drawRing) + { + DrawRing( + center, + innerRadius, + outerRadius, + startAngle, + endAngle, + (int)segments, + Fade(Color.Maroon, 0.3f) + ); + } + if (drawRingLines) + { + DrawRingLines( + center, + innerRadius, + outerRadius, + startAngle, + endAngle, + (int)segments, + Fade(Color.Black, 0.4f) + ); + } + if (drawCircleLines) + { + DrawCircleSectorLines( + center, + outerRadius, + startAngle, + endAngle, + (int)segments, + Fade(Color.Black, 0.4f) + ); + } + + // Draw GUI controls + //------------------------------------------------------------------------------ + 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", $"{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", $"{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); + //------------------------------------------------------------------------------ + + var minSegments = (int)MathF.Ceiling((endAngle - startAngle) / 90); + var color = (segments >= minSegments) ? Color.Maroon : Color.DarkGray; + DrawText($"MODE: {((segments >= minSegments) ? "MANUAL" : "AUTO")}", 600, 270, 10, color); + + 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 (!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 //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - ring drawing"); - InitWindow(screenWidth, screenHeight, "raylib [shapes] example - draw ring"); - - Vector2 center = new((GetScreenWidth() - 300) / 2, GetScreenHeight() / 2); - - float innerRadius = 80.0f; - float outerRadius = 190.0f; - - int startAngle = 0; - int endAngle = 360; - int segments = 0; - int minSegments = 4; - - bool drawRing = true; - bool drawRingLines = false; - bool drawCircleLines = false; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new DrawRing(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // NOTE: All variables update happens inside GUI control functions - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawLine(500, 0, 500, GetScreenHeight(), ColorAlpha(Color.LightGray, 0.6f)); - DrawRectangle(500, 0, GetScreenWidth() - 500, GetScreenHeight(), ColorAlpha(Color.LightGray, 0.3f)); - - if (drawRing) - { - DrawRing( - center, - innerRadius, - outerRadius, - startAngle, - endAngle, - segments, - ColorAlpha(Color.Maroon, 0.3f) - ); - } - if (drawRingLines) - { - DrawRingLines( - center, - innerRadius, - outerRadius, - startAngle, - endAngle, - segments, - ColorAlpha(Color.Black, 0.4f) - ); - } - if (drawCircleLines) - { - DrawCircleSectorLines( - center, - outerRadius, - startAngle, - endAngle, - segments, - ColorAlpha(Color.Black, 0.4f) - ); - } - - // Draw GUI controls - //------------------------------------------------------------------------------ - /*startAngle = GuiSliderBar(new Rectangle( 600, 40, 120, 20 ), "StartAngle", startAngle, -450, 450, true); - endAngle = GuiSliderBar(new Rectangle( 600, 70, 120, 20 ), "EndAngle", endAngle, -450, 450, true); - - innerRadius = GuiSliderBar(new Rectangle( 600, 140, 120, 20 ), "InnerRadius", innerRadius, 0, 100, true); - outerRadius = GuiSliderBar(new Rectangle( 600, 170, 120, 20 ), "OuterRadius", outerRadius, 0, 200, true); - - segments = GuiSliderBar(new Rectangle( 600, 240, 120, 20 ), "Segments", segments, 0, 100, true); - - drawRing = GuiCheckBox(new Rectangle( 600, 320, 20, 20 ), "Draw Ring", drawRing); - drawRingLines = GuiCheckBox(new Rectangle( 600, 350, 20, 20 ), "Draw RingLines", drawRingLines); - drawCircleLines = GuiCheckBox(new Rectangle( 600, 380, 20, 20 ), "Draw CircleLines", drawCircleLines);*/ - //------------------------------------------------------------------------------ - - minSegments = (int)MathF.Ceiling((endAngle - startAngle) / 90); - Color color = (segments >= minSegments) ? Color.Maroon : Color.DarkGray; - DrawText($"MODE: {((segments >= minSegments) ? "MANUAL" : "AUTO")}", 600, 270, 10, color); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Shapes/EasingsBallAnim.cs b/Examples/Shapes/EasingsBallAnim.cs index 1052e8a..5aae342 100644 --- a/Examples/Shapes/EasingsBallAnim.cs +++ b/Examples/Shapes/EasingsBallAnim.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [shapes] example - easings ball anim +* raylib [shapes] example - easings ball * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2014-2019 Ramon Santamaria (@raysan5) +* Example originally created with raylib 2.5, last time updated with raylib 2.5 +* +* 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) * ********************************************************************************************/ @@ -14,114 +18,138 @@ using Examples.Shared; namespace Examples.Shapes; -public class EasingsBallAnim +public partial class EasingsBallAnim : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Easings Ball Anim"; + + public string Title => "raylib [shapes] example - easings ball"; + + // Ball variable value to be animated with easings + private int ballPositionX; + private int ballRadius; + private float ballAlpha; + + private int state; + private int framesCounter; + + public void Init() + { + ballPositionX = -100; + ballRadius = 20; + ballAlpha = 0.0f; + + state = 0; + framesCounter = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (state == 0) // Move ball position X with easing + { + framesCounter++; + ballPositionX = (int)Easings.EaseElasticOut(framesCounter, -100, screenWidth / 2.0f + 100, 120); + + if (framesCounter >= 120) + { + framesCounter = 0; + state = 1; + } + } + else if (state == 1) // Increase ball radius with easing + { + framesCounter++; + ballRadius = (int)Easings.EaseElasticIn(framesCounter, 20, 500, 200); + + if (framesCounter >= 200) + { + framesCounter = 0; + state = 2; + } + } + else if (state == 2) // Change ball alpha with easing (background color blending) + { + framesCounter++; + ballAlpha = Easings.EaseCubicOut(framesCounter, 0.0f, 1.0f, 200); + + if (framesCounter >= 200) + { + framesCounter = 0; + state = 3; + } + } + else if (state == 3) // Reset state to play again + { + if (IsKeyPressed(KeyboardKey.Enter)) + { + // Reset required variables to play again + ballPositionX = -100; + ballRadius = 20; + ballAlpha = 0.0f; + state = 0; + } + } + + if (IsKeyPressed(KeyboardKey.R)) + { + framesCounter = 0; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + if (state >= 2) + { + DrawRectangle(0, 0, screenWidth, screenHeight, Color.Green); + } + + DrawCircle(ballPositionX, 200, ballRadius, Fade(Color.Red, 1.0f - ballAlpha)); + + if (state == 3) + { + DrawText("PRESS [ENTER] TO PLAY AGAIN!", 240, 200, 20, Color.Black); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - easings ball"); - InitWindow(screenWidth, screenHeight, "raylib [shapes] example - easings ball anim"); - - // Ball variable value to be animated with easings - int ballPositionX = -100; - int ballRadius = 20; - float ballAlpha = 0.0f; - - int state = 0; - int framesCounter = 0; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new EasingsBallAnim(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (state == 0) // Move ball position X with easing - { - framesCounter += 1; - ballPositionX = (int)Easings.EaseElasticOut(framesCounter, -100, screenWidth / 2 + 100, 120); - - if (framesCounter >= 120) - { - framesCounter = 0; - state = 1; - } - } - // Increase ball radius with easing - else if (state == 1) - { - framesCounter += 1; - ballRadius = (int)Easings.EaseElasticIn(framesCounter, 20, 500, 200); - - if (framesCounter >= 200) - { - framesCounter = 0; - state = 2; - } - } - // Change ball alpha with easing (background color blending) - else if (state == 2) - { - framesCounter += 1; - ballAlpha = Easings.EaseCubicOut(framesCounter, 0.0f, 1.0f, 200); - - if (framesCounter >= 200) - { - framesCounter = 0; - state = 3; - } - } - // Reset state to play again - else if (state == 3) - { - if (IsKeyPressed(KeyboardKey.Enter)) - { - // Reset required variables to play again - ballPositionX = -100; - ballRadius = 20; - ballAlpha = 0.0f; - state = 0; - } - } - - if (IsKeyPressed(KeyboardKey.R)) - { - framesCounter = 0; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - if (state >= 2) - { - DrawRectangle(0, 0, screenWidth, screenHeight, Color.Green); - } - - DrawCircle(ballPositionX, 200, ballRadius, ColorAlpha(Color.Red, 1.0f - ballAlpha)); - - if (state == 3) - { - DrawText("PRESS [ENTER] TO PLAY AGAIN!", 240, 200, 20, Color.Black); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } } - diff --git a/Examples/Shapes/EasingsBoxAnim.cs b/Examples/Shapes/EasingsBoxAnim.cs index ce07558..6ce381e 100644 --- a/Examples/Shapes/EasingsBoxAnim.cs +++ b/Examples/Shapes/EasingsBoxAnim.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [shapes] example - easings box anim +* raylib [shapes] example - easings box * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2014-2019 Ramon Santamaria (@raysan5) +* Example originally created with raylib 2.5, last time updated with raylib 2.5 +* +* 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) * ********************************************************************************************/ @@ -15,129 +19,152 @@ using Examples.Shared; namespace Examples.Shapes; -public class EasingsBoxAnim +public partial class EasingsBoxAnim : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Easings Box Anim"; + + public string Title => "raylib [shapes] example - easings box"; + + // Box variables to be animated with easings + private Rectangle rec; + private float rotation; + private float alpha; + + private int state; + private int framesCounter; + + public void Init() + { + rec = new(GetScreenWidth() / 2.0f, -100, 100, 100); + rotation = 0.0f; + alpha = 1.0f; + + state = 0; + framesCounter = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + switch (state) + { + case 0: // Move box down to center of screen + framesCounter++; + + // NOTE: Remember that 3rd parameter of easing function refers to + // desired value variation, do not confuse it with expected final value! + rec.Y = Easings.EaseElasticOut(framesCounter, -100, GetScreenHeight() / 2.0f + 100, 120); + + if (framesCounter >= 120) + { + framesCounter = 0; + state = 1; + } + break; + case 1: // Scale box to an horizontal bar + framesCounter++; + rec.Height = Easings.EaseBounceOut(framesCounter, 100, -90, 120); + rec.Width = Easings.EaseBounceOut(framesCounter, 100, GetScreenWidth(), 120); + + if (framesCounter >= 120) + { + framesCounter = 0; + state = 2; + } + break; + case 2: // Rotate horizontal bar rectangle + framesCounter++; + rotation = Easings.EaseQuadOut(framesCounter, 0.0f, 270.0f, 240); + + if (framesCounter >= 240) + { + framesCounter = 0; + state = 3; + } + break; + case 3: // Increase bar size to fill all screen + framesCounter++; + rec.Height = Easings.EaseCircOut(framesCounter, 10, GetScreenWidth(), 120); + + if (framesCounter >= 120) + { + framesCounter = 0; + state = 4; + } + break; + case 4: // Fade out animation + framesCounter++; + alpha = Easings.EaseSineOut(framesCounter, 1.0f, -1.0f, 160); + + if (framesCounter >= 160) + { + framesCounter = 0; + state = 5; + } + break; + default: + break; + } + + // Reset animation at any moment + if (IsKeyPressed(KeyboardKey.Space)) + { + rec = new Rectangle(GetScreenWidth() / 2.0f, -100, 100, 100); + rotation = 0.0f; + alpha = 1.0f; + state = 0; + framesCounter = 0; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawRectanglePro( + rec, + new Vector2(rec.Width / 2, rec.Height / 2), + rotation, + Fade(Color.Black, alpha) + ); + DrawText("PRESS [SPACE] TO RESET BOX ANIMATION!", 10, GetScreenHeight() - 25, 20, Color.LightGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - easings box"); - InitWindow(screenWidth, screenHeight, "raylib [shapes] example - easings box anim"); - - // Box variables to be animated with easings - Rectangle rec = new(GetScreenWidth() / 2, -100, 100, 100); - float rotation = 0.0f; - float alpha = 1.0f; - - int state = 0; - int framesCounter = 0; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new EasingsBoxAnim(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - switch (state) - { - // Move box down to center of screen - case 0: - framesCounter += 1; - - // NOTE: Remember that 3rd parameter of easing function refers to - // desired value variation, do not confuse it with expected final value! - rec.Y = Easings.EaseElasticOut(framesCounter, -100, GetScreenHeight() / 2 + 100, 120); - - if (framesCounter >= 120) - { - framesCounter = 0; - state = 1; - } - break; - // Scale box to an horizontal bar - case 1: - framesCounter += 1; - rec.Height = Easings.EaseBounceOut(framesCounter, 100, -90, 120); - rec.Width = Easings.EaseBounceOut(framesCounter, 100, GetScreenWidth(), 120); - - if (framesCounter >= 120) - { - framesCounter = 0; - state = 2; - } - break; - // Rotate horizontal bar rectangle - case 2: - framesCounter += 1; - rotation = Easings.EaseQuadOut(framesCounter, 0.0f, 270.0f, 240); - - if (framesCounter >= 240) - { - framesCounter = 0; - state = 3; - } - break; - // Increase bar size to fill all screen - case 3: - framesCounter += 1; - rec.Height = Easings.EaseCircOut(framesCounter, 10, GetScreenWidth(), 120); - - if (framesCounter >= 120) - { - framesCounter = 0; - state = 4; - } - break; - // Fade out animation - case 4: - framesCounter++; - alpha = Easings.EaseSineOut(framesCounter, 1.0f, -1.0f, 160); - - if (framesCounter >= 160) - { - framesCounter = 0; - state = 5; - } - break; - default: - break; - } - - // Reset animation at any moment - if (IsKeyPressed(KeyboardKey.Space)) - { - rec = new Rectangle(GetScreenWidth() / 2, -100, 100, 100); - rotation = 0.0f; - alpha = 1.0f; - state = 0; - framesCounter = 0; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawRectanglePro( - rec, - new Vector2(rec.Width / 2, rec.Height / 2), - rotation, - ColorAlpha(Color.Black, alpha) - ); - DrawText("PRESS [SPACE] TO RESET BOX ANIMATION!", 10, GetScreenHeight() - 25, 20, Color.LightGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Shapes/EasingsRectangleArray.cs b/Examples/Shapes/EasingsRectangleArray.cs index 6e74f52..f660c0b 100644 --- a/Examples/Shapes/EasingsRectangleArray.cs +++ b/Examples/Shapes/EasingsRectangleArray.cs @@ -1,14 +1,18 @@ /******************************************************************************************* * -* raylib [shapes] example - easings rectangle array +* raylib [shapes] example - easings rectangles +* +* Example complexity rating: [★★★☆] 3/4 * * NOTE: This example requires 'easings.h' library, provided on raylib/src. Just copy -* the library to same directory as example or make sure it's available on include path. +* the library to same directory as example or make sure it's available on include path * -* This example has been created using raylib 2.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 2.0, last time updated with raylib 2.5 * -* Copyright (c) 2014-2019 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) 2014-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -18,8 +22,11 @@ using Examples.Shared; namespace Examples.Shapes; -public class EasingsRectangleArray +public partial class EasingsRectangleArray : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + public const int RecsWidth = 50; public const int RecsHeight = 50; public const int MaxRecsX = 800 / RecsWidth; @@ -28,112 +35,133 @@ public class EasingsRectangleArray // At 60 fps = 4 seconds public const int PlayTimeInFrames = 240; - public static int Main() + public string Name => "Shapes / Easings Rectangle Array"; + + public string Title => "raylib [shapes] example - easings rectangles"; + + private Rectangle[] recs; + private float rotation; + private int framesCounter; + private int state; // Rectangles animation state: 0-Playing, 1-Finished + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + recs = new Rectangle[MaxRecsX * MaxRecsY]; - InitWindow(screenWidth, screenHeight, "raylib [shapes] example - easings rectangle array"); - - Rectangle[] recs = new Rectangle[MaxRecsX * MaxRecsY]; - - for (int y = 0; y < MaxRecsY; y++) + for (var y = 0; y < MaxRecsY; y++) { - for (int x = 0; x < MaxRecsX; x++) + for (var x = 0; x < MaxRecsX; x++) { - recs[y * MaxRecsX + x].X = RecsWidth / 2 + RecsWidth * x; - recs[y * MaxRecsX + x].Y = RecsHeight / 2 + RecsHeight * y; + recs[y * MaxRecsX + x].X = RecsWidth / 2.0f + RecsWidth * x; + recs[y * MaxRecsX + x].Y = RecsHeight / 2.0f + RecsHeight * y; recs[y * MaxRecsX + x].Width = RecsWidth; recs[y * MaxRecsX + x].Height = RecsHeight; } } - float rotation = 0.0f; - int framesCounter = 0; + rotation = 0.0f; + framesCounter = 0; + state = 0; + } - // Rectangles animation state: 0-Playing, 1-Finished - int state = 0; + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (state == 0) + { + framesCounter++; - SetTargetFPS(60); + for (var i = 0; i < MaxRecsX * MaxRecsY; i++) + { + recs[i].Height = Easings.EaseCircOut(framesCounter, RecsHeight, -RecsHeight, PlayTimeInFrames); + recs[i].Width = Easings.EaseCircOut(framesCounter, RecsWidth, -RecsWidth, PlayTimeInFrames); + + if (recs[i].Height < 0) + { + recs[i].Height = 0; + } + if (recs[i].Width < 0) + { + recs[i].Width = 0; + } + + // Finish playing + if ((recs[i].Height == 0) && (recs[i].Width == 0)) + { + state = 1; + } + rotation = Easings.EaseLinearIn(framesCounter, 0.0f, 360.0f, PlayTimeInFrames); + } + } + else if ((state == 1) && IsKeyPressed(KeyboardKey.Space)) + { + // When animation has finished, press space to restart + framesCounter = 0; + + for (var i = 0; i < MaxRecsX * MaxRecsY; i++) + { + recs[i].Height = RecsHeight; + recs[i].Width = RecsWidth; + } + + state = 0; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + if (state == 0) + { + for (var i = 0; i < MaxRecsX * MaxRecsY; i++) + { + DrawRectanglePro( + recs[i], + new Vector2(recs[i].Width / 2, recs[i].Height / 2), + rotation, + Color.Red + ); + } + } + else if (state == 1) + { + DrawText("PRESS [SPACE] TO PLAY AGAIN!", 240, 200, 20, Color.Gray); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - easings rectangles"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new EasingsRectangleArray(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (state == 0) - { - framesCounter++; - - for (int i = 0; i < MaxRecsX * MaxRecsY; i++) - { - recs[i].Height = Easings.EaseCircOut(framesCounter, RecsHeight, -RecsHeight, PlayTimeInFrames); - recs[i].Width = Easings.EaseCircOut(framesCounter, RecsWidth, -RecsWidth, PlayTimeInFrames); - - if (recs[i].Height < 0) - { - recs[i].Height = 0; - } - if (recs[i].Width < 0) - { - recs[i].Width = 0; - } - - // Finish playing - if ((recs[i].Height == 0) && (recs[i].Width == 0)) - { - state = 1; - } - rotation = Easings.EaseLinearIn(framesCounter, 0.0f, 360.0f, PlayTimeInFrames); - } - } - else if ((state == 1) && IsKeyPressed(KeyboardKey.Space)) - { - // When animation has finished, press space to restart - framesCounter = 0; - - for (int i = 0; i < MaxRecsX * MaxRecsY; i++) - { - recs[i].Height = RecsHeight; - recs[i].Width = RecsWidth; - } - - state = 0; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - if (state == 0) - { - for (int i = 0; i < MaxRecsX * MaxRecsY; i++) - { - DrawRectanglePro( - recs[i], - new Vector2(recs[i].Width / 2, recs[i].Height / 2), - rotation, - Color.Red - ); - } - } - else if (state == 1) - { - DrawText("PRESS [SPACE] TO PLAY AGAIN!", 240, 200, 20, Color.Gray); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; 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/FollowingEyes.cs b/Examples/Shapes/FollowingEyes.cs index 679ac3e..cea7b62 100644 --- a/Examples/Shapes/FollowingEyes.cs +++ b/Examples/Shapes/FollowingEyes.cs @@ -2,10 +2,14 @@ * * raylib [shapes] example - following eyes * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2013-2019 Ramon Santamaria (@raysan5) +* Example originally created with raylib 2.5, last time updated with raylib 2.5 +* +* 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) * ********************************************************************************************/ @@ -16,92 +20,127 @@ using static Raylib_cs.Raylib; namespace Examples.Shapes; -public class FollowingEyes +public partial class FollowingEyes : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Following Eyes"; + + public string Title => "raylib [shapes] example - following eyes"; + + private Vector2 scleraLeftPosition; + private Vector2 scleraRightPosition; + private float scleraRadius; + + private Vector2 irisLeftPosition; + private Vector2 irisRightPosition; + private float irisRadius; + + private float angle; + private float dx, dy, dxx, dyy; + + public void Init() + { + scleraLeftPosition = new(GetScreenWidth() / 2.0f - 100.0f, GetScreenHeight() / 2.0f); + scleraRightPosition = new(GetScreenWidth() / 2.0f + 100.0f, GetScreenHeight() / 2.0f); + scleraRadius = 80; + + irisLeftPosition = new(GetScreenWidth() / 2.0f - 100.0f, GetScreenHeight() / 2.0f); + irisRightPosition = new(GetScreenWidth() / 2.0f + 100.0f, GetScreenHeight() / 2.0f); + irisRadius = 24; + + angle = 0.0f; + dx = 0.0f; + dy = 0.0f; + dxx = 0.0f; + dyy = 0.0f; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + irisLeftPosition = GetMousePosition(); + irisRightPosition = GetMousePosition(); + + // Check not inside the left eye sclera + if (!CheckCollisionPointCircle(irisLeftPosition, scleraLeftPosition, scleraRadius - irisRadius)) + { + dx = irisLeftPosition.X - scleraLeftPosition.X; + dy = irisLeftPosition.Y - scleraLeftPosition.Y; + + angle = MathF.Atan2(dy, dx); + + dxx = (scleraRadius - irisRadius) * MathF.Cos(angle); + dyy = (scleraRadius - irisRadius) * MathF.Sin(angle); + + irisLeftPosition.X = scleraLeftPosition.X + dxx; + irisLeftPosition.Y = scleraLeftPosition.Y + dyy; + } + + // Check not inside the right eye sclera + if (!CheckCollisionPointCircle(irisRightPosition, scleraRightPosition, scleraRadius - irisRadius)) + { + dx = irisRightPosition.X - scleraRightPosition.X; + dy = irisRightPosition.Y - scleraRightPosition.Y; + + angle = MathF.Atan2(dy, dx); + + dxx = (scleraRadius - irisRadius) * MathF.Cos(angle); + dyy = (scleraRadius - irisRadius) * MathF.Sin(angle); + + irisRightPosition.X = scleraRightPosition.X + dxx; + irisRightPosition.Y = scleraRightPosition.Y + dyy; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawCircleV(scleraLeftPosition, scleraRadius, Color.LightGray); + DrawCircleV(irisLeftPosition, irisRadius, Color.Brown); + DrawCircleV(irisLeftPosition, 10, Color.Black); + + DrawCircleV(scleraRightPosition, scleraRadius, Color.LightGray); + DrawCircleV(irisRightPosition, irisRadius, Color.DarkGreen); + DrawCircleV(irisRightPosition, 10, Color.Black); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [shapes] example - following eyes"); - Vector2 scleraLeftPosition = new(GetScreenWidth() / 2 - 100, GetScreenHeight() / 2); - Vector2 scleraRightPosition = new(GetScreenWidth() / 2 + 100, GetScreenHeight() / 2); - float scleraRadius = 80; - - Vector2 irisLeftPosition = new(GetScreenWidth() / 2 - 100, GetScreenHeight() / 2); - Vector2 irisRightPosition = new(GetScreenWidth() / 2 + 100, GetScreenHeight() / 2); - float irisRadius = 24; - - float angle = 0.0f; - float dx = 0.0f, dy = 0.0f, dxx = 0.0f, dyy = 0.0f; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new FollowingEyes(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - irisLeftPosition = GetMousePosition(); - irisRightPosition = GetMousePosition(); - - // Check not inside the left eye sclera - if (!CheckCollisionPointCircle(irisLeftPosition, scleraLeftPosition, scleraRadius - 20)) - { - dx = irisLeftPosition.X - scleraLeftPosition.X; - dy = irisLeftPosition.Y - scleraLeftPosition.Y; - - angle = MathF.Atan2(dy, dx); - - dxx = (scleraRadius - irisRadius) * MathF.Cos(angle); - dyy = (scleraRadius - irisRadius) * MathF.Sin(angle); - - irisLeftPosition.X = scleraLeftPosition.X + dxx; - irisLeftPosition.Y = scleraLeftPosition.Y + dyy; - } - - // Check not inside the right eye sclera - if (!CheckCollisionPointCircle(irisRightPosition, scleraRightPosition, scleraRadius - 20)) - { - dx = irisRightPosition.X - scleraRightPosition.X; - dy = irisRightPosition.Y - scleraRightPosition.Y; - - angle = MathF.Atan2(dy, dx); - - dxx = (scleraRadius - irisRadius) * MathF.Cos(angle); - dyy = (scleraRadius - irisRadius) * MathF.Sin(angle); - - irisRightPosition.X = scleraRightPosition.X + dxx; - irisRightPosition.Y = scleraRightPosition.Y + dyy; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawCircleV(scleraLeftPosition, scleraRadius, Color.LightGray); - DrawCircleV(irisLeftPosition, irisRadius, Color.Brown); - DrawCircleV(irisLeftPosition, 10, Color.Black); - - DrawCircleV(scleraRightPosition, scleraRadius, Color.LightGray); - DrawCircleV(irisRightPosition, irisRadius, Color.DarkGreen); - DrawCircleV(irisRightPosition, 10, Color.Black); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- 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/LinesBezier.cs b/Examples/Shapes/LinesBezier.cs index 7861de4..0f8daa8 100644 --- a/Examples/Shapes/LinesBezier.cs +++ b/Examples/Shapes/LinesBezier.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [shapes] example - Cubic-bezier lines +* raylib [shapes] example - lines bezier * -* This example has been created using raylib 1.7 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 * -* Copyright (c) 2017 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.7, last time updated with raylib 1.7 +* +* 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) 2017-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,54 +18,110 @@ using static Raylib_cs.Raylib; namespace Examples.Shapes; -public class LinesBezier +public partial class LinesBezier : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Lines Bezier"; + + public string Title => "raylib [shapes] example - lines bezier"; + + public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint; + + private Vector2 startPoint; + private Vector2 endPoint; + private bool moveStartPoint; + private bool moveEndPoint; + + public void Init() + { + startPoint = new(30, 30); + endPoint = new(screenWidth - 30, screenHeight - 30); + moveStartPoint = false; + moveEndPoint = false; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + var mouse = GetMousePosition(); + + if (CheckCollisionPointCircle(mouse, startPoint, 10.0f) && IsMouseButtonDown(MouseButton.Left)) + { + moveStartPoint = true; + } + else if (CheckCollisionPointCircle(mouse, endPoint, 10.0f) && IsMouseButtonDown(MouseButton.Left)) + { + moveEndPoint = true; + } + + if (moveStartPoint) + { + startPoint = mouse; + if (IsMouseButtonReleased(MouseButton.Left)) + { + moveStartPoint = false; + } + } + + if (moveEndPoint) + { + endPoint = mouse; + if (IsMouseButtonReleased(MouseButton.Left)) + { + moveEndPoint = false; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText("MOVE START-END POINTS WITH MOUSE", 15, 20, 20, Color.Gray); + + // Draw line Cubic Bezier, in-out interpolation (easing), no control points + DrawLineBezier(startPoint, endPoint, 4.0f, Color.Blue); + + // Draw start-end spline circles with some details + DrawCircleV(startPoint, CheckCollisionPointCircle(mouse, startPoint, 10.0f) ? 14.0f : 8.0f, moveStartPoint ? Color.Red : Color.Blue); + DrawCircleV(endPoint, CheckCollisionPointCircle(mouse, endPoint, 10.0f) ? 14.0f : 8.0f, moveEndPoint ? Color.Red : Color.Blue); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - SetConfigFlags(ConfigFlags.Msaa4xHint); - InitWindow(screenWidth, screenHeight, "raylib [shapes] example - cubic-bezier lines"); + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - lines bezier"); - Vector2 start = new(0, 0); - Vector2 end = new(screenWidth, screenHeight); - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new LinesBezier(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (IsMouseButtonDown(MouseButton.Left)) - { - start = GetMousePosition(); - } - else if (IsMouseButtonDown(MouseButton.Right)) - { - end = GetMousePosition(); - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("USE MOUSE LEFT-RIGHT CLICK to DEFINE LINE START and END POINTS", 15, 20, 20, Color.Gray); - DrawLineBezier(start, end, 2.0f, Color.Red); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + 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/LogoRaylibAnim.cs b/Examples/Shapes/LogoRaylibAnim.cs index 7309e5b..4c2f022 100644 --- a/Examples/Shapes/LogoRaylibAnim.cs +++ b/Examples/Shapes/LogoRaylibAnim.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [shapes] example - raylib logo animation +* raylib [shapes] example - logo raylib anim * -* This example has been created using raylib 1.4 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2014 Ramon Santamaria (@raysan5) +* Example originally created with raylib 2.5, 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) 2014-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -13,179 +17,207 @@ using static Raylib_cs.Raylib; namespace Examples.Shapes; -public class LogoRaylibAnim +public partial class LogoRaylibAnim : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Logo Raylib Anim"; + + public string Title => "raylib [shapes] example - logo raylib anim"; + + private int logoPositionX; + private int logoPositionY; + + private int framesCounter; + private int lettersCount; + + private int topSideRecWidth; + private int leftSideRecHeight; + + private int bottomSideRecWidth; + private int rightSideRecHeight; + + private int state; // Tracking animation states (State Machine) + private float alpha; // Useful for fading + + private Color outline; + + public void Init() + { + logoPositionX = screenWidth / 2 - 128; + logoPositionY = screenHeight / 2 - 128; + + framesCounter = 0; + lettersCount = 0; + + topSideRecWidth = 16; + leftSideRecHeight = 16; + + bottomSideRecWidth = 16; + rightSideRecHeight = 16; + + state = 0; + alpha = 1.0f; + + outline = new(139, 71, 135, 255); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (state == 0) // State 0: Small box blinking + { + framesCounter++; + + if (framesCounter == 120) + { + state = 1; + framesCounter = 0; // Reset counter... will be used later... + } + } + else if (state == 1) // State 1: Top and left bars growing + { + topSideRecWidth += 4; + leftSideRecHeight += 4; + + if (topSideRecWidth == 256) + { + state = 2; + } + } + else if (state == 2) // State 2: Bottom and right bars growing + { + bottomSideRecWidth += 4; + rightSideRecHeight += 4; + + if (bottomSideRecWidth == 256) + { + state = 3; + } + } + else if (state == 3) // State 3: Letters appearing (one by one) + { + framesCounter++; + + // Every 12 frames, one more letter! + if (framesCounter / 12 != 0) + { + lettersCount++; + framesCounter = 0; + } + + // When all letters have appeared, just fade out everything + if (lettersCount >= 10) + { + alpha -= 0.02f; + + if (alpha <= 0.0f) + { + alpha = 0.0f; + state = 4; + } + } + } + else if (state == 4) // State 4: Reset and Replay + { + if (IsKeyPressed(KeyboardKey.R)) + { + framesCounter = 0; + lettersCount = 0; + + topSideRecWidth = 16; + leftSideRecHeight = 16; + + bottomSideRecWidth = 16; + rightSideRecHeight = 16; + + alpha = 1.0f; + state = 0; // Return to State 0 + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + if (state == 0) + { + if ((framesCounter / 15) % 2 != 0) + { + DrawRectangle(logoPositionX, logoPositionY, 16, 16, outline); + } + } + else if (state == 1) + { + DrawRectangle(logoPositionX, logoPositionY, topSideRecWidth, 16, outline); + DrawRectangle(logoPositionX, logoPositionY, 16, leftSideRecHeight, outline); + } + else if (state == 2) + { + DrawRectangle(logoPositionX, logoPositionY, topSideRecWidth, 16, outline); + DrawRectangle(logoPositionX, logoPositionY, 16, leftSideRecHeight, outline); + + DrawRectangle(logoPositionX + 240, logoPositionY, 16, rightSideRecHeight, outline); + DrawRectangle(logoPositionX, logoPositionY + 240, bottomSideRecWidth, 16, outline); + } + else if (state == 3) + { + var outlineFade = Fade(outline, alpha); + DrawRectangle(logoPositionX, logoPositionY, topSideRecWidth, 16, outlineFade); + DrawRectangle(logoPositionX, logoPositionY + 16, 16, leftSideRecHeight - 32, outlineFade); + + DrawRectangle(logoPositionX + 240, logoPositionY + 16, 16, rightSideRecHeight - 32, outlineFade); + DrawRectangle(logoPositionX, logoPositionY + 240, bottomSideRecWidth, 16, outlineFade); + + var whiteFade = Fade(Color.RayWhite, alpha); + DrawRectangle(screenWidth / 2 - 112, screenHeight / 2 - 112, 224, 224, whiteFade); + + var label = Fade(new Color(155, 79, 151, 255), alpha); + var text = "raylib".SubText(0, lettersCount); + DrawText(text, screenWidth / 2 - 44, screenHeight / 2 + 28, 50, label); + + DrawText("cs".SubText(0, lettersCount), screenWidth / 2 - 44, screenHeight / 2 + 58, 50, label); + } + else if (state == 4) + { + DrawText("[R] REPLAY", 340, 200, 20, Color.Gray); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - logo raylib anim"); - InitWindow(screenWidth, screenHeight, "raylib [shapes] example - raylib logo animation"); - - int logoPositionX = screenWidth / 2 - 128; - int logoPositionY = screenHeight / 2 - 128; - - int framesCounter = 0; - int lettersCount = 0; - - int topSideRecWidth = 16; - int leftSideRecHeight = 16; - - int bottomSideRecWidth = 16; - int rightSideRecHeight = 16; - - // Tracking animation states (State Machine) - int state = 0; - - // Useful for fading - float alpha = 1.0f; - - Color outline = new(139, 71, 135, 255); - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new LogoRaylibAnim(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // State 0: Small box blinking - if (state == 0) - { - framesCounter++; - - // Reset counter... will be used later... - if (framesCounter == 120) - { - state = 1; - framesCounter = 0; - } - } - // State 1: Top and left bars growing - else if (state == 1) - { - topSideRecWidth += 4; - leftSideRecHeight += 4; - - if (topSideRecWidth == 256) - { - state = 2; - } - } - // State 2: Bottom and right bars growing - else if (state == 2) - { - bottomSideRecWidth += 4; - rightSideRecHeight += 4; - - if (bottomSideRecWidth == 256) - { - state = 3; - } - } - // State 3: Letters appearing (one by one) - else if (state == 3) - { - framesCounter++; - - // Every 12 frames, one more letter! - if (framesCounter / 12 != 0) - { - lettersCount++; - framesCounter = 0; - } - - // When all letters have appeared, just fade out everything - if (lettersCount >= 10) - { - alpha -= 0.02f; - - if (alpha <= 0.0f) - { - alpha = 0.0f; - state = 4; - } - } - } - // State 4: Reset and Replay - else if (state == 4) - { - if (IsKeyPressed(KeyboardKey.R)) - { - framesCounter = 0; - lettersCount = 0; - - topSideRecWidth = 16; - leftSideRecHeight = 16; - - bottomSideRecWidth = 16; - rightSideRecHeight = 16; - - // Return to State 0 - alpha = 1.0f; - state = 0; - } - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - if (state == 0) - { - if ((framesCounter / 15) % 2 != 0) - { - DrawRectangle(logoPositionX, logoPositionY, 16, 16, outline); - } - } - else if (state == 1) - { - DrawRectangle(logoPositionX, logoPositionY, topSideRecWidth, 16, outline); - DrawRectangle(logoPositionX, logoPositionY, 16, leftSideRecHeight, outline); - } - else if (state == 2) - { - DrawRectangle(logoPositionX, logoPositionY, topSideRecWidth, 16, outline); - DrawRectangle(logoPositionX, logoPositionY, 16, leftSideRecHeight, outline); - - DrawRectangle(logoPositionX + 240, logoPositionY, 16, rightSideRecHeight, outline); - DrawRectangle(logoPositionX, logoPositionY + 240, bottomSideRecWidth, 16, outline); - } - else if (state == 3) - { - Color outlineFade = ColorAlpha(outline, alpha); - DrawRectangle(logoPositionX, logoPositionY, topSideRecWidth, 16, outlineFade); - DrawRectangle(logoPositionX, logoPositionY + 16, 16, leftSideRecHeight - 32, outlineFade); - - DrawRectangle(logoPositionX + 240, logoPositionY + 16, 16, rightSideRecHeight - 32, outlineFade); - DrawRectangle(logoPositionX, logoPositionY + 240, bottomSideRecWidth, 16, outlineFade); - - Color whiteFade = ColorAlpha(Color.RayWhite, alpha); - DrawRectangle(screenWidth / 2 - 112, screenHeight / 2 - 112, 224, 224, whiteFade); - - Color label = ColorAlpha(new Color(155, 79, 151, 255), alpha); - string text = "raylib".SubText(0, lettersCount); - DrawText(text, screenWidth / 2 - 44, screenHeight / 2 + 28, 50, label); - - DrawText("cs".SubText(0, lettersCount), screenWidth / 2 - 44, screenHeight / 2 + 58, 50, label); - } - else if (state == 4) - { - DrawText("[R] REPLAY", 340, 200, 20, Color.Gray); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Shapes/LogoRaylibShape.cs b/Examples/Shapes/LogoRaylibShape.cs index 341f330..660f849 100644 --- a/Examples/Shapes/LogoRaylibShape.cs +++ b/Examples/Shapes/LogoRaylibShape.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [shapes] example - Draw raylib logo using basic shapes +* raylib [shapes] example - logo raylib * -* This example has been created using raylib 1.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 * -* Copyright (c) 2014 Ramon Santamaria (@raysan5) +* 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) * ********************************************************************************************/ @@ -13,47 +17,64 @@ using static Raylib_cs.Raylib; namespace Examples.Shapes; -public class LogoRaylibShape +public partial class LogoRaylibShape : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Shapes / Logo Raylib Shape"; + + public string Title => "raylib [shapes] example - logo raylib"; + + public void Init() + { + } + + public void Update() + { + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawRectangle(screenWidth / 2 - 128, screenHeight / 2 - 128, 256, 256, new Color(139, 71, 135, 255)); + DrawRectangle(screenWidth / 2 - 112, screenHeight / 2 - 112, 224, 224, Color.RayWhite); + DrawText("raylib", screenWidth / 2 - 44, screenHeight / 2 + 28, 50, new Color(155, 79, 151, 255)); + DrawText("cs", screenWidth / 2 - 44, screenHeight / 2 + 58, 50, new Color(155, 79, 151, 255)); + + DrawText("this is NOT a texture!", 350, 370, 10, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - logo raylib"); - InitWindow(screenWidth, screenHeight, "raylib [shapes] example - raylib logo using shapes"); - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new LogoRaylibShape(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // TODO: Update your variables here - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawRectangle(screenWidth / 2 - 128, screenHeight / 2 - 128, 256, 256, new Color(139, 71, 135, 255)); - DrawRectangle(screenWidth / 2 - 112, screenHeight / 2 - 112, 224, 224, Color.RayWhite); - DrawText("raylib", screenWidth / 2 - 44, screenHeight / 2 + 28, 50, new Color(155, 79, 151, 255)); - DrawText("cs", screenWidth / 2 - 44, screenHeight / 2 + 58, 50, new Color(155, 79, 151, 255)); - - DrawText("this is NOT a texture!", 350, 370, 10, Color.Gray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + 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/RectangleScaling.cs b/Examples/Shapes/RectangleScaling.cs index 36c279b..5fbfc84 100644 --- a/Examples/Shapes/RectangleScaling.cs +++ b/Examples/Shapes/RectangleScaling.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [shapes] example - rectangle scaling by mouse +* raylib [shapes] example - rectangle scaling * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 +* +* Example originally created with raylib 2.5, last time updated with raylib 2.5 * * Example contributed by Vlad Adrian (@demizdor) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2018 Vlad Adrian (@demizdor) and 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) 2018-2025 Vlad Adrian (@demizdor) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -16,105 +20,145 @@ using static Raylib_cs.Raylib; namespace Examples.Shapes; -public class RectangleScaling +public partial class RectangleScaling : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + public const int MOUSE_SCALE_MARK_SIZE = 12; + public string Name => "Shapes / Rectangle Scaling"; + + public string Title => "raylib [shapes] example - rectangle scaling"; + + private Rectangle rec; + + private Vector2 mousePosition; + + private bool mouseScaleReady; + private bool mouseScaleMode; + + public void Init() + { + rec = new(100, 100, 200, 80); + + mousePosition = new(0, 0); + + mouseScaleReady = false; + mouseScaleMode = false; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + mousePosition = GetMousePosition(); + + Rectangle area = new( + rec.X + rec.Width - MOUSE_SCALE_MARK_SIZE, + rec.Y + rec.Height - MOUSE_SCALE_MARK_SIZE, + MOUSE_SCALE_MARK_SIZE, + MOUSE_SCALE_MARK_SIZE + ); + + if (CheckCollisionPointRec(mousePosition, area)) + { + mouseScaleReady = true; + if (IsMouseButtonPressed(MouseButton.Left)) + { + mouseScaleMode = true; + } + } + else + { + mouseScaleReady = false; + } + + if (mouseScaleMode) + { + mouseScaleReady = true; + + rec.Width = (mousePosition.X - rec.X); + rec.Height = (mousePosition.Y - rec.Y); + + // Check minimum rec size + if (rec.Width < MOUSE_SCALE_MARK_SIZE) + { + rec.Width = MOUSE_SCALE_MARK_SIZE; + } + if (rec.Height < MOUSE_SCALE_MARK_SIZE) + { + rec.Height = MOUSE_SCALE_MARK_SIZE; + } + + // Check maximum rec size + if (rec.Width > (GetScreenWidth() - rec.X)) + { + rec.Width = GetScreenWidth() - rec.X; + } + if (rec.Height > (GetScreenHeight() - rec.Y)) + { + rec.Height = GetScreenHeight() - rec.Y; + } + + if (IsMouseButtonReleased(MouseButton.Left)) + { + mouseScaleMode = false; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText("Scale rectangle dragging from bottom-right corner!", 10, 10, 20, Color.Gray); + + DrawRectangleRec(rec, Fade(Color.Green, 0.5f)); + + if (mouseScaleReady) + { + DrawRectangleLinesEx(rec, 1, Color.Red); + DrawTriangle( + new Vector2(rec.X + rec.Width - MOUSE_SCALE_MARK_SIZE, rec.Y + rec.Height), + new Vector2(rec.X + rec.Width, rec.Y + rec.Height), + new Vector2(rec.X + rec.Width, rec.Y + rec.Height - MOUSE_SCALE_MARK_SIZE), + Color.Red + ); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - rectangle scaling"); - InitWindow(screenWidth, screenHeight, "raylib [shapes] example - rectangle scaling mouse"); - - Rectangle rec = new(100, 100, 200, 80); - Vector2 mousePosition = new(0, 0); - - bool mouseScaleReady = false; - bool mouseScaleMode = false; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new RectangleScaling(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - mousePosition = GetMousePosition(); - - Rectangle area = new( - rec.X + rec.Width - MOUSE_SCALE_MARK_SIZE, - rec.Y + rec.Height - MOUSE_SCALE_MARK_SIZE, - MOUSE_SCALE_MARK_SIZE, - MOUSE_SCALE_MARK_SIZE - ); - - if (CheckCollisionPointRec(mousePosition, rec) && - CheckCollisionPointRec(mousePosition, area)) - { - mouseScaleReady = true; - if (IsMouseButtonPressed(MouseButton.Left)) - { - mouseScaleMode = true; - } - } - else - { - mouseScaleReady = false; - } - - if (mouseScaleMode) - { - mouseScaleReady = true; - - rec.Width = (mousePosition.X - rec.X); - rec.Height = (mousePosition.Y - rec.Y); - - if (rec.Width < MOUSE_SCALE_MARK_SIZE) - { - rec.Width = MOUSE_SCALE_MARK_SIZE; - } - if (rec.Height < MOUSE_SCALE_MARK_SIZE) - { - rec.Height = MOUSE_SCALE_MARK_SIZE; - } - - if (IsMouseButtonReleased(MouseButton.Left)) - { - mouseScaleMode = false; - } - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("Scale rectangle dragging from bottom-right corner!", 10, 10, 20, Color.Gray); - DrawRectangleRec(rec, ColorAlpha(Color.Green, 0.5f)); - - if (mouseScaleReady) - { - DrawRectangleLinesEx(rec, 1, Color.Red); - DrawTriangle( - new Vector2(rec.X + rec.Width - MOUSE_SCALE_MARK_SIZE, rec.Y + rec.Height), - new Vector2(rec.X + rec.Width, rec.Y + rec.Height), - new Vector2(rec.X + rec.Width, rec.Y + rec.Height - MOUSE_SCALE_MARK_SIZE), - Color.Red - ); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + 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/Shared/Easings.cs b/Examples/Shared/Easings.cs index 57cc879..4652f41 100644 --- a/Examples/Shared/Easings.cs +++ b/Examples/Shared/Easings.cs @@ -132,26 +132,26 @@ public static class Easings // Back Easing functions public static float EaseBackIn(float t, float b, float c, float d) { - float s = 1.70158f; - float postFix = t /= d; + var s = 1.70158f; + var postFix = t /= d; return (c * (postFix) * t * ((s + 1) * t - s) + b); } public static float EaseBackOut(float t, float b, float c, float d) { - float s = 1.70158f; + var s = 1.70158f; return (c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b); } public static float EaseBackInOut(float t, float b, float c, float d) { - float s = 1.70158f; + var s = 1.70158f; if ((t /= d / 2) < 1) { return (c / 2 * (t * t * (((s *= (1.525f)) + 1) * t - s)) + b); } - float postFix = t -= 2; + var postFix = t -= 2; return (c / 2 * ((postFix) * t * (((s *= (1.525f)) + 1) * t + s) + 2) + b); } @@ -164,17 +164,17 @@ public static class Easings } else if (t < (2 / 2.75f)) { - float postFix = t -= (1.5f / 2.75f); + var postFix = t -= (1.5f / 2.75f); return (c * (7.5625f * (postFix) * t + 0.75f) + b); } else if (t < (2.5 / 2.75)) { - float postFix = t -= (2.25f / 2.75f); + var postFix = t -= (2.25f / 2.75f); return (c * (7.5625f * (postFix) * t + 0.9375f) + b); } else { - float postFix = t -= (2.625f / 2.75f); + var postFix = t -= (2.625f / 2.75f); return (c * (7.5625f * (postFix) * t + 0.984375f) + b); } } @@ -208,10 +208,10 @@ public static class Easings return (b + c); } - float p = d * 0.3f; - float a = c; - float s = p / 4; - float postFix = a * MathF.Pow(2, 10 * (t -= 1)); + var p = d * 0.3f; + var a = c; + var s = p / 4; + var postFix = a * MathF.Pow(2, 10 * (t -= 1)); return (-(postFix * MathF.Sin((t * d - s) * (2 * MathF.PI) / p)) + b); } @@ -227,9 +227,9 @@ public static class Easings return (b + c); } - float p = d * 0.3f; - float a = c; - float s = p / 4; + var p = d * 0.3f; + var a = c; + var s = p / 4; return (a * MathF.Pow(2, -10 * t) * MathF.Sin((t * d - s) * (2 * MathF.PI) / p) + c + b); } @@ -245,11 +245,11 @@ public static class Easings return (b + c); } - float p = d * (0.3f * 1.5f); - float a = c; - float s = p / 4; + var p = d * (0.3f * 1.5f); + var a = c; + var s = p / 4; - float postFix = 0f; + var postFix = 0f; if (t < 1) { postFix = a * MathF.Pow(2, 10 * (t -= 1)); diff --git a/Examples/Shared/PbrLights.cs b/Examples/Shared/PbrLights.cs index 90d1966..1b64576 100644 --- a/Examples/Shared/PbrLights.cs +++ b/Examples/Shared/PbrLights.cs @@ -54,12 +54,12 @@ public class PbrLights ); light.Intensity = intensity; - string enabledName = "lights[" + lightsCount + "].enabled"; - string typeName = "lights[" + lightsCount + "].type"; - string posName = "lights[" + lightsCount + "].position"; - string targetName = "lights[" + lightsCount + "].target"; - string colorName = "lights[" + lightsCount + "].color"; - string intensityName = "lights[" + lightsCount + "].intensity"; + var enabledName = "lights[" + lightsCount + "].enabled"; + var typeName = "lights[" + lightsCount + "].type"; + var posName = "lights[" + lightsCount + "].position"; + var targetName = "lights[" + lightsCount + "].target"; + var colorName = "lights[" + lightsCount + "].color"; + var intensityName = "lights[" + lightsCount + "].intensity"; light.EnabledLoc = GetShaderLocation(shader, enabledName); light.TypeLoc = GetShaderLocation(shader, typeName); diff --git a/Examples/Shared/Rlights.cs b/Examples/Shared/Rlights.cs index 729b25f..21ba825 100644 --- a/Examples/Shared/Rlights.cs +++ b/Examples/Shared/Rlights.cs @@ -43,11 +43,11 @@ public static class Rlights light.Target = target; light.Color = color; - string enabledName = "lights[" + lightsCount + "].enabled"; - string typeName = "lights[" + lightsCount + "].type"; - string posName = "lights[" + lightsCount + "].position"; - string targetName = "lights[" + lightsCount + "].target"; - string colorName = "lights[" + lightsCount + "].color"; + var enabledName = "lights[" + lightsCount + "].enabled"; + var typeName = "lights[" + lightsCount + "].type"; + var posName = "lights[" + lightsCount + "].position"; + var targetName = "lights[" + lightsCount + "].target"; + var colorName = "lights[" + lightsCount + "].color"; light.EnabledLoc = GetShaderLocation(shader, enabledName); light.TypeLoc = GetShaderLocation(shader, typeName); @@ -78,7 +78,7 @@ public static class Rlights Raylib.SetShaderValue(shader, light.TargetLoc, light.Target, ShaderUniformDataType.Vec3); // Send to shader light color values - float[] color = new[] + var color = new[] { (float)light.Color.R / (float)255, (float)light.Color.G / (float)255, diff --git a/Examples/Text/CodepointsLoading.cs b/Examples/Text/CodepointsLoading.cs index cc41443..1846dd1 100644 --- a/Examples/Text/CodepointsLoading.cs +++ b/Examples/Text/CodepointsLoading.cs @@ -1,13 +1,15 @@ /******************************************************************************************* * -* raylib [text] example - Codepoints loading +* raylib [text] example - codepoints loading * -* Example originally created with raylib 4.2, last time updated with raylib 2.5 +* 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) 2022-2023 Ramon Santamaria (@raysan5) +* Copyright (c) 2022-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -19,32 +21,37 @@ using static Raylib_cs.Raylib; namespace Examples.Text; -class CodepointsLoading +public partial class CodepointsLoading : IExample { - public unsafe static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + // Text to be displayed, must be UTF-8 (save this code file as UTF-8) + // NOTE: It can contain all the required text for the game, + // this text will be scanned to get all the required codepoints + private const string text = + "いろはにほへと ちりぬるを\nわかよたれそ つねならむ\nうゐのおくやま けふこえて\nあさきゆめみし ゑひもせす"; + + public string Name => "Text / Codepoints Loading"; + + public string Title => "raylib [text] example - codepoints loading"; + + private List codepoints; + private int[] codepointsNoDuplicates; + private Font font; + private bool showFontAtlas; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [text] example - codepoints loading"); - - // Text to be displayed, must be UTF-8 (save this code file as UTF-8) - // NOTE: It can contain all the required text for the game, - // this text will be scanned to get all the required codepoints - const string text = - "いろはにほへと ちりぬるを\nわかよたれそ つねならむ\nうゐのおくやま けふこえて\nあさきゆめみし ゑひもせす"; - // Get codepoints from text - List codepoints = GetCodePoints(text); + codepoints = GetCodePoints(text); // Remove duplicate codepoints to generate smaller font atlas - int[] codepointsNoDuplicates = codepoints.Distinct().ToArray(); + codepointsNoDuplicates = codepoints.Distinct().ToArray(); // Load font containing all the provided codepoint glyphs // A texture font atlas is automatically generated - Font font = LoadFontEx( + font = LoadFontEx( "resources/fonts/DotGothic16-Regular.ttf", 36, codepointsNoDuplicates, @@ -54,64 +61,58 @@ class CodepointsLoading // Set bilinear scale filter for better font scaling SetTextureFilter(font.Texture, TextureFilter.Bilinear); - bool showFontAtlas = false; + SetTextLineSpacing(20); // Set line spacing for multiline text (when line breaks are included '\n') - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- + showFontAtlas = false; + } - // Main game loop - while (!WindowShouldClose()) + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.Space)) { - // Update - //---------------------------------------------------------------------------------- - if (IsKeyPressed(KeyboardKey.Space)) - { - showFontAtlas = !showFontAtlas; - } - //---------------------------------------------------------------------------------- + showFontAtlas = !showFontAtlas; + } + //---------------------------------------------------------------------------------- - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); - ClearBackground(Color.RayWhite); + ClearBackground(Color.RayWhite); - DrawRectangle(0, 0, GetScreenWidth(), 70, Color.Black); - DrawText($"Total codepoints contained in provided text: {codepoints.Count}", 10, 10, 20, Color.Green); - DrawText( - $"Total codepoints required for font atlas (duplicates excluded): {codepointsNoDuplicates.Length}", - 10, - 40, - 20, - Color.Green - ); + DrawRectangle(0, 0, GetScreenWidth(), 70, Color.Black); + DrawText($"Total codepoints contained in provided text: {codepoints.Count}", 10, 10, 20, Color.Green); + DrawText( + $"Total codepoints required for font atlas (duplicates excluded): {codepointsNoDuplicates.Length}", + 10, + 40, + 20, + Color.Green + ); - if (showFontAtlas) - { - // Draw generated font texture atlas containing provided codepoints - DrawTexture(font.Texture, 150, 100, Color.Black); - DrawRectangleLines(150, 100, font.Texture.Width, font.Texture.Height, Color.Black); - } - else - { - // Draw provided text with laoded font, containing all required codepoint glyphs - DrawTextEx(font, text, new Vector2(160, 110), 48, 5, Color.Black); - } - - DrawText("Press SPACE to toggle font atlas view!", 10, GetScreenHeight() - 30, 20, Color.Gray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + if (showFontAtlas) + { + // Draw generated font texture atlas containing provided codepoints + DrawTexture(font.Texture, 150, 100, Color.Black); + DrawRectangleLines(150, 100, font.Texture.Width, font.Texture.Height, Color.Black); + } + else + { + // Draw provided text with loaded font, containing all required codepoint glyphs + DrawTextEx(font, text, new Vector2(160, 110), 48, 5, Color.Black); } - // De-Initialization - //-------------------------------------------------------------------------------------- - UnloadFont(font); + DrawText("Press SPACE to toggle font atlas view!", 10, GetScreenHeight() - 30, 20, Color.Gray); - CloseWindow(); - //-------------------------------------------------------------------------------------- + EndDrawing(); + //---------------------------------------------------------------------------------- + } - return 0; + public void Unload() + { + UnloadFont(font); // Unload font } private static List GetCodePoints(string text) @@ -119,14 +120,42 @@ class CodepointsLoading List codePoints = new(); StringInfo stringInfo = new(text); - TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(text); + var enumerator = StringInfo.GetTextElementEnumerator(text); while (enumerator.MoveNext()) { - int codePoint = char.ConvertToUtf32(enumerator.Current.ToString(), 0); + var codePoint = char.ConvertToUtf32(enumerator.Current.ToString(), 0); codePoints.Add(codePoint); } return codePoints; } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [text] example - codepoints loading"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new CodepointsLoading(); + 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/FontFilters.cs b/Examples/Text/FontFilters.cs index 63cd9c3..e7e5260 100644 --- a/Examples/Text/FontFilters.cs +++ b/Examples/Text/FontFilters.cs @@ -1,15 +1,19 @@ /******************************************************************************************* * -* raylib [text] example - Font filters +* raylib [text] example - font filters * -* After font loading, font texture atlas filter could be configured for a softer +* Example complexity rating: [★★☆☆] 2/4 +* +* NOTE: After font loading, font texture atlas filter could be configured for a softer * display of the font when scaling it to different sizes, that way, it's not required * to generate multiple fonts at multiple sizes (as long as the scaling is not very different) * -* This example has been created using raylib 1.3.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.3, last time updated with raylib 4.2 * -* Copyright (c) 2015 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) 2015-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -18,129 +22,164 @@ using static Raylib_cs.Raylib; namespace Examples.Text; -public class FontFilters +public partial class FontFilters : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Text / Font Filters"; + + public string Title => "raylib [text] example - font filters"; + + private string msg; + private Font font; + private float fontSize; + private Vector2 fontPosition; + private Vector2 textSize; + private TextureFilter currentFontFilter; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [text] example - font filters"); - - string msg = "Loaded Font"; + msg = "Loaded Font"; // NOTE: Textures/Fonts MUST be loaded after Window initialization (OpenGL context is required) // TTF Font loading with custom generation parameters - Font font = LoadFontEx("resources/fonts/KAISG.ttf", 96, null, 0); + font = LoadFontEx("resources/fonts/KAISG.ttf", 96, null, 0); // Generate mipmap levels to use trilinear filtering - // NOTE: On 2D drawing it won't be noticeable, it looks like TEXTURE_FILTER_BILINEAR + // NOTE: On 2D drawing it won't be noticeable, it looks like FILTER_BILINEAR GenTextureMipmaps(ref font.Texture); - float fontSize = font.BaseSize; - Vector2 fontPosition = new(40, screenHeight / 2 - 80); - Vector2 textSize = new(0.0f, 0.0f); + fontSize = font.BaseSize; + fontPosition = new(40, screenHeight / 2 - 80); + textSize = new(0.0f, 0.0f); // Setup texture scaling filter SetTextureFilter(font.Texture, TextureFilter.Point); - TextureFilter currentFontFilter = TextureFilter.Point; + currentFontFilter = TextureFilter.Point; // TEXTURE_FILTER_POINT + } - SetTargetFPS(60); + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + fontSize += GetMouseWheelMove() * 4.0f; + + // Choose font texture filter method + if (IsKeyPressed(KeyboardKey.One)) + { + SetTextureFilter(font.Texture, TextureFilter.Point); + currentFontFilter = TextureFilter.Point; + } + else if (IsKeyPressed(KeyboardKey.Two)) + { + SetTextureFilter(font.Texture, TextureFilter.Bilinear); + currentFontFilter = TextureFilter.Bilinear; + } + else if (IsKeyPressed(KeyboardKey.Three)) + { + // NOTE: Trilinear filter won't be noticed on 2D drawing + SetTextureFilter(font.Texture, TextureFilter.Trilinear); + currentFontFilter = TextureFilter.Trilinear; + } + + textSize = MeasureTextEx(font, msg, fontSize, 0); + + if (IsKeyDown(KeyboardKey.Left)) + { + fontPosition.X -= 10; + } + else if (IsKeyDown(KeyboardKey.Right)) + { + fontPosition.X += 10; + } + +#if BROWSER + // NOTE: drag-and-drop font loading is not supported in the browser host; default loaded font is kept. +#else + // Load a dropped TTF file dynamically (at current fontSize) + if (IsFileDropped()) + { + var files = Raylib.GetDroppedFiles(); + + // NOTE: We only support first ttf file dropped + if (IsFileExtension(files[0], ".ttf")) + { + UnloadFont(font); + font = LoadFontEx(files[0], (int)fontSize, null, 0); + } + } +#endif + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText("Use mouse wheel to change font size", 20, 20, 10, Color.Gray); + DrawText("Use KEY_RIGHT and KEY_LEFT to move text", 20, 40, 10, Color.Gray); + DrawText("Use 1, 2, 3 to change texture filter", 20, 60, 10, Color.Gray); + DrawText("Drop a new TTF font for dynamic loading", 20, 80, 10, Color.DarkGray); + + DrawTextEx(font, msg, fontPosition, fontSize, 0, Color.Black); + + // TODO: It seems texSize measurement is not accurate due to chars offsets... + //DrawRectangleLines((int)fontPosition.X, (int)fontPosition.Y, (int)textSize.X, (int)textSize.Y, Color.Red); + + DrawRectangle(0, screenHeight - 80, screenWidth, 80, Color.LightGray); + DrawText($"Font size: {fontSize:00.00}", 20, screenHeight - 50, 10, Color.DarkGray); + DrawText($"Text size: [{textSize.X:00.00}, {textSize.Y:00.00}]", 20, screenHeight - 30, 10, Color.DarkGray); + DrawText("CURRENT TEXTURE FILTER:", 250, 400, 20, Color.Gray); + + if (currentFontFilter == TextureFilter.Point) + { + DrawText("POINT", 570, 400, 20, Color.Black); + } + else if (currentFontFilter == TextureFilter.Bilinear) + { + DrawText("BILINEAR", 570, 400, 20, Color.Black); + } + else if (currentFontFilter == TextureFilter.Trilinear) + { + DrawText("TRILINEAR", 570, 400, 20, Color.Black); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadFont(font); // Font unloading + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [text] example - font filters"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new FontFilters(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - fontSize += GetMouseWheelMove() * 4.0f; - - // Choose font texture filter method - if (IsKeyPressed(KeyboardKey.One)) - { - SetTextureFilter(font.Texture, TextureFilter.Point); - currentFontFilter = TextureFilter.Point; - } - else if (IsKeyPressed(KeyboardKey.Two)) - { - SetTextureFilter(font.Texture, TextureFilter.Bilinear); - currentFontFilter = TextureFilter.Bilinear; - } - else if (IsKeyPressed(KeyboardKey.Three)) - { - // NOTE: Trilinear filter won't be noticed on 2D drawing - SetTextureFilter(font.Texture, TextureFilter.Trilinear); - currentFontFilter = TextureFilter.Trilinear; - } - - textSize = MeasureTextEx(font, msg, fontSize, 0); - - if (IsKeyDown(KeyboardKey.Left)) - { - fontPosition.X -= 10; - } - else if (IsKeyDown(KeyboardKey.Right)) - { - fontPosition.X += 10; - } - - // Load a dropped TTF file dynamically (at current fontSize) - if (IsFileDropped()) - { - string[] files = Raylib.GetDroppedFiles(); - - // NOTE: We only support first ttf file dropped - if (IsFileExtension(files[0], ".ttf")) - { - UnloadFont(font); - font = LoadFontEx(files[0], (int)fontSize, null, 0); - } - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("Use mouse wheel to change font size", 20, 20, 10, Color.Gray); - DrawText("Use KEY_RIGHT and KEY_LEFT to move text", 20, 40, 10, Color.Gray); - DrawText("Use 1, 2, 3 to change texture filter", 20, 60, 10, Color.Gray); - DrawText("Drop a new TTF font for dynamic loading", 20, 80, 10, Color.DarkGray); - - DrawTextEx(font, msg, fontPosition, fontSize, 0, Color.Black); - - DrawRectangle(0, screenHeight - 80, screenWidth, 80, Color.LightGray); - DrawText("CURRENT TEXTURE FILTER:", 250, 400, 20, Color.Gray); - - if (currentFontFilter == TextureFilter.Point) - { - DrawText("POINT", 570, 400, 20, Color.Black); - } - else if (currentFontFilter == TextureFilter.Point) - { - DrawText("BILINEAR", 570, 400, 20, Color.Black); - } - else if (currentFontFilter == TextureFilter.Trilinear) - { - DrawText("TRILINEAR", 570, 400, 20, Color.Black); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadFont(font); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } } - diff --git a/Examples/Text/FontLoading.cs b/Examples/Text/FontLoading.cs index b7c37c5..15d842a 100644 --- a/Examples/Text/FontLoading.cs +++ b/Examples/Text/FontLoading.cs @@ -1,20 +1,24 @@ /******************************************************************************************* * -* raylib [text] example - Font loading +* raylib [text] example - font loading * -* raylib can load fonts from multiple file formats: +* Example complexity rating: [★☆☆☆] 1/4 +* +* NOTE: raylib can load fonts from multiple input file formats: * * - TTF/OTF > Sprite font atlas is generated on loading, user can configure * some of the generation parameters (size, characters to include) * - BMFonts > Angel code font fileformat, sprite font image must be provided -* together with the .fnt file, font generation cna not be configured +* together with the .fnt file, font generation can not be configured * - XNA Spritefont > Sprite font image, following XNA Spritefont conventions, * Characters in image must follow some spacing and order rules * -* This example has been created using raylib 2.6 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.4, last time updated with raylib 3.0 * -* Copyright (c) 2016-2019 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) 2016-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -23,78 +27,105 @@ using static Raylib_cs.Raylib; namespace Examples.Text; -public class FontLoading +public partial class FontLoading : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Text / Font Loading"; + + public string Title => "raylib [text] example - font loading"; + + private string msg; + private Font fontBm; + private Font fontTtf; + private bool useTtf; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [text] example - font loading"); - // Define characters to draw // NOTE: raylib supports UTF-8 encoding, following list is actually codified as UTF8 internally - string msg = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHI\nJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmn\nopqrstuvwxyz{|}~¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓ\nÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷\nøùúûüýþÿ"; + msg = "!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHI\nJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmn\nopqrstuvwxyz{|}~¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓ\nÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷\nøùúûüýþÿ"; // NOTE: Textures/Fonts MUST be loaded after Window initialization (OpenGL context is required) // BMFont (AngelCode) : Font data and image atlas have been generated using external program - Font fontBm = LoadFont("resources/fonts/pixantiqua.fnt"); + fontBm = LoadFont("resources/fonts/pixantiqua.fnt"); // Requires "resources/fonts/pixantiqua.png" // TTF font : Font data and atlas are generated directly from TTF // NOTE: We define a font base size of 32 pixels tall and up-to 250 characters - Font fontTtf = LoadFontEx("resources/fonts/pixantiqua.ttf", 32, null, 250); + fontTtf = LoadFontEx("resources/fonts/pixantiqua.ttf", 32, null, 250); - bool useTtf = false; + SetTextLineSpacing(16); // Set line spacing for multiline text (when line breaks are included '\n') - SetTargetFPS(60); + useTtf = false; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyDown(KeyboardKey.Space)) + { + useTtf = true; + } + else + { + useTtf = false; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText("Hold SPACE to use TTF generated font", 20, 20, 20, Color.LightGray); + + if (!useTtf) + { + DrawTextEx(fontBm, msg, new Vector2(20.0f, 100.0f), fontBm.BaseSize, 2, Color.Maroon); + DrawText("Using BMFont (Angelcode) imported", 20, GetScreenHeight() - 30, 20, Color.Gray); + } + else + { + DrawTextEx(fontTtf, msg, new Vector2(20.0f, 100.0f), fontTtf.BaseSize, 2, Color.Lime); + DrawText("Using TTF font generated", 20, GetScreenHeight() - 30, 20, Color.Gray); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadFont(fontBm); // AngelCode Font unloading + UnloadFont(fontTtf); // TTF Font unloading + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [text] example - font loading"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new FontLoading(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (IsKeyDown(KeyboardKey.Space)) - { - useTtf = true; - } - else - { - useTtf = false; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("Hold SPACE to use TTF generated font", 20, 20, 20, Color.LightGray); - - if (!useTtf) - { - DrawTextEx(fontBm, msg, new Vector2(20.0f, 100.0f), fontBm.BaseSize, 2, Color.Maroon); - DrawText("Using BMFont (Angelcode) imported", 20, GetScreenHeight() - 30, 20, Color.Gray); - } - else - { - DrawTextEx(fontTtf, msg, new Vector2(20.0f, 100.0f), fontTtf.BaseSize, 2, Color.Lime); - DrawText("Using TTF font generated", 20, GetScreenHeight() - 30, 20, Color.Gray); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadFont(fontBm); - UnloadFont(fontTtf); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Text/FontSdf.cs b/Examples/Text/FontSdf.cs index 3b01a9c..c323ccf 100644 --- a/Examples/Text/FontSdf.cs +++ b/Examples/Text/FontSdf.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [text] example - TTF loading and usage +* raylib [text] example - font sdf * -* This example has been created using raylib 1.3.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 * -* Copyright (c) 2015 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.3, 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) 2015-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -16,23 +20,44 @@ using static Raylib_cs.Raylib; namespace Examples.Text; -public class FontSdf +public partial class FontSdf : IExample { - public unsafe static int Main() + 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 => "Text / Font SDF"; + + public string Title => "raylib [text] example - font sdf"; + + private string msg; + + private Font fontDefault; + private Font fontSDF; + private Shader shader; + + private Vector2 fontPosition; + private Vector2 textSize; + private float fontSize; + private int currentFont; + + public unsafe void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [text] example - SDF fonts"); - // NOTE: Textures/Fonts MUST be loaded after Window initialization (OpenGL context is required) - string msg = "Signed Distance Fields"; + + msg = "Signed Distance Fields"; // Loading file to memory - int fileSize = 0; - byte* fileData = LoadFileData("resources/fonts/anonymous_pro_bold.ttf", ref fileSize); + var fileSize = 0; + var fileData = LoadFileData("resources/fonts/anonymous_pro_bold.ttf", ref fileSize); + + // Build the fonts in locals first: taking the address of a struct's field (&font.GlyphCount, + // &font.Recs) is only allowed for a stack local, not a heap field. Assign to the fields after. // Default font generation from TTF font Font fontDefault = new(); @@ -40,121 +65,141 @@ public class FontSdf fontDefault.GlyphCount = 95; // Loading font data from memory data - // Parameters > font size: 16, no chars array provided (0), chars count: 95 (autogenerate chars array) + // Parameters > font size: 16, no glyphs array provided (0), glyphs count: 95 (autogenerate chars array) fontDefault.Glyphs = LoadFontData(fileData, (int)fileSize, 16, null, 95, FontType.Default, &fontDefault.GlyphCount); - // Parameters > chars count: 95, font size: 16, chars padding in image: 4 px, pack method: 0 (default) - Image atlas = GenImageFontAtlas(fontDefault.Glyphs, &fontDefault.Recs, 95, 16, 4, 0); + // Parameters > glyphs count: 95, font size: 16, glyphs padding in image: 4 px, pack method: 0 (default) + var atlas = GenImageFontAtlas(fontDefault.Glyphs, &fontDefault.Recs, 95, 16, 4, 0); fontDefault.Texture = LoadTextureFromImage(atlas); UnloadImage(atlas); + this.fontDefault = fontDefault; // SDF font generation from TTF font Font fontSDF = new(); fontSDF.BaseSize = 16; fontSDF.GlyphCount = 95; - // Parameters > font size: 16, no chars array provided (0), chars count: 0 (defaults to 95) - fontSDF.Glyphs = LoadFontData(fileData, (int)fileSize, 16, null, 0, FontType.Sdf, &fontDefault.GlyphCount); - // Parameters > chars count: 95, font size: 16, chars padding in image: 0 px, pack method: 1 (Skyline algorythm) + // Parameters > font size: 16, no glyphs array provided (0), glyphs count: 0 (defaults to 95) + fontSDF.Glyphs = LoadFontData(fileData, (int)fileSize, 16, null, 0, FontType.Sdf, &fontSDF.GlyphCount); + // Parameters > glyphs count: 95, font size: 16, glyphs padding in image: 0 px, pack method: 1 (Skyline algorythm) atlas = GenImageFontAtlas(fontSDF.Glyphs, &fontSDF.Recs, 95, 16, 0, 1); fontSDF.Texture = LoadTextureFromImage(atlas); UnloadImage(atlas); + this.fontSDF = fontSDF; - // Free memory from loaded file - UnloadFileData(fileData); + UnloadFileData(fileData); // Free memory from loaded file // Load SDF required shader (we use default vertex shader) - Shader shader = LoadShader(null, "resources/shaders/glsl330/sdf.fs"); - // Required for SDF font - SetTextureFilter(fontSDF.Texture, TextureFilter.Bilinear); + shader = LoadShader(null, $"resources/shaders/glsl{GlslVersion}/sdf.fs"); + SetTextureFilter(fontSDF.Texture, TextureFilter.Bilinear); // Required for SDF font - Vector2 fontPosition = new(40, screenHeight / 2 - 50); - Vector2 textSize = new(0.0f); - float fontSize = 16.0f; - // 0 - fontDefault, 1 - fontSDF - int currentFont = 0; + fontPosition = new(40, screenHeight / 2.0f - 50); + textSize = new(0.0f); + fontSize = 16.0f; + currentFont = 0; // 0 - fontDefault, 1 - fontSDF + } - SetTargetFPS(60); + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + fontSize += GetMouseWheelMove() * 8.0f; + + if (fontSize < 6) + { + fontSize = 6; + } + + if (IsKeyDown(KeyboardKey.Space)) + { + currentFont = 1; + } + else + { + currentFont = 0; + } + + if (currentFont == 0) + { + textSize = MeasureTextEx(fontDefault, msg, fontSize, 0); + } + else + { + textSize = MeasureTextEx(fontSDF, msg, fontSize, 0); + } + + fontPosition.X = GetScreenWidth() / 2 - textSize.X / 2; + fontPosition.Y = GetScreenHeight() / 2 - textSize.Y / 2 + 80; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + if (currentFont == 1) + { + // NOTE: SDF fonts require a custom SDf shader to compute fragment color + BeginShaderMode(shader); // Activate SDF font shader + DrawTextEx(fontSDF, msg, fontPosition, fontSize, 0, Color.Black); + EndShaderMode(); // Activate our default shader for next drawings + + DrawTexture(fontSDF.Texture, 10, 10, Color.Black); + } + else + { + DrawTextEx(fontDefault, msg, fontPosition, fontSize, 0, Color.Black); + DrawTexture(fontDefault.Texture, 10, 10, Color.Black); + } + + if (currentFont == 1) + { + DrawText("SDF!", 320, 20, 80, Color.Red); + } + else + { + DrawText("default font", 315, 40, 30, Color.Gray); + } + + DrawText("FONT SIZE: 16.0", GetScreenWidth() - 240, 20, 20, Color.DarkGray); + DrawText($"RENDER SIZE: {fontSize:00.00}", GetScreenWidth() - 240, 50, 20, Color.DarkGray); + DrawText("Use MOUSE WHEEL to SCALE TEXT!", GetScreenWidth() - 240, 90, 10, Color.DarkGray); + + DrawText("HOLD SPACE to USE SDF FONT VERSION!", 340, GetScreenHeight() - 30, 20, Color.Maroon); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadFont(fontDefault); // Default font unloading + UnloadFont(fontSDF); // SDF font unloading + + UnloadShader(shader); // Unload SDF shader + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [text] example - font sdf"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new FontSdf(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - fontSize += GetMouseWheelMove() * 8.0f; - - if (fontSize < 6) - { - fontSize = 6; - } - - if (IsKeyDown(KeyboardKey.Space)) - { - currentFont = 1; - } - else - { - currentFont = 0; - } - - if (currentFont == 0) - { - textSize = MeasureTextEx(fontDefault, msg, fontSize, 0); - } - else - { - textSize = MeasureTextEx(fontSDF, msg, fontSize, 0); - } - - fontPosition.X = GetScreenWidth() / 2 - textSize.X / 2; - fontPosition.Y = GetScreenHeight() / 2 - textSize.Y / 2 + 80; - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - if (currentFont == 1) - { - // NOTE: SDF fonts require a custom SDf shader to compute fragment color - BeginShaderMode(shader); - DrawTextEx(fontSDF, msg, fontPosition, fontSize, 0, Color.Black); - EndShaderMode(); - - DrawTexture(fontSDF.Texture, 10, 10, Color.Black); - } - else - { - DrawTextEx(fontDefault, msg, fontPosition, fontSize, 0, Color.Black); - DrawTexture(fontDefault.Texture, 10, 10, Color.Black); - } - - if (currentFont == 1) - { - DrawText("SDF!", 320, 20, 80, Color.Red); - } - else - { - DrawText("default font", 315, 40, 30, Color.Gray); - } - - DrawText("FONT SIZE: 16.0", GetScreenWidth() - 240, 20, 20, Color.DarkGray); - DrawText($"RENDER SIZE: {fontSize:2F}", GetScreenWidth() - 240, 50, 20, Color.DarkGray); - DrawText("Use MOUSE WHEEL to SCALE TEXT!", GetScreenWidth() - 240, 90, 10, Color.DarkGray); - - DrawText("PRESS SPACE to USE SDF FONT VERSION!", 340, GetScreenHeight() - 30, 20, Color.Maroon); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadFont(fontDefault); - UnloadFont(fontSDF); - UnloadShader(shader); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Text/FontSpritefont.cs b/Examples/Text/FontSpritefont.cs index eb802a1..91c30d6 100644 --- a/Examples/Text/FontSpritefont.cs +++ b/Examples/Text/FontSpritefont.cs @@ -1,20 +1,25 @@ /******************************************************************************************* * -* raylib [text] example - Sprite font loading +* raylib [text] example - font spritefont +* +* Example complexity rating: [★☆☆☆] 1/4 +* +* NOTE: Sprite fonts should be generated following this conventions: * -* Loaded sprite fonts have been generated following XNA SpriteFont conventions: * - Characters must be ordered starting with character 32 (Space) * - Every character must be contained within the same Rectangle height -* - Every character and every line must be separated the same distance -* - Rectangles must be defined by a magenta color background +* - Every character and every line must be separated by the same distance (margin/padding) +* - Rectangles must be defined by a MAGENTA color background * -* If following this constraints, a font can be provided just by an image, -* this is quite handy to avoid additional information files (like BMFonts use). +* Following those constraints, a font can be provided just by an image, +* this is quite handy to avoid additional font descriptor files (like BMFonts use) * -* This example has been created using raylib 1.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.0, last time updated with raylib 1.0 * -* Copyright (c) 2014 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) 2014-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -23,72 +28,104 @@ using static Raylib_cs.Raylib; namespace Examples.Text; -public class FontSpritefont +public partial class FontSpritefont : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Text / Font Spritefont"; + + public string Title => "raylib [text] example - font spritefont"; + + private string msg1; + private string msg2; + private string msg3; + + private Font font1; + private Font font2; + private Font font3; + + private Vector2 fontPosition1; + private Vector2 fontPosition2; + private Vector2 fontPosition3; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [text] example - sprite font loading"); - - string msg1 = "THIS IS A custom SPRITE FONT..."; - string msg2 = "...and this is ANOTHER CUSTOM font..."; - string msg3 = "...and a THIRD one! GREAT! :D"; + msg1 = "THIS IS A custom SPRITE FONT..."; + msg2 = "...and this is ANOTHER CUSTOM font..."; + msg3 = "...and a THIRD one! GREAT! :D"; // NOTE: Textures/Fonts MUST be loaded after Window initialization (OpenGL context is required) - Font font1 = LoadFont("resources/fonts/custom_mecha.png"); - Font font2 = LoadFont("resources/fonts/custom_alagard.png"); - Font font3 = LoadFont("resources/fonts/custom_jupiter_crash.png"); + font1 = LoadFont("resources/custom_mecha.png"); // Font loading + font2 = LoadFont("resources/custom_alagard.png"); // Font loading + font3 = LoadFont("resources/custom_jupiter_crash.png"); // Font loading - Vector2 fontPosition1 = new( + fontPosition1 = new( screenWidth / 2 - MeasureTextEx(font1, msg1, font1.BaseSize, -3).X / 2, screenHeight / 2 - font1.BaseSize / 2 - 80 ); - Vector2 fontPosition2 = new( + fontPosition2 = new( screenWidth / 2 - MeasureTextEx(font2, msg2, font2.BaseSize, -2).X / 2, screenHeight / 2 - font2.BaseSize / 2 - 10 ); - Vector2 fontPosition3 = new( + fontPosition3 = new( screenWidth / 2 - MeasureTextEx(font3, msg3, font3.BaseSize, 2).X / 2, screenHeight / 2 - font3.BaseSize / 2 + 50 ); + } - SetTargetFPS(60); + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + // TODO: Update variables here... + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawTextEx(font1, msg1, fontPosition1, font1.BaseSize, -3, Color.White); + DrawTextEx(font2, msg2, fontPosition2, font2.BaseSize, -2, Color.White); + DrawTextEx(font3, msg3, fontPosition3, font3.BaseSize, 2, Color.White); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadFont(font1); // Font unloading + UnloadFont(font2); // Font unloading + UnloadFont(font3); // Font unloading + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [text] example - font spritefont"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new FontSpritefont(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // TODO: Update variables here... - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawTextEx(font1, msg1, fontPosition1, font1.BaseSize, -3, Color.White); - DrawTextEx(font2, msg2, fontPosition2, font2.BaseSize, -2, Color.White); - DrawTextEx(font3, msg3, fontPosition3, font3.BaseSize, 2, Color.White); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadFont(font1); - UnloadFont(font2); - UnloadFont(font3); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Text/FormatText.cs b/Examples/Text/FormatText.cs index b463e12..592b377 100644 --- a/Examples/Text/FormatText.cs +++ b/Examples/Text/FormatText.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [text] example - Text formatting +* raylib [text] example - format text * -* This example has been created using raylib 1.1 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 * -* Copyright (c) 2014 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.1, last time updated with raylib 3.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) * ********************************************************************************************/ @@ -13,49 +17,72 @@ using static Raylib_cs.Raylib; namespace Examples.Text; -public class FormatText +public partial class FormatText : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Text / Format Text"; + + public string Title => "raylib [text] example - format text"; + + private int score; + private int hiscore; + private int lives; + + public void Init() + { + score = 100020; + hiscore = 200450; + lives = 5; + } + + public void Update() + { + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText($"Score: {score:D8}", 200, 80, 20, Color.Red); + + DrawText($"HiScore: {hiscore:D8}", 200, 120, 20, Color.Green); + + DrawText($"Lives: {lives:D2}", 200, 160, 40, Color.Blue); + + DrawText($"Elapsed Time: {GetFrameTime() * 1000:F2} ms", 200, 220, 20, Color.Black); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [text] example - format text"); - InitWindow(screenWidth, screenHeight, "raylib [text] example - text formatting"); - - int score = 100020; - int hiscore = 200450; - int lives = 5; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new FormatText(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // TODO: Update your variables here - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText($"Score: {score}", 200, 80, 20, Color.Red); - DrawText($"HiScore: {hiscore}", 200, 120, 20, Color.Green); - DrawText($"Lives: {lives}", 200, 160, 40, Color.Blue); - DrawText($"Elapsed Time: {GetFrameTime() * 1000} ms", 200, 220, 20, Color.Black); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + 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/InputBox.cs b/Examples/Text/InputBox.cs index d15aee5..6356b5d 100644 --- a/Examples/Text/InputBox.cs +++ b/Examples/Text/InputBox.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [text] example - Input Box +* raylib [text] example - input box * -* This example has been created using raylib 1.7 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2017 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.7, last time updated with raylib 3.5 +* +* 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) 2017-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -13,149 +17,179 @@ using static Raylib_cs.Raylib; namespace Examples.Text; -public class InputBox +public partial class InputBox : IExample { public const int MaxInputChars = 9; + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Text / Input Box"; + + public string Title => "raylib [text] example - input box"; + + private char[] name; + private int letterCount; + + private Rectangle textBox; + private bool mouseOnText; + + private int framesCounter; + + public void Init() + { + // NOTE: One extra space required for null terminator char '\0' + name = new char[MaxInputChars + 1]; + letterCount = 0; + + textBox = new(screenWidth / 2 - 100, 180, 225, 50); + mouseOnText = false; + + framesCounter = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (CheckCollisionPointRec(GetMousePosition(), textBox)) + { + mouseOnText = true; + } + else + { + mouseOnText = false; + } + + if (mouseOnText) + { + // Set the window's cursor to the I-Beam + SetMouseCursor(MouseCursor.IBeam); + + // Get char pressed (unicode character) on the queue + var key = GetCharPressed(); + + // Check if more characters have been pressed on the same frame + while (key > 0) + { + // NOTE: Only allow keys in range [32..125] + if ((key >= 32) && (key <= 125) && (letterCount < MaxInputChars)) + { + name[letterCount] = (char)key; + name[letterCount + 1] = '\0'; // Add null terminator at the end of the string + letterCount++; + } + + key = GetCharPressed(); // Check next character in the queue + } + + if (IsKeyPressed(KeyboardKey.Backspace)) + { + letterCount -= 1; + if (letterCount < 0) + { + letterCount = 0; + } + name[letterCount] = '\0'; + } + } + else + { + SetMouseCursor(MouseCursor.Default); + } + + if (mouseOnText) + { + framesCounter += 1; + } + else + { + framesCounter = 0; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText("PLACE MOUSE OVER INPUT BOX!", 240, 140, 20, Color.Gray); + DrawRectangleRec(textBox, Color.LightGray); + + if (mouseOnText) + { + DrawRectangleLines( + (int)textBox.X, + (int)textBox.Y, + (int)textBox.Width, + (int)textBox.Height, + Color.Red + ); + } + else + { + DrawRectangleLines( + (int)textBox.X, + (int)textBox.Y, + (int)textBox.Width, + (int)textBox.Height, + Color.DarkGray + ); + } + + DrawText(new string(name), (int)textBox.X + 5, (int)textBox.Y + 8, 40, Color.Maroon); + DrawText($"INPUT CHARS: {letterCount}/{MaxInputChars}", 315, 250, 20, Color.DarkGray); + + if (mouseOnText) + { + if (letterCount < MaxInputChars) + { + // Draw blinking underscore char + if ((framesCounter / 20 % 2) == 0) + { + DrawText( + "_", + (int)textBox.X + 8 + MeasureText(new string(name), 40), + (int)textBox.Y + 12, + 40, + Color.Maroon + ); + } + } + else + { + DrawText("Press BACKSPACE to delete chars...", 230, 300, 20, Color.Gray); + } + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [text] example - input box"); - // NOTE: One extra space required for line ending char '\0' - char[] name = new char[MaxInputChars]; - int letterCount = 0; - - Rectangle textBox = new(screenWidth / 2 - 100, 180, 225, 50); - bool mouseOnText = false; - - int framesCounter = 0; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new InputBox(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (CheckCollisionPointRec(GetMousePosition(), textBox)) - { - mouseOnText = true; - } - else - { - mouseOnText = false; - } - - if (mouseOnText) - { - // Set the window's cursor to the I-Beam - SetMouseCursor(MouseCursor.IBeam); - - // Check if more characters have been pressed on the same frame - int key = GetCharPressed(); - - while (key > 0) - { - // NOTE: Only allow keys in range [32..125] - if ((key >= 32) && (key <= 125) && (letterCount < MaxInputChars)) - { - name[letterCount] = (char)key; - letterCount++; - } - - // Check next character in the queue - key = GetCharPressed(); - } - - if (IsKeyPressed(KeyboardKey.Backspace)) - { - letterCount -= 1; - if (letterCount < 0) - { - letterCount = 0; - } - name[letterCount] = '\0'; - } - } - else - { - SetMouseCursor(MouseCursor.Default); - } - - if (mouseOnText) - { - framesCounter += 1; - } - else - { - framesCounter = 0; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("PLACE MOUSE OVER INPUT BOX!", 240, 140, 20, Color.Gray); - DrawRectangleRec(textBox, Color.LightGray); - - if (mouseOnText) - { - DrawRectangleLines( - (int)textBox.X, - (int)textBox.Y, - (int)textBox.Width, - (int)textBox.Height, - Color.Red - ); - } - else - { - DrawRectangleLines( - (int)textBox.X, - (int)textBox.Y, - (int)textBox.Width, - (int)textBox.Height, - Color.DarkGray - ); - } - - DrawText(new string(name), (int)textBox.X + 5, (int)textBox.Y + 8, 40, Color.Maroon); - DrawText($"INPUT CHARS: {letterCount}/{MaxInputChars}", 315, 250, 20, Color.DarkGray); - - if (mouseOnText) - { - if (letterCount < MaxInputChars) - { - // Draw blinking underscore char - if ((framesCounter / 20 % 2) == 0) - { - DrawText( - "_", - (int)textBox.X + 8 + MeasureText(new string(name), 40), - (int)textBox.Y + 12, - 40, - Color.Maroon - ); - } - } - else - { - DrawText("Press BACKSPACE to delete chars...", 230, 300, 20, Color.Gray); - } - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- CloseWindow(); diff --git a/Examples/Text/RaylibFonts.cs b/Examples/Text/RaylibFonts.cs index 59c2ef0..ba1da0a 100644 --- a/Examples/Text/RaylibFonts.cs +++ b/Examples/Text/RaylibFonts.cs @@ -1,14 +1,18 @@ /******************************************************************************************* * -* raylib [text] example - raylib font loading and usage +* raylib [text] example - sprite fonts +* +* Example complexity rating: [★☆☆☆] 1/4 * * NOTE: raylib is distributed with some free to use fonts (even for commercial pourposes!) * To view details and credits for those fonts, check raylib license file * -* This example has been created using raylib 1.7 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.7, last time updated with raylib 3.7 * -* Copyright (c) 2017 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) 2017-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -17,32 +21,38 @@ using static Raylib_cs.Raylib; namespace Examples.Text; -public class RaylibFonts +public partial class RaylibFonts : IExample { public const int MaxFonts = 8; - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Text / Raylib Fonts"; + + public string Title => "raylib [text] example - sprite fonts"; + + private Font[] fonts; + private string[] messages; + private int[] spacings; + private Vector2[] positions; + private Color[] colors; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [text] example - raylib fonts"); - // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) - Font[] fonts = new Font[MaxFonts]; + fonts = new Font[MaxFonts]; - fonts[0] = LoadFont("resources/fonts/alagard.png"); - fonts[1] = LoadFont("resources/fonts/pixelplay.png"); - fonts[2] = LoadFont("resources/fonts/mecha.png"); - fonts[3] = LoadFont("resources/fonts/setback.png"); - fonts[4] = LoadFont("resources/fonts/romulus.png"); - fonts[5] = LoadFont("resources/fonts/pixantiqua.png"); - fonts[6] = LoadFont("resources/fonts/alpha_beta.png"); - fonts[7] = LoadFont("resources/fonts/jupiter_crash.png"); + fonts[0] = LoadFont("resources/sprite_fonts/alagard.png"); + fonts[1] = LoadFont("resources/sprite_fonts/pixelplay.png"); + fonts[2] = LoadFont("resources/sprite_fonts/mecha.png"); + fonts[3] = LoadFont("resources/sprite_fonts/setback.png"); + fonts[4] = LoadFont("resources/sprite_fonts/romulus.png"); + fonts[5] = LoadFont("resources/sprite_fonts/pixantiqua.png"); + fonts[6] = LoadFont("resources/sprite_fonts/alpha_beta.png"); + fonts[7] = LoadFont("resources/sprite_fonts/jupiter_crash.png"); - string[] messages = new string[MaxFonts] { + messages = new string[MaxFonts] { "ALAGARD FONT designed by Hewett Tsoi", "PIXELPLAY FONT designed by Aleksander Shevchuk", "MECHA FONT designed by Captain Falcon", @@ -53,12 +63,12 @@ public class RaylibFonts "JUPITER_CRASH FONT designed by Brian Kent (AEnigma)" }; - int[] spacings = new int[MaxFonts] { 2, 4, 8, 4, 3, 4, 4, 1 }; - Vector2[] positions = new Vector2[MaxFonts]; + spacings = new int[MaxFonts] { 2, 4, 8, 4, 3, 4, 4, 1 }; + positions = new Vector2[MaxFonts]; - for (int i = 0; i < MaxFonts; i++) + for (var i = 0; i < MaxFonts; i++) { - float halfWidth = MeasureTextEx(fonts[i], messages[i], fonts[i].BaseSize * 2, spacings[i]).X / 2; + var halfWidth = MeasureTextEx(fonts[i], messages[i], fonts[i].BaseSize * 2, spacings[i]).X / 2; positions[i].X = screenWidth / 2 - halfWidth; positions[i].Y = 60 + fonts[i].BaseSize + 45 * i; } @@ -68,7 +78,7 @@ public class RaylibFonts positions[4].Y += 2; positions[7].Y -= 8; - Color[] colors = new Color[MaxFonts] { + colors = new Color[MaxFonts] { Color.Maroon, Color.Orange, Color.DarkGreen, @@ -78,44 +88,61 @@ public class RaylibFonts Color.Gold, Color.Red }; - //-------------------------------------------------------------------------------------- + } - // Main game loop - while (!WindowShouldClose()) + public void Update() + { + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText("free sprite fonts included with raylib", 220, 20, 20, Color.DarkGray); + DrawLine(220, 50, 600, 50, Color.DarkGray); + + for (var i = 0; i < MaxFonts; i++) { - // Update - //---------------------------------------------------------------------------------- - // TODO: Update your variables here - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("free fonts included with raylib", 250, 20, 20, Color.DarkGray); - DrawLine(220, 50, 590, 50, Color.DarkGray); - - for (int i = 0; i < MaxFonts; i++) - { - DrawTextEx(fonts[i], messages[i], positions[i], fonts[i].BaseSize * 2, spacings[i], colors[i]); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + DrawTextEx(fonts[i], messages[i], positions[i], fonts[i].BaseSize * 2, spacings[i], colors[i]); } - // De-Initialization - //-------------------------------------------------------------------------------------- - for (int i = 0; i < MaxFonts; i++) + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + // Fonts unloading + for (var i = 0; i < MaxFonts; i++) { UnloadFont(fonts[i]); } + } - CloseWindow(); + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [text] example - sprite fonts"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new RaylibFonts(); + 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/RectangleBounds.cs b/Examples/Text/RectangleBounds.cs index b5ef7eb..3030298 100644 --- a/Examples/Text/RectangleBounds.cs +++ b/Examples/Text/RectangleBounds.cs @@ -1,153 +1,172 @@ +/******************************************************************************************* +* +* raylib [text] example - rectangle bounds +* +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 2.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) 2018-2025 Vlad Adrian (@demizdor) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + using System.Numerics; using static Raylib_cs.Raylib; namespace Examples.Text; -public class RectangleBounds +public partial class RectangleBounds : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + // Minimum width and heigh for the container rectangle + private const float minWidth = 60; + private const float minHeight = 60; + private const float maxWidth = screenWidth - 50.0f; + private const float maxHeight = screenHeight - 160.0f; + + public string Name => "Text / Rectangle Bounds"; + + public string Title => "raylib [text] example - rectangle bounds"; + + private string text; + private bool resizing; + private bool wordWrap; + + private Rectangle container; + private Rectangle resizer; + + private Vector2 lastMouse; + private Color borderColor; + private Font font; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + text = "Text cannot escape\tthis container\t...word wrap also works when active so here's " + + "a long text for testing.\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " + + "tempor incididunt ut labore et dolore magna aliqua. Nec ullamcorper sit amet risus nullam eget felis eget."; - InitWindow(screenWidth, screenHeight, "raylib [text] example - draw text inside a rectangle"); + resizing = false; + wordWrap = true; - string text = ""; - text += "Text cannot escape this container ...word wrap also works when active so here's a long text for testing."; - text += "\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor "; - text += "incididunt ut labore et dolore magna aliqua. Nec ullamcorper sit amet risus nullam eget felis eget."; - - bool resizing = false; - bool wordWrap = true; - - Rectangle container = new(25, 25, screenWidth - 50, screenHeight - 250); - Rectangle resizer = new( + container = new(25.0f, 25.0f, screenWidth - 50.0f, screenHeight - 250.0f); + resizer = new( container.X + container.Width - 17, container.Y + container.Height - 17, 14, 14 ); - // Minimum width and heigh for the container rectangle - const int minWidth = 60; - const int minHeight = 60; - const int maxWidth = screenWidth - 50; - const int maxHeight = screenHeight - 160; + lastMouse = new(0.0f, 0.0f); // Stores last mouse coordinates + borderColor = Color.Maroon; // Container border color + font = GetFontDefault(); // Get default system font + } - Vector2 lastMouse = new(0.0f, 0.0f); - Color borderColor = Color.Maroon; - Font font = GetFontDefault(); - - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- - - // Main game loop - while (!WindowShouldClose()) + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.Space)) { - // Update - //---------------------------------------------------------------------------------- - if (IsKeyPressed(KeyboardKey.Space)) - { - wordWrap = !wordWrap; - } - - Vector2 mouse = GetMousePosition(); - - // Check if the mouse is inside the container and toggle border color - if (CheckCollisionPointRec(mouse, container)) - { - borderColor = ColorAlpha(Color.Maroon, 0.4f); - } - else if (!resizing) - { - borderColor = Color.Maroon; - } - - // Container resizing logic - if (resizing) - { - if (IsMouseButtonReleased(MouseButton.Left)) - { - resizing = false; - } - - int width = (int)(container.Width + (mouse.X - lastMouse.X)); - container.Width = (width > minWidth) ? ((width < maxWidth) ? width : maxWidth) : minWidth; - - int height = (int)(container.Height + (mouse.Y - lastMouse.Y)); - container.Height = (height > minHeight) ? ((height < maxHeight) ? height : maxHeight) : minHeight; - } - else - { - // Check if we're resizing - if (IsMouseButtonDown(MouseButton.Left) && CheckCollisionPointRec(mouse, resizer)) - { - resizing = true; - } - } - - // Move resizer rectangle properly - resizer.X = container.X + container.Width - 17; - resizer.Y = container.Y + container.Height - 17; - - lastMouse = mouse; // Update mouse - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // Draw container border - DrawRectangleLinesEx(container, 3, borderColor); - - // Draw text in container (add some padding) - DrawTextBoxed( - font, - text, - new Rectangle(container.X + 4, container.Y + 4, container.Width - 4, container.Height - 4), - 20.0f, - 2.0f, - wordWrap, - Color.Gray - ); - - DrawRectangleRec(resizer, borderColor); - - // Draw bottom info - DrawRectangle(0, screenHeight - 54, screenWidth, 54, Color.Gray); - DrawRectangleRec(new Rectangle(382, screenHeight - 34, 12, 12), Color.Maroon); - - DrawText("Word Wrap: ", 313, screenHeight - 115, 20, Color.Black); - - if (wordWrap) - { - DrawText("ON", 447, screenHeight - 115, 20, Color.Red); - } - else - { - DrawText("OFF", 447, screenHeight - 115, 20, Color.Black); - } - - DrawText("Press [SPACE] to toggle word wrap", 218, screenHeight - 86, 20, Color.Gray); - DrawText("Click hold & drag the to resize the container", 155, screenHeight - 38, 20, Color.RayWhite); - - EndDrawing(); - //---------------------------------------------------------------------------------- + wordWrap = !wordWrap; } - // De-Initialization - //-------------------------------------------------------------------------------------- - CloseWindow(); - //-------------------------------------------------------------------------------------- + var mouse = GetMousePosition(); - return 0; + // Check if the mouse is inside the container and toggle border color + if (CheckCollisionPointRec(mouse, container)) + { + borderColor = Fade(Color.Maroon, 0.4f); + } + else if (!resizing) + { + borderColor = Color.Maroon; + } + + // Container resizing logic + if (resizing) + { + if (IsMouseButtonReleased(MouseButton.Left)) + { + resizing = false; + } + + var width = container.Width + (mouse.X - lastMouse.X); + container.Width = (width > minWidth) ? ((width < maxWidth) ? width : maxWidth) : minWidth; + + var height = container.Height + (mouse.Y - lastMouse.Y); + container.Height = (height > minHeight) ? ((height < maxHeight) ? height : maxHeight) : minHeight; + } + else + { + // Check if we're resizing + if (IsMouseButtonDown(MouseButton.Left) && CheckCollisionPointRec(mouse, resizer)) + { + resizing = true; + } + } + + // Move resizer rectangle properly + resizer.X = container.X + container.Width - 17; + resizer.Y = container.Y + container.Height - 17; + + lastMouse = mouse; // Update mouse + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawRectangleLinesEx(container, 3, borderColor); // Draw container border + + // Draw text in container (add some padding) + DrawTextBoxed( + font, + text, + new Rectangle(container.X + 4, container.Y + 4, container.Width - 4, container.Height - 4), + 20.0f, + 2.0f, + wordWrap, + Color.Gray + ); + + DrawRectangleRec(resizer, borderColor); // Draw the resize box + + // Draw bottom info + DrawRectangle(0, screenHeight - 54, screenWidth, 54, Color.Gray); + DrawRectangleRec(new Rectangle(382, screenHeight - 34, 12, 12), Color.Maroon); + + DrawText("Word Wrap: ", 313, screenHeight - 115, 20, Color.Black); + + if (wordWrap) + { + DrawText("ON", 447, screenHeight - 115, 20, Color.Red); + } + else + { + DrawText("OFF", 447, screenHeight - 115, 20, Color.Black); + } + + DrawText("Press [SPACE] to toggle word wrap", 218, screenHeight - 86, 20, Color.Gray); + DrawText("Click hold & drag the to resize the container", 155, screenHeight - 38, 20, Color.RayWhite); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { } // Draw text using font inside rectangle limits - static void DrawTextBoxed( + private static void DrawTextBoxed( Font font, string text, Rectangle rec, @@ -161,7 +180,7 @@ public class RectangleBounds } // Draw text using font inside rectangle limits with support for text selection - static unsafe void DrawTextBoxedSelectable( + private static unsafe void DrawTextBoxedSelectable( Font font, string text, Rectangle rec, @@ -175,37 +194,37 @@ public class RectangleBounds Color selectBackTint ) { - int length = text.Length; + var length = text.Length; // Offset between lines (on line break '\n') float textOffsetY = 0; // Offset X to next character to draw - float textOffsetX = 0.0f; + var textOffsetX = 0.0f; // Character rectangle scaling factor - float scaleFactor = fontSize / (float)font.BaseSize; + var scaleFactor = fontSize / (float)font.BaseSize; // Word/character wrapping mechanism variables - bool shouldMeasure = wordWrap; + var shouldMeasure = wordWrap; // Index where to begin drawing (where a line begins) - int startLine = -1; + var startLine = -1; // Index where to stop drawing (where a line ends) - int endLine = -1; + var endLine = -1; // Holds last value of the character position - int lastk = -1; + var lastk = -1; using var textNative = new Utf8Buffer(text); for (int i = 0, k = 0; i < length; i++, k++) { // Get next codepoint from byte string and glyph index in font - int codepointByteCount = 0; - int codepoint = GetCodepoint(&textNative.AsPointer()[i], &codepointByteCount); - int index = GetGlyphIndex(font, codepoint); + var codepointByteCount = 0; + var codepoint = GetCodepoint(&textNative.AsPointer()[i], &codepointByteCount); + var 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 @@ -275,7 +294,7 @@ public class RectangleBounds glyphWidth = 0; // Save character position when we switch states - int tmp = lastk; + var tmp = lastk; lastk = k - 1; k = tmp; } @@ -305,7 +324,7 @@ public class RectangleBounds } // Draw selection background - bool isGlyphSelected = false; + var isGlyphSelected = false; if ((selectStart >= 0) && (k >= selectStart) && (k < (selectStart + selectLength))) { DrawRectangleRec( @@ -354,4 +373,32 @@ public class RectangleBounds } } } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [text] example - rectangle bounds"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- + + var game = new RectangleBounds(); + 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 c8d3c81..70dbff0 100644 --- a/Examples/Text/Unicode.cs +++ b/Examples/Text/Unicode.cs @@ -1,228 +1,226 @@ /******************************************************************************************* * -* raylib [text] example - Using unicode with raylib +* raylib [text] example - unicode emojis * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★★] 4/4 +* +* Example originally created with raylib 2.5, last time updated with raylib 4.0 * * Example contributed by Vlad Adrian (@demizdor) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2019 Vlad Adrian (@demizdor) and 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 Vlad Adrian (@demizdor) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ +using System.Numerics; +using System.Text; using Raylib_cs; using static Raylib_cs.Raylib; -using static Raylib_cs.Color; -namespace Examples.Text +namespace Examples.Text; + +public partial class Unicode : IExample { - public class Unicode - { - const int EMOJI_PER_WIDTH = 8; - const int EMOJI_PER_HEIGHT = 4; + private const int screenWidth = 800; + private const int screenHeight = 450; - // String containing 180 emoji codepoints separated by a '\0' char - string emojiCodepoints = @"\xF0\x9F\x8C\x80\x00\xF0\x9F\x98\x80\x00\xF0\x9F\x98\x82\x00\xF0\x9F\xA4\xA3\x00\xF0\x9F\x98\x83\x00\xF0\x9F\x98\x86\x00\xF0\x9F\x98\x89\x00" - "\xF0\x9F\x98\x8B\x00\xF0\x9F\x98\x8E\x00\xF0\x9F\x98\x8D\x00\xF0\x9F\x98\x98\x00\xF0\x9F\x98\x97\x00\xF0\x9F\x98\x99\x00\xF0\x9F\x98\x9A\x00\xF0\x9F\x99\x82\x00" - "\xF0\x9F\xA4\x97\x00\xF0\x9F\xA4\xA9\x00\xF0\x9F\xA4\x94\x00\xF0\x9F\xA4\xA8\x00\xF0\x9F\x98\x90\x00\xF0\x9F\x98\x91\x00\xF0\x9F\x98\xB6\x00\xF0\x9F\x99\x84\x00" - "\xF0\x9F\x98\x8F\x00\xF0\x9F\x98\xA3\x00\xF0\x9F\x98\xA5\x00\xF0\x9F\x98\xAE\x00\xF0\x9F\xA4\x90\x00\xF0\x9F\x98\xAF\x00\xF0\x9F\x98\xAA\x00\xF0\x9F\x98\xAB\x00" - "\xF0\x9F\x98\xB4\x00\xF0\x9F\x98\x8C\x00\xF0\x9F\x98\x9B\x00\xF0\x9F\x98\x9D\x00\xF0\x9F\xA4\xA4\x00\xF0\x9F\x98\x92\x00\xF0\x9F\x98\x95\x00\xF0\x9F\x99\x83\x00" - "\xF0\x9F\xA4\x91\x00\xF0\x9F\x98\xB2\x00\xF0\x9F\x99\x81\x00\xF0\x9F\x98\x96\x00\xF0\x9F\x98\x9E\x00\xF0\x9F\x98\x9F\x00\xF0\x9F\x98\xA4\x00\xF0\x9F\x98\xA2\x00" - "\xF0\x9F\x98\xAD\x00\xF0\x9F\x98\xA6\x00\xF0\x9F\x98\xA9\x00\xF0\x9F\xA4\xAF\x00\xF0\x9F\x98\xAC\x00\xF0\x9F\x98\xB0\x00\xF0\x9F\x98\xB1\x00\xF0\x9F\x98\xB3\x00" - "\xF0\x9F\xA4\xAA\x00\xF0\x9F\x98\xB5\x00\xF0\x9F\x98\xA1\x00\xF0\x9F\x98\xA0\x00\xF0\x9F\xA4\xAC\x00\xF0\x9F\x98\xB7\x00\xF0\x9F\xA4\x92\x00\xF0\x9F\xA4\x95\x00" - "\xF0\x9F\xA4\xA2\x00\xF0\x9F\xA4\xAE\x00\xF0\x9F\xA4\xA7\x00\xF0\x9F\x98\x87\x00\xF0\x9F\xA4\xA0\x00\xF0\x9F\xA4\xAB\x00\xF0\x9F\xA4\xAD\x00\xF0\x9F\xA7\x90\x00" - "\xF0\x9F\xA4\x93\x00\xF0\x9F\x98\x88\x00\xF0\x9F\x91\xBF\x00\xF0\x9F\x91\xB9\x00\xF0\x9F\x91\xBA\x00\xF0\x9F\x92\x80\x00\xF0\x9F\x91\xBB\x00\xF0\x9F\x91\xBD\x00" - "\xF0\x9F\x91\xBE\x00\xF0\x9F\xA4\x96\x00\xF0\x9F\x92\xA9\x00\xF0\x9F\x98\xBA\x00\xF0\x9F\x98\xB8\x00\xF0\x9F\x98\xB9\x00\xF0\x9F\x98\xBB\x00\xF0\x9F\x98\xBD\x00" - "\xF0\x9F\x99\x80\x00\xF0\x9F\x98\xBF\x00\xF0\x9F\x8C\xBE\x00\xF0\x9F\x8C\xBF\x00\xF0\x9F\x8D\x80\x00\xF0\x9F\x8D\x83\x00\xF0\x9F\x8D\x87\x00\xF0\x9F\x8D\x93\x00" - "\xF0\x9F\xA5\x9D\x00\xF0\x9F\x8D\x85\x00\xF0\x9F\xA5\xA5\x00\xF0\x9F\xA5\x91\x00\xF0\x9F\x8D\x86\x00\xF0\x9F\xA5\x94\x00\xF0\x9F\xA5\x95\x00\xF0\x9F\x8C\xBD\x00" - "\xF0\x9F\x8C\xB6\x00\xF0\x9F\xA5\x92\x00\xF0\x9F\xA5\xA6\x00\xF0\x9F\x8D\x84\x00\xF0\x9F\xA5\x9C\x00\xF0\x9F\x8C\xB0\x00\xF0\x9F\x8D\x9E\x00\xF0\x9F\xA5\x90\x00" - "\xF0\x9F\xA5\x96\x00\xF0\x9F\xA5\xA8\x00\xF0\x9F\xA5\x9E\x00\xF0\x9F\xA7\x80\x00\xF0\x9F\x8D\x96\x00\xF0\x9F\x8D\x97\x00\xF0\x9F\xA5\xA9\x00\xF0\x9F\xA5\x93\x00" - "\xF0\x9F\x8D\x94\x00\xF0\x9F\x8D\x9F\x00\xF0\x9F\x8D\x95\x00\xF0\x9F\x8C\xAD\x00\xF0\x9F\xA5\xAA\x00\xF0\x9F\x8C\xAE\x00\xF0\x9F\x8C\xAF\x00\xF0\x9F\xA5\x99\x00" - "\xF0\x9F\xA5\x9A\x00\xF0\x9F\x8D\xB3\x00\xF0\x9F\xA5\x98\x00\xF0\x9F\x8D\xB2\x00\xF0\x9F\xA5\xA3\x00\xF0\x9F\xA5\x97\x00\xF0\x9F\x8D\xBF\x00\xF0\x9F\xA5\xAB\x00" - "\xF0\x9F\x8D\xB1\x00\xF0\x9F\x8D\x98\x00\xF0\x9F\x8D\x9D\x00\xF0\x9F\x8D\xA0\x00\xF0\x9F\x8D\xA2\x00\xF0\x9F\x8D\xA5\x00\xF0\x9F\x8D\xA1\x00\xF0\x9F\xA5\x9F\x00" - "\xF0\x9F\xA5\xA1\x00\xF0\x9F\x8D\xA6\x00\xF0\x9F\x8D\xAA\x00\xF0\x9F\x8E\x82\x00\xF0\x9F\x8D\xB0\x00\xF0\x9F\xA5\xA7\x00\xF0\x9F\x8D\xAB\x00\xF0\x9F\x8D\xAF\x00" - "\xF0\x9F\x8D\xBC\x00\xF0\x9F\xA5\x9B\x00\xF0\x9F\x8D\xB5\x00\xF0\x9F\x8D\xB6\x00\xF0\x9F\x8D\xBE\x00\xF0\x9F\x8D\xB7\x00\xF0\x9F\x8D\xBB\x00\xF0\x9F\xA5\x82\x00" - "\xF0\x9F\xA5\x83\x00\xF0\x9F\xA5\xA4\x00\xF0\x9F\xA5\xA2\x00\xF0\x9F\x91\x81\x00\xF0\x9F\x91\x85\x00\xF0\x9F\x91\x84\x00\xF0\x9F\x92\x8B\x00\xF0\x9F\x92\x98\x00" - "\xF0\x9F\x92\x93\x00\xF0\x9F\x92\x97\x00\xF0\x9F\x92\x99\x00\xF0\x9F\x92\x9B\x00\xF0\x9F\xA7\xA1\x00\xF0\x9F\x92\x9C\x00\xF0\x9F\x96\xA4\x00\xF0\x9F\x92\x9D\x00" + 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 + { + public int Index; // Index inside `emojiCodepoints` + public int Message; // Message index + public Color Color; // Emoji color + } + + private EmojiInfo[] emoji; + + private int hovered; + private int selected; + + struct Message + { + public string Text; + public string Language; + + public Message(string text, string language) + { + Text = text; + Language = language; + } + } + + // String containing 180 emoji codepoints separated by a '\0' char + const string EmojiCodepoints = + "\xF0\x9F\x8C\x80\x00\xF0\x9F\x98\x80\x00\xF0\x9F\x98\x82\x00\xF0\x9F\xA4\xA3\x00\xF0\x9F\x98\x83\x00\xF0\x9F\x98\x86\x00\xF0\x9F\x98\x89\x00" + + "\xF0\x9F\x98\x8B\x00\xF0\x9F\x98\x8E\x00\xF0\x9F\x98\x8D\x00\xF0\x9F\x98\x98\x00\xF0\x9F\x98\x97\x00\xF0\x9F\x98\x99\x00\xF0\x9F\x98\x9A\x00\xF0\x9F\x99\x82\x00" + + "\xF0\x9F\xA4\x97\x00\xF0\x9F\xA4\xA9\x00\xF0\x9F\xA4\x94\x00\xF0\x9F\xA4\xA8\x00\xF0\x9F\x98\x90\x00\xF0\x9F\x98\x91\x00\xF0\x9F\x98\xB6\x00\xF0\x9F\x99\x84\x00" + + "\xF0\x9F\x98\x8F\x00\xF0\x9F\x98\xA3\x00\xF0\x9F\x98\xA5\x00\xF0\x9F\x98\xAE\x00\xF0\x9F\xA4\x90\x00\xF0\x9F\x98\xAF\x00\xF0\x9F\x98\xAA\x00\xF0\x9F\x98\xAB\x00" + + "\xF0\x9F\x98\xB4\x00\xF0\x9F\x98\x8C\x00\xF0\x9F\x98\x9B\x00\xF0\x9F\x98\x9D\x00\xF0\x9F\xA4\xA4\x00\xF0\x9F\x98\x92\x00\xF0\x9F\x98\x95\x00\xF0\x9F\x99\x83\x00" + + "\xF0\x9F\xA4\x91\x00\xF0\x9F\x98\xB2\x00\xF0\x9F\x99\x81\x00\xF0\x9F\x98\x96\x00\xF0\x9F\x98\x9E\x00\xF0\x9F\x98\x9F\x00\xF0\x9F\x98\xA4\x00\xF0\x9F\x98\xA2\x00" + + "\xF0\x9F\x98\xAD\x00\xF0\x9F\x98\xA6\x00\xF0\x9F\x98\xA9\x00\xF0\x9F\xA4\xAF\x00\xF0\x9F\x98\xAC\x00\xF0\x9F\x98\xB0\x00\xF0\x9F\x98\xB1\x00\xF0\x9F\x98\xB3\x00" + + "\xF0\x9F\xA4\xAA\x00\xF0\x9F\x98\xB5\x00\xF0\x9F\x98\xA1\x00\xF0\x9F\x98\xA0\x00\xF0\x9F\xA4\xAC\x00\xF0\x9F\x98\xB7\x00\xF0\x9F\xA4\x92\x00\xF0\x9F\xA4\x95\x00" + + "\xF0\x9F\xA4\xA2\x00\xF0\x9F\xA4\xAE\x00\xF0\x9F\xA4\xA7\x00\xF0\x9F\x98\x87\x00\xF0\x9F\xA4\xA0\x00\xF0\x9F\xA4\xAB\x00\xF0\x9F\xA4\xAD\x00\xF0\x9F\xA7\x90\x00" + + "\xF0\x9F\xA4\x93\x00\xF0\x9F\x98\x88\x00\xF0\x9F\x91\xBF\x00\xF0\x9F\x91\xB9\x00\xF0\x9F\x91\xBA\x00\xF0\x9F\x92\x80\x00\xF0\x9F\x91\xBB\x00\xF0\x9F\x91\xBD\x00" + + "\xF0\x9F\x91\xBE\x00\xF0\x9F\xA4\x96\x00\xF0\x9F\x92\xA9\x00\xF0\x9F\x98\xBA\x00\xF0\x9F\x98\xB8\x00\xF0\x9F\x98\xB9\x00\xF0\x9F\x98\xBB\x00\xF0\x9F\x98\xBD\x00" + + "\xF0\x9F\x99\x80\x00\xF0\x9F\x98\xBF\x00\xF0\x9F\x8C\xBE\x00\xF0\x9F\x8C\xBF\x00\xF0\x9F\x8D\x80\x00\xF0\x9F\x8D\x83\x00\xF0\x9F\x8D\x87\x00\xF0\x9F\x8D\x93\x00" + + "\xF0\x9F\xA5\x9D\x00\xF0\x9F\x8D\x85\x00\xF0\x9F\xA5\xA5\x00\xF0\x9F\xA5\x91\x00\xF0\x9F\x8D\x86\x00\xF0\x9F\xA5\x94\x00\xF0\x9F\xA5\x95\x00\xF0\x9F\x8C\xBD\x00" + + "\xF0\x9F\x8C\xB6\x00\xF0\x9F\xA5\x92\x00\xF0\x9F\xA5\xA6\x00\xF0\x9F\x8D\x84\x00\xF0\x9F\xA5\x9C\x00\xF0\x9F\x8C\xB0\x00\xF0\x9F\x8D\x9E\x00\xF0\x9F\xA5\x90\x00" + + "\xF0\x9F\xA5\x96\x00\xF0\x9F\xA5\xA8\x00\xF0\x9F\xA5\x9E\x00\xF0\x9F\xA7\x80\x00\xF0\x9F\x8D\x96\x00\xF0\x9F\x8D\x97\x00\xF0\x9F\xA5\xA9\x00\xF0\x9F\xA5\x93\x00" + + "\xF0\x9F\x8D\x94\x00\xF0\x9F\x8D\x9F\x00\xF0\x9F\x8D\x95\x00\xF0\x9F\x8C\xAD\x00\xF0\x9F\xA5\xAA\x00\xF0\x9F\x8C\xAE\x00\xF0\x9F\x8C\xAF\x00\xF0\x9F\xA5\x99\x00" + + "\xF0\x9F\xA5\x9A\x00\xF0\x9F\x8D\xB3\x00\xF0\x9F\xA5\x98\x00\xF0\x9F\x8D\xB2\x00\xF0\x9F\xA5\xA3\x00\xF0\x9F\xA5\x97\x00\xF0\x9F\x8D\xBF\x00\xF0\x9F\xA5\xAB\x00" + + "\xF0\x9F\x8D\xB1\x00\xF0\x9F\x8D\x98\x00\xF0\x9F\x8D\x9D\x00\xF0\x9F\x8D\xA0\x00\xF0\x9F\x8D\xA2\x00\xF0\x9F\x8D\xA5\x00\xF0\x9F\x8D\xA1\x00\xF0\x9F\xA5\x9F\x00" + + "\xF0\x9F\xA5\xA1\x00\xF0\x9F\x8D\xA6\x00\xF0\x9F\x8D\xAA\x00\xF0\x9F\x8E\x82\x00\xF0\x9F\x8D\xB0\x00\xF0\x9F\xA5\xA7\x00\xF0\x9F\x8D\xAB\x00\xF0\x9F\x8D\xAF\x00" + + "\xF0\x9F\x8D\xBC\x00\xF0\x9F\xA5\x9B\x00\xF0\x9F\x8D\xB5\x00\xF0\x9F\x8D\xB6\x00\xF0\x9F\x8D\xBE\x00\xF0\x9F\x8D\xB7\x00\xF0\x9F\x8D\xBB\x00\xF0\x9F\xA5\x82\x00" + + "\xF0\x9F\xA5\x83\x00\xF0\x9F\xA5\xA4\x00\xF0\x9F\xA5\xA2\x00\xF0\x9F\x91\x81\x00\xF0\x9F\x91\x85\x00\xF0\x9F\x91\x84\x00\xF0\x9F\x92\x8B\x00\xF0\x9F\x92\x98\x00" + + "\xF0\x9F\x92\x93\x00\xF0\x9F\x92\x97\x00\xF0\x9F\x92\x99\x00\xF0\x9F\x92\x9B\x00\xF0\x9F\xA7\xA1\x00\xF0\x9F\x92\x9C\x00\xF0\x9F\x96\xA4\x00\xF0\x9F\x92\x9D\x00" + "\xF0\x9F\x92\x9F\x00\xF0\x9F\x92\x8C\x00\xF0\x9F\x92\xA4\x00\xF0\x9F\x92\xA2\x00\xF0\x9F\x92\xA3\x00"; - struct { - string text; - string language; - } - const messages[] = { // Array containing all of the emojis messages - {"\x46\x61\x6C\x73\x63\x68\x65\x73\x20\xC3\x9C\x62\x65\x6E\x20\x76\x6F\x6E\x20\x58\x79\x6C\x6F\x70\x68\x6F\x6E\x6D\x75\x73\x69\x6B\x20\x71\x75\xC3\xA4\x6C" - "\x74\x20\x6A\x65\x64\x65\x6E\x20\x67\x72\xC3\xB6\xC3\x9F\x65\x72\x65\x6E\x20\x5A\x77\x65\x72\x67", "German"}, - {"\x42\x65\x69\xC3\x9F\x20\x6E\x69\x63\x68\x74\x20\x69\x6E\x20\x64\x69\x65\x20\x48\x61\x6E\x64\x2C\x20\x64\x69\x65\x20\x64\x69\x63\x68\x20\x66\xC3\xBC\x74" - "\x74\x65\x72\x74\x2E", "German"}, - {"\x41\x75\xC3\x9F\x65\x72\x6F\x72\x64\x65\x6E\x74\x6C\x69\x63\x68\x65\x20\xC3\x9C\x62\x65\x6C\x20\x65\x72\x66\x6F\x72\x64\x65\x72\x6E\x20\x61\x75\xC3\x9F" - "\x65\x72\x6F\x72\x64\x65\x6E\x74\x6C\x69\x63\x68\x65\x20\x4D\x69\x74\x74\x65\x6C\x2E", "German"}, - {"\xD4\xBF\xD6\x80\xD5\xB6\xD5\xA1\xD5\xB4\x20\xD5\xA1\xD5\xBA\xD5\xA1\xD5\xAF\xD5\xAB\x20\xD5\xB8\xD6\x82\xD5\xBF\xD5\xA5\xD5\xAC\x20\xD6\x87\x20\xD5\xAB" - "\xD5\xB6\xD5\xAE\xD5\xAB\x20\xD5\xA1\xD5\xB6\xD5\xB0\xD5\xA1\xD5\xB6\xD5\xA3\xD5\xAB\xD5\xBD\xD5\xBF\x20\xD5\xB9\xD5\xA8\xD5\xB6\xD5\xA5\xD6\x80", "Armenian"}, - {"\xD4\xB5\xD6\x80\xD5\xA2\x20\xD5\xB8\xD6\x80\x20\xD5\xAF\xD5\xA1\xD6\x81\xD5\xAB\xD5\xB6\xD5\xA8\x20\xD5\xA5\xD5\xAF\xD5\xA1\xD6\x82\x20\xD5\xA1\xD5\xB6\xD5" - "\xBF\xD5\xA1\xD5\xBC\x2C\x20\xD5\xAE\xD5\xA1\xD5\xBC\xD5\xA5\xD6\x80\xD5\xA8\x20\xD5\xA1\xD5\xBD\xD5\xA1\xD6\x81\xD5\xAB\xD5\xB6\x2E\x2E\x2E\x20\xC2\xAB\xD4\xBF" - "\xD5\xB8\xD5\xBF\xD5\xA8\x20\xD5\xB4\xD5\xA5\xD6\x80\xD5\xB8\xD5\xB6\xD6\x81\xD5\xAB\xD6\x81\x20\xD5\xA7\x3A\xC2\xBB", "Armenian"}, - {"\xD4\xB3\xD5\xA1\xD5\xBC\xD5\xA8\xD5\x9D\x20\xD5\xA3\xD5\xA1\xD6\x80\xD5\xB6\xD5\xA1\xD5\xB6\x2C\x20\xD5\xB1\xD5\xAB\xD6\x82\xD5\xB6\xD5\xA8\xD5\x9D\x20\xD5" - "\xB1\xD5\xB4\xD5\xBC\xD5\xA1\xD5\xB6", "Armenian"}, - {"\x4A\x65\xC5\xBC\x75\x20\x6B\x6C\xC4\x85\x74\x77\x2C\x20\x73\x70\xC5\x82\xC3\xB3\x64\xC5\xBA\x20\x46\x69\x6E\x6F\x6D\x20\x63\x7A\xC4\x99\xC5\x9B\xC4\x87" - "\x20\x67\x72\x79\x20\x68\x61\xC5\x84\x62\x21", "Polish"}, - {"\x44\x6F\x62\x72\x79\x6D\x69\x20\x63\x68\xC4\x99\x63\x69\x61\x6D\x69\x20\x6A\x65\x73\x74\x20\x70\x69\x65\x6B\xC5\x82\x6F\x20\x77\x79\x62\x72\x75\x6B\x6F" - "\x77\x61\x6E\x65\x2E", "Polish"}, - {"\xC3\x8E\xC8\x9B\x69\x20\x6D\x75\x6C\xC8\x9B\x75\x6D\x65\x73\x63\x20\x63\xC4\x83\x20\x61\x69\x20\x61\x6C\x65\x73\x20\x72\x61\x79\x6C\x69\x62\x2E\x0A\xC8\x98" - "\x69\x20\x73\x70\x65\x72\x20\x73\xC4\x83\x20\x61\x69\x20\x6F\x20\x7A\x69\x20\x62\x75\x6E\xC4\x83\x21", "Romanian"}, - {"\xD0\xAD\xD1\x85\x2C\x20\xD1\x87\xD1\x83\xD0\xB6\xD0\xB0\xD0\xBA\x2C\x20\xD0\xBE\xD0\xB1\xD1\x89\xD0\xB8\xD0\xB9\x20\xD1\x81\xD1\x8A\xD1\x91\xD0\xBC\x20" - "\xD1\x86\xD0\xB5\xD0\xBD\x20\xD1\x88\xD0\xBB\xD1\x8F\xD0\xBF\x20\x28\xD1\x8E\xD1\x84\xD1\x82\xD1\x8C\x29\x20\xD0\xB2\xD0\xB4\xD1\x80\xD1\x8B\xD0\xB7\xD0\xB3\x21", "Russian"}, - {"\xD0\xAF\x20\xD0\xBB\xD1\x8E\xD0\xB1\xD0\xBB\xD1\x8E\x20\x72\x61\x79\x6C\x69\x62\x21", "Russian"}, - {"\xD0\x9C\xD0\xBE\xD0\xBB\xD1\x87\xD0\xB8\x2C\x20\xD1\x81\xD0\xBA\xD1\x80\xD1\x8B\xD0\xB2\xD0\xB0\xD0\xB9\xD1\x81\xD1\x8F\x20\xD0\xB8\x20\xD1\x82\xD0\xB0\xD0\xB8" - "\x0A\xD0\x98\x20\xD1\x87\xD1\x83\xD0\xB2\xD1\x81\xD1\x82\xD0\xB2\xD0\xB0\x20\xD0\xB8\x20\xD0\xBC\xD0\xB5\xD1\x87\xD1\x82\xD1\x8B\x20\xD1\x81\xD0\xB2\xD0\xBE\xD0\xB8\x20" - "\xE2\x80\x93\x0A\xD0\x9F\xD1\x83\xD1\x81\xD0\xBA\xD0\xB0\xD0\xB9\x20\xD0\xB2\x20\xD0\xB4\xD1\x83\xD1\x88\xD0\xB5\xD0\xB2\xD0\xBD\xD0\xBE\xD0\xB9\x20\xD0\xB3\xD0\xBB\xD1" - "\x83\xD0\xB1\xD0\xB8\xD0\xBD\xD0\xB5\x0A\xD0\x98\x20\xD0\xB2\xD1\x81\xD1\x85\xD0\xBE\xD0\xB4\xD1\x8F\xD1\x82\x20\xD0\xB8\x20\xD0\xB7\xD0\xB0\xD0\xB9\xD0\xB4\xD1\x83\xD1" - "\x82\x20\xD0\xBE\xD0\xBD\xD0\xB5\x0A\xD0\x9A\xD0\xB0\xD0\xBA\x20\xD0\xB7\xD0\xB2\xD0\xB5\xD0\xB7\xD0\xB4\xD1\x8B\x20\xD1\x8F\xD1\x81\xD0\xBD\xD1\x8B\xD0\xB5\x20\xD0\xB2" - "\x20\xD0\xBD\xD0\xBE\xD1\x87\xD0\xB8\x2D\x0A\xD0\x9B\xD1\x8E\xD0\xB1\xD1\x83\xD0\xB9\xD1\x81\xD1\x8F\x20\xD0\xB8\xD0\xBC\xD0\xB8\x20\xE2\x80\x93\x20\xD0\xB8\x20\xD0\xBC" - "\xD0\xBE\xD0\xBB\xD1\x87\xD0\xB8\x2E", "Russian"}, - {"\x56\x6F\x69\x78\x20\x61\x6D\x62\x69\x67\x75\xC3\xAB\x20\x64\xE2\x80\x99\x75\x6E\x20\x63\xC5\x93\x75\x72\x20\x71\x75\x69\x20\x61\x75\x20\x7A\xC3\xA9\x70" - "\x68\x79\x72\x20\x70\x72\xC3\xA9\x66\xC3\xA8\x72\x65\x20\x6C\x65\x73\x20\x6A\x61\x74\x74\x65\x73\x20\x64\x65\x20\x6B\x69\x77\x69", "French"}, - {"\x42\x65\x6E\x6A\x61\x6D\xC3\xAD\x6E\x20\x70\x69\x64\x69\xC3\xB3\x20\x75\x6E\x61\x20\x62\x65\x62\x69\x64\x61\x20\x64\x65\x20\x6B\x69\x77\x69\x20\x79\x20" - "\x66\x72\x65\x73\x61\x3B\x20\x4E\x6F\xC3\xA9\x2C\x20\x73\x69\x6E\x20\x76\x65\x72\x67\xC3\xBC\x65\x6E\x7A\x61\x2C\x20\x6C\x61\x20\x6D\xC3\xA1\x73\x20\x65\x78" - "\x71\x75\x69\x73\x69\x74\x61\x20\x63\x68\x61\x6D\x70\x61\xC3\xB1\x61\x20\x64\x65\x6C\x20\x6D\x65\x6E\xC3\xBA\x2E", "Spanish"}, - {"\xCE\xA4\xCE\xB1\xCF\x87\xCE\xAF\xCF\x83\xCF\x84\xCE\xB7\x20\xCE\xB1\xCE\xBB\xCF\x8E\xCF\x80\xCE\xB7\xCE\xBE\x20\xCE\xB2\xCE\xB1\xCF\x86\xCE\xAE\xCF\x82\x20" - "\xCF\x88\xCE\xB7\xCE\xBC\xCE\xAD\xCE\xBD\xCE\xB7\x20\xCE\xB3\xCE\xB7\x2C\x20\xCE\xB4\xCF\x81\xCE\xB1\xCF\x83\xCE\xBA\xCE\xB5\xCE\xBB\xCE\xAF\xCE\xB6\xCE\xB5\xCE" - "\xB9\x20\xCF\x85\xCF\x80\xCE\xAD\xCF\x81\x20\xCE\xBD\xCF\x89\xCE\xB8\xCF\x81\xCE\xBF\xCF\x8D\x20\xCE\xBA\xCF\x85\xCE\xBD\xCF\x8C\xCF\x82", "Greek"}, - {"\xCE\x97\x20\xCE\xBA\xCE\xB1\xCE\xBB\xCF\x8D\xCF\x84\xCE\xB5\xCF\x81\xCE\xB7\x20\xCE\xAC\xCE\xBC\xCF\x85\xCE\xBD\xCE\xB1\x20\xCE\xB5\xCE\xAF\xCE\xBD" - "\xCE\xB1\xCE\xB9\x20\xCE\xB7\x20\xCE\xB5\xCF\x80\xCE\xAF\xCE\xB8\xCE\xB5\xCF\x83\xCE\xB7\x2E", "Greek"}, - {"\xCE\xA7\xCF\x81\xCF\x8C\xCE\xBD\xCE\xB9\xCE\xB1\x20\xCE\xBA\xCE\xB1\xCE\xB9\x20\xCE\xB6\xCE\xB1\xCE\xBC\xCE\xAC\xCE\xBD\xCE\xB9\xCE\xB1\x21", "Greek"}, - {"\xCE\xA0\xCF\x8E\xCF\x82\x20\xCF\x84\xCE\xB1\x20\xCF\x80\xCE\xB1\xCF\x82\x20\xCF\x83\xCE\xAE\xCE\xBC\xCE\xB5\xCF\x81\xCE\xB1\x3B", "Greek"}, - - {"\xE6\x88\x91\xE8\x83\xBD\xE5\x90\x9E\xE4\xB8\x8B\xE7\x8E\xBB\xE7\x92\x83\xE8\x80\x8C\xE4\xB8\x8D\xE4\xBC\xA4\xE8\xBA\xAB\xE4\xBD\x93\xE3\x80\x82", "Chinese"}, - {"\xE4\xBD\xA0\xE5\x90\x83\xE4\xBA\x86\xE5\x90\x97\xEF\xBC\x9F", "Chinese"}, - {"\xE4\xB8\x8D\xE4\xBD\x9C\xE4\xB8\x8D\xE6\xAD\xBB\xE3\x80\x82", "Chinese"}, - {"\xE6\x9C\x80\xE8\xBF\x91\xE5\xA5\xBD\xE5\x90\x97\xEF\xBC\x9F", "Chinese"}, - {"\xE5\xA1\x9E\xE7\xBF\x81\xE5\xA4\xB1\xE9\xA9\xAC\xEF\xBC\x8C\xE7\x84\x89\xE7\x9F\xA5\xE9\x9D\x9E\xE7\xA6\x8F\xE3\x80\x82", "Chinese"}, - {"\xE5\x8D\x83\xE5\x86\x9B\xE6\x98\x93\xE5\xBE\x97\x2C\x20\xE4\xB8\x80\xE5\xB0\x86\xE9\x9A\xBE\xE6\xB1\x82", "Chinese"}, - {"\xE4\xB8\x87\xE4\xBA\x8B\xE5\xBC\x80\xE5\xA4\xB4\xE9\x9A\xBE\xE3\x80\x82", "Chinese"}, - {"\xE9\xA3\x8E\xE6\x97\xA0\xE5\xB8\xB8\xE9\xA1\xBA\xEF\xBC\x8C\xE5\x85\xB5\xE6\x97\xA0\xE5\xB8\xB8\xE8\x83\x9C\xE3\x80\x82", "Chinese"}, - {"\xE6\xB4\xBB\xE5\x88\xB0\xE8\x80\x81\xEF\xBC\x8C\xE5\xAD\xA6\xE5\x88\xB0\xE8\x80\x81\xE3\x80\x82", "Chinese"}, - {"\xE4\xB8\x80\xE8\xA8\x80\xE6\x97\xA2\xE5\x87\xBA\xEF\xBC\x8C\xE9\xA9\xB7\xE9\xA9\xAC\xE9\x9A\xBE\xE8\xBF\xBD\xE3\x80\x82", "Chinese"}, - {"\xE8\xB7\xAF\xE9\x81\xA5\xE7\x9F\xA5\xE9\xA9\xAC\xE5\x8A\x9B\xEF\xBC\x8C\xE6\x97\xA5\xE4\xB9\x85\xE8\xA7\x81\xE4\xBA\xBA\xE5\xBF\x83", "Chinese"}, - {"\xE6\x9C\x89\xE7\x90\x86\xE8\xB5\xB0\xE9\x81\x8D\xE5\xA4\xA9\xE4\xB8\x8B\xEF\xBC\x8C\xE6\x97\xA0\xE7\x90\x86\xE5\xAF\xB8\xE6\xAD\xA5\xE9\x9A\xBE\xE8\xA1\x8C\xE3\x80\x82", "Chinese"}, - - {"\xE7\x8C\xBF\xE3\x82\x82\xE6\x9C\xA8\xE3\x81\x8B\xE3\x82\x89\xE8\x90\xBD\xE3\x81\xA1\xE3\x82\x8B", "Japanese"}, - {"\xE4\xBA\x80\xE3\x81\xAE\xE7\x94\xB2\xE3\x82\x88\xE3\x82\x8A\xE5\xB9\xB4\xE3\x81\xAE\xE5\x8A\x9F", "Japanese"}, - {"\xE3\x81\x86\xE3\x82\x89\xE3\x82\x84\xE3\x81\xBE\xE3\x81\x97\x20\x20\xE6\x80\x9D\xE3\x81\xB2\xE5\x88\x87\xE3\x82\x8B\xE6\x99\x82\x20\x20\xE7\x8C\xAB\xE3\x81\xAE\xE6\x81\x8B", "Japanese"}, - {"\xE8\x99\x8E\xE7\xA9\xB4\xE3\x81\xAB\xE5\x85\xA5\xE3\x82\x89\xE3\x81\x9A\xE3\x82\x93\xE3\x81\xB0\xE8\x99\x8E\xE5\xAD\x90\xE3\x82\x92\xE5\xBE\x97\xE3\x81\x9A\xE3\x80\x82", "Japanese"}, - {"\xE4\xBA\x8C\xE5\x85\x8E\xE3\x82\x92\xE8\xBF\xBD\xE3\x81\x86\xE8\x80\x85\xE3\x81\xAF\xE4\xB8\x80\xE5\x85\x8E\xE3\x82\x92\xE3\x82\x82\xE5\xBE\x97\xE3\x81\x9A\xE3\x80\x82", "Japanese"}, - {"\xE9\xA6\xAC\xE9\xB9\xBF\xE3\x81\xAF\xE6\xAD\xBB\xE3\x81\xAA\xE3\x81\xAA\xE3\x81\x8D\xE3\x82\x83\xE6\xB2\xBB\xE3\x82\x89\xE3\x81\xAA\xE3\x81\x84\xE3\x80\x82", "Japanese"}, - {"\xE6\x9E\xAF\xE9\x87\x8E\xE8\xB7\xAF\xE3\x81\xAB\xE3\x80\x80\xE5\xBD\xB1\xE3\x81\x8B\xE3\x81\x95\xE3\x81\xAA\xE3\x82\x8A\xE3\x81\xA6\xE3\x80\x80\xE3\x82\x8F\xE3\x81\x8B\xE3\x82\x8C\xE3\x81\x91\xE3\x82\x8A", "Japanese"}, - {"\xE7\xB9\xB0\xE3\x82\x8A\xE8\xBF\x94\xE3\x81\x97\xE9\xBA\xA6\xE3\x81\xAE\xE7\x95\x9D\xE7\xB8\xAB\xE3\x81\xB5\xE8\x83\xA1\xE8\x9D\xB6\xE5\x93\x89", "Japanese"}, - - {"\xEC\x95\x84\xEB\x93\x9D\xED\x95\x9C\x20\xEB\xB0\x94\xEB\x8B\xA4\x20\xEC\x9C\x84\xEC\x97\x90\x20\xEA\xB0\x88\xEB\xA7\xA4\xEA\xB8\xB0\x20\xEB\x91\x90\xEC\x97\x87\x20" - "\xEB\x82\xA0\xEC\x95\x84\x20\xEB\x8F\x88\xEB\x8B\xA4\x2E\x0A\xEB\x84\x88\xED\x9B\x8C\xEB\x84\x88\xED\x9B\x8C\x20\xEC\x8B\x9C\xEB\xA5\xBC\x20\xEC\x93\xB4\xEB\x8B\xA4\x2E" - "\x20\xEB\xAA\xA8\xEB\xA5\xB4\xEB\x8A\x94\x20\xEB\x82\x98\xEB\x9D\xBC\x20\xEA\xB8\x80\xEC\x9E\x90\xEB\x8B\xA4\x2E\x0A\xEB\x84\x90\xEB\x94\xB0\xEB\x9E\x80\x20\xED\x95\x98" - "\xEB\x8A\x98\x20\xEB\xB3\xB5\xED\x8C\x90\xEC\x97\x90\x20\xEB\x82\x98\xEB\x8F\x84\x20\xEA\xB0\x99\xEC\x9D\xB4\x20\xEC\x8B\x9C\xEB\xA5\xBC\x20\xEC\x93\xB4\xEB\x8B\xA4\x2E", "Korean"}, - {"\xEC\xA0\x9C\x20\xEB\x88\x88\xEC\x97\x90\x20\xEC\x95\x88\xEA\xB2\xBD\xEC\x9D\xB4\xEB\x8B\xA4", "Korean"}, - {"\xEA\xBF\xA9\x20\xEB\xA8\xB9\xEA\xB3\xA0\x20\xEC\x95\x8C\x20\xEB\xA8\xB9\xEB\x8A\x94\xEB\x8B\xA4", "Korean"}, - {"\xEB\xA1\x9C\xEB\xA7\x88\xEB\x8A\x94\x20\xED\x95\x98\xEB\xA3\xA8\xEC\x95\x84\xEC\xB9\xA8\xEC\x97\x90\x20\xEC\x9D\xB4\xEB\xA3\xA8\xEC\x96\xB4\xEC\xA7\x84\x20\xEA\xB2\x83\xEC\x9D\xB4" - "\x20\xEC\x95\x84\xEB\x8B\x88\xEB\x8B\xA4", "Korean"}, - {"\xEA\xB3\xA0\xEC\x83\x9D\x20\xEB\x81\x9D\xEC\x97\x90\x20\xEB\x82\x99\xEC\x9D\xB4\x20\xEC\x98\xA8\xEB\x8B\xA4", "Korean"}, - {"\xEA\xB0\x9C\xEC\xB2\x9C\xEC\x97\x90\xEC\x84\x9C\x20\xEC\x9A\xA9\x20\xEB\x82\x9C\xEB\x8B\xA4", "Korean"}, - {"\xEC\x95\x88\xEB\x85\x95\xED\x95\x98\xEC\x84\xB8\xEC\x9A\x94\x3F", "Korean"}, - {"\xEB\xA7\x8C\xEB\x82\x98\xEC\x84\x9C\x20\xEB\xB0\x98\xEA\xB0\x91\xEC\x8A\xB5\xEB\x8B\x88\xEB\x8B\xA4", "Korean"}, - {"\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"}, + // Array containing all of the emojis messages + static Message[] messages = new Message[] + { + new Message("\x46\x61\x6C\x73\x63\x68\x65\x73\x20\xC3\x9C\x62\x65\x6E\x20\x76\x6F\x6E\x20\x58\x79\x6C\x6F\x70\x68\x6F\x6E\x6D\x75\x73\x69\x6B\x20\x71\x75\xC3\xA4\x6C\x74\x20\x6A\x65\x64\x65\x6E\x20\x67\x72\xC3\xB6\xC3\x9F\x65\x72\x65\x6E\x20\x5A\x77\x65\x72\x67", "German"), + new Message("\x42\x65\x69\xC3\x9F\x20\x6E\x69\x63\x68\x74\x20\x69\x6E\x20\x64\x69\x65\x20\x48\x61\x6E\x64\x2C\x20\x64\x69\x65\x20\x64\x69\x63\x68\x20\x66\xC3\xBC\x74\x74\x65\x72\x74\x2E", "German"), + new Message("\x41\x75\xC3\x9F\x65\x72\x6F\x72\x64\x65\x6E\x74\x6C\x69\x63\x68\x65\x20\xC3\x9C\x62\x65\x6C\x20\x65\x72\x66\x6F\x72\x64\x65\x72\x6E\x20\x61\x75\xC3\x9F\x65\x72\x6F\x72\x64\x65\x6E\x74\x6C\x69\x63\x68\x65\x20\x4D\x69\x74\x74\x65\x6C\x2E", "German"), + new Message("\xD4\xBF\xD6\x80\xD5\xB6\xD5\xA1\xD5\xB4\x20\xD5\xA1\xD5\xBA\xD5\xA1\xD5\xAF\xD5\xAB\x20\xD5\xB8\xD6\x82\xD5\xBF\xD5\xA5\xD5\xAC\x20\xD6\x87\x20\xD5\xAB\xD5\xB6\xD5\xAE\xD5\xAB\x20\xD5\xA1\xD5\xB6\xD5\xB0\xD5\xA1\xD5\xB6\xD5\xA3\xD5\xAB\xD5\xBD\xD5\xBF\x20\xD5\xB9\xD5\xA8\xD5\xB6\xD5\xA5\xD6\x80", "Armenian"), + new Message("\xD4\xB5\xD6\x80\xD5\xA2\x20\xD5\xB8\xD6\x80\x20\xD5\xAF\xD5\xA1\xD6\x81\xD5\xAB\xD5\xB6\xD5\xA8\x20\xD5\xA5\xD5\xAF\xD5\xA1\xD6\x82\x20\xD5\xA1\xD5\xB6\xD5\xBF\xD5\xA1\xD5\xBC\x2C\x20\xD5\xAE\xD5\xA1\xD5\xBC\xD5\xA5\xD6\x80\xD5\xA8\x20\xD5\xA1\xD5\xBD\xD5\xA1\xD6\x81\xD5\xAB\xD5\xB6\x2E\x2E\x2E\x20\xC2\xAB\xD4\xBF\xD5\xB8\xD5\xBF\xD5\xA8\x20\xD5\xB4\xD5\xA5\xD6\x80\xD5\xB8\xD5\xB6\xD6\x81\xD5\xAB\xD6\x81\x20\xD5\xA7\x3A\xC2\xBB", "Armenian"), + new Message("\xD4\xB3\xD5\xA1\xD5\xBC\xD5\xA8\xD5\x9D\x20\xD5\xA3\xD5\xA1\xD6\x80\xD5\xB6\xD5\xA1\xD5\xB6\x2C\x20\xD5\xB1\xD5\xAB\xD6\x82\xD5\xB6\xD5\xA8\xD5\x9D\x20\xD5\xB1\xD5\xB4\xD5\xBC\xD5\xA1\xD5\xB6", "Armenian"), + new Message("\x4A\x65\xC5\xBC\x75\x20\x6B\x6C\xC4\x85\x74\x77\x2C\x20\x73\x70\xC5\x82\xC3\xB3\x64\xC5\xBA\x20\x46\x69\x6E\x6F\x6D\x20\x63\x7A\xC4\x99\xC5\x9B\xC4\x87\x20\x67\x72\x79\x20\x68\x61\xC5\x84\x62\x21", "Polish"), + new Message("\x44\x6F\x62\x72\x79\x6D\x69\x20\x63\x68\xC4\x99\x63\x69\x61\x6D\x69\x20\x6A\x65\x73\x74\x20\x70\x69\x65\x6B\xC5\x82\x6F\x20\x77\x79\x62\x72\x75\x6B\x6F\x77\x61\x6E\x65\x2E", "Polish"), + new Message("\xC3\x8E\xC8\x9B\x69\x20\x6D\x75\x6C\xC8\x9B\x75\x6D\x65\x73\x63\x20\x63\xC4\x83\x20\x61\x69\x20\x61\x6C\x65\x73\x20\x72\x61\x79\x6C\x69\x62\x2E\x0A\xC8\x98\x69\x20\x73\x70\x65\x72\x20\x73\xC4\x83\x20\x61\x69\x20\x6F\x20\x7A\x69\x20\x62\x75\x6E\xC4\x83\x21", "Romanian"), + new Message("\xD0\xAD\xD1\x85\x2C\x20\xD1\x87\xD1\x83\xD0\xB6\xD0\xB0\xD0\xBA\x2C\x20\xD0\xBE\xD0\xB1\xD1\x89\xD0\xB8\xD0\xB9\x20\xD1\x81\xD1\x8A\xD1\x91\xD0\xBC\x20\xD1\x86\xD0\xB5\xD0\xBD\x20\xD1\x88\xD0\xBB\xD1\x8F\xD0\xBF\x20\x28\xD1\x8E\xD1\x84\xD1\x82\xD1\x8C\x29\x20\xD0\xB2\xD0\xB4\xD1\x80\xD1\x8B\xD0\xB7\xD0\xB3\x21", "Russian"), + new Message("\xD0\xAF\x20\xD0\xBB\xD1\x8E\xD0\xB1\xD0\xBB\xD1\x8E\x20\x72\x61\x79\x6C\x69\x62\x21", "Russian"), + new Message("\xD0\x9C\xD0\xBE\xD0\xBB\xD1\x87\xD0\xB8\x2C\x20\xD1\x81\xD0\xBA\xD1\x80\xD1\x8B\xD0\xB2\xD0\xB0\xD0\xB9\xD1\x81\xD1\x8F\x20\xD0\xB8\x20\xD1\x82\xD0\xB0\xD0\xB8\x0A\xD0\x98\x20\xD1\x87\xD1\x83\xD0\xB2\xD1\x81\xD1\x82\xD0\xB2\xD0\xB0\x20\xD0\xB8\x20\xD0\xBC\xD0\xB5\xD1\x87\xD1\x82\xD1\x8B\x20\xD1\x81\xD0\xB2\xD0\xBE\xD0\xB8\x20\xE2\x80\x93\x0A\xD0\x9F\xD1\x83\xD1\x81\xD0\xBA\xD0\xB0\xD0\xB9\x20\xD0\xB2\x20\xD0\xB4\xD1\x83\xD1\x88\xD0\xB5\xD0\xB2\xD0\xBD\xD0\xBE\xD0\xB9\x20\xD0\xB3\xD0\xBB\xD1\x83\xD0\xB1\xD0\xB8\xD0\xBD\xD0\xB5\x0A\xD0\x98\x20\xD0\xB2\xD1\x81\xD1\x85\xD0\xBE\xD0\xB4\xD1\x8F\xD1\x82\x20\xD0\xB8\x20\xD0\xB7\xD0\xB0\xD0\xB9\xD0\xB4\xD1\x83\xD1\x82\x20\xD0\xBE\xD0\xBD\xD0\xB5\x0A\xD0\x9A\xD0\xB0\xD0\xBA\x20\xD0\xB7\xD0\xB2\xD0\xB5\xD0\xB7\xD0\xB4\xD1\x8B\x20\xD1\x8F\xD1\x81\xD0\xBD\xD1\x8B\xD0\xB5\x20\xD0\xB2\x20\xD0\xBD\xD0\xBE\xD1\x87\xD0\xB8\x2D\x0A\xD0\x9B\xD1\x8E\xD0\xB1\xD1\x83\xD0\xB9\xD1\x81\xD1\x8F\x20\xD0\xB8\xD0\xBC\xD0\xB8\x20\xE2\x80\x93\x20\xD0\xB8\x20\xD0\xBC\xD0\xBE\xD0\xBB\xD1\x87\xD0\xB8\x2E", "Russian"), + new Message("\x56\x6F\x69\x78\x20\x61\x6D\x62\x69\x67\x75\xC3\xAB\x20\x64\xE2\x80\x99\x75\x6E\x20\x63\xC5\x93\x75\x72\x20\x71\x75\x69\x20\x61\x75\x20\x7A\xC3\xA9\x70\x68\x79\x72\x20\x70\x72\xC3\xA9\x66\xC3\xA8\x72\x65\x20\x6C\x65\x73\x20\x6A\x61\x74\x74\x65\x73\x20\x64\x65\x20\x6B\x69\x77\x69", "French"), + new Message("\x42\x65\x6E\x6A\x61\x6D\xC3\xAD\x6E\x20\x70\x69\x64\x69\xC3\xB3\x20\x75\x6E\x61\x20\x62\x65\x62\x69\x64\x61\x20\x64\x65\x20\x6B\x69\x77\x69\x20\x79\x20\x66\x72\x65\x73\x61\x3B\x20\x4E\x6F\xC3\xA9\x2C\x20\x73\x69\x6E\x20\x76\x65\x72\x67\xC3\xBC\x65\x6E\x7A\x61\x2C\x20\x6C\x61\x20\x6D\xC3\xA1\x73\x20\x65\x78\x71\x75\x69\x73\x69\x74\x61\x20\x63\x68\x61\x6D\x70\x61\xC3\xB1\x61\x20\x64\x65\x6C\x20\x6D\x65\x6E\xC3\xBA\x2E", "Spanish"), + new Message("\xCE\xA4\xCE\xB1\xCF\x87\xCE\xAF\xCF\x83\xCF\x84\xCE\xB7\x20\xCE\xB1\xCE\xBB\xCF\x8E\xCF\x80\xCE\xB7\xCE\xBE\x20\xCE\xB2\xCE\xB1\xCF\x86\xCE\xAE\xCF\x82\x20\xCF\x88\xCE\xB7\xCE\xBC\xCE\xAD\xCE\xBD\xCE\xB7\x20\xCE\xB3\xCE\xB7\x2C\x20\xCE\xB4\xCF\x81\xCE\xB1\xCF\x83\xCE\xBA\xCE\xB5\xCE\xBB\xCE\xAF\xCE\xB6\xCE\xB5\xCE\xB9\x20\xCF\x85\xCF\x80\xCE\xAD\xCF\x81\x20\xCE\xBD\xCF\x89\xCE\xB8\xCF\x81\xCE\xBF\xCF\x8D\x20\xCE\xBA\xCF\x85\xCE\xBD\xCF\x8C\xCF\x82", "Greek"), + new Message("\xCE\x97\x20\xCE\xBA\xCE\xB1\xCE\xBB\xCF\x8D\xCF\x84\xCE\xB5\xCF\x81\xCE\xB7\x20\xCE\xAC\xCE\xBC\xCF\x85\xCE\xBD\xCE\xB1\x20\xCE\xB5\xCE\xAF\xCE\xBD\xCE\xB1\xCE\xB9\x20\xCE\xB7\x20\xCE\xB5\xCF\x80\xCE\xAF\xCE\xB8\xCE\xB5\xCF\x83\xCE\xB7\x2E", "Greek"), + new Message("\xCE\xA7\xCF\x81\xCF\x8C\xCE\xBD\xCE\xB9\xCE\xB1\x20\xCE\xBA\xCE\xB1\xCE\xB9\x20\xCE\xB6\xCE\xB1\xCE\xBC\xCE\xAC\xCE\xBD\xCE\xB9\xCE\xB1\x21", "Greek"), + new Message("\xCE\xA0\xCF\x8E\xCF\x82\x20\xCF\x84\xCE\xB1\x20\xCF\x80\xCE\xB1\xCF\x82\x20\xCF\x83\xCE\xAE\xCE\xBC\xCE\xB5\xCF\x81\xCE\xB1\x3B", "Greek"), + new Message("\xE6\x88\x91\xE8\x83\xBD\xE5\x90\x9E\xE4\xB8\x8B\xE7\x8E\xBB\xE7\x92\x83\xE8\x80\x8C\xE4\xB8\x8D\xE4\xBC\xA4\xE8\xBA\xAB\xE4\xBD\x93\xE3\x80\x82", "Chinese"), + new Message("\xE4\xBD\xA0\xE5\x90\x83\xE4\xBA\x86\xE5\x90\x97\xEF\xBC\x9F", "Chinese"), + new Message("\xE4\xB8\x8D\xE4\xBD\x9C\xE4\xB8\x8D\xE6\xAD\xBB\xE3\x80\x82", "Chinese"), + new Message("\xE6\x9C\x80\xE8\xBF\x91\xE5\xA5\xBD\xE5\x90\x97\xEF\xBC\x9F", "Chinese"), + new Message("\xE5\xA1\x9E\xE7\xBF\x81\xE5\xA4\xB1\xE9\xA9\xAC\xEF\xBC\x8C\xE7\x84\x89\xE7\x9F\xA5\xE9\x9D\x9E\xE7\xA6\x8F\xE3\x80\x82", "Chinese"), + new Message("\xE5\x8D\x83\xE5\x86\x9B\xE6\x98\x93\xE5\xBE\x97\x2C\x20\xE4\xB8\x80\xE5\xB0\x86\xE9\x9A\xBE\xE6\xB1\x82", "Chinese"), + new Message("\xE4\xB8\x87\xE4\xBA\x8B\xE5\xBC\x80\xE5\xA4\xB4\xE9\x9A\xBE\xE3\x80\x82", "Chinese"), + new Message("\xE9\xA3\x8E\xE6\x97\xA0\xE5\xB8\xB8\xE9\xA1\xBA\xEF\xBC\x8C\xE5\x85\xB5\xE6\x97\xA0\xE5\xB8\xB8\xE8\x83\x9C\xE3\x80\x82", "Chinese"), + new Message("\xE6\xB4\xBB\xE5\x88\xB0\xE8\x80\x81\xEF\xBC\x8C\xE5\xAD\xA6\xE5\x88\xB0\xE8\x80\x81\xE3\x80\x82", "Chinese"), + new Message("\xE4\xB8\x80\xE8\xA8\x80\xE6\x97\xA2\xE5\x87\xBA\xEF\xBC\x8C\xE9\xA9\xB7\xE9\xA9\xAC\xE9\x9A\xBE\xE8\xBF\xBD\xE3\x80\x82", "Chinese"), + new Message("\xE8\xB7\xAF\xE9\x81\xA5\xE7\x9F\xA5\xE9\xA9\xAC\xE5\x8A\x9B\xEF\xBC\x8C\xE6\x97\xA5\xE4\xB9\x85\xE8\xA7\x81\xE4\xBA\xBA\xE5\xBF\x83", "Chinese"), + new Message("\xE6\x9C\x89\xE7\x90\x86\xE8\xB5\xB0\xE9\x81\x8D\xE5\xA4\xA9\xE4\xB8\x8B\xEF\xBC\x8C\xE6\x97\xA0\xE7\x90\x86\xE5\xAF\xB8\xE6\xAD\xA5\xE9\x9A\xBE\xE8\xA1\x8C\xE3\x80\x82", "Chinese"), + new Message("\xE7\x8C\xBF\xE3\x82\x82\xE6\x9C\xA8\xE3\x81\x8B\xE3\x82\x89\xE8\x90\xBD\xE3\x81\xA1\xE3\x82\x8B", "Japanese"), + new Message("\xE4\xBA\x80\xE3\x81\xAE\xE7\x94\xB2\xE3\x82\x88\xE3\x82\x8A\xE5\xB9\xB4\xE3\x81\xAE\xE5\x8A\x9F", "Japanese"), + new Message("\xE3\x81\x86\xE3\x82\x89\xE3\x82\x84\xE3\x81\xBE\xE3\x81\x97\x20\x20\xE6\x80\x9D\xE3\x81\xB2\xE5\x88\x87\xE3\x82\x8B\xE6\x99\x82\x20\x20\xE7\x8C\xAB\xE3\x81\xAE\xE6\x81\x8B", "Japanese"), + new Message("\xE8\x99\x8E\xE7\xA9\xB4\xE3\x81\xAB\xE5\x85\xA5\xE3\x82\x89\xE3\x81\x9A\xE3\x82\x93\xE3\x81\xB0\xE8\x99\x8E\xE5\xAD\x90\xE3\x82\x92\xE5\xBE\x97\xE3\x81\x9A\xE3\x80\x82", "Japanese"), + new Message("\xE4\xBA\x8C\xE5\x85\x8E\xE3\x82\x92\xE8\xBF\xBD\xE3\x81\x86\xE8\x80\x85\xE3\x81\xAF\xE4\xB8\x80\xE5\x85\x8E\xE3\x82\x92\xE3\x82\x82\xE5\xBE\x97\xE3\x81\x9A\xE3\x80\x82", "Japanese"), + new Message("\xE9\xA6\xAC\xE9\xB9\xBF\xE3\x81\xAF\xE6\xAD\xBB\xE3\x81\xAA\xE3\x81\xAA\xE3\x81\x8D\xE3\x82\x83\xE6\xB2\xBB\xE3\x82\x89\xE3\x81\xAA\xE3\x81\x84\xE3\x80\x82", "Japanese"), + new Message("\xE6\x9E\xAF\xE9\x87\x8E\xE8\xB7\xAF\xE3\x81\xAB\xE3\x80\x80\xE5\xBD\xB1\xE3\x81\x8B\xE3\x81\x95\xE3\x81\xAA\xE3\x82\x8A\xE3\x81\xA6\xE3\x80\x80\xE3\x82\x8F\xE3\x81\x8B\xE3\x82\x8C\xE3\x81\x91\xE3\x82\x8A", "Japanese"), + new Message("\xE7\xB9\xB0\xE3\x82\x8A\xE8\xBF\x94\xE3\x81\x97\xE9\xBA\xA6\xE3\x81\xAE\xE7\x95\x9D\xE7\xB8\xAB\xE3\x81\xB5\xE8\x83\xA1\xE8\x9D\xB6\xE5\x93\x89", "Japanese"), + new Message("\xEC\x95\x84\xEB\x93\x9D\xED\x95\x9C\x20\xEB\xB0\x94\xEB\x8B\xA4\x20\xEC\x9C\x84\xEC\x97\x90\x20\xEA\xB0\x88\xEB\xA7\xA4\xEA\xB8\xB0\x20\xEB\x91\x90\xEC\x97\x87\x20\xEB\x82\xA0\xEC\x95\x84\x20\xEB\x8F\x88\xEB\x8B\xA4\x2E\x0A\xEB\x84\x88\xED\x9B\x8C\xEB\x84\x88\xED\x9B\x8C\x20\xEC\x8B\x9C\xEB\xA5\xBC\x20\xEC\x93\xB4\xEB\x8B\xA4\x2E\x20\xEB\xAA\xA8\xEB\xA5\xB4\xEB\x8A\x94\x20\xEB\x82\x98\xEB\x9D\xBC\x20\xEA\xB8\x80\xEC\x9E\x90\xEB\x8B\xA4\x2E\x0A\xEB\x84\x90\xEB\x94\xB0\xEB\x9E\x80\x20\xED\x95\x98\xEB\x8A\x98\x20\xEB\xB3\xB5\xED\x8C\x90\xEC\x97\x90\x20\xEB\x82\x98\xEB\x8F\x84\x20\xEA\xB0\x99\xEC\x9D\xB4\x20\xEC\x8B\x9C\xEB\xA5\xBC\x20\xEC\x93\xB4\xEB\x8B\xA4\x2E", "Korean"), + new Message("\xEC\xA0\x9C\x20\xEB\x88\x88\xEC\x97\x90\x20\xEC\x95\x88\xEA\xB2\xBD\xEC\x9D\xB4\xEB\x8B\xA4", "Korean"), + new Message("\xEA\xBF\xA9\x20\xEB\xA8\xB9\xEA\xB3\xA0\x20\xEC\x95\x8C\x20\xEB\xA8\xB9\xEB\x8A\x94\xEB\x8B\xA4", "Korean"), + new Message("\xEB\xA1\x9C\xEB\xA7\x88\xEB\x8A\x94\x20\xED\x95\x98\xEB\xA3\xA8\xEC\x95\x84\xEC\xB9\xA8\xEC\x97\x90\x20\xEC\x9D\xB4\xEB\xA3\xA8\xEC\x96\xB4\xEC\xA7\x84\x20\xEA\xB2\x83\xEC\x9D\xB4\x20\xEC\x95\x84\xEB\x8B\x88\xEB\x8B\xA4", "Korean"), + new Message("\xEA\xB3\xA0\xEC\x83\x9D\x20\xEB\x81\x9D\xEC\x97\x90\x20\xEB\x82\x99\xEC\x9D\xB4\x20\xEC\x98\xA8\xEB\x8B\xA4", "Korean"), + new Message("\xEA\xB0\x9C\xEC\xB2\x9C\xEC\x97\x90\xEC\x84\x9C\x20\xEC\x9A\xA9\x20\xEB\x82\x9C\xEB\x8B\xA4", "Korean"), + new Message("\xEC\x95\x88\xEB\x85\x95\xED\x95\x98\xEC\x84\xB8\xEC\x9A\x94\x3F", "Korean"), + new Message("\xEB\xA7\x8C\xEB\x82\x98\xEC\x84\x9C\x20\xEB\xB0\x98\xEA\xB0\x91\xEC\x8A\xB5\xEB\x8B\x88\xEB\x8B\xA4", "Korean"), + 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"), }; - //-------------------------------------------------------------------------------------- - // Module functions declaration - //-------------------------------------------------------------------------------------- - static void RandomizeEmoji(void); // Fills the emoji array with random emojis + private Font fontDefault; + private Font fontAsian; + private Font fontEmoji; - //-------------------------------------------------------------------------------------- - // Global variables - //-------------------------------------------------------------------------------------- - // Arrays that holds the random emojis - struct emoji + private Vector2 hoveredPos; + private Vector2 selectedPos; + + public void Init() { - int index; // Index inside `emojiCodepoints` - int message; // Message index - Color color; // Emoji color + 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 + 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" + + hoveredPos = new(0.0f, 0.0f); + selectedPos = new(0.0f, 0.0f); + + // Set a random set of emojis when starting up + RandomizeEmoji(); } - emoji[EMOJI_PER_WIDTH * EMOJI_PER_HEIGHT] = { 0 }; -static int hovered = -1, selected = -1; - -int main(int argc, char** argv) -{ - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - SetConfigFlags(FLAG_MSAA_4X_HINT | FLAG_VSYNC_HINT); - InitWindow(screenWidth, screenHeight, "raylib [text] example - unicode"); - - // 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"); - Font fontAsian = LoadFont("resources/fonts/notoCJK.fnt"); - Font fontEmoji = LoadFont("resources/fonts/emoji.fnt"); - - Vector2 hoveredPos = new(0.0f, 0.0f); - Vector2 selectedPos = new(0.0f, 0.0f); - - // Set a random set of emojis when starting up - RandomizeEmoji(); - - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- - - // 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(KEY_SPACE)) RandomizeEmoji(); + if (IsKeyPressed(KeyboardKey.Space)) + { + RandomizeEmoji(); + } - // Set the selected emoji and copy its text to clipboard - if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && (hovered != -1) && (hovered != selected)) + // Set the selected emoji + if (IsMouseButtonPressed(MouseButton.Left) && (hovered != -1) && (hovered != selected)) { selected = hovered; selectedPos = hoveredPos; - SetClipboardText(messages[emoji[selected].message].text); } Vector2 mouse = GetMousePosition(); - Vector2 pos = new(28.8f, 10.0f); + Vector2 position = new(28.8f, 10.0f); hovered = -1; //---------------------------------------------------------------------------------- // Draw //---------------------------------------------------------------------------------- BeginDrawing(); - - ClearBackground(RAYWHITE); + ClearBackground(Color.RayWhite); // Draw random emojis in the background //------------------------------------------------------------------------------ - for (int i = 0; i < SIZEOF(emoji); ++i) + for (int i = 0; i < emoji.Length; i++) { - const char* txt = ref[emoji[i].index]; - Rectangle emojiRect = new(pos.X, pos.Y, fontEmoji.BaseSize, fontEmoji.BaseSize); + string txt = GetEmojiAt(emoji[i].Index); + Rectangle emojiRect = new(position.X, position.Y, fontEmoji.BaseSize, fontEmoji.BaseSize); if (!CheckCollisionPointRec(mouse, emojiRect)) { - DrawTextEx(fontEmoji, txt, pos, fontEmoji.BaseSize, 1.0, selected == i ? emoji[i].Color : ColorAlpha(LIGHTGRAY, 0.4f)); + DrawTextEx(fontEmoji, txt, position, fontEmoji.BaseSize, 1.0f, selected == i ? emoji[i].Color : Fade(Color.LightGray, 0.4f)); } else { - DrawTextEx(fontEmoji, txt, pos, fontEmoji.BaseSize, 1.0, emoji[i].Color); + DrawTextEx(fontEmoji, txt, position, fontEmoji.BaseSize, 1.0f, emoji[i].Color); hovered = i; - hoveredPos = pos; + hoveredPos = position; } - if ((i != 0) && (i % EMOJI_PER_WIDTH == 0)) { pos.Y += fontEmoji.BaseSize + 24.25f; pos.X = 28.8f; } - else pos.X += fontEmoji.BaseSize + 28.8f; + if ((i != 0) && (i % EmojiPerWidth == 0)) + { + position.Y += fontEmoji.BaseSize + 24.25f; + position.X = 28.8f; + } + else + { + position.X += fontEmoji.BaseSize + 28.8f; + } } //------------------------------------------------------------------------------ @@ -230,19 +228,30 @@ int main(int argc, char** argv) //------------------------------------------------------------------------------ if (selected != -1) { - const int message = emoji[selected].message; - const int horizontalPadding = 20, verticalPadding = 30; - Font* font = ref; + int message = emoji[selected].Message; + const int horizontalPadding = 20; + const int verticalPadding = 30; + Font font = fontDefault; // Set correct font for asian languages - if (TextIsEqual(messages[message].language, "Chinese") || - TextIsEqual(messages[message].language, "Korean") || - TextIsEqual(messages[message].language, "Japanese")) font = ref; + 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; + 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; @@ -253,7 +262,11 @@ int main(int argc, char** argv) Vector2 c = new(a.X + 10, a.Y); // Don't go outside the screen - if (msgRect.X < 10) msgRect.X += 28; + if (msgRect.X < 10) + { + msgRect.X += 28; + } + if (msgRect.Y < 10) { msgRect.Y = selectedPos.Y + 84; @@ -266,65 +279,269 @@ int main(int argc, char** argv) a = b; b = tmp; } - if (msgRect.X + msgRect.Width > screenWidth) msgRect.X -= (msgRect.X + msgRect.Width) - screenWidth + 10; + + 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 + horizontalPadding / 2, msgRect.Y + verticalPadding / 2, msgRect.Width - horizontalPadding, msgRect.Height); - DrawTextRec(*font, messages[message].text, textRect, font->baseSize, 1.0f, true, WHITE); + 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 = strlen(messages[message].text); - int len = GetCodepointsCount(messages[message].text); - const char* info = TextFormat("%s %u characters %i bytes", messages[message].language, len, size); + 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); - Vector2 pos = new(textRect.X + textRect.Width - sz.X, msgRect.Y + msgRect.Height - sz.Y - 2); - DrawText(info, pos.X, pos.Y, 10, RAYWHITE); + + 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, GRAY); - DrawText("Each emoji is a unicode character from a font, not a texture... Press [SPACEBAR] to refresh", (screenWidth - 484) / 2, screenHeight - 16, 10, GRAY); + 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(); //---------------------------------------------------------------------------------- } - // De-Initialization - //-------------------------------------------------------------------------------------- - 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) -{ - hovered = selected = -1; - int start = GetRandomValue(45, 360); - - for (int i = 0; i < SIZEOF(emoji); ++i) + public void Unload() { - // 0-179 emoji codepoints (from emoji char array) each 4bytes + null char - emoji[i].index = GetRandomValue(0, 179) * 5; + UnloadFont(fontDefault); // Unload font resource + UnloadFont(fontAsian); // Unload font resource + UnloadFont(fontEmoji); // Unload font resource + } - // Generate a random color for this emoji - Vector3 hsv = new((start * (i + 1)) % 360, 0.6f, 0.85f); - emoji[i].Color = ColorAlpha(ColorFromHSV(hsv), 0.8f); + // Fills the emoji array with random emoji (only those emojis present in fontEmoji) + void RandomizeEmoji() + { + hovered = selected = -1; + int start = GetRandomValue(45, 360); - // Set a random message for this emoji - emoji[i].message = GetRandomValue(0, SIZEOF(messages) - 1); + for (int i = 0; i < emoji.Length; i++) + { + // 0-179 emoji codepoints (from emoji char array) each 4bytes + null char + emoji[i].Index = GetRandomValue(0, 179) * 5; + + // Generate a random color for this emoji + emoji[i].Color = Fade(ColorFromHSV((start * (i + 1)) % 360, 0.6f, 0.85f), 0.8f); + + // Set a random message for this emoji + emoji[i].Message = GetRandomValue(0, messages.Length - 1); + } + } + + // Get the emoji codepoint (4 bytes + null) located at the given byte index inside EmojiCodepoints + static string GetEmojiAt(int index) + { + // Each emoji takes 4 bytes followed by a null separator, codepoints are stored as raw bytes + var sb = new StringBuilder(); + for (int i = index; (i < EmojiCodepoints.Length) && (EmojiCodepoints[i] != '\0'); i++) + { + sb.Append(EmojiCodepoints[i]); + } + return sb.ToString(); + } + + //-------------------------------------------------------------------------------------- + // Module Functions Definition + //-------------------------------------------------------------------------------------- + + // Draw text using font inside rectangle limits + static void DrawTextBoxed(Font font, string text, Rectangle rec, float fontSize, float spacing, bool wordWrap, Color tint) + { + DrawTextBoxedSelectable(font, text, rec, fontSize, spacing, wordWrap, tint, 0, 0, Color.White, Color.White); + } + + // Draw text using font inside rectangle limits with support for text selection + static unsafe void DrawTextBoxedSelectable(Font font, string text, Rectangle rec, float fontSize, float spacing, bool wordWrap, Color tint, int selectStart, int selectLength, Color selectTint, Color selectBackTint) + { + int length = text.Length; // 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 scaleFactor = fontSize / (float)font.BaseSize; // Character rectangle scaling factor + + // Word/character wrapping mechanism variables + const int MeasureState = 0; + const int DrawState = 1; + int state = wordWrap ? MeasureState : DrawState; + + int startLine = -1; // Index where to begin drawing (where a line begins) + int endLine = -1; // Index where to stop drawing (where a line ends) + int lastk = -1; // Holds last value of the character position + + using var textNative = new Utf8Buffer(text); + + for (int i = 0, k = 0; i < length; i++, k++) + { + // Get next codepoint from byte string and glyph index in font + int codepointByteCount = 0; + int codepoint = GetCodepoint(&textNative.AsPointer()[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; + } + + i += (codepointByteCount - 1); + + float glyphWidth = 0; + if (codepoint != '\n') + { + glyphWidth = (font.Glyphs[index].AdvanceX == 0) ? + font.Recs[index].Width * scaleFactor : + font.Glyphs[index].AdvanceX * scaleFactor; + + if (i + 1 < length) + { + glyphWidth = glyphWidth + spacing; + } + } + + // NOTE: When wordWrap is ON we first measure how much of the text we can draw before going outside of the rec container + // We store this info in startLine and endLine, then we change states, draw the text between those two variables + // and change states again and again recursively until the end of the text (or until we get outside of the container) + // When wordWrap is OFF we don't need the measure state so we go to the drawing state immediately + // and begin drawing on the next line before we can get outside the container + if (state == MeasureState) + { + // TODO: There are multiple types of spaces in UNICODE, maybe it's a good idea to add support for more + // Ref: http://jkorpela.fi/chars/spaces.html + if ((codepoint == ' ') || (codepoint == '\t') || (codepoint == '\n')) + { + endLine = i; + } + + if ((textOffsetX + glyphWidth) > rec.Width) + { + endLine = (endLine < 1) ? i : endLine; + if (i == endLine) + { + endLine -= codepointByteCount; + } + if ((startLine + codepointByteCount) == endLine) + { + endLine = (i - codepointByteCount); + } + + state = 1 - state; + } + else if ((i + 1) == length) + { + endLine = i; + state = 1 - state; + } + else if (codepoint == '\n') + { + state = 1 - state; + } + + if (state == DrawState) + { + textOffsetX = 0; + i = startLine; + glyphWidth = 0; + + // Save character position when we switch states + int tmp = lastk; + lastk = k - 1; + k = tmp; + } + } + else + { + if (codepoint == '\n') + { + if (!wordWrap) + { + textOffsetY += (font.BaseSize + font.BaseSize / 2) * scaleFactor; + textOffsetX = 0; + } + } + else + { + if (!wordWrap && ((textOffsetX + glyphWidth) > rec.Width)) + { + textOffsetY += (font.BaseSize + font.BaseSize / 2) * scaleFactor; + textOffsetX = 0; + } + + // When text overflows rectangle height limit, just stop drawing + if ((textOffsetY + font.BaseSize * scaleFactor) > rec.Height) + { + break; + } + + // Draw selection background + bool isGlyphSelected = false; + if ((selectStart >= 0) && (k >= selectStart) && (k < (selectStart + selectLength))) + { + DrawRectangleRec(new Rectangle(rec.X + textOffsetX - 1, rec.Y + textOffsetY, glyphWidth, (float)font.BaseSize * scaleFactor), selectBackTint); + isGlyphSelected = true; + } + + // Draw current character glyph + if ((codepoint != ' ') && (codepoint != '\t')) + { + DrawTextCodepoint(font, codepoint, new Vector2(rec.X + textOffsetX, rec.Y + textOffsetY), fontSize, isGlyphSelected ? selectTint : tint); + } + } + + if (wordWrap && (i == endLine)) + { + textOffsetY += (font.BaseSize + font.BaseSize / 2) * scaleFactor; + textOffsetX = 0; + startLine = endLine; + endLine = -1; + glyphWidth = 0; + selectStart += lastk - k; + k = lastk; + + state = 1 - state; + } + } + + 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/Text/WritingAnim.cs b/Examples/Text/WritingAnim.cs index 94ed451..a5c9382 100644 --- a/Examples/Text/WritingAnim.cs +++ b/Examples/Text/WritingAnim.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [text] example - Text Writing Animation +* raylib [text] example - writing anim * -* This example has been created using raylib 1.4 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2016 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.4, last time updated with raylib 1.4 +* +* 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) 2016-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -13,60 +17,85 @@ using static Raylib_cs.Raylib; namespace Examples.Text; -public class WritingAnim +public partial class WritingAnim : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Text / Writing Animation"; + + public string Title => "raylib [text] example - writing anim"; + + private string message; + private int framesCounter; + + public void Init() + { + message = "This sample illustrates a text writing\nanimation effect! Check it out! ;)"; + + framesCounter = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyDown(KeyboardKey.Space)) + { + framesCounter += 8; + } + else + { + framesCounter += 1; + } + + if (IsKeyPressed(KeyboardKey.Enter)) + { + framesCounter = 0; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText(message.SubText(0, framesCounter / 10), 210, 160, 20, Color.Maroon); + + DrawText("PRESS [ENTER] to RESTART!", 240, 260, 20, Color.LightGray); + DrawText("HOLD [SPACE] to SPEED UP!", 239, 300, 20, Color.LightGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [text] example - writing anim"); - InitWindow(screenWidth, screenHeight, "raylib [text] example - text writing anim"); - - string message = "This sample illustrates a text writing\nanimation effect! Check it out! ;)"; - int framesCounter = 0; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new WritingAnim(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (IsKeyDown(KeyboardKey.Space)) - { - framesCounter += 8; - } - else - { - framesCounter += 1; - } - - if (IsKeyPressed(KeyboardKey.Enter)) - { - framesCounter = 0; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText(message.SubText(0, framesCounter / 10), 210, 160, 20, Color.Maroon); - - DrawText("PRESS [ENTER] to RESTART!", 240, 260, 20, Color.LightGray); - DrawText("PRESS [SPACE] to SPEED UP!", 239, 300, 20, Color.LightGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Textures/BackgroundScrolling.cs b/Examples/Textures/BackgroundScrolling.cs index 3aa27d4..1f91529 100644 --- a/Examples/Textures/BackgroundScrolling.cs +++ b/Examples/Textures/BackgroundScrolling.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [textures] example - Background scrolling +* raylib [textures] example - background scrolling * -* This example has been created using raylib 2.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 * -* Copyright (c) 2019 Ramon Santamaria (@raysan5) +* Example originally created with raylib 2.0, last time updated with raylib 2.5 +* +* 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 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,101 +18,126 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class BackgroundScrolling +public partial class BackgroundScrolling : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / Background Scrolling"; + + public string Title => "raylib [textures] example - background scrolling"; + + private Texture2D background; + private Texture2D midground; + private Texture2D foreground; + + private float scrollingBack; + private float scrollingMid; + private float scrollingFore; + + public void Init() + { + // NOTE: Be careful, background width must be equal or bigger than screen width + // if not, texture should be draw more than two times for scrolling effect + background = LoadTexture("resources/cyberpunk_street_background.png"); + midground = LoadTexture("resources/cyberpunk_street_midground.png"); + foreground = LoadTexture("resources/cyberpunk_street_foreground.png"); + + scrollingBack = 0.0f; + scrollingMid = 0.0f; + scrollingFore = 0.0f; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + scrollingBack -= 0.1f; + scrollingMid -= 0.5f; + scrollingFore -= 1.0f; + + // NOTE: Texture is scaled twice its size, so it sould be considered on scrolling + if (scrollingBack <= -background.Width * 2) + { + scrollingBack = 0; + } + if (scrollingMid <= -midground.Width * 2) + { + scrollingMid = 0; + } + if (scrollingFore <= -foreground.Width * 2) + { + scrollingFore = 0; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(GetColor(0x052c46ff)); + + // Draw background image twice + // NOTE: Texture is scaled twice its size + DrawTextureEx(background, new Vector2(scrollingBack, 20), 0.0f, 2.0f, Color.White); + DrawTextureEx( + background, + new Vector2(background.Width * 2 + scrollingBack, 20), + 0.0f, + 2.0f, + Color.White + ); + + // Draw midground image twice + DrawTextureEx(midground, new Vector2(scrollingMid, 20), 0.0f, 2.0f, Color.White); + DrawTextureEx(midground, new Vector2(midground.Width * 2 + scrollingMid, 20), 0.0f, 2.0f, Color.White); + + // Draw foreground image twice + DrawTextureEx(foreground, new Vector2(scrollingFore, 70), 0.0f, 2.0f, Color.White); + DrawTextureEx( + foreground, + new Vector2(foreground.Width * 2 + scrollingFore, 70), + 0.0f, + 2.0f, + Color.White + ); + + DrawText("BACKGROUND SCROLLING & PARALLAX", 10, 10, 20, Color.Red); + DrawText("(c) Cyberpunk Street Environment by Luis Zuno (@ansimuz)", screenWidth - 330, screenHeight - 20, 10, Color.RayWhite); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(background); // Unload background texture + UnloadTexture(midground); // Unload midground texture + UnloadTexture(foreground); // Unload foreground texture + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [textures] example - background scrolling"); - // NOTE: Be careful, background width must be equal or bigger than screen width - // if not, texture should be draw more than two times for scrolling effect - Texture2D background = LoadTexture("resources/cyberpunk_street_background.png"); - Texture2D midground = LoadTexture("resources/cyberpunk_street_midground.png"); - Texture2D foreground = LoadTexture("resources/cyberpunk_street_foreground.png"); - - float scrollingBack = 0.0f; - float scrollingMid = 0.0f; - float scrollingFore = 0.0f; - - SetTargetFPS(60); // Set our game to run at 60 frames-per-second + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new BackgroundScrolling(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - scrollingBack -= 0.1f; - scrollingMid -= 0.5f; - scrollingFore -= 1.0f; - - // NOTE: Texture is scaled twice its size, so it sould be considered on scrolling - if (scrollingBack <= -background.Width * 2) - { - scrollingBack = 0; - } - if (scrollingMid <= -midground.Width * 2) - { - scrollingMid = 0; - } - if (scrollingFore <= -foreground.Width * 2) - { - scrollingFore = 0; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(GetColor(0x052c46ff)); - - // Draw background image twice - // NOTE: Texture is scaled twice its size - DrawTextureEx(background, new Vector2(scrollingBack, 20), 0.0f, 2.0f, Color.White); - DrawTextureEx( - background, - new Vector2(background.Width * 2 + scrollingBack, 20), - 0.0f, - 2.0f, - Color.White - ); - - // Draw midground image twice - DrawTextureEx(midground, new Vector2(scrollingMid, 20), 0.0f, 2.0f, Color.White); - DrawTextureEx(midground, new Vector2(midground.Width * 2 + scrollingMid, 20), 0.0f, 2.0f, Color.White); - - // Draw foreground image twice - DrawTextureEx(foreground, new Vector2(scrollingFore, 70), 0.0f, 2.0f, Color.White); - DrawTextureEx( - foreground, - new Vector2(foreground.Width * 2 + scrollingFore, 70), - 0.0f, - 2.0f, - Color.White - ); - - DrawText("BACKGROUND SCROLLING & PARALLAX", 10, 10, 20, Color.Red); - - int x = screenWidth - 330; - int y = screenHeight - 20; - DrawText("(c) Cyberpunk Street Environment by Luis Zuno (@ansimuz)", x, y, 10, Color.RayWhite); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(background); - UnloadTexture(midground); - UnloadTexture(foreground); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Textures/BlendModes.cs b/Examples/Textures/BlendModes.cs index 49a26b9..a8f5665 100644 --- a/Examples/Textures/BlendModes.cs +++ b/Examples/Textures/BlendModes.cs @@ -2,14 +2,18 @@ * * raylib [textures] example - blend modes * +* Example complexity rating: [★☆☆☆] 1/4 +* * NOTE: Images are loaded in CPU memory (RAM); textures are loaded in GPU memory (VRAM) * -* This example has been created using raylib 3.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 3.5, last time updated with raylib 3.5 * * Example contributed by Karlo Licudine (@accidentalrebel) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2020 Karlo Licudine (@accidentalrebel) +* 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) 2020-2025 Karlo Licudine (@accidentalrebel) * ********************************************************************************************/ @@ -17,99 +21,128 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class BlendModes +public partial class BlendModes : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + private const int blendCountMax = 4; + + public string Name => "Textures / Blend Modes"; + + public string Title => "raylib [textures] example - blend modes"; + + private Texture2D bgTexture; + private Texture2D fgTexture; + + private BlendMode blendMode; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [textures] example - blend modes"); - // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) - Image bgImage = LoadImage("resources/cyberpunk_street_background.png"); - Texture2D bgTexture = LoadTextureFromImage(bgImage); + var bgImage = LoadImage("resources/cyberpunk_street_background.png"); + bgTexture = LoadTextureFromImage(bgImage); - Image fgImage = LoadImage("resources/cyberpunk_street_foreground.png"); - Texture2D fgTexture = LoadTextureFromImage(fgImage); + var fgImage = LoadImage("resources/cyberpunk_street_foreground.png"); + fgTexture = LoadTextureFromImage(fgImage); // Once image has been converted to texture and uploaded to VRAM, it can be unloaded from RAM UnloadImage(bgImage); UnloadImage(fgImage); - const int blendCountMax = 4; - BlendMode blendMode = 0; + blendMode = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyPressed(KeyboardKey.Space)) + { + if ((int)blendMode >= (blendCountMax - 1)) + { + blendMode = 0; + } + else + { + blendMode++; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + var bgX = screenWidth / 2 - bgTexture.Width / 2; + var bgY = screenHeight / 2 - bgTexture.Height / 2; + DrawTexture(bgTexture, bgX, bgY, Color.White); + + // Apply the blend mode and then draw the foreground texture + BeginBlendMode(blendMode); + var fgX = screenWidth / 2 - fgTexture.Width / 2; + var fgY = screenHeight / 2 - fgTexture.Height / 2; + DrawTexture(fgTexture, fgX, fgY, Color.White); + EndBlendMode(); + + // Draw the texts + DrawText("Press SPACE to change blend modes.", 310, 350, 10, Color.Gray); + + switch (blendMode) + { + case BlendMode.Alpha: + DrawText("Current: BLEND_ALPHA", (screenWidth / 2) - 60, 370, 10, Color.Gray); + break; + case BlendMode.Additive: + DrawText("Current: BLEND_ADDITIVE", (screenWidth / 2) - 60, 370, 10, Color.Gray); + break; + case BlendMode.Multiplied: + DrawText("Current: BLEND_MULTIPLIED", (screenWidth / 2) - 60, 370, 10, Color.Gray); + break; + case BlendMode.AddColors: + DrawText("Current: BLEND_ADD_COLORS", (screenWidth / 2) - 60, 370, 10, Color.Gray); + break; + default: + break; + } + + var text = "(c) Cyberpunk Street Environment by Luis Zuno (@ansimuz)"; + DrawText(text, screenWidth - 330, screenHeight - 20, 10, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(fgTexture); // Unload foreground texture + UnloadTexture(bgTexture); // Unload background texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - blend modes"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //--------------------------------------------------------------------------------------- + + var game = new BlendModes(); + game.Init(); // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (IsKeyPressed(KeyboardKey.Space)) - { - if ((int)blendMode >= (blendCountMax - 1)) - { - blendMode = 0; - } - else - { - blendMode++; - } - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - int bgX = screenWidth / 2 - bgTexture.Width / 2; - int bgY = screenHeight / 2 - bgTexture.Height / 2; - DrawTexture(bgTexture, bgX, bgY, Color.White); - - // Apply the blend mode and then draw the foreground texture - BeginBlendMode(blendMode); - int fgX = screenWidth / 2 - fgTexture.Width / 2; - int fgY = screenHeight / 2 - fgTexture.Height / 2; - DrawTexture(fgTexture, fgX, fgY, Color.White); - EndBlendMode(); - - // Draw the texts - DrawText("Press SPACE to change blend modes.", 310, 350, 10, Color.Gray); - - switch (blendMode) - { - case BlendMode.Alpha: - DrawText("Current: BLEND_ALPHA", (screenWidth / 2) - 60, 370, 10, Color.Gray); - break; - case BlendMode.Additive: - DrawText("Current: BLEND_ADDITIVE", (screenWidth / 2) - 60, 370, 10, Color.Gray); - break; - case BlendMode.Multiplied: - DrawText("Current: BLEND_MULTIPLIED", (screenWidth / 2) - 60, 370, 10, Color.Gray); - break; - case BlendMode.AddColors: - DrawText("Current: BLEND_ADD_COLORS", (screenWidth / 2) - 60, 370, 10, Color.Gray); - break; - default: - break; - } - - string text = "(c) Cyberpunk Street Environment by Luis Zuno (@ansimuz)"; - DrawText(text, screenWidth - 330, screenHeight - 20, 10, Color.Gray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(fgTexture); - UnloadTexture(bgTexture); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Textures/Bunnymark.cs b/Examples/Textures/Bunnymark.cs index b610692..22304c5 100644 --- a/Examples/Textures/Bunnymark.cs +++ b/Examples/Textures/Bunnymark.cs @@ -1,12 +1,15 @@ /******************************************************************************************* * -* raylib [textures] example - Bunnymark +* raylib [textures] example - bunnymark * -* This example has been created using raylib 1.6 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 * -* Copyright (c) 2014-2019 Ramon Santamaria (@raysan5), 2024 Moritz Voss (@thygrrr) +* Example originally created with raylib 1.6, last time updated with raylib 2.5 * +* 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), 2024 Moritz Voss (@thygrrr) * ********************************************************************************************/ @@ -16,8 +19,11 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public static class Bunnymark +public partial class Bunnymark : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + // limits private const int MaxBunnies = 500_000; private const int BunnyIncrement = 500; @@ -28,6 +34,10 @@ public static class Bunnymark // This is the maximum amount of elements (quads) per batch private const int MAX_BATCH_ELEMENTS = Rlgl.DEFAULT_BATCH_BUFFER_ELEMENTS; + public string Name => "Textures / Bunnymark"; + + public string Title => "raylib [textures] example - bunnymark"; + private record struct Bunny() { public Vector2 Position { get; set; } = GetMousePosition(); @@ -46,99 +56,123 @@ public static class Bunnymark GetRandomValue(100, 240), 255); } + private Texture2D texBunny; + private Vector2 halfSize; + private Bunny[] bunnies; + private int bunniesCount; + + public void Init() + { + // Load bunny texture + texBunny = LoadTexture("resources/wabbit_alpha.png"); + halfSize = new Vector2(texBunny.Width, texBunny.Height) / 2; + + // Initialize bunnies storage + bunnies = new Bunny[MaxBunnies]; + bunniesCount = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + Span bunnies = this.bunnies; + + if (IsMouseButtonDown(MouseButton.Left) && bunniesCount < MaxBunnies) + { + // Add a range of new bunnies + foreach (ref var bunny in bunnies[bunniesCount..(bunniesCount + BunnyIncrement)]) + { + bunny = new(); + } + bunniesCount += BunnyIncrement; + } + else if (IsMouseButtonDown(MouseButton.Right)) + { + // Remove the oldest bunnies, shifting them back in the span + if (bunniesCount > BunnyDecrement) + { + bunnies[BunnyDecrement..bunniesCount].CopyTo(bunnies); + } + bunniesCount = Math.Max(0, bunniesCount - BunnyDecrement); + } + + // Update bunnies + foreach (ref var bunny in bunnies[..bunniesCount]) + { + // Integrate position + bunny.Position += bunny.Speed; + + // Bounce bunnies off the screen borders + bunny.Speed *= (bunny.Position + halfSize) switch + { + { X: < 0 or > screenWidth, Y: < 40 or > screenHeight } => new(-1, -1), + { X: < 0 or > screenWidth } => new(-1, 1), + { Y: < 40 or > screenHeight } => new(1, -1), + _ => Vector2.One, + }; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + foreach (var bunny in bunnies[..bunniesCount]) + { + // NOTE: When internal batch buffer limit is reached (MAX_BATCH_ELEMENTS), + // a draw call is launched and buffer starts being filled again; + // before issuing a draw call, updated vertex data from internal CPU buffer is send to GPU... + // Process of sending data is costly and it could happen that GPU data has not been completely + // processed for drawing while new data is tried to be sent (updating current in-use buffers) + // it could generates a stall and consequently a frame drop, limiting the number of drawn bunnies + DrawTexture(texBunny, (int)bunny.Position.X, (int)bunny.Position.Y, bunny.Color); + } + + DrawRectangle(0, 0, screenWidth, 40, Color.Black); + DrawText($"bunnies: {bunniesCount}", 120, 10, 20, Color.Green); + DrawText($"batched draw calls: {1 + bunniesCount / MAX_BATCH_ELEMENTS}", 320, 10, 20, Color.Maroon); + DrawText("Left Mouse: Add Bunnies!!! :D", 10, 400, 20, Color.LightGray); + DrawText("Right Mouse: Remove Bunnies", 10, 420, 20, Color.LightGray); + + DrawFPS(10, 10); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texBunny); +#if BROWSER + bunnies = null; + bunniesCount = 0; +#endif + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [textures] example - bunnymark"); - // Load bunny texture - Texture2D texBunny = LoadTexture("resources/wabbit_alpha.png"); - Vector2 halfSize = new Vector2(texBunny.Width, texBunny.Height) / 2; - - // Initialize bunnies storage - Span bunnies = new Bunny[MaxBunnies]; - int bunniesCount = 0; - SetTargetFPS(TARGET_FPS); //-------------------------------------------------------------------------------------- + var game = new Bunnymark(); + game.Init(); + // Main game loop while (!WindowShouldClose()) { - // Update - //---------------------------------------------------------------------------------- - if (IsMouseButtonDown(MouseButton.Left) && bunniesCount < MaxBunnies) - { - // Add a range of new bunnies - foreach (ref var bunny in bunnies[bunniesCount..(bunniesCount + BunnyIncrement)]) - { - bunny = new(); - } - bunniesCount += BunnyIncrement; - } - else if (IsMouseButtonDown(MouseButton.Right)) - { - // Remove the oldest bunnies, shifting them back in the span - if (bunniesCount > BunnyDecrement) - { - bunnies[BunnyDecrement..bunniesCount].CopyTo(bunnies); - } - bunniesCount = Math.Max(0, bunniesCount - BunnyDecrement); - } - - // Update bunnies - foreach (ref var bunny in bunnies[..bunniesCount]) - { - // Integrate position - bunny.Position += bunny.Speed; - - // Bounce bunnies off the screen borders - bunny.Speed *= (bunny.Position + halfSize) switch - { - { X: < 0 or > screenWidth, Y: < 40 or > screenHeight } => new(-1, -1), - { X: < 0 or > screenWidth } => new(-1, 1), - { Y: < 40 or > screenHeight } => new(1, -1), - _ => Vector2.One, - }; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - foreach (var bunny in bunnies[..bunniesCount]) - { - // NOTE: When internal batch buffer limit is reached (MAX_BATCH_ELEMENTS), - // a draw call is launched and buffer starts being filled again; - // before issuing a draw call, updated vertex data from internal CPU buffer is send to GPU... - // Process of sending data is costly, and it could happen that GPU data has not been completely - // processed for drawing while new data is tried to be sent (updating current in-use buffers) - // it could generate a stall and consequently a frame drop, limiting the number of drawn bunnies - DrawTexture(texBunny, (int)bunny.Position.X, (int)bunny.Position.Y, bunny.Color); - } - - DrawRectangle(0, 0, screenWidth, 40, Color.Black); - DrawText($"bunnies: {bunniesCount}", 120, 10, 20, Color.Green); - DrawText($"batched draw calls: {1 + bunniesCount / MAX_BATCH_ELEMENTS}", 320, 10, 20, Color.Maroon); - DrawText("Left Mouse: Add Bunnies!!! :D", 10, 400, 20, Color.LightGray); - DrawText("Right Mouse: Remove Bunnies", 10, 420, 20, Color.LightGray); - - DrawFPS(10, 10); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(texBunny); - CloseWindow(); //-------------------------------------------------------------------------------------- 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/DrawTiled.cs b/Examples/Textures/DrawTiled.cs index e069926..1e59525 100644 --- a/Examples/Textures/DrawTiled.cs +++ b/Examples/Textures/DrawTiled.cs @@ -1,11 +1,17 @@ /******************************************************************************************* * -* raylib [textures] example - Draw part of the texture tiled +* raylib [textures] example - tiled drawing * -* This example has been created using raylib 3.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 * -* Copyright (c) 2020 Vlad Adrian (@demizdor) and Ramon Santamaria (@raysan5) +* Example originally created with raylib 3.0, last time updated with raylib 4.2 +* +* 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) 2020-2025 Vlad Adrian (@demizdor) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,53 +20,69 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class DrawTiled +public partial class DrawTiled : IExample { - const int OptWidth = 220; - const int MarginSize = 8; - const int ColorSize = 16; + private const int OptWidth = 220; + private const int MarginSize = 8; + private const int ColorSize = 16; - public static int Main() + public string Name => "Textures / Draw Tiled"; + + public string Title => "raylib [textures] example - tiled drawing"; + + public ConfigFlags ConfigFlags => ConfigFlags.ResizableWindow; + + private int screenWidth; + private int screenHeight; + + private Texture2D texPattern; + + // Coordinates for all patterns inside the texture + private Rectangle[] recPattern; + + // Setup colors + private Color[] colors; + private Rectangle[] colorRec; + + private int activePattern; + private int activeCol; + private float scale; + private float rotation; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - int screenWidth = 800; - int screenHeight = 450; - - SetConfigFlags(ConfigFlags.ResizableWindow); - InitWindow(screenWidth, screenHeight, "raylib [textures] example - Draw part of a texture tiled"); + screenWidth = 800; + screenHeight = 450; // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) - Texture2D texPattern = LoadTexture("resources/patterns.png"); - - // Makes the texture smoother when upscaled - SetTextureFilter(texPattern, TextureFilter.Trilinear); + texPattern = LoadTexture("resources/patterns.png"); + SetTextureFilter(texPattern, TextureFilter.Bilinear); // Makes the texture smoother when upscaled // Coordinates for all patterns inside the texture - Rectangle[] recPattern = new[] { - new Rectangle(3, 3, 66, 66), - new Rectangle(75, 3, 100, 100), - new Rectangle(3, 75, 66, 66), - new Rectangle(7, 156, 50, 50), - new Rectangle(85, 106, 90, 45), - new Rectangle(75, 154, 100, 60) - }; + recPattern = new[] { + new Rectangle(3, 3, 66, 66), + new Rectangle(75, 3, 100, 100), + new Rectangle(3, 75, 66, 66), + new Rectangle(7, 156, 50, 50), + new Rectangle(85, 106, 90, 45), + new Rectangle(75, 154, 100, 60) + }; // Setup colors - Color[] colors = new[] + colors = new[] { - Color.Black, - Color.Maroon, - Color.Orange, - Color.Blue, - Color.Purple, - Color.Beige, - Color.Lime, - Color.Red, - Color.DarkGray, - Color.SkyBlue - }; - Rectangle[] colorRec = new Rectangle[colors.Length]; + Color.Black, + Color.Maroon, + Color.Orange, + Color.Blue, + Color.Purple, + Color.Beige, + Color.Lime, + Color.Red, + Color.DarkGray, + Color.SkyBlue + }; + colorRec = new Rectangle[colors.Length]; // Calculate rectangle for each color for (int i = 0, x = 0, y = 0; i < colors.Length; i++) @@ -81,155 +103,176 @@ public class DrawTiled } } - int activePattern = 0, activeCol = 0; - float scale = 1.0f, rotation = 0.0f; + activePattern = 0; + activeCol = 0; + scale = 1.0f; + rotation = 0.0f; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + screenWidth = GetScreenWidth(); + screenHeight = GetScreenHeight(); + + // Handle mouse + if (IsMouseButtonPressed(MouseButton.Left)) + { + var mouse = GetMousePosition(); + + // Check which pattern was clicked and set it as the active pattern + for (var i = 0; i < recPattern.Length; i++) + { + Rectangle rec = new( + 2 + MarginSize + recPattern[i].X, + 40 + MarginSize + recPattern[i].Y, + recPattern[i].Width, + recPattern[i].Height + ); + if (CheckCollisionPointRec(mouse, rec)) + { + activePattern = i; + break; + } + } + + // Check to see which color was clicked and set it as the active color + for (var i = 0; i < colors.Length; ++i) + { + if (CheckCollisionPointRec(mouse, colorRec[i])) + { + activeCol = i; + break; + } + } + } + + // Handle keys: change scale + if (IsKeyPressed(KeyboardKey.Up)) + { + scale += 0.25f; + } + if (IsKeyPressed(KeyboardKey.Down)) + { + scale -= 0.25f; + } + if (scale > 10.0f) + { + scale = 10.0f; + } + else if (scale <= 0.0f) + { + scale = 0.25f; + } + + // Handle keys: change rotation + if (IsKeyPressed(KeyboardKey.Left)) + { + rotation -= 25.0f; + } + if (IsKeyPressed(KeyboardKey.Right)) + { + rotation += 25.0f; + } + + // Handle keys: reset + if (IsKeyPressed(KeyboardKey.Space)) + { + rotation = 0.0f; + scale = 1.0f; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // Draw the tiled area + var source = recPattern[activePattern]; + Rectangle dest = new( + OptWidth + MarginSize, + MarginSize, + screenWidth - OptWidth - 2 * MarginSize, + screenHeight - 2 * MarginSize + ); + DrawTextureTiled(texPattern, source, dest, Vector2.Zero, rotation, scale, colors[activeCol]); + + // Draw options + var color = ColorAlpha(Color.LightGray, 0.5f); + DrawRectangle(MarginSize, MarginSize, OptWidth - MarginSize, screenHeight - 2 * MarginSize, color); + + DrawText("Select Pattern", 2 + MarginSize, 30 + MarginSize, 10, Color.Black); + DrawTexture(texPattern, 2 + MarginSize, 40 + MarginSize, Color.Black); + DrawRectangle( + 2 + MarginSize + (int)recPattern[activePattern].X, + 40 + MarginSize + (int)recPattern[activePattern].Y, + (int)recPattern[activePattern].Width, + (int)recPattern[activePattern].Height, + ColorAlpha(Color.DarkBlue, 0.3f) + ); + + DrawText("Select Color", 2 + MarginSize, 10 + 256 + MarginSize, 10, Color.Black); + for (var i = 0; i < colors.Length; i++) + { + DrawRectangleRec(colorRec[i], colors[i]); + if (activeCol == i) + { + DrawRectangleLinesEx(colorRec[i], 3, ColorAlpha(Color.White, 0.5f)); + } + } + + DrawText("Scale (UP/DOWN to change)", 2 + MarginSize, 80 + 256 + MarginSize, 10, Color.Black); + DrawText($"{scale:F2}x", 2 + MarginSize, 92 + 256 + MarginSize, 20, Color.Black); + + DrawText("Rotation (LEFT/RIGHT to change)", 2 + MarginSize, 122 + 256 + MarginSize, 10, Color.Black); + DrawText($"{rotation:F0} degrees", 2 + MarginSize, 134 + 256 + MarginSize, 20, Color.Black); + + DrawText("Press [SPACE] to reset", 2 + MarginSize, 164 + 256 + MarginSize, 10, Color.DarkBlue); + + // Draw FPS + DrawText($"{GetFPS()} FPS", 2 + MarginSize, 2 + MarginSize, 20, Color.Black); + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texPattern); // Unload texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.ResizableWindow); // Make the window resizable + InitWindow(800, 450, "raylib [textures] example - tiled drawing"); SetTargetFPS(60); //--------------------------------------------------------------------------------------- + var game = new DrawTiled(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - screenWidth = GetScreenWidth(); - screenHeight = GetScreenHeight(); - - // Handle mouse - if (IsMouseButtonPressed(MouseButton.Left)) - { - Vector2 mouse = GetMousePosition(); - - // Check which pattern was clicked and set it as the active pattern - for (int i = 0; i < recPattern.Length; i++) - { - Rectangle rec = new( - 2 + MarginSize + recPattern[i].X, - 40 + MarginSize + recPattern[i].Y, - recPattern[i].Width, - recPattern[i].Height - ); - if (CheckCollisionPointRec(mouse, rec)) - { - activePattern = i; - break; - } - } - - // Check to see which color was clicked and set it as the active color - for (int i = 0; i < colors.Length; ++i) - { - if (CheckCollisionPointRec(mouse, colorRec[i])) - { - activeCol = i; - break; - } - } - } - - // Handle keys - - // Change scale - if (IsKeyPressed(KeyboardKey.Up)) - { - scale += 0.25f; - } - if (IsKeyPressed(KeyboardKey.Down)) - { - scale -= 0.25f; - } - if (scale > 10.0f) - { - scale = 10.0f; - } - else if (scale <= 0.0f) - { - scale = 0.25f; - } - - // Change rotation - if (IsKeyPressed(KeyboardKey.Left)) - { - rotation -= 25.0f; - } - if (IsKeyPressed(KeyboardKey.Right)) - { - rotation += 25.0f; - } - - // Reset - if (IsKeyPressed(KeyboardKey.Space)) - { - rotation = 0.0f; - scale = 1.0f; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // Draw the tiled area - Rectangle source = recPattern[activePattern]; - Rectangle dest = new( - OptWidth + MarginSize, - MarginSize, - screenWidth - OptWidth - 2 * MarginSize, - screenHeight - 2 * MarginSize - ); - DrawTextureTiled(texPattern, source, dest, Vector2.Zero, rotation, scale, colors[activeCol]); - - // Draw options - Color color = ColorAlpha(Color.LightGray, 0.5f); - DrawRectangle(MarginSize, MarginSize, OptWidth - MarginSize, screenHeight - 2 * MarginSize, color); - - DrawText("Select Pattern", 2 + MarginSize, 30 + MarginSize, 10, Color.Black); - DrawTexture(texPattern, 2 + MarginSize, 40 + MarginSize, Color.Black); - DrawRectangle( - 2 + MarginSize + (int)recPattern[activePattern].X, - 40 + MarginSize + (int)recPattern[activePattern].Y, - (int)recPattern[activePattern].Width, - (int)recPattern[activePattern].Height, - ColorAlpha(Color.DarkBlue, 0.3f) - ); - - DrawText("Select Color", 2 + MarginSize, 10 + 256 + MarginSize, 10, Color.Black); - for (int i = 0; i < colors.Length; i++) - { - DrawRectangleRec(colorRec[i], colors[i]); - if (activeCol == i) - { - DrawRectangleLinesEx(colorRec[i], 3, ColorAlpha(Color.White, 0.5f)); - } - } - - DrawText("Scale (UP/DOWN to change)", 2 + MarginSize, 80 + 256 + MarginSize, 10, Color.Black); - DrawText($"{scale}x", 2 + MarginSize, 92 + 256 + MarginSize, 20, Color.Black); - - DrawText("Rotation (LEFT/RIGHT to change)", 2 + MarginSize, 122 + 256 + MarginSize, 10, Color.Black); - DrawText($"{rotation} degrees", 2 + MarginSize, 134 + 256 + MarginSize, 20, Color.Black); - - DrawText("Press [SPACE] to reset", 2 + MarginSize, 164 + 256 + MarginSize, 10, Color.DarkBlue); - - // Draw FPS - DrawText($"{GetFPS()}", 2 + MarginSize, 2 + MarginSize, 20, Color.Black); - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(texPattern); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } // Draw part of a texture (defined by a rectangle) with rotation and scale tiled into dest. - static void DrawTextureTiled( + private static void DrawTextureTiled( Texture2D texture, Rectangle source, Rectangle dest, @@ -268,7 +311,7 @@ public class DrawTiled else if (dest.Width <= tileWidth) { // Tiled vertically (one column) - int dy = 0; + var dy = 0; for (; dy + tileHeight < dest.Height; dy += tileHeight) { DrawTexturePro( @@ -307,7 +350,7 @@ public class DrawTiled else if (dest.Height <= tileHeight) { // Tiled horizontally (one row) - int dx = 0; + var dx = 0; for (; dx + tileWidth < dest.Width; dx += tileWidth) { DrawTexturePro( @@ -351,10 +394,10 @@ public class DrawTiled else { // Tiled both horizontally and vertically (rows and columns) - int dx = 0; + var dx = 0; for (; dx + tileWidth < dest.Width; dx += tileWidth) { - int dy = 0; + var dy = 0; for (; dy + tileHeight < dest.Height; dy += tileHeight) { DrawTexturePro( @@ -397,7 +440,7 @@ public class DrawTiled // Fit last column of tiles if (dx < dest.Width) { - int dy = 0; + var dy = 0; for (; dy + tileHeight < dest.Height; dy += tileHeight) { DrawTexturePro( @@ -446,4 +489,3 @@ public class DrawTiled } } } - 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/ImageDrawing.cs b/Examples/Textures/ImageDrawing.cs index 7b048d0..25cee29 100644 --- a/Examples/Textures/ImageDrawing.cs +++ b/Examples/Textures/ImageDrawing.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [textures] example - Image loading and drawing on it +* raylib [textures] example - image drawing +* +* Example complexity rating: [★★☆☆] 2/4 * * NOTE: Images are loaded in CPU memory (RAM); textures are loaded in GPU memory (VRAM) * -* This example has been created using raylib 1.4 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.4, last time updated with raylib 1.4 * -* Copyright (c) 2016 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) 2016-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -16,85 +20,99 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class ImageDrawing +public partial class ImageDrawing : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / Image Drawing"; + + public string Title => "raylib [textures] example - image drawing"; + + private Texture2D texture; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [textures] example - image drawing"); - // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) - Image cat = LoadImage("resources/cat.png"); - ImageCrop(ref cat, new Rectangle(100, 10, 280, 380)); - ImageFlipHorizontal(ref cat); - ImageResize(ref cat, 150, 200); + var cat = LoadImage("resources/cat.png"); // Load image in CPU memory (RAM) + ImageCrop(ref cat, new Rectangle(100, 10, 280, 380)); // Crop an image piece + ImageFlipHorizontal(ref cat); // Flip cropped image horizontally + ImageResize(ref cat, 150, 200); // Resize flipped-cropped image - Image parrots = LoadImage("resources/parrots.png"); + var parrots = LoadImage("resources/parrots.png"); // Load image in CPU memory (RAM) // Draw one image over the other with a scaling of 1.5f Rectangle src = new(0, 0, cat.Width, cat.Height); ImageDraw(ref parrots, cat, src, new Rectangle(30, 40, cat.Width * 1.5f, cat.Height * 1.5f), Color.White); - ImageCrop(ref parrots, new Rectangle(0, 50, parrots.Width, parrots.Height - 100)); + ImageCrop(ref parrots, new Rectangle(0, 50, parrots.Width, parrots.Height - 100)); // Crop resulting image // Draw on the image with a few image draw methods ImageDrawPixel(ref parrots, 10, 10, Color.RayWhite); - ImageDrawCircle(ref parrots, 10, 10, 5, Color.RayWhite); + ImageDrawCircleLines(ref parrots, 10, 10, 5, Color.RayWhite); ImageDrawRectangle(ref parrots, 5, 20, 10, 10, Color.RayWhite); - UnloadImage(cat); + UnloadImage(cat); // Unload image from RAM - // Load custom font for frawing on image - Font font = LoadFont("resources/fonts/custom_jupiter_crash.png"); + // Load custom font for drawing on image + var font = LoadFont("resources/custom_jupiter_crash.png"); // Draw over image using custom font ImageDrawTextEx(ref parrots, font, "PARROTS & CAT", new Vector2(300, 230), font.BaseSize, -2, Color.White); - // Unload custom spritefont (already drawn used on image) - UnloadFont(font); + UnloadFont(font); // Unload custom font (already drawn used on image) - Texture2D texture = LoadTextureFromImage(parrots); - UnloadImage(parrots); + texture = LoadTextureFromImage(parrots); // Image converted to texture, uploaded to GPU memory (VRAM) + UnloadImage(parrots); // Once image has been converted to texture and uploaded to VRAM, it can be unloaded from RAM + } + + public void Update() + { + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + var x = screenWidth / 2 - texture.Width / 2; + var y = screenHeight / 2 - texture.Height / 2; + DrawTexture(texture, x, y - 40, Color.White); + DrawRectangleLines(x, y - 40, texture.Width, texture.Height, Color.DarkGray); + + DrawText("We are drawing only one texture from various images composed!", 240, 350, 10, Color.DarkGray); + DrawText("Source images have been cropped, scaled, flipped and copied one over the other.", 190, 370, 10, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texture); // Texture unloading + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - image drawing"); SetTargetFPS(60); - //--------------------------------------------------------------------------------------- + //-------------------------------------------------------------------------------------- + + var game = new ImageDrawing(); + game.Init(); // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // TODO: Update your variables here - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - int x = screenWidth / 2 - texture.Width / 2; - int y = screenHeight / 2 - texture.Height / 2; - DrawTexture(texture, x, y - 40, Color.White); - DrawRectangleLines(x, y - 40, texture.Width, texture.Height, Color.DarkGray); - - DrawText("We are drawing only one texture from various images composed!", 240, 350, 10, Color.DarkGray); - - string text = "Source images have been cropped, scaled, flipped and copied one over the other."; - DrawText(text, 90, 370, 10, Color.DarkGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(texture); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Textures/ImageGeneration.cs b/Examples/Textures/ImageGeneration.cs index dacff9a..5cd901d 100644 --- a/Examples/Textures/ImageGeneration.cs +++ b/Examples/Textures/ImageGeneration.cs @@ -1,11 +1,17 @@ /******************************************************************************************* * -* raylib [textures] example - Procedural images generation +* raylib [textures] example - image generation * -* This example has been created using raylib 1.8 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2O17 Wilhem Barbier (@nounoursheureux) +* Example originally created with raylib 1.8, last time updated with raylib 1.8 +* +* Example contributed by Wilhem Barbier (@nounoursheureux) 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) 2017-2025 Wilhem Barbier (@nounoursheureux) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -13,104 +19,147 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class ImageGeneration +public partial class ImageGeneration : IExample { - public const int NumTextures = 6; + private const int screenWidth = 800; + private const int screenHeight = 450; + + // Currently we have 8 generation algorithms but some have multiple purposes (Linear and Square Gradients) + public const int NumTextures = 9; + + public string Name => "Textures / Image Generation"; + + public string Title => "raylib [textures] example - image generation"; + + private Texture2D[] textures; + private int currentTexture; + + public void Init() + { + var verticalGradient = GenImageGradientLinear(screenWidth, screenHeight, 0, Color.Red, Color.Blue); + var horizontalGradient = GenImageGradientLinear(screenWidth, screenHeight, 90, Color.Red, Color.Blue); + var diagonalGradient = GenImageGradientLinear(screenWidth, screenHeight, 45, Color.Red, Color.Blue); + var radialGradient = GenImageGradientRadial(screenWidth, screenHeight, 0.0f, Color.White, Color.Black); + var squareGradient = GenImageGradientSquare(screenWidth, screenHeight, 0.0f, Color.White, Color.Black); + var isChecked = GenImageChecked(screenWidth, screenHeight, 32, 32, Color.Red, Color.Blue); + var whiteNoise = GenImageWhiteNoise(screenWidth, screenHeight, 0.5f); + var perlinNoise = GenImagePerlinNoise(screenWidth, screenHeight, 50, 50, 4.0f); + var cellular = GenImageCellular(screenWidth, screenHeight, 32); + + textures = new Texture2D[NumTextures]; + textures[0] = LoadTextureFromImage(verticalGradient); + textures[1] = LoadTextureFromImage(horizontalGradient); + textures[2] = LoadTextureFromImage(diagonalGradient); + textures[3] = LoadTextureFromImage(radialGradient); + textures[4] = LoadTextureFromImage(squareGradient); + textures[5] = LoadTextureFromImage(isChecked); + textures[6] = LoadTextureFromImage(whiteNoise); + textures[7] = LoadTextureFromImage(perlinNoise); + textures[8] = LoadTextureFromImage(cellular); + + // Unload image data (CPU RAM) + UnloadImage(verticalGradient); + UnloadImage(horizontalGradient); + UnloadImage(diagonalGradient); + UnloadImage(radialGradient); + UnloadImage(squareGradient); + UnloadImage(isChecked); + UnloadImage(whiteNoise); + UnloadImage(perlinNoise); + UnloadImage(cellular); + + currentTexture = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsMouseButtonPressed(MouseButton.Left) || IsKeyPressed(KeyboardKey.Right)) + { + // Cycle between the textures + currentTexture = (currentTexture + 1) % NumTextures; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawTexture(textures[currentTexture], 0, 0, Color.White); + + DrawRectangle(30, 400, 325, 30, Fade(Color.SkyBlue, 0.5f)); + DrawRectangleLines(30, 400, 325, 30, Fade(Color.White, 0.5f)); + DrawText("MOUSE LEFT BUTTON to CYCLE PROCEDURAL TEXTURES", 40, 410, 10, Color.White); + + switch (currentTexture) + { + case 0: + DrawText("VERTICAL GRADIENT", 560, 10, 20, Color.RayWhite); + break; + case 1: + DrawText("HORIZONTAL GRADIENT", 540, 10, 20, Color.RayWhite); + break; + case 2: + DrawText("DIAGONAL GRADIENT", 540, 10, 20, Color.RayWhite); + break; + case 3: + DrawText("RADIAL GRADIENT", 580, 10, 20, Color.LightGray); + break; + case 4: + DrawText("SQUARE GRADIENT", 580, 10, 20, Color.LightGray); + break; + case 5: + DrawText("CHECKED", 680, 10, 20, Color.RayWhite); + break; + case 6: + DrawText("WHITE NOISE", 640, 10, 20, Color.Red); + break; + case 7: + DrawText("PERLIN NOISE", 640, 10, 20, Color.Red); + break; + case 8: + DrawText("CELLULAR", 670, 10, 20, Color.RayWhite); + break; + default: + break; + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + for (var i = 0; i < textures.Length; i++) + { + UnloadTexture(textures[i]); + } + } public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [textures] example - procedural images generation"); - - Image verticalGradient = GenImageGradientLinear(screenWidth, screenHeight, 0, Color.Red, Color.Blue); - Image horizontalGradient = GenImageGradientLinear(screenWidth, screenHeight, 90, Color.Red, Color.Blue); - Image radialGradient = GenImageGradientRadial(screenWidth, screenHeight, 0.0f, Color.White, Color.Black); - Image isChecked = GenImageChecked(screenWidth, screenHeight, 32, 32, Color.Red, Color.Blue); - Image whiteNoise = GenImageWhiteNoise(screenWidth, screenHeight, 0.5f); - Image cellular = GenImageCellular(screenWidth, screenHeight, 32); - - Texture2D[] textures = new Texture2D[NumTextures]; - textures[0] = LoadTextureFromImage(verticalGradient); - textures[1] = LoadTextureFromImage(horizontalGradient); - textures[2] = LoadTextureFromImage(radialGradient); - textures[3] = LoadTextureFromImage(isChecked); - textures[4] = LoadTextureFromImage(whiteNoise); - textures[5] = LoadTextureFromImage(cellular); - - UnloadImage(verticalGradient); - UnloadImage(horizontalGradient); - UnloadImage(radialGradient); - UnloadImage(isChecked); - UnloadImage(whiteNoise); - UnloadImage(cellular); - - int currentTexture = 0; + InitWindow(screenWidth, screenHeight, "raylib [textures] example - image generation"); SetTargetFPS(60); //--------------------------------------------------------------------------------------- + var game = new ImageGeneration(); + game.Init(); + // Main game loop while (!WindowShouldClose()) { - // Update - //---------------------------------------------------------------------------------- - if (IsMouseButtonPressed(MouseButton.Left) || IsKeyPressed(KeyboardKey.Right)) - { - // Cycle between the textures - currentTexture = (currentTexture + 1) % NumTextures; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawTexture(textures[currentTexture], 0, 0, Color.White); - - DrawRectangle(30, 400, 325, 30, ColorAlpha(Color.SkyBlue, 0.5f)); - DrawRectangleLines(30, 400, 325, 30, ColorAlpha(Color.White, 0.5f)); - DrawText("MOUSE LEFT BUTTON to CYCLE PROCEDURAL TEXTURES", 40, 410, 10, Color.White); - - switch (currentTexture) - { - case 0: - DrawText("VERTICAL GRADIENT", 560, 10, 20, Color.RayWhite); - break; - case 1: - DrawText("HORIZONTAL GRADIENT", 540, 10, 20, Color.RayWhite); - break; - case 2: - DrawText("RADIAL GRADIENT", 580, 10, 20, Color.LightGray); - break; - case 3: - DrawText("CHECKED", 680, 10, 20, Color.RayWhite); - break; - case 4: - DrawText("Color.WHITE NOISE", 640, 10, 20, Color.Red); - break; - case 5: - DrawText("CELLULAR", 670, 10, 20, Color.RayWhite); - break; - default: - break; - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - for (int i = 0; i < textures.Length; i++) - { - UnloadTexture(textures[i]); - } - CloseWindow(); //-------------------------------------------------------------------------------------- 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/ImageLoading.cs b/Examples/Textures/ImageLoading.cs index 5772b06..d525900 100644 --- a/Examples/Textures/ImageLoading.cs +++ b/Examples/Textures/ImageLoading.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [textures] example - Image loading and texture creation +* raylib [textures] example - image loading +* +* Example complexity rating: [★☆☆☆] 1/4 * * NOTE: Images are loaded in CPU memory (RAM); textures are loaded in GPU memory (VRAM) * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.3, last time updated with raylib 1.3 * -* Copyright (c) 2015 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) 2015-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -15,56 +19,74 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class ImageLoading +public partial class ImageLoading : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / Image Loading"; + + public string Title => "raylib [textures] example - image loading"; + + private Texture2D texture; + + public void Init() + { + // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) + + var image = LoadImage("resources/raylib-cs_logo.png"); // Loaded in CPU memory (RAM) + texture = LoadTextureFromImage(image); // Image converted to texture, GPU memory (VRAM) + UnloadImage(image); // Once image has been converted to texture and uploaded to VRAM, it can be unloaded from RAM + } + + public void Update() + { + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawTexture( + texture, + screenWidth / 2 - texture.Width / 2, + screenHeight / 2 - texture.Height / 2, + Color.White + ); + + DrawText("this IS a texture loaded from an image!", 300, 370, 10, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texture); // Texture unloading + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [textures] example - image loading"); - // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) - - Image image = LoadImage("resources/raylib-cs_logo.png"); - Texture2D texture = LoadTextureFromImage(image); - - UnloadImage(image); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //--------------------------------------------------------------------------------------- + var game = new ImageLoading(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // TODO: Update your variables here - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawTexture( - texture, - screenWidth / 2 - texture.Width / 2, - screenHeight / 2 - texture.Height / 2, - Color.White - ); - - DrawText("this IS a texture loaded from an image!", 300, 370, 10, Color.Gray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(texture); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Textures/ImageProcessing.cs b/Examples/Textures/ImageProcessing.cs index c28a263..91b546d 100644 --- a/Examples/Textures/ImageProcessing.cs +++ b/Examples/Textures/ImageProcessing.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [textures] example - Image processing +* raylib [textures] example - image processing +* +* Example complexity rating: [★★★☆] 3/4 * * NOTE: Images are loaded in CPU memory (RAM); textures are loaded in GPU memory (VRAM) * -* This example has been created using raylib 1.4 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.4, last time updated with raylib 3.5 * -* Copyright (c) 2016 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) 2016-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -15,11 +19,18 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class ImageProcessing +public partial class ImageProcessing : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + public const int NumProcesses = 9; - enum ImageProcess + public string Name => "Textures / Image Processing"; + + public string Title => "raylib [textures] example - image processing"; + + private enum ImageProcess { None = 0, ColorGrayScale, @@ -32,188 +43,210 @@ public class ImageProcessing FlipHorizontal } - static string[] processText = { - "NO PROCESSING", - "COLOR GRAYSCALE", - "COLOR TINT", - "COLOR INVERT", - "COLOR CONTRAST", - "COLOR BRIGHTNESS", - "GAUSSIAN BLUR", - "FLIP VERTICAL", - "FLIP HORIZONTAL" - }; + private string[] processText = { + "NO PROCESSING", + "COLOR GRAYSCALE", + "COLOR TINT", + "COLOR INVERT", + "COLOR CONTRAST", + "COLOR BRIGHTNESS", + "GAUSSIAN BLUR", + "FLIP VERTICAL", + "FLIP HORIZONTAL" + }; - public unsafe static int Main() + private Image imageOrigin; + private Image imageCopy; + private Texture2D texture; + private ImageProcess currentProcess; + private bool textureReload; + private Rectangle[] toggleRecs; + private int mouseHoverRec; + + public void Init() + { + // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) + + imageOrigin = LoadImage("resources/parrots.png"); // Loaded in CPU memory (RAM) + ImageFormat(ref imageOrigin, PixelFormat.UncompressedR8G8B8A8); // Format image to RGBA 32bit (required for texture update) <-- ISSUE + texture = LoadTextureFromImage(imageOrigin); // Image converted to texture, GPU memory (VRAM) + + imageCopy = ImageCopy(imageOrigin); + + currentProcess = ImageProcess.None; + textureReload = false; + + toggleRecs = new Rectangle[NumProcesses]; + mouseHoverRec = -1; + + for (var i = 0; i < NumProcesses; i++) + { + toggleRecs[i] = new Rectangle(40.0f, (float)(50 + 32 * i), 150.0f, 30.0f); + } + } + + public unsafe void Update() + { + // Update + //---------------------------------------------------------------------------------- + + // Mouse toggle group logic + for (var i = 0; i < NumProcesses; i++) + { + if (CheckCollisionPointRec(GetMousePosition(), toggleRecs[i])) + { + mouseHoverRec = i; + + if (IsMouseButtonReleased(MouseButton.Left)) + { + currentProcess = (ImageProcess)i; + textureReload = true; + } + break; + } + else + { + mouseHoverRec = -1; + } + } + + // Keyboard toggle group logic + if (IsKeyPressed(KeyboardKey.Down)) + { + currentProcess++; + if ((int)currentProcess > (NumProcesses - 1)) + { + currentProcess = 0; + } + + textureReload = true; + } + else if (IsKeyPressed(KeyboardKey.Up)) + { + currentProcess--; + if (currentProcess < 0) + { + currentProcess = ImageProcess.FlipHorizontal; + } + + textureReload = true; + } + + // Reload texture when required + if (textureReload) + { + UnloadImage(imageCopy); // Unload image-copy data + imageCopy = ImageCopy(imageOrigin); // Restore image-copy from image-origin + + // NOTE: Image processing is a costly CPU process to be done every frame, + // If image processing is required in a frame-basis, it should be done + // with a texture and by shaders + switch (currentProcess) + { + case ImageProcess.ColorGrayScale: + ImageColorGrayscale(ref imageCopy); + break; + case ImageProcess.ColorTint: + ImageColorTint(ref imageCopy, Color.Green); + break; + case ImageProcess.ColorInvert: + ImageColorInvert(ref imageCopy); + break; + case ImageProcess.ColorContrast: + ImageColorContrast(ref imageCopy, -40); + break; + case ImageProcess.ColorBrightness: + ImageColorBrightness(ref imageCopy, -80); + break; + case ImageProcess.GaussianBlur: + ImageBlurGaussian(ref imageCopy, 10); + break; + case ImageProcess.FlipVertical: + ImageFlipVertical(ref imageCopy); + break; + case ImageProcess.FlipHorizontal: + ImageFlipHorizontal(ref imageCopy); + break; + default: + break; + } + + var pixels = LoadImageColors(imageCopy); // Load pixel data from image (RGBA 32bit) + UpdateTexture(texture, pixels); // Update texture with new image data + UnloadImageColors(pixels); // Unload pixels data from RAM + + textureReload = false; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText("IMAGE PROCESSING:", 40, 30, 10, Color.DarkGray); + + // Draw rectangles + for (var i = 0; i < NumProcesses; i++) + { + DrawRectangleRec(toggleRecs[i], ((i == (int)currentProcess) || (i == mouseHoverRec)) ? Color.SkyBlue : Color.LightGray); + DrawRectangleLines( + (int)toggleRecs[i].X, + (int)toggleRecs[i].Y, + (int)toggleRecs[i].Width, + (int)toggleRecs[i].Height, + ((i == (int)currentProcess) || (i == mouseHoverRec)) ? Color.Blue : Color.Gray + ); + + var labelX = (int)(toggleRecs[i].X + toggleRecs[i].Width / 2); + DrawText( + processText[i], + (int)(labelX - MeasureText(processText[i], 10) / 2), + (int)toggleRecs[i].Y + 11, + 10, + ((i == (int)currentProcess) || (i == mouseHoverRec)) ? Color.DarkBlue : Color.DarkGray + ); + } + + var x = screenWidth - texture.Width - 60; + var y = screenHeight / 2 - texture.Height / 2; + DrawTexture(texture, x, y, Color.White); + DrawRectangleLines(x, y, texture.Width, texture.Height, Color.Black); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texture); // Unload texture from VRAM + UnloadImage(imageOrigin); // Unload image-origin from RAM + UnloadImage(imageCopy); // Unload image-copy from RAM + } + + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [textures] example - image processing"); - // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) - Image imageOrigin = LoadImage("resources/parrots.png"); - ImageFormat(ref imageOrigin, PixelFormat.UncompressedR8G8B8A8); - Texture2D texture = LoadTextureFromImage(imageOrigin); - - Image imageCopy = ImageCopy(imageOrigin); - - ImageProcess currentProcess = ImageProcess.None; - bool textureReload = false; - - Rectangle[] toggleRecs = new Rectangle[NumProcesses]; - int mouseHoverRec = -1; - - for (int i = 0; i < NumProcesses; i++) - { - toggleRecs[i] = new Rectangle(40, 50 + 32 * i, 150, 30); - } - SetTargetFPS(60); //--------------------------------------------------------------------------------------- + var game = new ImageProcessing(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - - // Mouse toggle group logic - for (int i = 0; i < NumProcesses; i++) - { - if (CheckCollisionPointRec(GetMousePosition(), toggleRecs[i])) - { - mouseHoverRec = i; - - if (IsMouseButtonReleased(MouseButton.Left)) - { - currentProcess = (ImageProcess)i; - textureReload = true; - } - break; - } - else - { - mouseHoverRec = -1; - } - } - - // Keyboard toggle group logic - if (IsKeyPressed(KeyboardKey.Down)) - { - currentProcess++; - if ((int)currentProcess > (NumProcesses - 1)) - { - currentProcess = 0; - } - - textureReload = true; - } - else if (IsKeyPressed(KeyboardKey.Up)) - { - currentProcess--; - if (currentProcess < 0) - { - currentProcess = ImageProcess.FlipHorizontal; - } - - textureReload = true; - } - - if (textureReload) - { - UnloadImage(imageCopy); - imageCopy = ImageCopy(imageOrigin); - - // NOTE: Image processing is a costly CPU process to be done every frame, - // If image processing is required in a frame-basis, it should be done - // with a texture and by shaders - switch (currentProcess) - { - case ImageProcess.ColorGrayScale: - ImageColorGrayscale(ref imageCopy); - break; - case ImageProcess.ColorTint: - ImageColorTint(ref imageCopy, Color.Green); - break; - case ImageProcess.ColorInvert: - ImageColorInvert(ref imageCopy); - break; - case ImageProcess.ColorContrast: - ImageColorContrast(ref imageCopy, -40); - break; - case ImageProcess.ColorBrightness: - ImageColorBrightness(ref imageCopy, -80); - break; - case ImageProcess.GaussianBlur: - ImageBlurGaussian(ref imageCopy, 10); - break; - case ImageProcess.FlipVertical: - ImageFlipVertical(ref imageCopy); - break; - case ImageProcess.FlipHorizontal: - ImageFlipHorizontal(ref imageCopy); - break; - default: - break; - } - - // Get pixel data from image (RGBA 32bit) - Color* pixels = LoadImageColors(imageCopy); - UpdateTexture(texture, pixels); - UnloadImageColors(pixels); - - textureReload = false; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("IMAGE PROCESSING:", 40, 30, 10, Color.DarkGray); - - // Draw rectangles - for (int i = 0; i < NumProcesses; i++) - { - DrawRectangleRec(toggleRecs[i], (i == (int)currentProcess) ? Color.SkyBlue : Color.LightGray); - DrawRectangleLines( - (int)toggleRecs[i].X, - (int)toggleRecs[i].Y, - (int)toggleRecs[i].Width, - (int)toggleRecs[i].Height, - (i == (int)currentProcess) ? Color.Blue : Color.Gray - ); - - int labelX = (int)(toggleRecs[i].X + toggleRecs[i].Width / 2); - DrawText( - processText[i], - (int)(labelX - MeasureText(processText[i], 10) / 2), - (int)toggleRecs[i].Y + 11, - 10, - (i == (int)currentProcess) ? Color.DarkBlue : Color.DarkGray - ); - } - - int x = screenWidth - texture.Width - 60; - int y = screenHeight / 2 - texture.Height / 2; - DrawTexture(texture, x, y, Color.White); - DrawRectangleLines(x, y, texture.Width, texture.Height, Color.Black); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(texture); - UnloadImage(imageOrigin); - UnloadImage(imageCopy); - - CloseWindow(); + 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/ImageText.cs b/Examples/Textures/ImageText.cs index 71fed41..cbed26e 100644 --- a/Examples/Textures/ImageText.cs +++ b/Examples/Textures/ImageText.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [texture] example - Image text drawing using TTF generated spritefont +* raylib [textures] example - image text * -* This example has been created using raylib 1.8 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2017 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.8, 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) 2017-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,21 +18,26 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class ImageText +public partial class ImageText : IExample { - public static int Main() - { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + private const int screenWidth = 800; + private const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [texture] example - image text drawing"); + public string Name => "Textures / Image Text"; + + public string Title => "raylib [textures] example - image text"; + + private Font font; + private Texture2D texture; + private Vector2 position; + private bool showFont; + + public void Init() + { + var parrots = LoadImage("resources/parrots.png"); // Load image in CPU memory (RAM) // TTF Font loading with custom generation parameters - Font font = LoadFontEx("resources/fonts/KAISG.ttf", 64, null, 95); - - Image parrots = LoadImage("resources/parrots.png"); + font = LoadFontEx("resources/fonts/KAISG.ttf", 64, null, 0); // Draw over image using custom font ImageDrawTextEx( @@ -38,69 +47,89 @@ public class ImageText new Vector2(20, 20), font.BaseSize, 0, - Color.White + Color.Red ); - // Image converted to texture, uploaded to GPU memory (VRAM) - Texture2D texture = LoadTextureFromImage(parrots); - UnloadImage(parrots); + texture = LoadTextureFromImage(parrots); // Image converted to texture, uploaded to GPU memory (VRAM) + UnloadImage(parrots); // Once image has been converted to texture and uploaded to VRAM, it can be unloaded from RAM - Vector2 position = new( + position = new( screenWidth / 2 - texture.Width / 2, screenHeight / 2 - texture.Height / 2 - 20 ); - bool showFont = false; + showFont = false; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + if (IsKeyDown(KeyboardKey.Space)) + { + showFont = true; + } + else + { + showFont = false; + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + if (!showFont) + { + // Draw texture with text already drawn inside + DrawTextureV(texture, position, Color.White); + + // Draw text directly using sprite font + Vector2 textPosition = new(position.X + 20, position.Y + 20 + 280); + DrawTextEx(font, "[Parrots font drawing]", textPosition, font.BaseSize, 0, Color.White); + } + else + { + DrawTexture(font.Texture, screenWidth / 2 - font.Texture.Width / 2, 50, Color.Black); + } + + DrawText("PRESS SPACE to SHOW FONT ATLAS USED", 290, 420, 10, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texture); // Texture unloading + + UnloadFont(font); // Unload custom font + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - image text"); SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new ImageText(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - if (IsKeyDown(KeyboardKey.Space)) - { - showFont = true; - } - else - { - showFont = false; - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - if (!showFont) - { - // Draw texture with text already drawn inside - DrawTextureV(texture, position, Color.White); - - // Draw text directly using sprite font - Vector2 textPosition = new(position.X + 20, position.Y + 20 + 280); - DrawTextEx(font, "[Parrots font drawing]", textPosition, font.BaseSize, 0, Color.White); - } - else - { - DrawTexture(font.Texture, screenWidth / 2 - font.Texture.Width / 2, 50, Color.Black); - } - - DrawText("PRESS SPACE to SEE USED SPRITEFONT ", 290, 420, 10, Color.DarkGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(texture); - UnloadFont(font); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Textures/LogoRaylibTexture.cs b/Examples/Textures/LogoRaylibTexture.cs index 995c90e..64b5a6c 100644 --- a/Examples/Textures/LogoRaylibTexture.cs +++ b/Examples/Textures/LogoRaylibTexture.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [textures] example - Texture loading and drawing +* raylib [textures] example - logo raylib * -* This example has been created using raylib 1.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 * -* Copyright (c) 2014 Ramon Santamaria (@raysan5) +* 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) * ********************************************************************************************/ @@ -13,52 +17,71 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class LogoRaylibTexture +public partial class LogoRaylibTexture : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / Logo Raylib Texture"; + + public string Title => "raylib [textures] example - logo raylib"; + + private Texture2D texture; + + public void Init() + { + // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) + texture = LoadTexture("resources/raylib-cs_logo.png"); // Texture loading + } + + public void Update() + { + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawTexture( + texture, + screenWidth / 2 - texture.Width / 2, + screenHeight / 2 - texture.Height / 2, + Color.White + ); + + DrawText("this IS a texture!", 360, 370, 10, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texture); // Texture unloading + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [textures] example - logo raylib"); - InitWindow(screenWidth, screenHeight, "raylib [textures] example - texture loading and drawing"); - - // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) - Texture2D texture = LoadTexture("resources/raylib-cs_logo.png"); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //--------------------------------------------------------------------------------------- + var game = new LogoRaylibTexture(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // TODO: Update your variables here - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawTexture( - texture, - screenWidth / 2 - texture.Width / 2, - screenHeight / 2 - texture.Height / 2, - Color.White - ); - - DrawText("this IS a texture!", 360, 370, 10, Color.Gray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(texture); - - CloseWindow(); + 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/MousePainting.cs b/Examples/Textures/MousePainting.cs index 62f4692..e828201 100644 --- a/Examples/Textures/MousePainting.cs +++ b/Examples/Textures/MousePainting.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [textures] example - Mouse painting +* raylib [textures] example - mouse painting * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 3.0, last time updated with raylib 3.0 * * Example contributed by Chris Dill (@MysteriousSpace) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2019 Chris Dill (@MysteriousSpace) and 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 Chris Dill (@MysteriousSpace) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -16,48 +20,66 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class MousePainting +public partial class MousePainting : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / Mouse Painting"; + + public string Title => "raylib [textures] example - mouse painting"; + + public int TargetFps => 120; + + private Color[] colors; + private Rectangle[] colorsRecs; + + private int colorSelected; + private int colorSelectedPrev; + private int colorMouseHover; + private float brushSize; + private bool mouseWasPressed; + + private Rectangle btnSaveRec; + private bool btnSaveMouseHover; + private bool showSaveMessage; + private int saveMessageCounter; + + private RenderTexture2D target; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [textures] example - mouse painting"); - - // Colours to choose from - Color[] colors = new Color[] { - Color.RayWhite, - Color.Yellow, - Color.Gold, - Color.Orange, - Color.Pink, - Color.Red, - Color.Maroon, - Color.Green, - Color.Lime, - Color.DarkGreen, - Color.SkyBlue, - Color.Blue, - Color.DarkBlue, - Color.Purple, - Color.Violet, - Color.DarkPurple, - Color.Beige, - Color.Brown, - Color.DarkBrown, - Color.LightGray, - Color.Gray, - Color.DarkGray, - Color.Black - }; + // Colors to choose from + colors = new Color[] { + Color.RayWhite, + Color.Yellow, + Color.Gold, + Color.Orange, + Color.Pink, + Color.Red, + Color.Maroon, + Color.Green, + Color.Lime, + Color.DarkGreen, + Color.SkyBlue, + Color.Blue, + Color.DarkBlue, + Color.Purple, + Color.Violet, + Color.DarkPurple, + Color.Beige, + Color.Brown, + Color.DarkBrown, + Color.LightGray, + Color.Gray, + Color.DarkGray, + Color.Black + }; // Define colorsRecs data (for every rectangle) - Rectangle[] colorsRecs = new Rectangle[colors.Length]; + colorsRecs = new Rectangle[colors.Length]; - for (int i = 0; i < colorsRecs.Length; i++) + for (var i = 0; i < colorsRecs.Length; i++) { colorsRecs[i].X = 10 + 30 * i + 2 * i; colorsRecs[i].Y = 10; @@ -65,226 +87,255 @@ public class MousePainting colorsRecs[i].Height = 30; } - int colorSelected = 0; - int colorSelectedPrev = colorSelected; - int colorMouseHover = 0; - int brushSize = 20; + colorSelected = 0; + colorSelectedPrev = colorSelected; + colorMouseHover = 0; + brushSize = 20.0f; + mouseWasPressed = false; - Rectangle btnSaveRec = new(750, 10, 40, 30); - bool btnSaveMouseHover = false; - bool showSaveMessage = false; - int saveMessageCounter = 0; + btnSaveRec = new(750, 10, 40, 30); + btnSaveMouseHover = false; + showSaveMessage = false; + saveMessageCounter = 0; // Create a RenderTexture2D to use as a canvas - RenderTexture2D target = LoadRenderTexture(screenWidth, screenHeight); + target = LoadRenderTexture(screenWidth, screenHeight); // Clear render texture before entering the game loop BeginTextureMode(target); ClearBackground(colors[0]); EndTextureMode(); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + var mousePos = GetMousePosition(); + + // Move between colors with keys + if (IsKeyPressed(KeyboardKey.Right)) + { + colorSelected++; + } + else if (IsKeyPressed(KeyboardKey.Left)) + { + colorSelected--; + } + + if (colorSelected >= colors.Length) + { + colorSelected = colors.Length - 1; + } + else if (colorSelected < 0) + { + colorSelected = 0; + } + + // Choose color with mouse + for (var i = 0; i < colors.Length; i++) + { + if (CheckCollisionPointRec(mousePos, colorsRecs[i])) + { + colorMouseHover = i; + break; + } + else + { + colorMouseHover = -1; + } + } + + if ((colorMouseHover >= 0) && IsMouseButtonPressed(MouseButton.Left)) + { + colorSelected = colorMouseHover; + colorSelectedPrev = colorSelected; + } + + // Change brush size + brushSize += GetMouseWheelMove() * 5; + if (brushSize < 2) + { + brushSize = 2; + } + + if (brushSize > 50) + { + brushSize = 50; + } + + if (IsKeyPressed(KeyboardKey.C)) + { + // Clear render texture to clear color + BeginTextureMode(target); + ClearBackground(colors[0]); + EndTextureMode(); + } + + if (IsMouseButtonDown(MouseButton.Left) || (GetGestureDetected() == Gesture.Drag)) + { + // Paint circle into render texture + // NOTE: To avoid discontinuous circles, we could store + // previous-next mouse points and just draw a line using brush size + BeginTextureMode(target); + if (mousePos.Y > 50) + { + DrawCircle((int)mousePos.X, (int)mousePos.Y, brushSize, colors[colorSelected]); + } + + EndTextureMode(); + } + + if (IsMouseButtonDown(MouseButton.Right)) + { + if (!mouseWasPressed) + { + colorSelectedPrev = colorSelected; + colorSelected = 0; + } + + mouseWasPressed = true; + + // Erase circle from render texture + BeginTextureMode(target); + if (mousePos.Y > 50) + { + DrawCircle((int)mousePos.X, (int)mousePos.Y, brushSize, colors[0]); + } + + EndTextureMode(); + } + else if (IsMouseButtonReleased(MouseButton.Right) && mouseWasPressed) + { + colorSelected = colorSelectedPrev; + mouseWasPressed = false; + } + + // Check mouse hover save button + if (CheckCollisionPointRec(mousePos, btnSaveRec)) + { + btnSaveMouseHover = true; + } + else + { + btnSaveMouseHover = false; + } + + // Image saving logic + // NOTE: Saving painted texture to a default named image + if ((btnSaveMouseHover && IsMouseButtonReleased(MouseButton.Left)) || + IsKeyPressed(KeyboardKey.S)) + { + var image = LoadImageFromTexture(target.Texture); + ImageFlipVertical(ref image); + ExportImage(image, "my_amazing_texture_painting.png"); + UnloadImage(image); + showSaveMessage = true; + } + + if (showSaveMessage) + { + // On saving, show a full screen message for 2 seconds + saveMessageCounter++; + if (saveMessageCounter > 240) + { + showSaveMessage = false; + saveMessageCounter = 0; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // NOTE: Render texture must be y-flipped due to default OpenGL coordinates (left-bottom) + Rectangle source = new(0, 0, target.Texture.Width, -target.Texture.Height); + DrawTextureRec(target.Texture, source, new Vector2(0, 0), Color.White); + + // Draw drawing circle for reference + if (mousePos.Y > 50) + { + if (IsMouseButtonDown(MouseButton.Right)) + { + DrawCircleLines((int)mousePos.X, (int)mousePos.Y, brushSize, Color.Gray); + } + else + { + DrawCircle(GetMouseX(), GetMouseY(), brushSize, colors[colorSelected]); + } + } + + // Draw top panel + DrawRectangle(0, 0, GetScreenWidth(), 50, Color.RayWhite); + DrawLine(0, 50, GetScreenWidth(), 50, Color.LightGray); + + // Draw color selection rectangles + for (var i = 0; i < colors.Length; i++) + { + DrawRectangleRec(colorsRecs[i], colors[i]); + } + + DrawRectangleLines(10, 10, 30, 30, Color.LightGray); + + if (colorMouseHover >= 0) + { + DrawRectangleRec(colorsRecs[colorMouseHover], Fade(Color.White, 0.6f)); + } + + Rectangle rec = new( + colorsRecs[colorSelected].X - 2, + colorsRecs[colorSelected].Y - 2, + colorsRecs[colorSelected].Width + 4, + colorsRecs[colorSelected].Height + 4 + ); + DrawRectangleLinesEx(rec, 2, Color.Black); + + // Draw save image button + DrawRectangleLinesEx(btnSaveRec, 2, btnSaveMouseHover ? Color.Red : Color.Black); + DrawText("SAVE!", 755, 20, 10, btnSaveMouseHover ? Color.Red : Color.Black); + + // Draw save image message + if (showSaveMessage) + { + DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Fade(Color.RayWhite, 0.8f)); + DrawRectangle(0, 150, GetScreenWidth(), 80, Color.Black); + DrawText("IMAGE SAVED!", 150, 180, 20, Color.RayWhite); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadRenderTexture(target); // Unload render texture + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - mouse painting"); SetTargetFPS(120); // Set our game to run at 120 frames-per-second //-------------------------------------------------------------------------------------- + var game = new MousePainting(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - Vector2 mousePos = GetMousePosition(); - - // Move between colors with keys - if (IsKeyPressed(KeyboardKey.Right)) - { - colorSelected++; - } - else if (IsKeyPressed(KeyboardKey.Left)) - { - colorSelected--; - } - - if (colorSelected >= colors.Length) - { - colorSelected = colors.Length - 1; - } - else if (colorSelected < 0) - { - colorSelected = 0; - } - - // Choose color with mouse - for (int i = 0; i < colors.Length; i++) - { - if (CheckCollisionPointRec(mousePos, colorsRecs[i])) - { - colorMouseHover = i; - break; - } - else - { - colorMouseHover = -1; - } - } - - if ((colorMouseHover >= 0) && IsMouseButtonPressed(MouseButton.Left)) - { - colorSelected = colorMouseHover; - colorSelectedPrev = colorSelected; - } - - // Change brush size - brushSize += (int)(GetMouseWheelMove() * 5); - if (brushSize < 2) - { - brushSize = 2; - } - - if (brushSize > 50) - { - brushSize = 50; - } - - if (IsKeyPressed(KeyboardKey.C)) - { - // Clear render texture to clear color - BeginTextureMode(target); - ClearBackground(colors[0]); - EndTextureMode(); - } - - if (IsMouseButtonDown(MouseButton.Left)) - { - // Paint circle into render texture - // NOTE: To avoid discontinuous circles, we could store - // previous-next mouse points and just draw a line using brush size - BeginTextureMode(target); - if (mousePos.Y > 50) - { - DrawCircle((int)mousePos.X, (int)mousePos.Y, brushSize, colors[colorSelected]); - } - - EndTextureMode(); - } - else if (IsMouseButtonDown(MouseButton.Right)) - { - colorSelected = 0; - - // Erase circle from render texture - BeginTextureMode(target); - if (mousePos.Y > 50) - { - DrawCircle((int)mousePos.X, (int)mousePos.Y, brushSize, colors[0]); - } - - EndTextureMode(); - } - else - { - colorSelected = colorSelectedPrev; - } - - // Check mouse hover save button - if (CheckCollisionPointRec(mousePos, btnSaveRec)) - { - btnSaveMouseHover = true; - } - else - { - btnSaveMouseHover = false; - } - - // Image saving logic - // NOTE: Saving painted texture to a default named image - if ((btnSaveMouseHover && IsMouseButtonReleased(MouseButton.Left)) || - IsKeyPressed(KeyboardKey.S)) - { - Image image = LoadImageFromTexture(target.Texture); - ImageFlipVertical(ref image); - ExportImage(image, "my_amazing_texture_painting.png"); - UnloadImage(image); - showSaveMessage = true; - } - - if (showSaveMessage) - { - // On saving, show a full screen message for 2 seconds - saveMessageCounter++; - if (saveMessageCounter > 240) - { - showSaveMessage = false; - saveMessageCounter = 0; - } - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // NOTE: Render texture must be y-flipped due to default OpenGL coordinates (left-bottom) - Rectangle source = new(0, 0, target.Texture.Width, -target.Texture.Height); - DrawTextureRec(target.Texture, source, new Vector2(0, 0), Color.White); - - // Draw drawing circle for reference - if (mousePos.Y > 50) - { - if (IsMouseButtonDown(MouseButton.Right)) - { - DrawCircleLines((int)mousePos.X, (int)mousePos.Y, brushSize, colors[colorSelected]); - } - else - { - DrawCircle(GetMouseX(), GetMouseY(), brushSize, colors[colorSelected]); - } - } - - // Draw top panel - DrawRectangle(0, 0, GetScreenWidth(), 50, Color.RayWhite); - DrawLine(0, 50, GetScreenWidth(), 50, Color.LightGray); - - // Draw color selection rectangles - for (int i = 0; i < colors.Length; i++) - { - DrawRectangleRec(colorsRecs[i], colors[i]); - } - - DrawRectangleLines(10, 10, 30, 30, Color.LightGray); - - if (colorMouseHover >= 0) - { - DrawRectangleRec(colorsRecs[colorMouseHover], ColorAlpha(Color.White, 0.6f)); - } - - Rectangle rec = new( - colorsRecs[colorSelected].X - 2, - colorsRecs[colorSelected].Y - 2, - colorsRecs[colorSelected].Width + 4, - colorsRecs[colorSelected].Height + 4 - ); - DrawRectangleLinesEx(rec, 2, Color.Black); - - // Draw save image button - DrawRectangleLinesEx(btnSaveRec, 2, btnSaveMouseHover ? Color.Red : Color.Black); - DrawText("SAVE!", 755, 20, 10, btnSaveMouseHover ? Color.Red : Color.Black); - - // Draw save image message - if (showSaveMessage) - { - DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), ColorAlpha(Color.RayWhite, 0.8f)); - DrawRectangle(0, 150, GetScreenWidth(), 80, Color.Black); - DrawText("IMAGE SAVED: my_amazing_texture_painting.png", 150, 180, 20, Color.RayWhite); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadRenderTexture(target); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Textures/NpatchDrawing.cs b/Examples/Textures/NpatchDrawing.cs index 58315ca..f641535 100644 --- a/Examples/Textures/NpatchDrawing.cs +++ b/Examples/Textures/NpatchDrawing.cs @@ -1,15 +1,19 @@ /******************************************************************************************* * -* raylib [textures] example - N-patch drawing +* raylib [textures] example - npatch drawing +* +* Example complexity rating: [★★★☆] 3/4 * * NOTE: Images are loaded in CPU memory (RAM); textures are loaded in GPU memory (VRAM) * -* This example has been created using raylib 2.0 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 2.0, last time updated with raylib 2.5 * * Example contributed by Jorge A. Gomes (@overdev) and reviewed by Ramon Santamaria (@raysan5) * -* Copyright (c) 2018 Jorge A. Gomes (@overdev) and 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) 2018-2025 Jorge A. Gomes (@overdev) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -19,31 +23,46 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class NpatchDrawing +public partial class NpatchDrawing : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / N-patch Drawing"; + + public string Title => "raylib [textures] example - npatch drawing"; + + private Texture2D nPatchTexture; + + private Vector2 mousePosition; + private Vector2 origin; + + private Rectangle dstRec1; + private Rectangle dstRec2; + private Rectangle dstRecH; + private Rectangle dstRecV; + + private NPatchInfo ninePatchInfo1; + private NPatchInfo ninePatchInfo2; + private NPatchInfo h3PatchInfo; + private NPatchInfo v3PatchInfo; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [textures] example - N-patch drawing"); - // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) - Texture2D nPatchTexture = LoadTexture("resources/ninepatch_button.png"); + nPatchTexture = LoadTexture("resources/ninepatch_button.png"); - Vector2 mousePosition = new(0.0f, 0.0f); - Vector2 origin = new(0.0f, 0.0f); + mousePosition = new(0.0f, 0.0f); + origin = new(0.0f, 0.0f); // Position and size of the n-patches - Rectangle dstRec1 = new(480.0f, 160.0f, 32.0f, 32.0f); - Rectangle dstRec2 = new(160.0f, 160.0f, 32.0f, 32.0f); - Rectangle dstRecH = new(160.0f, 93.0f, 32.0f, 32.0f); - Rectangle dstRecV = new(92.0f, 160.0f, 32.0f, 32.0f); + dstRec1 = new(480.0f, 160.0f, 32.0f, 32.0f); + dstRec2 = new(160.0f, 160.0f, 32.0f, 32.0f); + dstRecH = new(160.0f, 93.0f, 32.0f, 32.0f); + dstRecV = new(92.0f, 160.0f, 32.0f, 32.0f); - // A 9-patch (NPT_9PATCH) changes its sizes in both axis - NPatchInfo ninePatchInfo1 = new NPatchInfo + // A 9-patch (NPATCH_NINE_PATCH) changes its sizes in both axis + ninePatchInfo1 = new NPatchInfo { Source = new Rectangle(0.0f, 0.0f, 64.0f, 64.0f), Left = 12, @@ -52,7 +71,7 @@ public class NpatchDrawing Bottom = 12, Layout = NPatchLayout.NinePatch }; - NPatchInfo ninePatchInfo2 = new NPatchInfo + ninePatchInfo2 = new NPatchInfo { Source = new Rectangle(0.0f, 128.0f, 64.0f, 64.0f), Left = 16, @@ -62,8 +81,8 @@ public class NpatchDrawing Layout = NPatchLayout.NinePatch }; - // A horizontal 3-patch (NPT_3PATCH_HORIZONTAL) changes its sizes along the x axis only - NPatchInfo h3PatchInfo = new NPatchInfo + // A horizontal 3-patch (NPATCH_THREE_PATCH_HORIZONTAL) changes its sizes along the x axis only + h3PatchInfo = new NPatchInfo { Source = new Rectangle(0.0f, 64.0f, 64.0f, 64.0f), Left = 8, @@ -73,8 +92,8 @@ public class NpatchDrawing Layout = NPatchLayout.ThreePatchHorizontal }; - // A vertical 3-patch (NPT_3PATCH_VERTICAL) changes its sizes along the y axis only - NPatchInfo v3PatchInfo = new NPatchInfo + // A vertical 3-patch (NPATCH_THREE_PATCH_VERTICAL) changes its sizes along the y axis only + v3PatchInfo = new NPatchInfo { Source = new Rectangle(0.0f, 192.0f, 64.0f, 64.0f), Left = 6, @@ -83,61 +102,81 @@ public class NpatchDrawing Bottom = 6, Layout = NPatchLayout.ThreePatchVertical }; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + mousePosition = GetMousePosition(); + + // Resize the n-patches based on mouse position + dstRec1.Width = mousePosition.X - dstRec1.X; + dstRec1.Height = mousePosition.Y - dstRec1.Y; + dstRec2.Width = mousePosition.X - dstRec2.X; + dstRec2.Height = mousePosition.Y - dstRec2.Y; + dstRecH.Width = mousePosition.X - dstRecH.X; + dstRecV.Height = mousePosition.Y - dstRecV.Y; + + // Set a minimum width and/or height + dstRec1.Width = Math.Clamp(dstRec1.Width, 1.0f, 300.0f); + dstRec1.Height = MathF.Max(dstRec1.Height, 1.0f); + dstRec2.Width = Math.Clamp(dstRec2.Width, 1.0f, 300.0f); + dstRec2.Height = MathF.Max(dstRec2.Height, 1.0f); + dstRecH.Width = MathF.Max(dstRecH.Width, 1.0f); + dstRecV.Height = MathF.Max(dstRecV.Height, 1.0f); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // Draw the n-patches + DrawTextureNPatch(nPatchTexture, ninePatchInfo2, dstRec2, origin, 0.0f, Color.White); + DrawTextureNPatch(nPatchTexture, ninePatchInfo1, dstRec1, origin, 0.0f, Color.White); + DrawTextureNPatch(nPatchTexture, h3PatchInfo, dstRecH, origin, 0.0f, Color.White); + DrawTextureNPatch(nPatchTexture, v3PatchInfo, dstRecV, origin, 0.0f, Color.White); + + // Draw the source texture + DrawRectangleLines(5, 88, 74, 266, Color.Blue); + DrawTexture(nPatchTexture, 10, 93, Color.White); + DrawText("TEXTURE", 15, 360, 10, Color.DarkGray); + + DrawText("Move the mouse to stretch or shrink the n-patches", 10, 20, 20, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(nPatchTexture); // Texture unloading + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - npatch drawing"); SetTargetFPS(60); //--------------------------------------------------------------------------------------- + var game = new NpatchDrawing(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - mousePosition = GetMousePosition(); - - // Resize the n-patches based on mouse position - dstRec1.Width = mousePosition.X - dstRec1.X; - dstRec1.Height = mousePosition.Y - dstRec1.Y; - dstRec2.Width = mousePosition.X - dstRec2.X; - dstRec2.Height = mousePosition.Y - dstRec2.Y; - dstRecH.Width = mousePosition.X - dstRecH.X; - dstRecV.Height = mousePosition.Y - dstRecV.Y; - - // Set a minimum width and/or height - dstRec1.Width = Math.Clamp(dstRec1.Width, 1.0f, 300.0f); - dstRec1.Height = MathF.Max(dstRec1.Height, 1.0f); - dstRec2.Width = Math.Clamp(dstRec2.Width, 1.0f, 300.0f); - dstRec2.Height = MathF.Max(dstRec2.Height, 1.0f); - dstRecH.Width = MathF.Max(dstRecH.Width, 1.0f); - dstRecV.Height = MathF.Max(dstRecV.Height, 1.0f); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // Draw the n-patches - DrawTextureNPatch(nPatchTexture, ninePatchInfo2, dstRec2, origin, 0.0f, Color.White); - DrawTextureNPatch(nPatchTexture, ninePatchInfo1, dstRec1, origin, 0.0f, Color.White); - DrawTextureNPatch(nPatchTexture, h3PatchInfo, dstRecH, origin, 0.0f, Color.White); - DrawTextureNPatch(nPatchTexture, v3PatchInfo, dstRecV, origin, 0.0f, Color.White); - - // Draw the source texture - DrawRectangleLines(5, 88, 74, 266, Color.Blue); - DrawTexture(nPatchTexture, 10, 93, Color.White); - DrawText("TEXTURE", 15, 360, 10, Color.DarkGray); - - DrawText("Move the mouse to stretch or shrink the n-patches", 10, 20, 20, Color.DarkGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(nPatchTexture); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Textures/ParticlesBlending.cs b/Examples/Textures/ParticlesBlending.cs index e011f44..257eda9 100644 --- a/Examples/Textures/ParticlesBlending.cs +++ b/Examples/Textures/ParticlesBlending.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib example - particles blending +* raylib [textures] example - particles blending * -* This example has been created using raylib 1.7 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 * -* Copyright (c) 2017 Ramon Santamaria (@raysan5) +* Example originally created with raylib 1.7, last time updated with raylib 3.5 +* +* 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) 2017-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,12 +18,19 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class ParticlesBlending +public partial class ParticlesBlending : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + public const int MaxParticles = 200; + public string Name => "Textures / Particles Blending"; + + public string Title => "raylib [textures] example - particles blending"; + // Particle structure with basic data - struct Particle + private struct Particle { public Vector2 Position; public Color Color; @@ -30,20 +41,18 @@ public class ParticlesBlending public bool Active; } - public static int Main() + private Particle[] mouseTail; + private float gravity; + private Texture2D smoke; + private BlendMode blending; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [textures] example - particles blending"); - // Particles pool, reuse them! - Particle[] mouseTail = new Particle[MaxParticles]; + mouseTail = new Particle[MaxParticles]; // Initialize particles - for (int i = 0; i < mouseTail.Length; i++) + for (var i = 0; i < mouseTail.Length; i++) { mouseTail[i].Position = new Vector2(0, 0); mouseTail[i].Color = new Color( @@ -58,113 +67,133 @@ public class ParticlesBlending mouseTail[i].Active = false; } - float gravity = 3.0f; - Texture2D smoke = LoadTexture("resources/spark_flame.png"); - BlendMode blending = BlendMode.Alpha; + gravity = 3.0f; + smoke = LoadTexture("resources/spark_flame.png"); + blending = BlendMode.Alpha; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + + // Activate one particle every frame and Update active particles + // NOTE: Particles initial position should be mouse position when activated + // NOTE: Particles fall down with gravity and rotation... and disappear after 2 seconds (alpha = 0) + // NOTE: When a particle disappears, active = false and it can be reused + for (var i = 0; i < mouseTail.Length; i++) + { + if (!mouseTail[i].Active) + { + mouseTail[i].Active = true; + mouseTail[i].Alpha = 1.0f; + mouseTail[i].Position = GetMousePosition(); + i = mouseTail.Length; + } + } + + for (var i = 0; i < mouseTail.Length; i++) + { + if (mouseTail[i].Active) + { + mouseTail[i].Position.Y += gravity / 2; + mouseTail[i].Alpha -= 0.005f; + + if (mouseTail[i].Alpha <= 0.0f) + { + mouseTail[i].Active = false; + } + + mouseTail[i].Rotation += 2.0f; + } + } + + if (IsKeyPressed(KeyboardKey.Space)) + { + if (blending == BlendMode.Alpha) + { + blending = BlendMode.Additive; + } + else + { + blending = BlendMode.Alpha; + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.DarkGray); + + BeginBlendMode(blending); + + // Draw active particles + for (var i = 0; i < mouseTail.Length; i++) + { + if (mouseTail[i].Active) + { + Rectangle source = new(0, 0, smoke.Width, smoke.Height); + Rectangle dest = new( + mouseTail[i].Position.X, + mouseTail[i].Position.Y, + smoke.Width * mouseTail[i].Size, + smoke.Height * mouseTail[i].Size + ); + Vector2 position = new( + smoke.Width * mouseTail[i].Size / 2, + smoke.Height * mouseTail[i].Size / 2 + ); + var color = Fade(mouseTail[i].Color, mouseTail[i].Alpha); + DrawTexturePro(smoke, source, dest, position, mouseTail[i].Rotation, color); + } + } + + EndBlendMode(); + + DrawText("PRESS SPACE to CHANGE BLENDING MODE", 180, 20, 20, Color.Black); + + if (blending == BlendMode.Alpha) + { + DrawText("ALPHA BLENDING", 290, screenHeight - 40, 20, Color.Black); + } + else + { + DrawText("ADDITIVE BLENDING", 280, screenHeight - 40, 20, Color.RayWhite); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(smoke); + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - particles blending"); SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new ParticlesBlending(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - - // Activate one particle every frame and Update active particles - // NOTE: Particles initial position should be mouse position when activated - // NOTE: Particles fall down with gravity and rotation... and disappear after 2 seconds (alpha = 0) - // NOTE: When a particle disappears, active = false and it can be reused. - for (int i = 0; i < mouseTail.Length; i++) - { - if (!mouseTail[i].Active) - { - mouseTail[i].Active = true; - mouseTail[i].Alpha = 1.0f; - mouseTail[i].Position = GetMousePosition(); - i = mouseTail.Length; - } - } - - for (int i = 0; i < mouseTail.Length; i++) - { - if (mouseTail[i].Active) - { - mouseTail[i].Position.Y += gravity / 2; - mouseTail[i].Alpha -= 0.005f; - - if (mouseTail[i].Alpha <= 0.0f) - { - mouseTail[i].Active = false; - } - - mouseTail[i].Rotation += 2.0f; - } - } - - if (IsKeyPressed(KeyboardKey.Space)) - { - if (blending == BlendMode.Alpha) - { - blending = BlendMode.Additive; - } - else - { - blending = BlendMode.Alpha; - } - } - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.DarkGray); - - BeginBlendMode(blending); - - // Draw active particles - for (int i = 0; i < mouseTail.Length; i++) - { - if (mouseTail[i].Active) - { - Rectangle source = new(0, 0, smoke.Width, smoke.Height); - Rectangle dest = new( - mouseTail[i].Position.X, - mouseTail[i].Position.Y, - smoke.Width * mouseTail[i].Size, - smoke.Height * mouseTail[i].Size - ); - Vector2 position = new( - smoke.Width * mouseTail[i].Size / 2, - smoke.Height * mouseTail[i].Size / 2 - ); - Color color = ColorAlpha(mouseTail[i].Color, mouseTail[i].Alpha); - DrawTexturePro(smoke, source, dest, position, mouseTail[i].Rotation, color); - } - } - - EndBlendMode(); - - DrawText("PRESS SPACE to CHANGE BLENDING MODE", 180, 20, 20, Color.Black); - - if (blending == BlendMode.Alpha) - { - DrawText("ALPHA BLENDING", 290, screenHeight - 40, 20, Color.Black); - } - else - { - DrawText("ADDITIVE BLENDING", 280, screenHeight - 40, 20, Color.RayWhite); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(smoke); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Textures/Polygon.cs b/Examples/Textures/Polygon.cs index b9fb586..787a751 100644 --- a/Examples/Textures/Polygon.cs +++ b/Examples/Textures/Polygon.cs @@ -1,12 +1,17 @@ /******************************************************************************************* * -* raylib [shapes] example - Draw Textured Polygon +* raylib [textures] example - polygon drawing * -* This example has been created using raylib 99.98 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★☆☆☆] 1/4 * -* Copyright (c) 2014 Ramon Santamaria (@raysan5) -* Copyright (c) 2021 Chris Camacho (codifies - bedroomcoders.co.uk) +* Example originally created with raylib 3.7, last time updated with raylib 3.7 +* +* Example contributed by Chris Camacho (@chriscamacho) 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 Chris Camacho (@chriscamacho) and Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -15,37 +20,43 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class Polygon +public partial class Polygon : IExample { - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / Textured Polygon"; + + public string Title => "raylib [textures] example - polygon drawing"; + + private Vector2[] texcoords; + private Vector2[] points; + private Vector2[] positions; + private Texture2D texture; + private float angle; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + texcoords = new[] { + new Vector2(0.75f, 0), + new Vector2(0.25f, 0), + new Vector2(0, 0.5f), + new Vector2(0, 0.75f), + new Vector2(0.25f, 1), + new Vector2(0.375f, 0.875f), + new Vector2(0.625f, 0.875f), + new Vector2(0.75f, 1), + new Vector2(1, 0.75f), + new Vector2(1, 0.5f), + // Close the poly + new Vector2(0.75f, 0) + }; - InitWindow(screenWidth, screenHeight, "raylib [textures] example - Textured Polygon"); - - Vector2[] texcoords = new[] { - new Vector2(0.75f, 0), - new Vector2(0.25f, 0), - new Vector2(0, 0.5f), - new Vector2(0, 0.75f), - new Vector2(0.25f, 1), - new Vector2(0.375f, 0.875f), - new Vector2(0.625f, 0.875f), - new Vector2(0.75f, 1), - new Vector2(1, 0.75f), - new Vector2(1, 0.5f), - // Close the poly - new Vector2(0.75f, 0) - }; - - Vector2[] points = new Vector2[11]; + points = new Vector2[11]; // Define the base poly vertices from the UV's // NOTE: They can be specified in any other way - for (int i = 0; i < points.Length; i++) + for (var i = 0; i < points.Length; i++) { points[i].X = (texcoords[i].X - 0.5f) * 256.0f; points[i].Y = (texcoords[i].Y - 0.5f) * 256.0f; @@ -53,58 +64,49 @@ public class Polygon // Define the vertices drawing position // NOTE: Initially same as points but updated every frame - Vector2[] positions = new Vector2[points.Length]; - for (int i = 0; i < positions.Length; i++) + positions = new Vector2[points.Length]; + for (var i = 0; i < positions.Length; i++) { positions[i] = points[i]; } - Texture2D texture = LoadTexture("resources/cat.png"); - float angle = 0; + texture = LoadTexture("resources/cat.png"); + angle = 0; + } - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- - - // Main game loop - while (!WindowShouldClose()) + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + angle += 1; + for (var i = 0; i < positions.Length; i++) { - // Update - //---------------------------------------------------------------------------------- - // Update your variables here - //---------------------------------------------------------------------------------- - angle += 1; - for (int i = 0; i < positions.Length; i++) - { - positions[i] = Raymath.Vector2Rotate(points[i], angle * Raylib.DEG2RAD); - } - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawText("Textured Polygon", 20, 20, 20, Color.DarkGray); - Vector2 center = new(screenWidth / 2, screenHeight / 2); - DrawTexturePoly(texture, center, positions, texcoords, positions.Length, Color.White); - - EndDrawing(); - //---------------------------------------------------------------------------------- + positions[i] = Raymath.Vector2Rotate(points[i], angle * Raylib.DEG2RAD); } + //---------------------------------------------------------------------------------- + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawText("textured polygon", 20, 20, 20, Color.DarkGray); + Vector2 center = new(screenWidth / 2, screenHeight / 2); + DrawTexturePoly(texture, center, positions, texcoords, positions.Length, Color.White); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { UnloadTexture(texture); - - // De-Initialization - //-------------------------------------------------------------------------------------- - CloseWindow(); - //-------------------------------------------------------------------------------------- - - return 0; } // Draw textured polygon, defined by vertex and texture coordinates // NOTE: Polygon center must have straight line path to all points // without crossing perimeter, points must be in anticlockwise order - static void DrawTexturePoly( + private static void DrawTexturePoly( Texture2D texture, Vector2 center, Vector2[] points, @@ -114,13 +116,11 @@ public class Polygon ) { Rlgl.SetTexture(texture.Id); - - // Texturing is only supported on RL_QUADS - Rlgl.Begin(DrawMode.Quads); + Rlgl.Begin(DrawMode.Triangles); Rlgl.Color4ub(tint.R, tint.G, tint.B, tint.A); - for (int i = 0; i < pointCount - 1; i++) + for (var i = 0; i < pointCount - 1; i++) { Rlgl.TexCoord2f(0.5f, 0.5f); Rlgl.Vertex2f(center.X, center.Y); @@ -130,12 +130,37 @@ public class Polygon Rlgl.TexCoord2f(texcoords[i + 1].X, texcoords[i + 1].Y); Rlgl.Vertex2f(points[i + 1].X + center.X, points[i + 1].Y + center.Y); - - Rlgl.TexCoord2f(texcoords[i + 1].X, texcoords[i + 1].Y); - Rlgl.Vertex2f(points[i + 1].X + center.X, points[i + 1].Y + center.Y); } Rlgl.End(); Rlgl.SetTexture(0); } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - polygon drawing"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new Polygon(); + game.Init(); + + // Main game loop + while (!WindowShouldClose()) + { + game.Update(); + } + + game.Unload(); + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); + //-------------------------------------------------------------------------------------- + + return 0; + } } diff --git a/Examples/Textures/RawData.cs b/Examples/Textures/RawData.cs index 40470c3..0af7ebe 100644 --- a/Examples/Textures/RawData.cs +++ b/Examples/Textures/RawData.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [textures] example - Load textures from raw data +* raylib [textures] example - raw data +* +* Example complexity rating: [★★★☆] 3/4 * * NOTE: Images are loaded in CPU memory (RAM); textures are loaded in GPU memory (VRAM) * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.3, last time updated with raylib 3.5 * -* Copyright (c) 2015 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) 2015-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -15,98 +19,125 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class RawData +public partial class RawData : IExample { - public unsafe static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / Raw Data"; + + public string Title => "raylib [textures] example - raw data"; + + private Texture2D fudesumi; + private Texture2D checkedTex; + + public unsafe void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - InitWindow(screenWidth, screenHeight, "raylib [textures] example - texture from raw data"); - // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) // Load RAW image data (512x512, 32bit RGBA, no file header) - Image fudesumiRaw = LoadImageRaw( + var fudesumiRaw = LoadImageRaw( "resources/fudesumi.raw", 384, 512, PixelFormat.UncompressedR8G8B8A8, 0 ); - Texture2D fudesumi = LoadTextureFromImage(fudesumiRaw); - UnloadImage(fudesumiRaw); + fudesumi = LoadTextureFromImage(fudesumiRaw); // Upload CPU (RAM) image to GPU (VRAM) + UnloadImage(fudesumiRaw); // Unload CPU (RAM) image data // Generate a checked texture by code - int width = 960; - int height = 480; + var imWidth = 960; + var imHeight = 480; - // Store pixel data - Color* pixels = (Color*)Raylib.MemAlloc((uint)(width * height * sizeof(Color))); - for (int y = 0; y < height; y++) + // Dynamic memory allocation to store pixels data (Color type) + // WARNING: Using raylib provided MemAlloc() that uses default raylib + // internal memory allocator, so this data can be freed using UnloadImage() + // that also uses raylib internal memory de-allocator + var pixels = (Color*)Raylib.MemAlloc((uint)(imWidth * imHeight * sizeof(Color))); + + for (var y = 0; y < imHeight; y++) { - for (int x = 0; x < width; x++) + for (var x = 0; x < imWidth; x++) { if (((x / 32 + y / 32) / 1) % 2 == 0) { - pixels[y * width + x] = Color.Orange; + pixels[y * imWidth + x] = Color.Orange; } else { - pixels[y * width + x] = Color.Gold; + pixels[y * imWidth + x] = Color.Gold; } } } // Load pixels data into an image structure and create texture - Image checkedIm = new Image + // NOTE: We can assign pixels directly to data because Color is R8G8B8A8 + // data structure defining that pixelformat, format must be set properly + var checkedIm = new Image { Data = pixels, - Width = width, - Height = height, + Width = imWidth, + Height = imHeight, Format = PixelFormat.UncompressedR8G8B8A8, Mipmaps = 1, }; - Texture2D checkedTex = LoadTextureFromImage(checkedIm); - Raylib.MemFree(pixels); + + checkedTex = LoadTextureFromImage(checkedIm); + Raylib.MemFree(pixels); // Unload CPU (RAM) image data (pixels) + } + + public void Update() + { + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + var x = screenWidth / 2 - checkedTex.Width / 2; + var y = screenHeight / 2 - checkedTex.Height / 2; + DrawTexture(checkedTex, x, y, Fade(Color.White, 0.5f)); + DrawTexture(fudesumi, 430, -30, Color.White); + + DrawText("CHECKED TEXTURE ", 84, 85, 30, Color.Brown); + DrawText("GENERATED by CODE", 72, 148, 30, Color.Brown); + DrawText("and RAW IMAGE LOADING", 46, 210, 30, Color.Brown); + + DrawText("(c) Fudesumi sprite by Eiden Marsal", 310, screenHeight - 20, 10, Color.Brown); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(fudesumi); // Texture unloading + UnloadTexture(checkedTex); // Texture unloading + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - raw data"); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //--------------------------------------------------------------------------------------- + var game = new RawData(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // TODO: Update your variables here - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - int x = screenWidth / 2 - checkedTex.Width / 2; - int y = screenHeight / 2 - checkedTex.Height / 2; - DrawTexture(checkedTex, x, y, ColorAlpha(Color.White, 0.5f)); - DrawTexture(fudesumi, 430, -30, Color.White); - - DrawText("CHECKED TEXTURE ", 84, 85, 30, Color.Brown); - DrawText("GENERATED by CODE", 72, 148, 30, Color.Brown); - DrawText("and RAW IMAGE LOADING", 46, 210, 30, Color.Brown); - - DrawText("(c) Fudesumi sprite by Eiden Marsal", 310, screenHeight - 20, 10, Color.Brown); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(fudesumi); - UnloadTexture(checkedTex); - - CloseWindow(); + 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/SpriteAnim.cs b/Examples/Textures/SpriteAnim.cs index 4ae3759..099a0bf 100644 --- a/Examples/Textures/SpriteAnim.cs +++ b/Examples/Textures/SpriteAnim.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [textures] example - Texture loading and drawing a part defined by a rectangle +* raylib [textures] example - sprite animation * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2014 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) 2014-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -15,108 +19,133 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class SpriteAnim +public partial class SpriteAnim : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + public const int MaxFrameSpeed = 15; public const int MinFrameSpeed = 1; + public string Name => "Textures / Sprite Anim"; + + public string Title => "raylib [textures] example - sprite animation"; + + private Texture2D scarfy; + private Vector2 position; + private Rectangle frameRec; + private int currentFrame; + private int framesCounter; + private int framesSpeed; + + public void Init() + { + // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) + scarfy = LoadTexture("resources/scarfy.png"); // Texture loading + + position = new(350.0f, 280.0f); + frameRec = new(0.0f, 0.0f, (float)scarfy.Width / 6, (float)scarfy.Height); + currentFrame = 0; + + framesCounter = 0; + framesSpeed = 8; // Number of spritesheet frames shown by second + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + framesCounter++; + + if (framesCounter >= (60 / framesSpeed)) + { + framesCounter = 0; + currentFrame++; + + if (currentFrame > 5) + { + currentFrame = 0; + } + + frameRec.X = (float)currentFrame * (float)scarfy.Width / 6; + } + + if (IsKeyPressed(KeyboardKey.Right)) + { + framesSpeed++; + } + else if (IsKeyPressed(KeyboardKey.Left)) + { + framesSpeed--; + } + + framesSpeed = Math.Clamp(framesSpeed, MinFrameSpeed, MaxFrameSpeed); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawTexture(scarfy, 15, 40, Color.White); + DrawRectangleLines(15, 40, scarfy.Width, scarfy.Height, Color.Lime); + DrawRectangleLines( + 15 + (int)frameRec.X, + 40 + (int)frameRec.Y, + (int)frameRec.Width, + (int)frameRec.Height, + Color.Red + ); + + DrawText("FRAME SPEED: ", 165, 210, 10, Color.DarkGray); + DrawText($"{framesSpeed:D2} FPS", 575, 210, 10, Color.DarkGray); + DrawText("PRESS RIGHT/LEFT KEYS to CHANGE SPEED!", 290, 240, 10, Color.DarkGray); + + for (var i = 0; i < MaxFrameSpeed; i++) + { + if (i < framesSpeed) + { + DrawRectangle(250 + 21 * i, 205, 20, 20, Color.Red); + } + DrawRectangleLines(250 + 21 * i, 205, 20, 20, Color.Maroon); + } + + DrawTextureRec(scarfy, frameRec, position, Color.White); // Draw part of the texture + + DrawText("(c) Scarfy sprite by Eiden Marsal", screenWidth - 200, screenHeight - 20, 10, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(scarfy); // Texture unloading + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [textures] example - sprite animation"); - InitWindow(screenWidth, screenHeight, "raylib [texture] example - texture rectangle"); - - // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) - Texture2D scarfy = LoadTexture("resources/scarfy.png"); - - Vector2 position = new(350.0f, 280.0f); - Rectangle frameRec = new(0.0f, 0.0f, (float)scarfy.Width / 6, (float)scarfy.Height); - int currentFrame = 0; - - int framesCounter = 0; - - // Number of spritesheet frames shown by second - int framesSpeed = 8; - - SetTargetFPS(60); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new SpriteAnim(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - framesCounter++; - - if (framesCounter >= (60 / framesSpeed)) - { - framesCounter = 0; - currentFrame++; - - if (currentFrame > 5) - { - currentFrame = 0; - } - - frameRec.X = (float)currentFrame * (float)scarfy.Width / 6; - } - - if (IsKeyPressed(KeyboardKey.Right)) - { - framesSpeed++; - } - else if (IsKeyPressed(KeyboardKey.Left)) - { - framesSpeed--; - } - - framesSpeed = Math.Clamp(framesSpeed, MinFrameSpeed, MaxFrameSpeed); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawTexture(scarfy, 15, 40, Color.White); - DrawRectangleLines(15, 40, scarfy.Width, scarfy.Height, Color.Lime); - DrawRectangleLines( - 15 + (int)frameRec.X, - 40 + (int)frameRec.Y, - (int)frameRec.Width, - (int)frameRec.Height, - Color.Red - ); - - DrawText("FRAME SPEED: ", 165, 210, 10, Color.DarkGray); - DrawText($"{framesSpeed:2F} FPS", 575, 210, 10, Color.DarkGray); - DrawText("PRESS RIGHT/LEFT KEYS to CHANGE SPEED!", 290, 240, 10, Color.DarkGray); - - for (int i = 0; i < MaxFrameSpeed; i++) - { - if (i < framesSpeed) - { - DrawRectangle(250 + 21 * i, 205, 20, 20, Color.Red); - } - DrawRectangleLines(250 + 21 * i, 205, 20, 20, Color.Maroon); - } - - // Draw part of the texture - DrawTextureRec(scarfy, frameRec, position, Color.White); - DrawText("(c) Scarfy sprite by Eiden Marsal", screenWidth - 200, screenHeight - 20, 10, Color.Gray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(scarfy); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Textures/SpriteButton.cs b/Examples/Textures/SpriteButton.cs index c2de041..eb01ebf 100644 --- a/Examples/Textures/SpriteButton.cs +++ b/Examples/Textures/SpriteButton.cs @@ -2,10 +2,14 @@ * * raylib [textures] example - sprite button * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2019 Ramon Santamaria (@raysan5) +* Example originally created with raylib 2.5, last time updated with raylib 2.5 +* +* 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 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,31 +18,40 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class SpriteButton +public partial class SpriteButton : IExample { // Number of frames (rectangles) for the button sprite texture public const int NumFrames = 3; - public static int Main() + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / Sprite Button"; + + public string Title => "raylib [textures] example - sprite button"; + + private Sound fxButton; + private Texture2D button; + private int frameHeight; + private Rectangle sourceRec; + private Rectangle btnBounds; + private int btnState; + private bool btnAction; + private Vector2 mousePoint; + + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitAudioDevice(); // Initialize audio device - InitWindow(screenWidth, screenHeight, "raylib [textures] example - sprite button"); - - InitAudioDevice(); - - Sound fxButton = LoadSound("resources/audio/buttonfx.wav"); - Texture2D button = LoadTexture("resources/button.png"); + fxButton = LoadSound("resources/audio/buttonfx.wav"); // Load button sound + button = LoadTexture("resources/button.png"); // Load button texture // Define frame rectangle for drawing - int frameHeight = button.Height / NumFrames; - Rectangle sourceRec = new(0, 0, button.Width, frameHeight); + frameHeight = button.Height / NumFrames; + sourceRec = new(0, 0, button.Width, frameHeight); // Define button bounds on screen - Rectangle btnBounds = new( + btnBounds = new( screenWidth / 2 - button.Width / 2, screenHeight / 2 - button.Height / NumFrames / 2, button.Width, @@ -46,75 +59,95 @@ public class SpriteButton ); // Button state: 0-NORMAL, 1-MOUSE_HOVER, 2-PRESSED - int btnState = 0; + btnState = 0; // Button action should be activated - bool btnAction = false; + btnAction = false; - Vector2 mousePoint = new(0.0f, 0.0f); + mousePoint = new(0.0f, 0.0f); + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + mousePoint = GetMousePosition(); + btnAction = false; + + // Check button state + if (CheckCollisionPointRec(mousePoint, btnBounds)) + { + if (IsMouseButtonDown(MouseButton.Left)) + { + btnState = 2; + } + else + { + btnState = 1; + } + + if (IsMouseButtonReleased(MouseButton.Left)) + { + btnAction = true; + } + } + else + { + btnState = 0; + } + + if (btnAction) + { + PlaySound(fxButton); + // TODO: Any desired action + } + + // Calculate button frame rectangle to draw depending on button state + sourceRec.Y = btnState * frameHeight; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawTextureRec(button, sourceRec, new Vector2(btnBounds.X, btnBounds.Y), Color.White); // Draw button frame + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(button); // Unload button texture + UnloadSound(fxButton); // Unload sound + + CloseAudioDevice(); // Close audio device + } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + InitWindow(screenWidth, screenHeight, "raylib [textures] example - sprite button"); SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new SpriteButton(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - mousePoint = GetMousePosition(); - btnAction = false; - - // Check button state - if (CheckCollisionPointRec(mousePoint, btnBounds)) - { - if (IsMouseButtonDown(MouseButton.Left)) - { - btnState = 2; - } - else - { - btnState = 1; - } - - if (IsMouseButtonReleased(MouseButton.Left)) - { - btnAction = true; - } - } - else - { - btnState = 0; - } - - if (btnAction) - { - PlaySound(fxButton); - // TODO: Any desired action - } - - // Calculate button frame rectangle to draw depending on button state - sourceRec.Y = btnState * frameHeight; - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // Draw button frame - DrawTextureRec(button, sourceRec, new Vector2(btnBounds.X, btnBounds.Y), Color.White); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(button); - UnloadSound(fxButton); - - CloseAudioDevice(); - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Textures/SpriteExplosion.cs b/Examples/Textures/SpriteExplosion.cs index f748686..fd7523a 100644 --- a/Examples/Textures/SpriteExplosion.cs +++ b/Examples/Textures/SpriteExplosion.cs @@ -2,10 +2,14 @@ * * raylib [textures] example - sprite explosion * -* This example has been created using raylib 2.5 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★☆☆] 2/4 * -* Copyright (c) 2019 Anata and Ramon Santamaria (@raysan5) +* Example originally created with raylib 2.5, last time updated with raylib 3.5 +* +* 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 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,117 +18,144 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class SpriteExplosion +public partial class SpriteExplosion : IExample { - const int NumFramesPerLine = 5; - const int NumLines = 5; + private const int NumFramesPerLine = 5; + private const int NumLines = 5; + + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / Sprite Explosion"; + + public string Title => "raylib [textures] example - sprite explosion"; + + private Sound fxBoom; + private Texture2D explosion; + private int frameWidth; + private int frameHeight; + private int currentFrame; + private int currentLine; + private Rectangle frameRec; + private Vector2 position; + private bool active; + private int framesCounter; + + public void Init() + { + InitAudioDevice(); + + // Load explosion sound + fxBoom = LoadSound("resources/audio/boom.wav"); + + // Load explosion texture + explosion = LoadTexture("resources/explosion.png"); + + // Init variables for animation + frameWidth = explosion.Width / NumFramesPerLine; // Sprite one frame rectangle width + frameHeight = explosion.Height / NumLines; // Sprite one frame rectangle height + currentFrame = 0; + currentLine = 0; + + frameRec = new(0, 0, frameWidth, frameHeight); + position = new(0.0f, 0.0f); + + active = false; + framesCounter = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + + // Check for mouse button pressed and activate explosion (if not active) + if (IsMouseButtonPressed(MouseButton.Left) && !active) + { + position = GetMousePosition(); + active = true; + + position.X -= frameWidth / 2; + position.Y -= frameHeight / 2; + + PlaySound(fxBoom); + } + + // Compute explosion animation frames + if (active) + { + framesCounter++; + + if (framesCounter > 2) + { + currentFrame++; + + if (currentFrame >= NumFramesPerLine) + { + currentFrame = 0; + currentLine++; + + if (currentLine >= NumLines) + { + currentLine = 0; + active = false; + } + } + + framesCounter = 0; + } + } + + frameRec.X = frameWidth * currentFrame; + frameRec.Y = frameHeight * currentLine; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // Draw explosion required frame rectangle + if (active) + { + DrawTextureRec(explosion, frameRec, position, Color.White); + } + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(explosion); // Unload texture + UnloadSound(fxBoom); // Unload sound + + CloseAudioDevice(); + } public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - InitWindow(screenWidth, screenHeight, "raylib [textures] example - sprite explosion"); - InitAudioDevice(); - // Load explosion sound - Sound fxBoom = LoadSound("resources/audio/boom.wav"); - - // Load explosion texture - Texture2D explosion = LoadTexture("resources/explosion.png"); - - // Init variables for animation - - // Sprite one frame rectangle width - int frameWidth = explosion.Width / NumFramesPerLine; - - // Sprite one frame rectangle height - int frameHeight = explosion.Height / NumLines; - - int currentFrame = 0; - int currentLine = 0; - - Rectangle frameRec = new(0, 0, frameWidth, frameHeight); - Vector2 position = new(0.0f, 0.0f); - - bool active = false; - int framesCounter = 0; - - SetTargetFPS(120); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- + var game = new SpriteExplosion(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - - // Check for mouse button pressed and activate explosion (if not active) - if (IsMouseButtonPressed(MouseButton.Left) && !active) - { - position = GetMousePosition(); - active = true; - - position.X -= frameWidth / 2; - position.Y -= frameHeight / 2; - - PlaySound(fxBoom); - } - - // Compute explosion animation frames - if (active) - { - framesCounter++; - - if (framesCounter > 2) - { - currentFrame++; - - if (currentFrame >= NumFramesPerLine) - { - currentFrame = 0; - currentLine++; - - if (currentLine >= NumLines) - { - currentLine = 0; - active = false; - } - } - - framesCounter = 0; - } - } - - frameRec.X = frameWidth * currentFrame; - frameRec.Y = frameHeight * currentLine; - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // Draw explosion required frame rectangle - if (active) - { - DrawTextureRec(explosion, frameRec, position, Color.White); - } - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(explosion); - UnloadSound(fxBoom); - - CloseAudioDevice(); - - CloseWindow(); + 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/Textures/SrcRecDstRec.cs b/Examples/Textures/SrcRecDstRec.cs index dafc610..ea97511 100644 --- a/Examples/Textures/SrcRecDstRec.cs +++ b/Examples/Textures/SrcRecDstRec.cs @@ -1,11 +1,15 @@ /******************************************************************************************* * -* raylib [textures] example - Texture source and destination rectangles +* raylib [textures] example - srcrec dstrec * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example complexity rating: [★★★☆] 3/4 * -* Copyright (c) 2015 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 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -14,72 +18,98 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class SrcRecDstRec +public partial class SrcRecDstRec : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / Src and Dst Rectangles"; + + public string Title => "raylib [textures] example - srcrec dstrec"; + + private Texture2D scarfy; + private Rectangle sourceRec; + private Rectangle destRec; + private Vector2 origin; + private int rotation; + + public void Init() + { + // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) + + scarfy = LoadTexture("resources/scarfy.png"); // Texture loading + + var frameWidth = scarfy.Width / 6; + var frameHeight = scarfy.Height; + + // Source rectangle (part of the texture to use for drawing) + sourceRec = new(0, 0, frameWidth, frameHeight); + + // Destination rectangle (screen rectangle where drawing part of texture) + destRec = new(screenWidth / 2, screenHeight / 2, frameWidth * 2, frameHeight * 2); + + // Origin of the texture (rotation/scale point), it's relative to destination rectangle size + origin = new(frameWidth, frameHeight); + + rotation = 0; + } + + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + rotation++; + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + // NOTE: Using DrawTexturePro() we can easily rotate and scale the part of the texture we draw + // sourceRec defines the part of the texture we use for drawing + // destRec defines the rectangle where our texture part will fit (scaling it to fit) + // origin defines the point of the texture used as reference for rotation and scaling + // rotation defines the texture rotation (using origin as rotation point) + DrawTexturePro(scarfy, sourceRec, destRec, origin, rotation, Color.White); + + DrawLine((int)destRec.X, 0, (int)destRec.X, screenHeight, Color.Gray); + DrawLine(0, (int)destRec.Y, screenWidth, (int)destRec.Y, Color.Gray); + + DrawText("(c) Scarfy sprite by Eiden Marsal", screenWidth - 200, screenHeight - 20, 10, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(scarfy); // Texture unloading + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - string title = "raylib [textures] examples - texture source and destination rectangles"; - InitWindow(screenWidth, screenHeight, title); - - // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) - Texture2D scarfy = LoadTexture("resources/scarfy.png"); - - int frameWidth = scarfy.Width / 6; - int frameHeight = scarfy.Height; - - // NOTE: Source rectangle (part of the texture to use for drawing) - Rectangle sourceRec = new(0, 0, frameWidth, frameHeight); - - // NOTE: Destination rectangle (screen rectangle where drawing part of texture) - Rectangle destRec = new(screenWidth / 2, screenHeight / 2, frameWidth * 2, frameHeight * 2); - - // NOTE: Origin of the texture (rotation/scale point), it's relative to destination rectangle size - Vector2 origin = new(frameWidth, frameHeight); - - int rotation = 0; + InitWindow(screenWidth, screenHeight, "raylib [textures] example - srcrec dstrec"); SetTargetFPS(60); //-------------------------------------------------------------------------------------- + var game = new SrcRecDstRec(); + game.Init(); + // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - rotation++; - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - // NOTE: Using DrawTexturePro() we can easily rotate and scale the part of the texture we draw - // sourceRec defines the part of the texture we use for drawing - // destRec defines the rectangle where our texture part will fit (scaling it to fit) - // origin defines the point of the texture used as reference for rotation and scaling - // rotation defines the texture rotation (using origin as rotation point) - DrawTexturePro(scarfy, sourceRec, destRec, origin, rotation, Color.White); - - DrawLine((int)destRec.X, 0, (int)destRec.X, screenHeight, Color.Gray); - DrawLine(0, (int)destRec.Y, screenWidth, (int)destRec.Y, Color.Gray); - - DrawText("(c) Scarfy sprite by Eiden Marsal", screenWidth - 200, screenHeight - 20, 10, Color.Gray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(scarfy); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Textures/TexturedCurve.cs b/Examples/Textures/TexturedCurve.cs index 4ee8def..d2d1118 100644 --- a/Examples/Textures/TexturedCurve.cs +++ b/Examples/Textures/TexturedCurve.cs @@ -1,11 +1,37 @@ +/******************************************************************************************* +* +* raylib [textures] example - textured curve +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 4.5, last time updated with raylib 4.5 +* +* 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) and Ramon Santamaria (@raysan5) +* +********************************************************************************************/ + using System; using System.Numerics; using static Raylib_cs.Raylib; namespace Examples.Textures; -public unsafe class TexturedCurve +public unsafe partial class TexturedCurve : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / Textured Curve"; + + public string Title => "raylib [textures] example - textured curve"; + + public ConfigFlags ConfigFlags => ConfigFlags.VSyncHint | ConfigFlags.Msaa4xHint; + public class CurvePoint { public Vector2 value; @@ -17,26 +43,18 @@ public unsafe class TexturedCurve public static implicit operator Vector2(CurvePoint v) => v.value; } - static Texture2D texRoad; - static bool showCurve = false; - static float curveWidth = 50; - static int curveSegments = 24; - static CurvePoint curveStartPosition; - static CurvePoint curveStartPositionTangent; - static CurvePoint curveEndPosition; - static CurvePoint curveEndPositionTangent; - static CurvePoint curveSelectedPoint; + private Texture2D texRoad; + private bool showCurve; + private float curveWidth; + private int curveSegments; + private CurvePoint curveStartPosition; + private CurvePoint curveStartPositionTangent; + private CurvePoint curveEndPosition; + private CurvePoint curveEndPositionTangent; + private CurvePoint curveSelectedPoint; - public static int Main() + public void Init() { - // Initialization - //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; - - SetConfigFlags(ConfigFlags.VSyncHint | ConfigFlags.Msaa4xHint); - InitWindow(screenWidth, screenHeight, "raylib [textures] examples - textured curve"); - // Load the road texture texRoad = LoadTexture("resources/road.png"); SetTextureFilter(texRoad, TextureFilter.Bilinear); @@ -48,45 +66,42 @@ public unsafe class TexturedCurve curveEndPosition = new Vector2(700, 350); curveEndPositionTangent = new Vector2(600, 100); - SetTargetFPS(60); - //-------------------------------------------------------------------------------------- - - // Main game loop - while (!WindowShouldClose()) - { - // Update - //---------------------------------------------------------------------------------- - UpdateCurve(); - UpdateOptions(); - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - DrawTexturedCurve(); - DrawCurve(); - - DrawText("Drag points to move curve, press SPACE to show/hide base curve", 10, 10, 10, Color.DarkGray); - DrawText($"Curve width: {curveWidth} (Use + and - to adjust)", 10, 30, 10, Color.DarkGray); - DrawText($"Curve segments: {curveSegments} (Use LEFT and RIGHT to adjust)", 10, 50, 10, Color.DarkGray); - - EndDrawing(); - //---------------------------------------------------------------------------------- - } - - // De-Initialization - //-------------------------------------------------------------------------------------- - UnloadTexture(texRoad); - - CloseWindow(); - //-------------------------------------------------------------------------------------- - - return 0; + showCurve = false; + curveWidth = 50; + curveSegments = 24; + curveSelectedPoint = null; } - static void DrawCurve() + public void Update() + { + // Update + //---------------------------------------------------------------------------------- + UpdateCurve(); + UpdateOptions(); + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + DrawTexturedCurve(); + DrawCurve(); + + DrawText("Drag points to move curve, press SPACE to show/hide base curve", 10, 10, 10, Color.DarkGray); + DrawText($"Curve width: {curveWidth} (Use + and - to adjust)", 10, 30, 10, Color.DarkGray); + DrawText($"Curve segments: {curveSegments} (Use LEFT and RIGHT to adjust)", 10, 50, 10, Color.DarkGray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texRoad); + } + + private void DrawCurve() { if (showCurve) { @@ -104,7 +119,7 @@ public unsafe class TexturedCurve DrawLineV(curveStartPosition, curveStartPositionTangent, Color.SkyBlue); DrawLineV(curveStartPositionTangent, curveEndPositionTangent, Fade(Color.LightGray, 0.4f)); DrawLineV(curveEndPosition, curveEndPositionTangent, Color.Purple); - Vector2 mouse = GetMousePosition(); + var mouse = GetMousePosition(); if (CheckCollisionPointCircle(mouse, curveStartPosition, 6)) { @@ -131,11 +146,12 @@ public unsafe class TexturedCurve DrawCircleV(curveEndPositionTangent, 5, Color.DarkGreen); } - static void UpdateCurve() + private void UpdateCurve() { // If the mouse is not down, we are not editing the curve so clear the selection if (!IsMouseButtonDown(MouseButton.Left)) { + curveSelectedPoint = null; return; } @@ -146,7 +162,7 @@ public unsafe class TexturedCurve } // The mouse is down, and nothing was selected, so see if anything was picked - Vector2 mouse = GetMousePosition(); + var mouse = GetMousePosition(); if (CheckCollisionPointCircle(mouse, curveStartPosition, 6)) { @@ -166,28 +182,28 @@ public unsafe class TexturedCurve } } - static void DrawTexturedCurve() + private void DrawTexturedCurve() { - float step = 1.0f / curveSegments; + var step = 1.0f / curveSegments; Vector2 previous = curveStartPosition; - Vector2 previousTangent = Vector2.Zero; + var previousTangent = Vector2.Zero; float previousV = 0; // We can't compute a tangent for the first point, so we need to reuse the tangent from the first segment - bool tangentSet = false; + var tangentSet = false; - Vector2 current = Vector2.Zero; - float t = 0.0f; + var current = Vector2.Zero; + var t = 0.0f; - for (int i = 1; i <= curveSegments; i++) + for (var i = 1; i <= curveSegments; i++) { // Segment the curve t = step * i; - float a = MathF.Pow(1 - t, 3); - float b = 3 * MathF.Pow(1 - t, 2) * t; - float c = 3 * (1 - t) * MathF.Pow(t, 2); - float d = MathF.Pow(t, 3); + var a = MathF.Pow(1 - t, 3); + var b = 3 * MathF.Pow(1 - t, 2) * t; + var c = 3 * (1 - t) * MathF.Pow(t, 2); + var d = MathF.Pow(t, 3); // Compute the endpoint for this segment current.Y = a * curveStartPosition.Y + b * curveStartPositionTangent.Y; @@ -199,10 +215,10 @@ public unsafe class TexturedCurve Vector2 delta = new(current.X - previous.X, current.Y - previous.Y); // The right hand normal to the delta vector - Vector2 normal = Vector2.Normalize(new Vector2(-delta.Y, delta.X)); + var normal = Vector2.Normalize(new Vector2(-delta.Y, delta.X)); - // The v teXture coordinate of the segment (add up the length of all the segments so far) - float v = previousV + delta.Length(); + // The v texture coordinate of the segment (add up the length of all the segments so far) + var v = previousV + delta.Length() / (texRoad.Height * 2); // Make sure the start point has a normal if (!tangentSet) @@ -211,12 +227,12 @@ public unsafe class TexturedCurve tangentSet = true; } - // EXtend out the normals from the previous and current points to get the quad for this segment - Vector2 prevPosNormal = previous + (previousTangent * curveWidth); - Vector2 prevNegNormal = previous + (previousTangent * -curveWidth); + // Extend out the normals from the previous and current points to get the quad for this segment + var prevPosNormal = previous + (previousTangent * curveWidth); + var prevNegNormal = previous + (previousTangent * -curveWidth); - Vector2 currentPosNormal = current + (normal * curveWidth); - Vector2 currentNegNormal = current + (normal * -curveWidth); + var currentPosNormal = current + (normal * curveWidth); + var currentNegNormal = current + (normal * -curveWidth); // Draw the segment as a quad Rlgl.SetTexture(texRoad.Id); @@ -239,21 +255,21 @@ public unsafe class TexturedCurve Rlgl.End(); - // The current step is the start of the neXt step + // The current step is the start of the next step previous = current; previousTangent = normal; previousV = v; } } - static void UpdateOptions() + private void UpdateOptions() { if (IsKeyPressed(KeyboardKey.Space)) { showCurve = !showCurve; } - // Update with + // Update width if (IsKeyPressed(KeyboardKey.Equal)) { curveWidth += 2; @@ -282,4 +298,33 @@ public unsafe class TexturedCurve curveSegments = 2; } } + + public static int Main() + { + // Initialization + //-------------------------------------------------------------------------------------- + SetConfigFlags(ConfigFlags.VSyncHint | ConfigFlags.Msaa4xHint); + InitWindow(screenWidth, screenHeight, "raylib [textures] example - textured curve"); + + SetTargetFPS(60); + //-------------------------------------------------------------------------------------- + + var game = new TexturedCurve(); + 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/ToImage.cs b/Examples/Textures/ToImage.cs index a0dbc32..9f71118 100644 --- a/Examples/Textures/ToImage.cs +++ b/Examples/Textures/ToImage.cs @@ -1,13 +1,17 @@ /******************************************************************************************* * -* raylib [textures] example - Retrieve image data from texture: GetTextureData() +* raylib [textures] example - to image +* +* Example complexity rating: [★☆☆☆] 1/4 * * NOTE: Images are loaded in CPU memory (RAM); textures are loaded in GPU memory (VRAM) * -* This example has been created using raylib 1.3 (www.raylib.com) -* raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) +* Example originally created with raylib 1.3, last time updated with raylib 4.0 * -* Copyright (c) 2015 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) 2015-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ @@ -15,58 +19,77 @@ using static Raylib_cs.Raylib; namespace Examples.Textures; -public class ToImage +public partial class ToImage : IExample { + private const int screenWidth = 800; + private const int screenHeight = 450; + + public string Name => "Textures / Texture to Image"; + + public string Title => "raylib [textures] example - to image"; + + private Texture2D texture; + + public void Init() + { + // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) + + var image = LoadImage("resources/raylib-cs_logo.png"); // Load image data into CPU memory (RAM) + texture = LoadTextureFromImage(image); // Image converted to texture, GPU memory (RAM -> VRAM) + UnloadImage(image); // Unload image data from CPU memory (RAM) + + image = LoadImageFromTexture(texture); // Load image from GPU texture (VRAM -> RAM) + UnloadTexture(texture); // Unload texture from GPU memory (VRAM) + + texture = LoadTextureFromImage(image); // Recreate texture from retrieved image data (RAM -> VRAM) + UnloadImage(image); // Unload retrieved image data from CPU memory (RAM) + } + + public void Update() + { + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + ClearBackground(Color.RayWhite); + + var x = screenWidth / 2 - texture.Width / 2; + var y = screenHeight / 2 - texture.Height / 2; + DrawTexture(texture, x, y, Color.White); + + DrawText("this IS a texture loaded from an image!", 300, 370, 10, Color.Gray); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + public void Unload() + { + UnloadTexture(texture); // Texture unloading + } + public static int Main() { // Initialization //-------------------------------------------------------------------------------------- - const int screenWidth = 800; - const int screenHeight = 450; + InitWindow(screenWidth, screenHeight, "raylib [textures] example - to image"); - InitWindow(screenWidth, screenHeight, "raylib [textures] example - texture to image"); + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //-------------------------------------------------------------------------------------- - // NOTE: Textures MUST be loaded after Window initialization (OpenGL context is required) - - Image image = LoadImage("resources/raylib-cs_logo.png"); - Texture2D texture = LoadTextureFromImage(image); - UnloadImage(image); - - image = LoadImageFromTexture(texture); - UnloadTexture(texture); - - texture = LoadTextureFromImage(image); - UnloadImage(image); - //--------------------------------------------------------------------------------------- + var game = new ToImage(); + game.Init(); // Main game loop - while (!WindowShouldClose()) + while (!WindowShouldClose()) // Detect window close button or ESC key { - // Update - //---------------------------------------------------------------------------------- - // TODO: Update your variables here - //---------------------------------------------------------------------------------- - - // Draw - //---------------------------------------------------------------------------------- - BeginDrawing(); - ClearBackground(Color.RayWhite); - - int x = screenWidth / 2 - texture.Width / 2; - int y = screenHeight / 2 - texture.Height / 2; - DrawTexture(texture, x, y, Color.White); - - DrawText("this IS a texture loaded from an image!", 300, 370, 10, Color.Gray); - - EndDrawing(); - //---------------------------------------------------------------------------------- + game.Update(); } + game.Unload(); + // De-Initialization //-------------------------------------------------------------------------------------- - UnloadTexture(texture); - - CloseWindow(); + CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; diff --git a/Examples/Web/Host.cs b/Examples/Web/Host.cs new file mode 100644 index 0000000..6ddcd6d --- /dev/null +++ b/Examples/Web/Host.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices.JavaScript; + +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; + + private static readonly List _examples = new(ExampleRegistry.BrowserExamples); + + private static IExample _current; + + public static void Main() + { + InitWindow(screenWidth, screenHeight, "raylib-cs web examples"); + SetTargetFPS(60); + + // main.js selects the first example, so its init failures surface through SetExample. + } + + /// Render one frame of the current example; false if it threw and was torn down. + [JSExport] + public static bool UpdateFrame() + { + if (_current == null) + { + return true; + } + + try + { + _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}"); + TryUnload(_current); + _current = null; + return false; + } + } + + /// Switch the active example by name; false on unknown name or failed Init. + [JSExport] + public static bool SetExample(string name) + { + var next = _examples.Find(e => e.Name == name); + if (next == null) + { + 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 + { + next.Init(); + _current = next; + return true; + } + catch (Exception ex) + { + Console.WriteLine($"[Examples.Web] '{next.Name}' failed to initialize: {ex}"); + 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() + { + return string.Join("\n", _examples.ConvertAll(e => e.Name)); + } + + /// + /// Map browser CSS mouse coordinates to the fixed 800x450 framebuffer when the canvas + /// is CSS-scaled (integer/fit/native display modes in main.js). + /// + [JSExport] + public static void SetMouseScaleFromDisplay(float scaleX, float scaleY) + { + 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 new file mode 100644 index 0000000..b6d38df --- /dev/null +++ b/Examples/Web/README.md @@ -0,0 +1,91 @@ +# Examples/Web — raylib-cs in the browser (WebAssembly) + +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 only activates when publishing with +`RuntimeIdentifier=browser-wasm`; normal solution builds don't need the `wasm-tools` workload. + +## Prerequisites + +- .NET 10 SDK +- `dotnet workload install wasm-tools` +- The `Raylib-cs` package matching `$(RaylibCsVersion)` (see `Directory.Build.props`) — from + nuget.org, or built locally into the repo's `./nuget` feed (`dotnet pack Raylib-cs -c Release --output nuget`). + +## Build + +```bash +dotnet publish Examples -f net10.0 -r browser-wasm -c Release +# -> Examples/bin/Release/net10.0/browser-wasm/AppBundle/ +``` + +### Toolchain caveat + +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 + +WebAssembly must be served over HTTP (not `file://`): + +```bash +dotnet serve -d Examples/bin/Release/net10.0/browser-wasm/AppBundle # dotnet tool install -g dotnet-serve +# or: npx http-server Examples/bin/Release/net10.0/browser-wasm/AppBundle +``` + +Open the printed URL and use the **Example** dropdown to switch examples. + +## Canvas scaling modes + +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. +- `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 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; `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 + +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()){ } } + -> Init() { } // loop-spanning locals become fields + Update(){ } // keep BeginDrawing..EndDrawing + Unload(){ } +``` + +Keep the standalone `static Main()` as a thin driver so the example still runs on its own. + +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 new file mode 100644 index 0000000..3609b22 --- /dev/null +++ b/Examples/Web/index.html @@ -0,0 +1,79 @@ + + + + + + raylib-cs — browser examples + + + + + + +

raylib-cs running in the browser (WebAssembly)

+
+ + + + +
+ +
+ +
+
Initializing .NET + raylib…
+ + + + diff --git a/Examples/Web/main.js b/Examples/Web/main.js new file mode 100644 index 0000000..fe5d157 --- /dev/null +++ b/Examples/Web/main.js @@ -0,0 +1,176 @@ +import { dotnet } from './_framework/dotnet.js' +import { + CANVAS_WIDTH, + CANVAS_HEIGHT, + normalizeScaleMode, + computeDisplaySize +} 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'); +const scaleModeSelect = document.getElementById('scaleMode'); + +function readScaleModeFromQuery() { + const params = new URLSearchParams(window.location.search); + return normalizeScaleMode(params.get('scale')); +} + +function writeScaleModeToQuery(mode) { + const params = new URLSearchParams(window.location.search); + params.set('scale', mode); + const query = params.toString(); + const nextUrl = query.length > 0 ? `${window.location.pathname}?${query}` : window.location.pathname; + window.history.replaceState(null, '', nextUrl); +} + +let scaleMode = readScaleModeFromQuery(); +let Host = null; + +function ensureBackingBufferSize() { + if (canvas.width !== CANVAS_WIDTH) { + canvas.width = CANVAS_WIDTH; + } + + if (canvas.height !== CANVAS_HEIGHT) { + canvas.height = CANVAS_HEIGHT; + } +} + +function updateScaleDiagnostics(displayScale, cssWidth, cssHeight, deviceWidth, deviceHeight, dpr) { + status.textContent = + `Running. Example: ${scaleMode} scaling | DPR ${dpr.toFixed(2)} | ` + + `CSS ${cssWidth.toFixed(2)}x${cssHeight.toFixed(2)} | ` + + `Device ${deviceWidth}x${deviceHeight} | Backing ${canvas.width}x${canvas.height} | ` + + `Scale ${displayScale.toFixed(2)}`; +} + +function syncMouseScale() { + if (!Host) { + return; + } + + const cssWidth = canvas.clientWidth; + const cssHeight = canvas.clientHeight; + if (cssWidth <= 0 || cssHeight <= 0) { + return; + } + + // Map CSS/display mouse coords to the fixed backing buffer (canvas.width/height). + const scaleX = canvas.width / cssWidth; + const scaleY = canvas.height / cssHeight; + Host.SetMouseScaleFromDisplay(scaleX, scaleY); +} + +function applyDisplayScale() { + ensureBackingBufferSize(); + + const viewportWidth = viewport.clientWidth || CANVAS_WIDTH; + const viewportHeight = viewport.clientHeight || CANVAS_HEIGHT; + const dprRaw = window.devicePixelRatio ?? 1; + const display = computeDisplaySize(scaleMode, viewportWidth, viewportHeight, dprRaw); + + canvas.style.width = `${display.cssWidth}px`; + canvas.style.height = `${display.cssHeight}px`; + + viewport.style.justifyContent = 'center'; + viewport.style.alignItems = 'center'; + + syncMouseScale(); + + updateScaleDiagnostics( + display.scale, + display.cssWidth, + display.cssHeight, + display.deviceWidth, + display.deviceHeight, + display.dpr + ); +} + +function setScaleMode(mode, updateQuery) { + const nextMode = normalizeScaleMode(mode); + if (nextMode === scaleMode && !updateQuery) { + return; + } + + scaleMode = nextMode; + if (scaleModeSelect) { + scaleModeSelect.value = scaleMode; + } + + if (updateQuery) { + writeScaleModeToQuery(scaleMode); + } + + applyDisplayScale(); +} + +if (scaleModeSelect) { + scaleModeSelect.value = scaleMode; + scaleModeSelect.addEventListener('change', () => { + setScaleMode(scaleModeSelect.value, true); + }); +} + +window.addEventListener('resize', applyDisplayScale); +if (window.visualViewport) { + window.visualViewport.addEventListener('resize', applyDisplayScale); +} + +applyDisplayScale(); + +const { getAssemblyExports, getConfig, runMain } = await dotnet + .withDiagnosticTracing(false) + .create(); + +const config = getConfig(); +const exports = await getAssemblyExports(config.mainAssemblyName); +Host = exports.Examples.Web.Host; + +// raylib's GLFW/emscripten backend renders into this canvas. +dotnet.instance.Module['canvas'] = canvas; + +// Runs Host.Main() -> InitWindow. Must come after the canvas is bound. +await runMain(); +applyDisplayScale(); + +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'); + opt.value = name; + opt.textContent = name; + select.appendChild(opt); +} +select.addEventListener('change', () => selectExample(select.value)); +selectExample(select.value); + +setScaleMode(scaleMode, false); + +// 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/Web/scaleUtils.js b/Examples/Web/scaleUtils.js new file mode 100644 index 0000000..e1e4b4f --- /dev/null +++ b/Examples/Web/scaleUtils.js @@ -0,0 +1,67 @@ +export const CANVAS_WIDTH = 800; +export const CANVAS_HEIGHT = 450; +export const SCALE_MODES = new Set(['integer', 'native', 'fit']); +export const DEFAULT_SCALE_MODE = 'native'; + +export function normalizeScaleMode(mode) { + if (!mode) { + return DEFAULT_SCALE_MODE; + } + + const normalized = String(mode).toLowerCase(); + return SCALE_MODES.has(normalized) ? normalized : DEFAULT_SCALE_MODE; +} + +export function computeDisplaySize(mode, viewportWidth, viewportHeight, dpr = 1) { + const safeDpr = Number.isFinite(dpr) && dpr > 0 ? dpr : 1; + + if (viewportWidth <= 0 || viewportHeight <= 0) { + return { + cssWidth: CANVAS_WIDTH / safeDpr, + cssHeight: CANVAS_HEIGHT / safeDpr, + deviceWidth: CANVAS_WIDTH, + deviceHeight: CANVAS_HEIGHT, + scale: 1, + dpr: safeDpr + }; + } + + // Convert viewport from CSS pixels to device pixels first so scaling is DPI/zoom aware. + const viewportDeviceWidth = Math.max(1, Math.floor(viewportWidth * safeDpr)); + const viewportDeviceHeight = Math.max(1, Math.floor(viewportHeight * safeDpr)); + + let deviceWidth; + let deviceHeight; + let scale; + + if (mode === 'native') { + // 1 source pixel -> 1 physical device pixel. + scale = 1; + deviceWidth = CANVAS_WIDTH; + deviceHeight = CANVAS_HEIGHT; + } else if (mode === 'fit') { + const fitRatio = Math.min(viewportDeviceWidth / CANVAS_WIDTH, viewportDeviceHeight / CANVAS_HEIGHT); + const safeRatio = Number.isFinite(fitRatio) && fitRatio > 0 ? fitRatio : 1; + scale = safeRatio; + deviceWidth = Math.max(1, Math.floor(CANVAS_WIDTH * safeRatio)); + deviceHeight = Math.max(1, Math.floor(CANVAS_HEIGHT * safeRatio)); + } else { + // integer mode + const integerScale = Math.max(1, Math.floor(Math.min( + viewportDeviceWidth / CANVAS_WIDTH, + viewportDeviceHeight / CANVAS_HEIGHT + ))); + scale = integerScale; + deviceWidth = CANVAS_WIDTH * integerScale; + deviceHeight = CANVAS_HEIGHT * integerScale; + } + + return { + cssWidth: deviceWidth / safeDpr, + cssHeight: deviceHeight / safeDpr, + deviceWidth, + deviceHeight, + scale, + dpr: safeDpr + }; +} diff --git a/Examples/Web/scaleUtils.test.js b/Examples/Web/scaleUtils.test.js new file mode 100644 index 0000000..cecb25c --- /dev/null +++ b/Examples/Web/scaleUtils.test.js @@ -0,0 +1,92 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { + CANVAS_WIDTH, + CANVAS_HEIGHT, + normalizeScaleMode, + computeDisplaySize +} from './scaleUtils.js'; + +describe('normalizeScaleMode', () => { + it('returns native for null, undefined, and empty string', () => { + assert.equal(normalizeScaleMode(null), 'native'); + assert.equal(normalizeScaleMode(undefined), 'native'); + assert.equal(normalizeScaleMode(''), 'native'); + }); + + it('normalizes allowlisted values case-insensitively', () => { + assert.equal(normalizeScaleMode('INTEGER'), 'integer'); + assert.equal(normalizeScaleMode('Fit'), 'fit'); + assert.equal(normalizeScaleMode('native'), 'native'); + }); + + it('returns native for unknown values', () => { + assert.equal(normalizeScaleMode('stretch'), 'native'); + assert.equal(normalizeScaleMode('bogus'), 'native'); + }); +}); + +describe('computeDisplaySize', () => { + it('native mode uses 1:1 device pixels at scale 1', () => { + const result = computeDisplaySize('native', 1200, 800, 1); + + assert.equal(result.scale, 1); + assert.equal(result.deviceWidth, CANVAS_WIDTH); + assert.equal(result.deviceHeight, CANVAS_HEIGHT); + assert.equal(result.cssWidth, CANVAS_WIDTH); + assert.equal(result.cssHeight, CANVAS_HEIGHT); + assert.equal(result.dpr, 1); + }); + + it('integer mode scales by floored ratio with minimum scale 1', () => { + const doubled = computeDisplaySize('integer', 1600, 900, 1); + + assert.equal(doubled.scale, 2); + assert.equal(doubled.deviceWidth, CANVAS_WIDTH * 2); + assert.equal(doubled.deviceHeight, CANVAS_HEIGHT * 2); + + const single = computeDisplaySize('integer', 900, 500, 1); + + assert.equal(single.scale, 1); + assert.equal(single.deviceWidth, CANVAS_WIDTH); + assert.equal(single.deviceHeight, CANVAS_HEIGHT); + }); + + it('fit mode scales proportionally and keeps device dimensions at least 1', () => { + const result = computeDisplaySize('fit', 400, 225, 1); + + assert.equal(result.scale, 0.5); + assert.equal(result.deviceWidth, 400); + assert.equal(result.deviceHeight, 225); + }); + + it('fit mode falls back to scale 1 for zero or negative viewport', () => { + const zero = computeDisplaySize('fit', 0, 0, 1); + + assert.equal(zero.scale, 1); + assert.equal(zero.deviceWidth, CANVAS_WIDTH); + assert.equal(zero.deviceHeight, CANVAS_HEIGHT); + + const negative = computeDisplaySize('fit', -100, -50, 1); + + assert.equal(negative.scale, 1); + assert.equal(negative.deviceWidth, CANVAS_WIDTH); + assert.equal(negative.deviceHeight, CANVAS_HEIGHT); + }); + + it('applies dpr to viewport before scaling and converts css size back', () => { + const result = computeDisplaySize('native', 400, 225, 2); + + assert.equal(result.dpr, 2); + assert.equal(result.deviceWidth, CANVAS_WIDTH); + assert.equal(result.deviceHeight, CANVAS_HEIGHT); + assert.equal(result.cssWidth, CANVAS_WIDTH / 2); + assert.equal(result.cssHeight, CANVAS_HEIGHT / 2); + }); + + it('treats invalid dpr as 1', () => { + assert.equal(computeDisplaySize('native', 800, 450, 0).dpr, 1); + assert.equal(computeDisplaySize('native', 800, 450, NaN).dpr, 1); + assert.equal(computeDisplaySize('native', 800, 450, Infinity).dpr, 1); + }); +}); diff --git a/Examples/resources/booth.png b/Examples/resources/booth.png new file mode 100644 index 0000000000000000000000000000000000000000..b4147f77b9818ad3a70ffc66dce16ef3e0f56e8a GIT binary patch literal 63521 zcmeAS@N?(olHy`uVBq!ia0y~yU?^Z<&^g1w#=yX^_-n~p1_lPk;vjb?hIQv;UNSH! zFnGE+hE&XXb9aBYNc2DZf8nPa?tkYnl6N`c`R-`S!_(>->%`T>Om4E?w11#1+|eoA z(Rq+Xw#1ew&uQvUwdIUVinZi%ZVk9abGuX7PJoukc>H?(>6pa}Q3ve*X29qrbDw zm6e=gf0sVb3Y{Rsn5H*7ckix`r3?%V3>y-v_QtIZ&J^@ZOZRV>DV*?(WrihVnqLD* z5)Tfz*x7MoS7B+u@{G?%txG$vl*--NYrNyrugbZ*D(c^+xwx#z-v2IUE!QOF17}P( zoxOMSZg)q=@!RrgMtkSp-5U`ZCuP0%)6U4Ny;Wyb-p;v_`I>=&p+W3R)!wN5C(3iY zPR|QA>|C*~_HJJJqnI^~NDiARtZ=5w{1YCxF)%O~h+7E?Mw_p@CbhqYu={+syuEw# zZuiuuOPlBS-%fk~=AEp~x@&LVygT~%E&u(iQ`hUqTmSjI;$?5uS&hZ7R$Z1fKO6YD zd{)`KE7uAy&o24Lz`$@Kc>VR&rFtfh_UAvzj5pnrx81r>&u?j}UxR`6~}p^u5~5 zd*|NWE5B@)ci-sM*Tbd#*Yx=NX3zJ0zCQ16!R=#Tb0gUq7n0Y!ax<2Ucxp#3cE^Bo3RqvkLyn>mb z;pNw=y;1H91^?GdJN{nvuyps`&f<@5a%rivd^JE;UoJ4RPWDT4zCGs*C^LeDuwn(( zx$NcdB`VC5+Z4~wVPIrn_;mI4*R1&=FXy;AuUIb?xBd21@4jV`%#0xIMUO9c^!zkQ z;`?EO*MkjHO62bBO@45EmJ>t^tZ!| zshMUAK}Iv|{8$)ttY!{N?&m@wumBE%=QSuh7u`62R{Xt{py1H>h!)Z`_@*Q(|*>@$Sq|{2yva;`d|Mb`D=*vcj*Pe~sW_;C(q2bt< zs=c?^wdQWl;P@JP_;uOtqLUX*vK7FVA9zwDH^Xo~`#Hw>cq8<{hI7|?S1Y&wy;JjX zov@(cyPt(uOry=s4t&3!=jL(*RMzB``(J{a^c~(Zr-)tw(aFV7KRL; z_19O|F1Tdb-4+1OQQFgGd^fW}V#Md%^G38SH*VTfl5_XmyL%I__pDlbZR&Yfmpy;?{IB?W=+eadQ|;~V{kzcFQL&}0 zGN5){RJh&0|N8&ylW$vkN*})(8Xuhdd&jQ8`tpih$G@K5Va3o81}@h=DJxmQlf~0X zi!-l5a(~0gpEd@`cKmag?f6+BUP!3RUGtqy`t{EmqjzD^5HVD8LLWFX^7D56d=#s$ zlyv9w;}zN4@^Tv9ufN;V(V@M5{`HyjcD22}v(dbm=}gSp+6U*}#jVlNm#qZ(>fqz4 zPnT}I{pM5Y<5%+e^%g4$u4cf~ty1 z_t@XPc{exWd&O^Ob)`oaZ#|8=to=IZ=Jd0>_!t%#?v4BVQEQTXR9axj;n!uCJWXVRamn+g#>SCu`xy zehL0nN!6L#&L&3H|2!Sf$Mk#u|L_0(_I&#pl?ZBN%)a1q&{2NIL~Z;?YsRk%`T-!-nZAzJmN>{{OYye)~-kuyLqFgCw*vdL4Fnw*I}9-b=8ykhp6PU$0a+ zaAxjHv+DG6eI=!^Y18|z*hcM+$*$_(ZeOCL^e%pX$m(}5Pxl}9Tc2}%!<=u8rE&ZW z1!`4$tsMXdjW&$NCbq9;y z%W|z%>nAe2htxk3*~xp&e|(*OpG{B@9whtPH*J{Dp103x-WiTvbiBI z_urhS$ITAfmG~fRH(O0b(zbDj#%k{$lzm__>99h2p z{-Z}f52xneIqx4|XEWp8?eo9iM(o{SQWW)cPw%Y#4}Z#=<}ZGwR(cCmGJ>kh$6*#- zo-Uw96QYQ}AoBc9RN0-E#?M`Z%P;Lb?|H8GS5Po0;V`E8!5XUXjia7Hym;WG*te9I z$A10ue6mR6(Y?dBZ+>g;pRDxgdVJmKZ--8`@^85Re*0aPWm`AilYX-28#rsACJ+XL za%f{~SNgMO-Q}-@R=&2`QJr4!^y_D<(vSaco$crdxL$Yn(ETFIa!`qy`yNuF_TB-v z8299*R=zy({?z=_yWF#SN!_)c;ZW5|IJowR~1zq{v3YJ+tU89+t<@8{>@&JcctX`QwD|$s~#Vl zB7Z#4M+wwojNSPpW0t7^14F~nN3!+ZYL$=Qt?^k@wfEkGB@F77yt*rddjy0-%qKjK zd++TFsyv__ATaaBB&+w#=XdG)B!;CY%}LLmSPUw|HzbICiv4%&*FVV#ki-`G{h-tQ zGw;6t{s;=l+D#P;nN^ZOW$wFdd5q9vU|^Uq&Clh?wcgcR^J;eed^C5L%PTAXclRVg ztvbKhJ^Raym6Wb*jekDxCdd(fcfP@zQI`(BUv?9e+CUwXxg8zu+0&0-+xvHq_-gTK zeSg-i*c=YBkAeH8a2o z$hfR#8LZ}mx^sakcctXzW551|I=BWN>5mUnR(f>p+4;zCM-SaiTf(ESwCMi(S-e^hN&ly_*EwT8+k3pJhP=jHV4mfe#S!JJ)PI{q^;+q~~(p1G`s ztu|$7kbDX1pGbT?@~pdjZbwJq`ToW0ZtmL1xDT9q*X^l`ORst*wXXQ`XHePWZnn^R zclE|idqf1kZoirOwgA)!-3YBfmR7*4kBOImfB72i;u2$1|L(!_?&s%^g3|I8-u-z$ z@~7kTZZlMvr{q|f7 z@qA??bw~@F0Rl2E9jnUUu<_pN(%hO=V((rp$tu|Y?^Ed2GW2u}{`Nq+kEOt|;Ov zXkQoE+tCp{+dloKICptzkYt>T%l`ZMn>CF$7BKSt{{DZ@*|N9Y0tqfIfA)Pn{^0vs z`%Qa#j~`vzPNA|XF-yHiJ$$SKJ#zIK%#dHt;-iz3~c{e}1HOySS6`cCP zLmTho;_aaI>BU=5led4(w|~FC4%9K*_4~TM?3LPIzjXEH*^AHLSJ&NPQBm{p&%>hc zU!ER+|Eewf^!0H0>EN-Cw%eaT)yw0=RO|lR)8D>%S9{%k@;m1C-xarN)i1C#$Ehs3 zZY^_p#AGy5S;ZHV_Q*0%oT z`hKh`KS0Fw&yQ47B_&W3Ej3=}$yHBq(0=*-_s8%3|1Opl`=x$aEAcEsGH#_t*N^f& zxvMRgZ<~;v5W4?-cT-Y=h*1a9TZ<=4M(aj^k4bED(0-Q8dJa>2XH*YgWM z{@M9nF5I^E=f@kn3Qz3{d|UIoQa?;zr}q1fTjxT)HKv@i;Ouwi zhJRnD-{1G!>o+Lfyqdg5%Gz7{wflN^jjl%z3jGxIo;*!G{P1axBD2iW`|sUd;pNd;ikZ&b@!59MdUv((Ra@|2&5ruNm&(D_ON@kMI4rW0&U?P>CQPy?s+n$(d=_()1x!L^4|pV=N!Nqe{EkoMW)j|x-JkscjdA}+GmqH&b13h(oyy-8?rqPDSkGm5!clR~Wn6LCs@VGm;R9Cj2E(acnvitw-^S}R~YOqdE zKKwPPDm2*ZAHVC-Q(&hw`muHs`5n>aT?scP@xhxNPezB{s0#r8*NFtfumotxhqFZV32zV`Uy za*dO!*Az}n_Re$Tb&m57GPZo}`#`B>#u=V@SN~<-bM{GkcJ9}Mz70rRf z9{=y>hDWZYtyud#=d)Onw(;d}((fv{Hhz@MwBB=RPs@xqD^1jt!FdYO_d;j-rKO)c z?g@&|XHoMN<5ybDRY^NFeb%z+XL^=?*m|h<>tAl4u0X+>wt6V)`w*N+S zTX?95>nBCE>pRT$McHfZ33{cd_T8;c?Dy)(*6Gbpn0hQ?J2u-Sc-Fq{_9zgJx_X6m z-p+Ubm4x^=cwQAK=Hy4qQ($g`h9HpS~1G~t4qJHkGEO@YPWzJa)mB3e_}uxZsM+ovjY45SIthTdirPY-#z69{EOG< zytg;sqksG6UF(W>7R&FJGZ{=(*}ibO?7^IQ>@_me?gYNjX`7#2J>^a2*Q&4XLEc%O zxy1}8wuOR5v4yu=&y@5@%IxgV5-I4>xixSU;kjK{goYVEm@%k0vhC6cm@ zuUwn>Xiw{nQ)jl+&an4aG_zCAZ4aL{T|jznXUHM#v(6p8XC1O0PA%d$+!>sC1z)5g zMWx+^vvbymq=35E&$C-arC+ySR~K;=6cqd}chAo%C;rXf^;s36u7N7DNvqu#`sSTl zHaBel{eDnI()stB#@hY88A?YcFEh-0d-~wX?Ex7F_;25yE26J|I?;Yc$l+Ui4is1D zo>AP>afGR7Mr_CBHI-Kd=G&|;Uzi#_DQC%S8F0j*6PB=eWLu3V9@p|)A9%@OP%NBV zU0(LR@6Vr#--jpediZ=bC=&BPqY!uhzMg)}?>(duaPjG@m#6#PU)k0E6%!N`KR@64 z)31lO0!y#X3cS4Pnr+l>n+kBdK!$I9;N1C_PJR0KIPmwTP5Gy;_Y~c-Vpulmj9JN* z1o=|-4>OM5KjACnzA)XgFX8s$)@v7EUzK-V`*rE&m3)4+zH8d9zxA`rn=EJ}eChR9 zw$el025ouYdXnZ^K0cAY?&3@j)jgMNT5jCnSt1sv`CVXgh5U&xu+K7(NJ;+tL8=uzYtj&;Sn!_4Th1L9x01g*nz`|;FalXg5cx-6XVEN$1>d!Q+icZlwu@B5dh z`_qG<{ewFP|AS}qpq?DlnK_FuoQY(Hh9E3}EyZV9zGnsvk6X^ZaOP%9+Br27~x zCNgm=HM&%;@9Sv0eqf4|&r03p7eSHJCw;w5{k&WJt*h_9KmNYfev(aBUaI_!v<`LO zYKtR_G#;(5|MmH%*(;H#SZTMt<~Hk^@2^?Qd+Pb>{`8OAi?9CT*?AIs!_4!A&5mmI zld~(0C7J6}{2CY>rbS8fEMD1Y`K*>Ea>>4}YbrNyGh1PomvqbbY{I9(nxNe@@(*D&+***Avo+0CzgUmvQ`r37kS zK`Q)rdl#D)v>5H_=&0K-532BQ<(%6%?N**c)~bh7yB2lSAHQujt5_*J;h7Y-!O`OW z#kamZx%qv1`PbinoBt+cNyRb;bA8;gEyP);OX=UQ<@V;6S7>gzv~Y2shd5!`@#O5WHAf@ZE0`bCH4MVLoVp%I{pdveDw!j5J{>d#<@@ z3MV!_JA3Qaf?w~q)a#q>z4zVu{@f)ORSmcHd1r!)hC2JETNZ5zzB)&&9=dWEtYldJ z(8uSQRQ#`?Na5o6&tBNG|MQfxXLCAF&M`G8{sxHtv7{Z4g-^~vqB*A56mo3xrE=_cEq`xkC!1GVCc zjjDTM_>!XRXWE{$djSoeb1QCr+PQf~9^aiOpaK8`C%9Qfo3HEJ!~2QAV9NDhpe$&7 z7c{#C9RmD)UH|H>YxcE24OfH4fB0X8P4kyySpKqBok2aT^vSek>#s|f?7kb7o0Y>BB>iL66t9lG_Z}{oTS6>feL2Kv7%JqJF+F zZt?U_jy|jJcdp$0bJkJSYs+WWts)St!tV$KE2x8LP%Qi&G>4v)`mS3HXncb zzP#-FBSg8I`x-pr0_qqG3WB=EKfZj826ZMPS6}O`j{W-m>EnkldH3hV)Pm?1!t?;$=%N^+gtn%$g9lKD@eR`?YHaLY8b|3xl+1 zPD;(_Fxq1E&~=N=kI>mck)QoeOJQbrSTI8Z6O7>#y$B=<4YB@$qz)i_4u`e=KKq zC$YeTy`w|^|CU=()yFn+u6=m_|7Ww6TlJk9ZVD&xf=1u&Z~y1 zM-P5x#eRJs9v}Yq?uuOx_glXDdiZV4ma@tZpmDVK;rcqVUk(<`cQM%y8hGsOkC}Ap zh3;pgEbqnc3s<&?PYjFp+8ejNY_?Q1SMZw+9VOR}fTp(1JI!Q2rsd`a{JT8u;<8BQ zy%YUUm}Jj`HPB$q3)B$`;zYM!DPs=Wwz2L<==Vh$;5kvj!tBq_Q`W_9VCxIyMa*Pu zn`J!BbVJ)BjYroGmt5P&k)803CBk|C{eE!xPudb@ws2+abn{b_)9;?!(L7^{$u3)e z-5nw8&e`&sTS9^zjhrzNGQ4?!(Bh1(cA$ZZnQxijBaQg}NPYV9bU%Bk`rduNYPRNG z%IsY=I|N*$oY*z>>EDSmpcOuH?q|~EcWmBWo+8EYM76ciJ}Bex8j~uKsFynq*8g{X z`d{ML-4)kmzW*t#f4+N`i1VATRa32xgW>dF7sL^Gi_=Y5Xk3?B*uZ8BKo4Y0r`pH}Ri| zxy*R(Xwd!>EGN!H%viSIa$3$r{v@#4GiPFSeD1kT(B-}?QX*w+EY3Z-R$(hg!7;I) zy*4v9!wo4~`JB`2+V}nM;=n^`yNfTqo_(#PnN8QHI4iYcsfe@BDnnlDvP3Dq^;4oc z?wv?K_xph1Je${b+ovtv_RZB~2j|*2pPz@nm93r|uw0ifN$XVK@5Jir9lXs8kEiZ# ztGACa{_}su^W-0<`PZ&J+W*!=Z^DzM1(C&ExoGhP;VKx;WiNlfqdNE49Z;9e4ue&qk+I`fnNFWdM* z!cj%gvSoS@b8r^7j$HNwSQz*#uSnIKR&^;MG3n!yEmxLb5sR?7c0lumN!qbPCaiOh zL4t9HBskQzXf8E1Sh~%@Sm*1Rp4Ln*tK4HX>T1PvC&wFYXEQwbCx4B6-QB&AdD@`b zYhR>hTvFQ-R+HoTJ z#W_~>%_pa=JqQZLWLU^e%XwjvmZm4BmjA@~d9TgZ2 zoxbcdu3WO_7yH>;!ZUyeSDdo``!?VoG5!e{@R zC6nvq)&JQ1sqa};xp|w|R8SaBH(h=K>$=2*e%Q#2F#-wC z)b1gsfN$iM{hU<@8L+>zzb^C17f`QU{Cxd>A5a~6V^^VHG-!E7XQ}q-_QU>Lx4vgl z;Me5YHPJ~rZRy3u7Z&zOhy0W8`Ej@KKmY&UTm8X28?V2;FD}6J^r6baK2=u&2(}<$)rucdBL_yCuHy#{DyXD|Lgjtq!D2yE9{xE~w>r|CjR2 z57MtMK;uX%W$v~mbpn@HfNFTJ=f1U-UoM=V{HOj&x%1lveIizRt!8Km8^Zm(To|&L zL3%+Rp2ZBG4#*cfHq5jxyS;n!ZgJ2O#=kaMv-fV^UH)O$_W9qFz8?DCe8}b=Xqg~r z|wArigsdpD|J>C4UvhsIjyhtz^n$m6M;%7q}T;#vgManxHcF+ zd#tia63 zZh6XVKl$hT|2?^zG>=C;0hNC~xfAcR%k4I;__W6Q+s`8V8BLGh*Bd%tU0n<+z)FLo z_HNkh7rlzzko)D?TelKk7`s9>*BW$_M0!g2+YuW=(^xNXnmew%#8OnfXhe7j}nynhJVK{D^{+$PkE9}8@V4$A#%*7Yo zq$fpnOpcj5``2v_oXscL@|Z)r@GOs6_Z8fS01sg*DM34Gi?^O;hpsC)ym8Aa`~83S z#DCvj|8KdlAZUHsK}a_I`*e9t%wKyYzIpY(K0Q9MYgORZtGixhpE`auGi1K@>r+3q zZ`<3{U%kIpM_=~MY0$c)H9Gb7>u#KT7uWNvwp2di+s75xdsj0zxTd_A+XC)bMCVt? zLpv51Lul5qu>Prah;{qMx*xeO)z81G|M+c9By&HgF$-GhCJnB-YE|PKl6l(21uwq; zzWDxocP~XQ==|PuuDi)SxA?`t6`$X;j4M(VJ%RbsL0tThq7RisObLXYn2meIsc|M7bbs%+M7pa1<){{KJz>PnA53*cbO-_-S;LF)ro zXxD>AGAC}mdcS{_^w(Vv_lI8lJOBUR{cOk2`hVRy_36^&@2RQwYjx`XfA`z{o+Ux6 zH`h5>x@>vLr4QCCi$R^}_rEvJ^1F0P>c+K6^S!LwKr;jC@~^f|HQjL5&s%jvVroX| zq3kQOw{CUx`hD=qEo0r^umnm$KZ7#|yh8iy`sj>9FHZY+cKm&?%Krba)A`Mi`I^sP z%d-=B|JLn)t5)OQklb@S<=gMS&8CK|OlM*qGcGG|y^wh;K%^Bmt~@FGN&n6V#TE85 zZru`BJdm>f1}kcGKzIuhL2Xo(O7&Q^05WH+G%U+~JT%r&)w-x`&Bqkacn zt!U7U5iFTy1{ztbecgZU`NAuwq;8~jO!oPGa`VUQFP3ik-i14i6`#H=l zEk1VLEvm1$6U7M$6;$$o3}o=u`lS(ctdU`I#pZ2h8PzZ5hOjHnbX&Z5;*x8H>bJc zrB{|lWxe%`>yn+8G<(w2tn{U~);(ErCs1>{uH?${pjU6RWBFvKI7>%Q_J024(z#<# zuNYcx-D%3JroYk7_iz0F zFV7=QQ`KUUY>%yg&IZ3t`BpE;%PvtUk#u`{gul_>hXV6$ynnokM2#5;Z$T+>V-%8Q z3ZUNcMBf*%Z9#K)yw#1jeuFk7HzO3(ZaRK#t&Y6xv{lfN&j+>2KFZC=~xo_6+v zHF%Lq=gTs6dBtTPFP`?wExGr>BshA(vWuq#)}*>`6MbaEa=mMcmEO{AbKH`%9kw10 zy7~RgVaDFz%!%J`?YO`gc70EbbIc0{StE1p4qi(ce7Sk z@XD&mzl1iJ7W6#)5TtrQ@L22h_un61kFWFAx^avt=X2MR%4+5B6y0|L6UG(}=wVC0E)&?dKom+NXcp^M6mj+5YtNVf*QRtC$t!uNrpGk~z85 z>g*M_36{Lh@4;)dUM;`oBI##XplQDe|Hu_S!xCImtSXkHyb9 zc+I{rGpFCYFwlj^3~L5|Z~$f1_5Y$FJs>9g^T0=;8ui-12Jk zF~6l#?|<{EWw+1Eef|BnuX(CcFRKNAIQ!S%e?bfSCq*$x$1<<}zg=_s+Jh=lKTRGj zG4Q{C_J&vgg@I*H;#I{XXb+{Xkax){XjeN`Cpz zE44oC*!N2oHI5)WhtTxb$F5E+KX(`Z4z0F?vvU}X%Tt7RZQd;|D5!nf+kM;Kr|&_t zzui~4z3=y+j0DAnH^+Ccz^tf{@2Up#=PII zt`493&~!siu$H3V>WvJm+l~v>e0_CwW_eQStd+T(ev3c)W@vfWc;$AUNRW?+eHHw2 zM+kq$j0-loUW^a66iMIRHP!snO8Ku#@7XahFeGqk&e{0!>>JIc0&)yXmb|Q8+&?+( zMB~{x0xBCGfYyEWetoZR<#{$n&S%cC6`+m`q<+61-#gb!d;i<1kNNi9fA0>R*WbC# zTgRXOeP~*EJ(a1K)mZ;}<@DH`8 zb6f*W8g2a!+SukE_x;=FxHWMRRzLoKgsmU$gDsidwI`$=v{F*J^l9;7aGvduh%Ars z{aO6;$Bvg%>+7#P24z^tvQ_q{Hq-wK2yU!O)C`-pUw+!gm$m8_SdtaNd2;*8+jDDr zx{mQDtx6U%wXE_kou%1xKO~el|H}E}nI*hFYaa(mc)s1SK`l}^TJoq>(*Do))~w~N zyK(Q`pD&mF)1}_54qwl=EHu~s=kCpOzC~v199Z>mBTM|=s?uk3Bj%=MMK@i}s1crZ zN@#viv|Fy5y6uF_9JW>7wOX>_+caCh`BaPSy6`MBn2TZg)JHRCJqBg(*2ViCFCi4= zI$b;Oh|j%t?AOJ7FZ;@A@5FN_=SD=g9q#VXK%P`xbzY`5tcMFchU{+t>*IC%zdK*5 z&0Vpvc;Vr2m8Z)zx}IFy*TKe|^LW*r$G0Zk{%(If^XcV|$%kxwkI(yJ{#T?svOb^dYLTro(aJnysQ#Sc;uZ#S5IYn|*u7E&Twz*Za|_*Uw9rEnoN0%6S85R&sV0XsFoy8^0ZVK%(>Z zFUTo`E^#86F@O(ur|y3)cv_nx15hkW(?!)SN-wo>T2WnM_!o(-xiwV_IUP% zkZC_cZWY~0E1ASR^H`d~QN@VUW?S#hyDZbHc5z~^XR2<`jDNKX3}?Ku(zE<7os#|; z!Y6agb8jsJ1H%E4AdBS3vu_4_DcUf2cwYYPCa->W0`qK3MUM!P;+^_dYkR*gj^9%I zN$2kSOtsq^wt;#~fyhgCL*`p@t(6dk_G`a9USI#K__D86VDfXP_Vpg8d;>*JuB{d1 zWxH`Kbm#G{s@vb!_gy}dr_m0*Kb|eS^)EXoVr^# ze(B%kwd2?49gEK{(wKDTzpKd}wlM28y|oWjF3XEP+S|U}db?TuyCpl27s(K46)x(z0yfy8CiPZg;Wt)yHzcMXw_C-(r6sg&bBJByfx2HH&FSF%} z&G@`UleOWp*GHeTg;ABt^A82yFAT~DZG$mr6*+g~N7}b3K^*)HL6^SlyLjBw_{o7Z zKMtXY0_oR#4<7sVB%E_=DyUjb4#|MFbKYpXFPs^DqdVHoGH>$CS5Y@^oP1-p?v(91 z-JP}ni|2&d^;&42xm@zbv;Omr{ePd_{IR+TwD(Sj-0>}SMg zSKZpTf90>RufJIz-TC?|>ThId?A_SAQ`a>v^NDiGITh95nYMW5|Mbm~@1+fQ`sM06 zN;l7C2nvpLvAMyXHK!>wyPZw6^~w|`h6~g3H%D7rPPl&W_2oEFb;-bRp`p##tmjOg zhjRr3Lq<~d{S!}@@Lk-<$>PW)v17OS^3S`?>-W5LnXOeqoV*gny>{*yGt&TP@!LIruLMlnHYcW6gM0JYHr?v8i>}vwQTe@1Z|zq9 zBS{S}mt8)(-2Sy*;_cKIv#v_bxRan~7B}_l^6ZC;)hAE9b_;9ah=?_Z=@UCnygC}Z zb>6()pFYW+){M+6d4H|<=Sg$@S32tN&&LP}3QjBDFB80>s*l0;`l_@0ivLxteJZ#A zSH!M6&-1N|zFhcsK6L8SS8MdMWnB-=p0+qLQp2l!7~cITYCHz_8a zqgYMyTDtz`o%@VtExR|ZiACKCZGT3n7yu5R=d7(t?GxiWxUH*>f}ab+nM+s%sRDXsf}-emhYc+z0G_E zk)LaL3okHs{g8k3fLY|DwF}dq+3OXB5}NuIg%Y~*6@?Pm<~ykzklEMb(ZE{6F_FPL zgMT7}_5~Rw#;_%(E=+42lRH=VD>QgB>9Mz!r?v$>Uw?_FZtCvSo?&f4V zcYJ3n>Mr%VR%g(qw6pgAtW(SGS1p|M{mITXjjq3w=JP2d;twn9g`tpOJJVZ9=}^(T>B;vv&I>MhuC;aV{Czdklim4FE!V&A z>hk2_v-XJ{C+2=WZan?>pIrAoIjapn?pOSImHez|{{FWe9XittU1q{O;S?FPGoFDcdJ|bN17NIl&YE)Fyn&-!$VX z14Et1xyY2_NBvK%ww_hE%ohA!!*K4qvtq_GL+0xqyY(de<%csS7WVmhea9Xbu0MD# zZFAtYmgr~cwd;20`=#l7pI5b*eYVK_(fj>+=UrXue&%L}eA#UE`pPw-3g^phmqVtR ztud89$-8Xn%~eK|B}KTGiZL-P{j0@djY%3^Prm)l z)|y?n+SFj@!I(GPtJm1eK6?B6x?2>=LdgdEclYL;+stGtIs z-Jn|ej3R^6v*6MFZQ2tn=UH{fU-<>xZo6zY`BsV5Tz|E%YIC-HW83sh_t*JW?$4k+ z_4wnC>j~+bZzjd2epcUUH20lt*iyd^Kc+RiF4p}ot-OEi-_z}D1FxOWnR2vE&Gdqn zukVUwn<^H*T+%&1e(s4szxT{H*9^XWy-~S^o#BAoeJeBe)Bl%9NA>UBl%Wr*V;LAS z91crLyQP^gQM6%TFxj{_|4C6$`->YaO$v<*If~-1xK+Kn?(Q$bUn zU#_bP3hvyqX-!`BMQ&Ze;~S3%o&LHxJVsn~^48*oY9{j!886b9w7q?Mthn;ztqieu zrbdT|-9J|Utgy&*k1}UJyW_0a9~L_PG0A>K?^m@e1s`AL7nSFUxMqE?_Y0{! z;dtM4!>QNz_gcR&1WL$G+Ip#Ud}HF?&LX?opZk79R>{x!6Dzn`hT#BzsM^05*?V7pZ9jk(&GSwrzU56h8~tTI zxA-&UtbKIXtTXdd4wvlu9?)*3XvMkVl-uvW*PqHe{RKtyz4O0yx^;cC7KE10zHolM z- zQWpx2wN1Rqo5{h?z~GznUV7)8D~eWTjS3#_HFLh{omjS}bIpUA8MafHe=?Ql`#pc1 zExjsRh4J6o727iTF21@@byj)br*E}|GPm`-bgwa)wDT>LbJ}5aVs`YVC}w#poya+F zm`@+d5iYu!ZC3gAa{TlsKVMDz%pWKA+IPjnMQYRT&Yc~~y;$Sb?ufdTpC&Z#%U;ly zhP*O8V_A_(SW(Zi^^1?@1a2>^+3S8#wC!58-&6J3m^(`o&U?6|e0{Vsir*o&@@L=E zDLUr6%3mM*^JKPo>9on;)Ac$#I8N`3@7q1u#=T+Q>bu|Setn9S%d3;Y8vaecRs2tFee)~Osx_0}o)A`X`@3mjkyg2*H0Y^WEl`C~TOAiZ6zF?W4 zRPDQC+9#9Hnyv-Tx9r}wT`OW_NSK{3E1f-8V(sy`$Su<$voIzL9`^7mZ&89vBFmM|dRAtRc_-8^eHPm`)ATHlQeOP3A{jT2Z%Zb>&X(@Yo_toUjiLBR;T{`cIzi*AHiQ8^&NL@ek`|mwz zCXh<_R@mVx;jA4$zD&P6ced!FXCLOQ07qv62g+8g7n4tYlzW{m{n?^V{Qhs7yv_M> zJ!j7C`pL5U3hRnWiQR5lvzMh9dkKRMOGB-^i+bMO-F5J+id=AcxzH;`2L^@)tyMmM zE?%#?{F=YWpDGGxq=A6=QiNzV7y_uXbDfI`1uE<11qF@J;nxB|XohLD(VAR=xQ1 z`WNqgidW_CkGbv`t-a}M=Ksg%t9G;IZWUl)2(Nj4BjWXu4BKy~7VWJ8yLSO2Z+ecz z%)1lXJ}@v`IPz`Z#M7RfX|r}Oo1DAz#f=qrCTvSR>n{Or)UGX@u+44>i^fl3pE&z! zR~Hw(^*XEX`Ze40>CBS-^73`MS@)$A514g6fBUfQwUUTbO+%=a&aCCFOT}O9bi8+G zQdkK0? znG14m!!J->*}abaSHgyL$x8=KW!WF!da$1TS0BT}^#b1-85kH|T>5-tGl9&fsuh>6;iH|FIts*ICHjVuGQRg>XWPZ=WPX7dPb93`;R}4Yo2>H zt-EyU+4)aSmF)Vi(|}YCo?7_t)9tXe-xrn|ol-L`m>}$srR5!T_k#=5Y=%#-EN8~s zs(=1+JrW#!GCiw&Gv+Qc>;1sMaN}|G@n;Lxie?gsxO(sN7r(sl{41EGXJz)vYzcRy zaE@!B%J%p1*;{JQC_MV6s%`#*p*zjpIQT(qTx8hpxfXmnAHIAwzV51#nk)Zeu}0UE zPqllu`nO0$WYy+6f0Y#9YQFtO$QJpYMIJ6o3fq_EUwhAgMQmQ6+11xyH;0FmrLjda ze?B#-+@kQ;>q#9i-t=1CWS@3-<&|d*JKmVS-uDgE+?gIe&0XrSaA86Hf`^aRxZb?G zvwxnC+eu>s-*d-U)iYK&`~bZy&a;S)Y66blV@^nYS3W_Ay;{d+KK7oBr#W zP~X+MCEM5*NnM@);>o`LThBGE3}Dd=r6rm!zB(E_lQ}&+Zl82#$CJNy{HJfm>X{z^ z7r-Y#eWx}3Pj&Q_T1U1%)a)$H6z2XkeNRgum8I2x;)eC&C_qj#7kbET(A^c2!s`> zZ=T-yCeOxL_HKpEJJTCCbGAi<-~8jb&S!b`_DR<{1K)lrMlJ(Pd|RhqS;7`If3MGB zw#y;Ymh5`i8NqYG?fBDImNUg`=cmjH&i(y#SIB%X?QE&b`~N@o*ZgDGe_nCbUG`gx zw#J06T-Ru(;{17|8lRnm;C^S71N(nUDKV=3kWgY=_CrjGG3|q<3)7j0-W@Cv2igTV zH?({c;M~A;R#7OyHOg7#fRss_M}ui5_e2Ki3nEI4W=m9Em~tGQJ6LW^dM&`YfmzSl zCZjl2YU%QCor^qNT=MSk56iA_y*49Qn$1>iSw`G4jjpWrj2Vtc&U%N*`ernK-P-GB zdg>Yb6j0gzW*5g)fd$XmWD__hd+}L0O#CD;`yG4k#=80i)8sOu;`&Xxlq~<2db*UH zTO)hrtZwP4N&7Y)z5eXVPu_XkuFcT(hS4UhSh>tAr-`n@*H^Vaeq4apkv8o(-I$3xyw> z%3Eu>ve|D4KXQJSd4{6Q0cM7_#=oIQf@K$c3%n?`!j>;4{`QRDMeP&3 zv$7K|&(>bZ!+sZ<%3u^UwKb;^NNvl%-Qx&xZkV@v3;&-lPk-;bpZou7?6f+o9Y5?} zK7@7Y>MQP^hcxVG+E@R1^*9OC&UbMs`fv8r>gCfj=P$?4zJ59?KJMnF*J6S{ukZhF zEhJc3@bJi^zp1(HZ}*pNzj?`@{ppQ~%{zZ@da}*7?8))ne=k|4{N;sp^`A_NS-wp& zp?6j1E_wTi4|2Z}FWyyQ9PgaVn<{tfp+%`BSxz%;nRflZ$ zPifAXHxg!@niOJgGtnXI!T;~`{kzv+pZEWN_w{{Me$VRv%=|CsJ97(bWc${`ztp5( zPQSm+&DC+%eRXGVEw1&xJ1<W%sf{FE0T}$<^ zw9odA36@R>tFYs5eBXVGA^-khd`HJ=xq`xSBSQ&HCYUI$Z_7dvZxUqD70ZcJ?M-uC@q zt8n-I{QqBTH$J%J&n|8s{p;m}NB`Sr%eyHlm7kNJ-`!CIZm+(r^V_t~?7w~8SHt3+ zi=Mi={QLU;zuq+M=tR& zTML8j^P_bu^;3r2vTkO#UE_+_Z-1d+qvFse2@F2hWZRgav>|ax7w2HUBiHfXf z<5%Q+R}^&hLyh4M250AndBOLOm9O;s{POTW{}!niw)%Fdu^cO(&rHdUSy8r+HN7J8 z#j;OX3)esF+`!+`4{FmnTQ6Y$^|7Wj`(I6I_pM2XyK5g@zJ9*Ry8Wi;RmOGebI(jJ z=M;R@A5u2ock<6~AGRGS{B!ty?YnQ~zNNfpxcGms{IAmf#i8loqaBdog%!693$A2* zeYEn-$7$O7_j_GjKGn@{fBHsmy7YtJ#-NcOP*>~P@uF$dgKP6yP6;k}E`~_Wch&os z+>ZNPoeUWaIaqtYyW`L6`~PLZ-LD!wrB6R^_jPoD>V3QV+Y=YJm)RRX%l`cTak%_< z-SiaQ$u)PM{Q7il>e5%g&b(d<9#^s1$$RFvf8OLJ?X%^*zBj()aoJSt@lTH7LH`q; zG_&-4&vnL2UY4)9_ZHm3+?xt%VU{ac&)n;iQvP`6$)@AbQM9AGww#-9GWXrx#u#;% zt(VXL{r9@)X+ZtY+|?gT9!4>VWQH-XJs-C$u{l!<+{D!JKRD~pe*XV|jyL|a|6iDJ z|8=~rI+mpDb@}J<`A2ti=5F1fr_VKiU8s`M|IhoaNlyg&e#8Yd|=P}zw_VAg4fSG>|iTCaO?Qt%V$IXZCihtZTTgsOGUeKH<(`dP%G7Z z@cIGJkoniGyrNrIb{=H;@#194lj_{lLU*LKf=ntsL|m1Wihlpw8g~0g+3{nq7O0)Q ze(RCNG>g8azQwN|?d4s?)-}aH@w%(;mPl^)&HQUtbxU4u%5mDtvDVml2ZOWl8TE~w zi~c_Rx;Y#&s`o0_@OO+VXUXaB$i3pxLvczRg;=mFe1h zev_CSkG18`wo+Hae@X1TX8x7+P4LaFj)H#``92+-9NP4m zb^2r*^BoM%t7?^)c{cF9{cS(7kyRq87mY{KL$@zC%*H_KwHf*R9C6o3}pi?EHOo+kVVb%eTKOom`un zJg;=Sfa1HrN|}sVXO@55yvbhHhELgMxzT~s+NYJ@ma}FqtW*AJ;m4{88tqVw<3b%x za+nTo+aCTJhQDpQyePwVm9oS6tM|Z-+Yd;M+py^WKdv5+gAN|w`n@MCQg5$M(N1=8 zzbjhHZ|Z>#5MijDWIW^MoV}_$&rMi%e`D3=B`78FfmgUoV&-hLk~r$U^Xo6gzF7;t z-aKuRV*u&hc0ARLnEe0yLs((_ruy&d=PJdu%Pg;Mi(hv-i`V4;D!t%qCSjRY|9@<* zFkiSkG%W)pFQ37Vbg`uP|lCPZfYxS zvQBq>&-yChgKx%*J-@bW4Gk0Co#A!%!%?&Kf$diveQ&MbdbCjIb+&X|Y0jEhewjn2 zW}oG9uQ`2d)XdFZ*m0Nt^#UfIhiAN_3i4O2$#?f4{D@$9;;JOl%2!>-0!OR1+n;cxgx!gKXy}>{knX0 zxz)nVb*;^78tty{*y6|*(f;g~y3qkhQohZ8jn}^#l!_Y^+tiH?sBPhjKO`*a!Mi5t zH*y;0V62jzcf#RYqf%X5<^hHk|3yG$^ka*-8vnC5{l6abXipb-B)4_$j6EMV%!f2O zjLNwb=ig!o?0v2!MKd!B+nVhs0>WuYe62g^ZYof7Mw8(vl390)pMnqig4xy9MV z@Db;|WuP&uo6(TwX!-xelW(4ODKnopQ>Vg4?z_~R9JA;(-mm|7UVeG+Z-1#%i1pt) z@AvD4^L?Fue_PgG{ep}y8f4wP+`Rx8xr>B_qhR!+%tKc2Iuj?^1XzeS_;ky+z5ivZu!?fewruD`l z;evm5Tv)_@blv*gisA$F)PFGiUh=T#>;mSV#}U`An?3W*TGaNzg-vdO$7l7&*Y|F_ zIsG8J#wO`U&`z!B)>V8=Ut&HmhS`Qx{Q5O>kL{kjRkho`_lB{X?%Z`P?RCZZqt(Ux zgtskybwn{x7aiW7)eZ1C{qyhyo=t37THeuqRSQ$gwYIKYnYvywN;~22KZnCd+pbiV z=ShcUClodByiu|4I{%*r|HP(#VVNR(ttBpO*gST;x%C28IupU)) zeXaM&Su=RfZkN+wTJY_en8uQ)e-b^|BAHF6F4a}vdtsCH)LU1sY`V^~R&+yM{ep){ z&RM8Eqxa4N`HQ^w@zw>!TFmb9+nmWaF*tLcvct;qi{a8tUt}1o`WTL$Kkaes0W+w> zyq`%qzs%igXm~3`_QdL+5&R-QZ^6ytZ zV!Nt(VdTR^`>l*|aglOQV>Q253Ituh!WPjcP`;Ay!t&Er>nb)k)<}Qi164#YykI7D z0>ACW+C4SLi@c^u-`!oBT=(g0x7F#B;3jL!(>waht70@jja4r2bYHsOmx9lSJzSog z`}uh4tvBZ(BLbg(Ts?jiGR1r1qVv=F3!fC7yl(tIJa*lxZKrK_6g+Bw+9!LmevvnH zX1}r|7d*4?arr%q?crj{+|Ap4AcNEEv|-~?kssjWQq!;foV@Jv-rLV#wX7)nwex;` z>^hlO`ulUQR>?;#pPwCs*7{_7k@asMQkCufb>)^#%Qk{rn@@Lh&UX&k&A-Jpd*`a4 zSms=hwr9z{GBpiKb?qJv^`A{$n0!7Mx-j{C)OBGx^B}l`Wyay@0-W%Hf4)aPKH``8 zGHjEDYk1F88@#zY`*P(~PX*U6*N!}3))6{hxK@+bq)novAyn>k%=d$@ZaMK-9ngZz z@n!2S);w~MJwr`&6@Eu(FMK@l#x0h3=GF7mKCgWy*}S4rk=yFPrd!v)tW)uA;w#`_lU?ww z`HL2tbOOi2PM!phR{zJ*O`*>N6`YN4sF{dl%@z)Rd{6G)zlFQ~jd!@$@}98@`wU6= z6_u%#Azv8eobD_&b6EVsN>)*ef5R!$oeSGnSUCvR>#f=K=32{qMK?_np4vzuRB6a>vg^6{Vu*e?EHUetVv8 zt)%p6ef|IP`=D%b;-WLV*#G6z4M8Jk$q$R3eVx4i%zbyaG!(7JcJ22CtG_Xmr83?++{@A6mG%jaob;+q zJ;~5PTvl9gA|l)Hxm{J;;uk#It%8B!&BwLL$!}MFg-?c;-q~w8xBW-Lo(JXEA{VxP zsoDDAx!76yS0CHXe}0+Q>VVZFi*H4fbhjSn+8y@Qj5pSNCWaL`N$AlA3pd zfm!6H{G#&@6&adewk~3hIw#+jGQl(;IPz9ek=n91eoM_vx4toby=NQ$i3p15!apjuSflM;RtYX@vDzLCA)I1rHx>l=(DIRC)D#!2ewIRfO3HQ z%d{)gtJOoy`D=|9pP$4$@fE=mxDGn7Fw8TfN*RPCC5cF&2jBZ@dhT;`I|p zo?`A?4{fe5?!(e-T7B-1rMS4rtD-@CIPjxIZWJ+(Ao z^6Ko|ZGLliR^&IYXmktoeY1&kYb5u}BVP`r30tj@nlWWfuVU<6hJ?wtW=AiWFzs`@ zqGSs1nxJW46x*-$^-Vt90c$v|`E)Dtcz0B~*Je=jX_4hs2L2Z{H|e!q%ofPu{qplqnrySpg#JYydiTHYekOVF+5siS zx7_TK2_8)uZl9jKW#1Ynm6cRrYOv%3+rxKfU)E^Y23-ZYE_dEbAuat`!oo2!$Aw%k zE)w@Fei7K<8v9?7edRvk&o!m14QwVpoKt#ok)6ZDy`QK3nD~o9f4xq>-g=$>OOHc0 zc-J-@d;9x3hhg({>-zY-eaJJt`=mM{t860f;ag>MugA;TjOj~E+U?&jPk%pp{dWD^ zy1id6JKp^FdbiHsk^uN}jGB$06_iR!MPDx5OMmov``p_5_H|ziH^%%bcRf8_H+`Re zRQ$#J%dXFjTbC32vdC^nLDIZCd7j7n{``n&+VvhCp( z-@V3j{hm+u-22;R&Y3v1O}jsyNjdl!zN#wfx&JJ`?|C;?3NO0#@_F6=(w#*s_W#)y z`non_7MK1?Pu?wd)ic|8mqFHLY?N5g-FNzGvC;l2KcnmEklDdCwrejOUH-S<{&czE ztt>@`1rycImp(2E^5T%49XEq_aqVN!QY&A!LLQM%&5cd(kEDEk((EPLeFWNy*)GILri=h*Dkhi zm3zK_e)jA{X@ycFdxTn-Qe}0;?#t(T-lmmaVEksNyZUX^%RQVMH1p?Y$Su2f^vH>{ zA4#{buRL{YhQwOupSzwc3YU7)rZWk=45uWjX!h2E3%?x>fB*YH!En7o$bwbrA6_yU z>A2v-stfcY^PPCbr8k|ryVJE@Op4!n z&9wth*orUq?24kWE)m)wUhWVl;)1TSL{zi+k%y{rv<@B|4M>4`i;;v7hz00M6 zb=RJt`{G+!ixq1xcRzd3P+%K>cb0wp-A{|cz4S#G0*)>J8zuXO*;f2ZVV~uGN!N=> zZ(Cxc_$c{d3w#~yA_Kq^?QGD`RtHcpPuo8ZL9rzrhD}hv&xNvXQqDI^RB+bbhXyk z{5vLdpGE4m?3f=ucg6(11G^txU0zn3n;a+dNFaM*xM@Jj`-derp2jxh^XI4Y9gF*=_P8Kb$-kx{He_v%_x@9GJ8fe3u-%s~QUupTvaiO?1 z_dR$5CW$Q?I6`_O|92K7zDtstgR;u7`ZQ#f;k%CoM%6PUT ze`x(axa;@+3k62e6V=LP_%;8gnz zIfdwQBOT#+uTKXy&)%z}*`+jb{n=@yvU3(L5~w}BsEXwxuPmW(AO~Ad^bWP9X+Jwj%xlVf3XU*2H-58z) zTF83E1#K+6mgA9c$;s{WR;)U-<5_ftlFW(slTEj-E^C$x3Y_}vB5A4gI#r?48 zb9MQixlygjPQ2>-vy!|f?ofmIN46~Tc2>OV^sRB8bV|d*D|Qndn6Fq=t)#g*{4kgGOKgbq5H9WtrHdp!knNtDH zU()0wzSW#pl(kZ+6n^mb_jNZ=ujr@6k8S?7Uuo?q$DdCWv*Q^<0K09#$;;kQX5QO&PqPYf`Zoc+}gE_p^EF(N_f}xzc{O-{@X`Sz80-;$Rl@8iI!l;IO{D!q1617#T(*o<|r3xu19O6X9pu2LU-QQysHTTRPS)|fAB>-Mj|d3UE>Sqh{i->6@*_n#fhN3~rc zCKoq#?29^8n817Mkg((j(Vteo_AuNLV@&&y*%!y;171era!>Bwz3+R&SY{kHnRz~Z z`xV*W;1aRT`{~cUdQ0CQx?1J8dF6t8K2g6eO|U#J8Pw6dYmUgJ%bGn2%sdYn*+N+= zjefs7oB1&G>jai)=Fcr+Zyqr5@G@5E&3IQbi~l_LTc?10wY8^r=RNdz;lKRDJK@Y@ zD`q&lm(J6dRm!(EER=a~t-`T$n&?M8@YGK5?+1~>S+mYBUqAopzq1!+1-;K&&=Ie^ z{P(lB%T2TAn{T-#-g1Fo7^zz@PwMXO(&p7S}rG1!ZJMeQ{0@GrJ|@l4YAzuJZb;eBe}AKX3A# z(wWafZrAP9%RcW_{1LR`@KbEdwy22duMUOZE#rE*bq`I$*_3zO=?O4y4om~J?^c$?4iW#?;?Cr2N8e{asY&8PBp9c~|wiKf|4aP-|a^LqxH;O&U?57JwGrm6*ho+9_fe?%KquUw@mKFWjZ(*!*>CZ)Me!S=X{Vc;4tP-0LQk(3esm zl`ZGhVp0X3qh(-lxG{b5nY-Kb`SZIyW~eW1mJj>!ubxdIGh(; z@O)3U-}ccj8qgpRNreQ71}+*P^tg5LkbvfDv|0LZ^<{GVf6dq#Blo%L|F7@$^$~inzg~W|PJi#6W^Q$_>DFZ_?;B5S z`#zU}p%zxapA&fIW3;9(<=VD~CTEwJ&3$)wit)@4`NX{3dmU>kyHVQYCyQ>(S+hgm zPwk%vcymkLjSl#tJuO~E$o{iyvoEZg|6?6q46D_>rUSodVI#i@T~A2v@{wKg^T-hHzA=UJc3m$Snrmn%NMyGN=- zI4ac0MEY#zFGrr2(MRvg-TU{@Pzy8w9?4(8v5oCoZP={Uvt}%P(b)wOJo#N9g;%HQ zipRzGIaiKo^*ipY`_>g>U+ua`Lu;<1$qcE8uRjd}MLKOx{@>d+YsR}hB73(dR4pu( z`%wJ%ji{jJyQg0x?`g@-Sk<($G4}OlwRgAq#ka79vh3v7`TXVUW-osG?RTwnm1Z!= z+s^FyRdJ#Jn?N(?2LD;kJbDNA$!%Y1dgK9<$V#y!nd4iRKe{N9G=Z^eN_#|E!yNbM zj%J&ljzuf3`lLzmed|r&d(!>o>xW|A>5JF|6XwV4U!U;3aodi2>Oss)XJ=N$?A?Em zE#m8w^2q|Z`|M|3-~+8o+hqH03WK-F%=RX!6+ZVWM0nYx6XL`5bEI<`d^6UxHuGQj z9+jbc;ESy1tRFH-7tFVdOjT`>j&_leJ<+{mr@wyo+>>GpbL5wATowiDBFz7{=tIw* z*Db#5<-eWHQdipv+RJnL+W!|PQ%ZjS+dA>uhD{Os7p&GiVgnle)!FD0xMhd`!!OsD z-aCEmh{F04;e~(Sh*oOf7IK|<>jNxhH)NN-Kk)V90=K2D5*Zt=Ghe@M_RRlrdiQ!) zmnW-F3w>3-!7y3O|9|%^`%7M{7u@R!G}*Q0?ca~Lc(UX*g8u0E$+Jo>IQDUNEvJz~ zuld3+swWuQRc*3B;dL*ay!#S*nEul*lCVu(FeJss34x^13%NII*a?Yi%a3x|b2 z+8j`F=I~d!z|bx*TF`92@9#3_ z_^j&J+x~6Nj6EmUBM-lJm#Uslth@Gn*Y9Uf9PMB3WJ%wAlVAEHy#2LQQRVu|wW=A1 ztAwMz3g0hn$UU_vaszlx{bxucOzl^k^jgz}+w4KTgteud3&osfTJ;~5IdYlH)TrOd z)6Z;X9Y_2pO_|Di;gZi2>*9K*?JnOuFM0A;k)vOdL(a5JIr)9zBi_=5=ML9)HJQwv zweMSRMgXx8=9+NZmXK!Y$LU*y=YcXmFmxagv4K1Dn9*$dsQPwe6g)Ie*jUDrB_Hp>Wu zvZO2f!DT7FuU5Ig*~9tLX|kV>2ERT3BDO5ceZ`6$46bF)GD#B-vSje+9$9cdXX<9T z+>9N|Bw~1dUTrq}RVRDV8dPdHS4mje2!65K(6e%4|2&^Mi~BFU(@%6em95PRzNO-F zTXwdBp8D6N@9Zugt(K1A&ABvjWwdbawL=T|m18#EY~fqS)@8E4Y5m4zXWlP!cKJ{7 zHT%|HyE$w|3jf)!KM!wN=eA{M!dgz>`xBRRZVPzD@$K2PO`Cp{`OG@*slE)l?do$~ zd>Hp)jiAWjwU8Fywb(;1_j}%3{eT&=u<{KzB5MX(xtwKQ5F&PJ>zBQpX%E)6H6F<1 zd80dF-%Z3g(1)H~+*4w`?AHap4*hD@RPEfxdEQ3m9M5D9-6IRyuc^;}GrjIqyas6M zqbbX;XVbz@Qpd~9TzS8MCn0Uc&+I!Oap|A`kex zB|w8x%7PR3+<%^Ex%>1}wjck_s0s>Nf;N4A-8kpz(>?M*h9xX7c6$9j;2*#5{qMWq z;_s)vT;4qMzCC=~UyRF>&)a1M1ud&GzHk2Y^Y*!Y>(9Q2E>ZXWlt1~jTK&8jz1R7d zUK`v0zP|te?);r!-ov(q6@R|xnsWc!iCb=IyJx)p`|@=Z;oCizx1>S zY>yw;bnCyRnTCp~lRleYX|?)%pj(9f=JT1e_7qLCJ#F&$!L29vtt=VAUgwI1^7Z`Ta3BYU*_3 zwyl`c%pH(o?!x=|Xu08D%j6^-&+<9DX3f6$n!!{??8-!){P45e!=)AcFP}T}b#r(T z|Ge6VrMxDyt{mCKoE~5C_vPu}5M}4t3@RPw6Vi!t2I7Q%~rK|7?hUpzik7|yXl zX{{n(p1j5-J)Ycl@oQX*w~9xppV2$Nxa??@@T`5xp55;LYRb7=SMg;t&XQX3kflJ} z{8q-pCBnhb?X8vj^;fq_&Gr{$_g(Su(dGH2(+fWRcK26Xyyjw7)M4R`G0c;t&m7&i z;nXkC;$kD?MKkMBx433xfEKcS-LgM`#roRgpRcaPxIB6JI$dc|bwZG|<9+GGL&i>P zIvv?|?AvDO-#)#@CE25i$3TCLMc>tXsaHZKF!Fg8Ft&vqKE-lXoBQQQ#n8&XX=fHT zRm#*yx~p#a;KEe-q-d&KTF1lA3b_Zj58rO)^H(|G=M!}TEbwx@cdP$BURxW%^RojE z-#`yXJ0!O;Ffb_8EWX;k+^GH0+C4SqLV_oM*UC-XygMeZ0lZy);^h3eE0OZ2 zBje7!HZ)XlcpnO0Ay@lx_uKtt`{RDSe6Vl5s|#Wf_~S0{rs@+HoqyZwpPX+GUfuk} z=;eFYB_%u8l-B>-rVMIxe%GBIe(cNZnP0&Z@o&?%wI*)sJaMn=HISx-)?wrj(@YbeO73gaMES>L{L`Bdi>>UgzoHi@j@BzYe!Cd zzGRo`e8ZYoDwh5E{57l1M+#$p+_|vt)b#I(*}oN{^uIUeX3yiiI9>fmTwLTiU3F(I znOBbtFR;A0@N&I(P}z@wHU?SE*E>R<9<^KGary7UN|*49|CVMiTN5kSUnRf5V{`IB z(3b8ac@4E6c73SqV&cvH*O73~V_M0C*!fD0I4>*wPFD_@>mJv(}rj`uE8k87va zt($iHL5h5YSla9fr{gxC%yti4vguoAgq--SKlfaH&+PKH`1u#KvS4NH_49EPU#G9H z2$fz7-I6%<`ud{7C`bQ=1X*5M#r;zKPwSVIE){+GvT%2q<4T*vS6qA8=*wE2f8_Sz z+n+!ky6-2~I}4>WJo8)hWyZND%)!orIl@`9&Rt*)2RAt@AGS(l1u%-WimytjN=bd# zTH>D&Bw$+5VS4_KHIM2@ela<5c zZ-28FgSwm<{*Mu7I2z=DbCrD7u1}xtdbp^(|6Un!?p@JN#(nmn{mXu!q2yYf^vazN z*iBpe7;M9<<$t_9{r!-54SdS}&bEJ_x64{qW&BTn{&97`z0UNn<(Z+=PXGEB|Nmcp zre*gv(6}-9cmhz(cXwmr$@RU-?q&P8?|t8*F4467NXahqujf;OY7LASwI+b;p|5^E zQ#A9!6BVY6Pp%dRRo>jR-_-PCY3>>Bm09{% zeyU$k)!iDfg z8-G79ezz^x%Cz>a^yJickERu5Gpbcr=bHQ7eQ>GEEM~s!>@`KTn9)G#6nrR*WH-VYwX`@3N z>x!oq>(;F)at~AiZOzM-KlCoNLFuitkG=ctX(c;%SJ^C^KL3SIX!VM9^Os+H8YwZc zf$7Yp?>_^5!;J$bw`F&797tGQuQ7f89-WflQoo}=g;tce3vM&9y_#0NKch5YvcL5| zi@xmk7mxNV(fXCNS8VuV9${O~*9eS|+5yH)HNvKOT0n+~|yqzda-UZsL<)l9{!f&vigL_?G$d z!_b*&2g)7afO8yZ%H>|7bWw#;Bg+icCuzo-Gt4D^Ns5D91H}p8rHEPTTOD>Lu$%I- zGBPo=bsb#q<+HqcbGM(`)#WEQ-|O@|myiZt6B~Pe!L(^S`mcX0L62jRO3T;eTJ7b# zrK~yN+7h*^8mp{HzPC189{%yp59?qs&7gxzju2*Z=Q|0$eNvZx%WX!u&_35XU zD4tlh_ktt$O3l{gO)FQG=fAt(Z!*{Tp837~Crb)47qY*6I$N7t*ZZn_-;EbHW@w(@ zUBS^DzBgcE+Cx>>z>psbE&KTOCp~?iR8_n_E8+HsN$ckCP5SF87Wp(knlFFel`E+g zfg+Lf<7`&{iJj7{C@NO+^P#u0gZr*mM+6o;-^00K_2Zpu*dOgolH=E0g%eBzx|dB~G|BYh>flVN zmWrc?ewzn`t_auNqf-(aTD5Ln^-AB$&>PEE$yF`s7o6RaS-oM(>AS*si^5^s<%XPvlmJv?ru){eAudk!F5(^#=1M%W-}OE|Ja6T7_DG< zT~%GNd$KlL?Iy7WJN3_h&3FMx*ehNJ1&4wXwttb7wLIUuz0zSBt94E;3odT(`_`Dc zJz{%i+UdB>@3OP`{EvKUtoijWBKq~Y==4DE1-2H4vv_^-48+f_0B^s6CKC`Xmo$J?YAG~_t!uQI|FHYcLZ;3=DM!yUJg?ZTQa1 z!U?oV?Kh*@ueBGe z9!aTiyDBjEOp@-V{cL?0ZT)wCMpz2dS*MgP)yQY~h6itmvsGVuI{fGCl$p1tu=5GZ z&AEL2npw&BXaCk!bQB*;|M<^$S+h5{t;hR&F_8(GCwD$vZL`(s^~_l|alX!R=6g(p z(=JZd$&rl%Z4BOBv8e58pt(I;@vHqr9?Y2LJF z(*gt+Jof@GYyJOWGkn+J`m1U7bzcL&!WQmDtp}Y=we+;C^(OPr@sK&=lagn0U@KT7 z=I=f8?ANJ`S$k8h?#{kkV6@Wi@rucjYg|Da1^V*f8wIo@`Q(=!)=t(vptAYmng5$_ zuKC_OBWC%wX$Pcdnax?YY0JaAEEB@dpNw&N(r>@{blkV!Ym9#0>9NXmx~;*z%&Q>N z?mahqt)7YGto6RD%6&8DeYx@|^m(uN$7;V=edaT>*zNqUD%A2`T&?!y-PxCW{3?`` zUS~^B{de#Bvn#V1Ub9W#0xDvJmh>yG%6GcWG97Y!*xFAITRZ+eyE0qqhUj$DjVJXM zoJnBnS+wHX{c_nPqaAwAGCmEyC89}T38B{EsaMz|o>#0}^?Y^o=jW@V&s8svZCPRG zFtN^Q&EnjS$fx~}cjRt#IdZsoVX4aZ>d>nBu9K!O++|fRb$lZzYuui4b~bpJHfw%k z<)vK7UmP2CzaDRlyfx?h8>j2Xw}6_Xr?pS_-fK2DHCXAHP$|9sWOnAN&bX4KTUuFF zv*jk6zPz!-UMEn*Q^d75dw$1q-we^xrj=D)y56^+ZOQ%Y-%wvHm?X}2u_a9Kc;Q;+ zKPuD14LJ-RzI>hDJH7PkM9yuTrn5|zD!t|Vll#qQWuxtepNzbA4%eRt{uZlgP)cl< z_+w|_U9^A3z6-a%E!m#i%p<`2)9BJ^_2xB=vp|~`R|`g(N8b{7w6_tobar*S;7Q(R zNso8N*&`)DEX;-ty)G_c`tw$ORk@3Al>?h$vG)o1DhKiSeDEr|TXUB_fX^{JS5lh4 z|Luv3&b#Ngez~r+>Bqm1tNXj3c1<&kmp=LQ)4yXL|MaqVrc{2Kc2+xGax-X};n~Tz zsY~k1-kf-M{bX%I(d^mxpBtWLgsm%O6Q6Co6S1ze^JPiC-{F%>ezi=!rncX!JzZ_l zFQnsHdN--v*<=}67(eGsT$+5zwQ&9jC_XHo;eJl117pV{(OA?@6+?Yd)D~u zGhf^+>ZQx_plf<=eUaP`rT%##_VYeX{be3ux<&`Gtt4x&`+X}jKfWic^X8a6-1^YF zLa8e|A^f3hpvv<7dYv67o=#2O`hNS$2kTE9FVyjmw=a(Exo2THH7yvlpXpQBHreSZ z>qB2|Di*Eby`r?!`$f_Hb!G?F*)1)UIsJ0I_txsgwbAjnU#Pf*=4*N1KFpyjJ7dlF zi>^Pfyl3|HT_c?`IoaX%Ba3MfNjvMlc|E=!@R#lKDW@xS&s5ioZTq=kW!>S*OVW$7 z3Vf%Z4*uR%caMF8Z^x_I7j#0+YNo1f_uU?Mda1_Ny%)@0z0;pvG6ggmeAV{pHFsh8 zmxbzOFJ+gTBX2%+z>sv3J_Vbx&cD#3)_5EivE`AGeMVh3J;@)lbV|yfb z^Bs1xo99bzPTY9&5KAQUr8L~~Kd4$6e8KFL)0L7-yY<#{-446A(?d}4fRXcy zRyKd+eKvf`r}8%D1qYvwGu{$*AgbY||E_M{nWfV=yfrF03#sL$&wu@eTm@rEx-GfT zGr^WWUx#OxfcTR;p3Y7v>!Qo{$Nze{zy@uE5IjEk0W?i^V&D6-*ZbpZV|1p+J$tc` zxjVIY&C^ZSrf+|zby~GPEoG|l>srSalO)}E6~e(@{;4GLCv!v+OG>Ln4CERL0$tPh%gZL_!i{(G_d_r^%}7ojG$ zY+E$@k_81VYs*$%eZ7J&Af-MySnS!O{P|yER`Bh$-E%jq@++g-%!XBS%r+P9THD?H z&!SKKY^Lz4*)x~DGTZIjlv8wS?TQ%R*?%AKDbIRiJX`1U=V#A64r^{-z?5?mtdX{fg-26^xTWs69NN~4{q_X(S9eibR)10bw4~`4P_FF)Q9EuB0Om^pc_uCF} zAcpttH<0Zwch8>$uQ!`HU%meS&)escc0M`(@9X>j|2FJ<^6A<8$??mtPv7n{?Mup8hyZNjOj`qIL_Ejz9w0c}{B((pf4(fk>+xyY}-z3QwA{Nnv;7Ikux!sQ={OV&@?Ui9X_3l{q zrnaDNKqmm4o^^XuqEw`!8)R5wdib=j@0ibaWW*m|sM36zd*iEW z&`BF!Y&V}-T)4F(VE(#|p@~jDY>D@huO9B!>x*r^wLTiKgIPK=rjeHzqwlIwU$>s9nx1!4LS=}~!F2xs#SY(7cq<~pm6WRD> zHUE0iP5-wm zr#2Fl#QQoxe|NZ~Z_0PQRpZaqYygBjj{?@zYhmuuR94PtqxW;F)=T=Ki zjtd*teCW|qjCy}W<+;u#%WChUxlzx2w7Axvdhe=pUVet@*S>Cnv)2{R+)L6u09xSS zCewd>Z<__^G*>QN7j@%`S-olh=EH`PO%}#(*v9YE{%q2+OOHE}5_#7I|K0>zsNiyo^~K}|>rU9${ykGF zxTdH`Yps8v$jRuNWwAWjO{*K77wa9^*3J%^lHX)3Y*os*Ez!!rdC|7zVcX56Tql;4 zN?hEocV2I0?PRy93X3&hBt+`{(_sOCER)y?Ysdshd&RplU)uxMAMlRNzJMubmBx`h6YB5XktwY_r3+dtwBz5$Yy-w^maOT19pZb_ zZU@(-f=@3a3_ z(^tJw+!uN}u30U4|MubAGja)Yqk_NgEsYdzw(7~)Eq~_sm+P{!0na}u{k*j*dqEk< zk2mc$tg_mBIpv2h-?QdbjY?;;5XZCwe}UGDSOxMQHW*9_-bMBbHDeke$UqhMG}F~DIm}Zr;uI2 zXLfn-IQcrobZV5I#P@ZO)4LX4b;3;cC?XE*pbp=Ims{nMK?jv8FeI#KGmLCyUv_!# z&lzfyUrp|acz*l1!5dIg z<5rejo-!s6-_(;?ZmY{u*lbx>Yff(aWb(F^FUrGL(`M}JX%PNovT2dWoH<^vPv1U#d+s~_S^*zk8Lg@FuPyt1 z@OAonoqcKYFJ?YTJUU?o&xUu7RU)qU-u?y^ezUikc)q${a&6@nt>xFZ%xgN$we~D~ zz|q;SCY=o~@Li|*Z0h+p`(MA?-ORNiZ*s`DllrTU`ugphI`#dpo7!8MLw53SQSD2P zjK6*2&WcEB>z|uJXRpT2FRK3aD>AS06N_-dWj@Eij@WhXOXmMAFksv!czmNx{M}jV z{%ztfSFzt@WqK2F)1Y@<_QI_UXFD=_9|tVbm~>kE^x_ouMW@>Qt{+2wO{h`+5(RNMzfzGyKUNMh2Gnh zp3-DB!C&i@s+pw!;?fDB8yL&ho=R-)7I@hjw&ZC_#jS+=yiBj?;H<4|YX8eme!r%G zRQTR*3kx~ibdP=6Y(Me&|JLko*ELBlCg)}|Q_m#?Bskf0kT$9pRv*_*Owvb+SHi+yT33K8u^1p6MEGO@fXeNH5J^pgDP(!0b(836s$3^kWI?4Oc?M z1o^);p7h^4jd%H0=jJY_EU$-}Uwz%H;_TGo?)bkwcEK!qOPH3vdgx5v;@aHB)BjG- z-yFUAb?dd6r%bw(1O<22{*Ow3?+j|HhO*{b@#l)T&TV@kJk>WtxYl&#Emq+N0fGyj zb6hAs$YOEDuV{9hCKKY)7ZR(2*4s2Pr{F2H1rq`QtvhIb1@3LS`k9&M^@=1-K zrN7?KlKGN8b^Te+&IG16uXy~f@;hA4?sQ#$|4ECBz}y+T(hFb(SMt3tECvwLE zG+^Aer1A}DZ962DVIw6Lfe&FhzbA&kYAS=^_dKGERqH;1_nA(O`+yi>fXymDoj)18 za_aQ2zt6$vwKpF9A38mI*S<}lZFwIL7u~LZo40uBe7=7h=G@eaJl)wLWBuu|khA3NB%_sf-(AYCFK3VZEcW;Lq1KP&X()f&8%`i)#UnxvPPt{y?`f=uypC$#|5skWK{opXS1($g_?tJ4cEoj z4EN>kP21kSy}B|t-sy|k0gbMz_;!B9D(M87<3h(b&iS|N%Im5v?Jsg0l$5TYkK4FO z?yTY}&JE`pD@9zd9Wvh1w&Ip5qt(+AFEBpKyrf)G>hZ9&A$R>L@wr{Af89EH3$(IkMR(!y(yWEo8N06V z8+I)^rG993okPyGV+UnUEO++4wr7){^VIA6YX3*Q{T}jd*Rztuz@JT=25Vnm%&Fzw zbgKHUMwgiUCP%g#?TZ9vUputt`nq3@3J0WC&9Qiqd?!mVd+v-cvYvLEWc@_1F_jdC z=LJUo+7*2L+Y2l8@9b%alk1sheQOQle=-@o#$)x@>|5fiG&}k~Wi2|}u-ASn+m^7e zeOlO(JzQ*$0cc6nXU%E?M{qrETn{>6GZcKlX6^m=+y3ABTlaGL|J(QeHhWBc^H=`= zH>D1}4)N=|Ejp8wZ0^X*xa~ZwU+EAbTq!VFeRns%j+&H?8k=y*g@Y`!eV(XIwmEdR z?xc}TjuH2v$!DAS)OXly=-hEqr#f^U$7+tG|4jK?O_!gazv}byRr{9T*B3}%_WRGv z&o5U6hdwX4{%H;C{kQh>J38*{tVz>T`t<8AyJhL0%fFYr`u=|Y9-Y{CUw89&Kc98~ z?f>r6qT2(%7T^8$>f3dl^7_Yb_EgC5)z|MY@i}w%Y% zZ_~R}gsDSy=PvuDFMUltUKLGxEmLECdw28P-CKIEs9fgyI@jUjv!$=Q=daI=d-=9B zwkEGBQ{qlAH48Q5AIKEB2t8@Cvt-b$N`K`S&&7o$A*#YkS z>nZuKKi1^GE;M?6ULquHVdS*VJ#W)vkFrhKW|tQR2TenPEdwBU0(dqsRFVBx@*jK}JVv|qa{oR4TX4T$jsg3b|^Z3tdD~J1^ zL#CyIP6E5v+j+NY@3zX+OG{O@1#a7`bNb1l+gp#??OdbY>E^=~WOx2*Mj#ETo(yt*LWnBG%a;&t%G=|;x^fU{9EsB&dq}Otq(yGK;-%-TeZ@Ie^v&fD8d|US?O3Ou zp?&3Feb274cW(t^r87?636lt#UhDbxQ}C4Hgv;)0c86^Cef=h?b$5&Kr=)@pT30#> z^>ltuDR@!)BKm?*hWrKz_Vw%EE)U_lx}Q1O^J$DngY1=7iB*cu4YHa0bp_%15euV1 zb@JNP&%O2=6@I?BURZGAs^hCS#N2qQ#r)^(qi`jqPfst;ui19IsBwDoBmSox0Xvs! zeYraOyOPqUyZ=6ScAWUzySZ3!;^F)D|7zcUP7eC|`_}X0VyEx!tXZ}ttM2c+=8ldz zR{w5JoWH;RQ_;pbKXVA_PQfAYA zdXC-BJ$;WXKU|&ted&2-h6?U~ysy)~i5Q1wuHbYyv+d2>_8aR~-+Xg<`+fiA@9x*g ze{o%6_BBRJ@zz7hb&71+=cb&EPVqYW>({RZ2jygBe7aOGyb0D)+$OrzHfoOdhX)T5 zJ~U_K8*iPpp!-2+hSvKZQ@**n9JDl8%Bj#BB5hJ&vTT*ei|cozH&>om>9f^q)jR*x zAXnc>JLf!!x>dHVoLzV7t!KOZ7m2=#4o!l1UvI|pQmbH7}GVvdU0_wvB*JpHdi^RM)o%3j$N@M_x0O*tQnBRT98r>#|P^{WqJymjW_ zT5V6e-uasZcA3lz+Bt1TPJ`aBti8Y1EPr3T=(P0KzFlD}E^fOYX4dq$_`|O^`nO(Z z?T*{ZxNYL|%BRU2owmHqd4I09og-Z9fEvG?pL*o9&Y$6`r#GG2cta1IS>&GciY?fA zZrZP{+iri)^Ep$wNz0{pL3d4Dbgbq#&cL@%m$36MpRCya;LPVY%#q=&-8EIcio3Xm(s3O~B5<$2M$Qq9&?%q5l_&l1XIeRo#A{B`Mm*|zfM z)zSSRTy3vwGPTUP)c(RV>nYd5HipDh>{e4!x^A|9%dNP}9zmC-^G%v>E@YdlzbAIG zeBHmoz*al!!W)^jx#ymmMJ<%QvG$_g_KK3q&#c@Vesrp=U6&g-GbOU@iA-u!PhqHi z2IrRl{h23jEy(RiU3y{ZL(!-E0w(_Ik0^iXZn}1Jj(yGcjhpvcmuITBS?IH`cj}c; z-Pr%Z<&IaI#|-OtP2ZT`GK)Qqk*NOdEOE5EW8rIEh1x({A>m*xb8FR&!n3YUJrd%? zh9z&og$|rfdHrhVrTp8sYred8aar=}qn3_%+0Ii8^}Bye?dYia`9*#H+l?^~p8hx# z{*)uYa((fG)#mTD!%Keru8#cuXGYET|20ZVMbAIYJX*B=eyxDu#Ngxh)}MC&-Tr?5 zo*13)kSc2Psi$X>m-p3&?VPjM=VSiSw>QtPzIFdm-M#x3*5>P@T~a_f^xN*lzV66% zFTR}YKW~2g#-DrN?rclYbqRwy5vw^`%K8{Kba!87)sOluGS zRy}uts=-#~>4)CvmL~3;awI|6;hJyV)_wczGB&WYvrpc1a=nvZn9URo;|j6oLN*o_ z5`PqK-S%0l_Cl#bbk-?ryQQ_7tKDLyjRK$X`#e1`v(_uI{?<09@LMxh1!+u83FI|Z z|0AJNnlMW(ROLkK$r!()nSz^7$Glw^B(rkmWEbr#>6;$toSvhun5Vq-U2C^W*%fmK z!S6Gd9M`K7l}cLm&ahifVOOV-*e%z2&%3j>tX_3#{mQMoH>qw|_v%{6T6g{n-|yO$ zXorCE;;VOSZ^k%#`aGNarf9<}ulesk%+9u0IsM0yvNdM|qV5{mHD2FY`EplBN6hU# zMJchGCBG(f1uXe27@|DnKYXLYvO`At5bZ1}gu$F8rv)Vm=>c8|_! zvBmbj)3?;Ywp{-JI|+X*7)9f%;Mwo%6%EW*LK#vKL)CFlj7&kjnLcr?Bk}e zO!?oMpA0=N|Io@iE~ax}pR;jj-2B%jr}$=guQXV{vHzXxyyry^FGW56>{idGQ>J|D z#_cMb>D%XfCC7FOynKK5*PA1W$*av*sufD-T7R-zwd#yr$=&*|suxUbyrPTwf8RVN zaPX_@Ohiq0_m8*rfLjp1R#~0$@PXH3tGV>GbIWF|6K{FDpxb`; zlotzRe?D5C?3}whYj&*H{McK&R&iZ)h16?TGkV_g*G^3q;q8iu@qRdAyJnH;qUIY9 zI|G=i1H*l@c6mxHdS2!N&uLf~5|_Z4Pd|;w%=aFg`Mj}YzFWKgecjDp^yGc#~dr&X%sts_x{3b*_OpIU8m>TMcNlS+%8t1^5FLzVZ+(Azh*sqdF5~QHckV^ zvWm9sj)T&M-+0m#)AKiOpLvNVEA`>kzk4-)yh&BueqH68N{pVO>C__{n4u3pZ}5Tv8i(zjHcGET9yYXXFS`pltnCK_W!Wd2Jw6APTsrr_#oewWdfmW5>3+t7PG&oK6i3c z&WhMT&roHx)0<8m^WJ#m%S8!8X7*SqALn4vgqw|PR=s+6Z`Rq@Oyv`w@9a47s3!VN z`LYWy`{P=7X=w-gK3{2bJ~h3%eBbu36(84U#g^9n_<4Nk{-s8DrnxkfvVS>v_~jDs z(hRSO=gng0+`m8nh+eG{XXwIx>|6T79yPaL?mS(!NoR6;(T&pLW)HOkidTE@+{l+H zzPH6Y_tn~aQ#meN1u3%mb0g#D!{_o(L5<`|&u>X&UEc8WQd3`1e8!97cH5%IRjh|& zK5B5UTNnCjcYbQ1Ag|TaO7CvdHoo(0 z(K?l~TwC_s)?4%YdOv#JeDuvZ_xroGH}`1GSaxBoAa-(*!3=h=jvI-1AQ^) zik-g&a&A!HXm{PC!S<5P9K>3BEcx!hp(nTV_xUBMd2N||glAqxKLg_e(|tk~jGJ$M z`kw2*JTLybPs7Bit?4^jB=!h?K7H(1O-E)y%(mz|bvdOEciQL)RXh{>ec&k1#XmCD zLhI~YW*&`ta@oY<`Sg58=Y zEE6=}dWUP+8(7mZyz&ekk*Gaaosp&Hv(EH!WW2@QdYJ&W7AI*N+vSn8!Hl zZPxl}i+he738^`p!EZFPctL-@=g7%y?&T6(% znaa5BY4NuN zh{`j*$$5WnH5*^hi!ZBJI-T|k3HChoc~8lYwf%PST8DElGM8A+P_(}J&C=}r=Tpx? z-A8cpXVD0f54f5f8{G+N`muCI@%qzs z!L57?vUw*zuS^wR;ZfS)d$p%+;*@o6pC8?t=NuGJu9Z1_MY{xuCPKV({%>B#>6f!P1BFs5BoF0D$9-TwAe$jtM8GJ5~Q%D*0J z_dm%S#q^e0Y_XJV(eK>Dn-6R4cDj^O!rLX$_1sRpwzN8=|Hu-hunMIq2kssDl_A6U z&Es={9F|m!5aOPkzyGII=S+=@eDOb4$uKfGWY?$`Fr{sN`u_GjzwiXM^M)GS=Lwk@RKMWF@s;^!9Ao0+ zH#Z;Mc1$d3ilj|=<9Bg=V+xwnboEW}nZGY@ zL5lTLmK(h71x~5XG@9(O)BDwdBeM?qZU(1io;{oQT339wUpi4r+;fXlYIbZkvT8`YJjaR0xVBA)FJicPLT6J{)RhcJ>Yh5_M zOnqKCLC;(M3M_F?dEJ*f&&6iWp=W2Q8N-3{F0L-DWjG(|wfFbgut$GS69bM|{?yoyi=AQf908mw1~> zE#}IN<~sZO@2W^#;JoLb4xhKTvJHRAEi-x9 zWrxesfwi1h*7PgdMlGE9#Z+$hB>B9~v*`khJcYK4wwXQIcO$lOQ(j|!g)i?$4aP2+ zzYXa(i*L)_SXMgit^4e^A~PL7+noO$F}FD0ph7h3h{RS2EgR*M3Z=&Ah2I#v1AF`#wlML%;&K-Hpe_`!dYh{TA=3OniI zlJfeM<$C>;+cjUbPpfXa11f*#)q^@0PoHe>jtAW-b~L{4J$Gmy!_@3G)$w~P_kY>` z{@Z+i7ngT2cELOEye^l2a^>UV_xM^uYx-2ilC1ADYgAl&K-fzChDJ4ttcok z_~E!^`%EvYZeVL`I-e7XZ?&ux)(zO>vj;;w>?c}vient|fh-dl!7}aPd zXJ47r_GKFjCx>w6aIJYeCHLspX=~lHJ$;t3?3!X`e6isA*J*!!vpszlvHS{|8ad6> z@9f*%al2izv^!5uIWXhRf~`|snEt4*RdnOOuzl&w=as9A7rx(@!D*P);9M?zYfIEy z^Y%7@#nT<)?Rq5y{|oqX-oIV7$K=ekq>fiiIg5QewoP&F5leXSa_8=Ix94Sbtnylr z|26W7@T1Mb?cVj?hDTP&R^AVa5OcU{_3F^3=_fbktUP5>zrJeok}EgELz%i?-D4Y1Ez2-mNHP3wZk#&BHr#nn< zbG1p?*UFdq;&c4PpMU>) zzEHTzc4Moy!4kVw7xu0_-u>~0)ydF>cA(L+RZ)vK9?H&MdHVCUT+6GBT`z3YrM^jj zIMEUCY}=k&S|vPMhqZt1~O9A$Ped1oq&#q z>o?8rtj(Xl=R?`rkClPPoLnE3)STXSGrghA`_fmLu%lZG#rAOV?%b>9+;ZL2LU8}I z(hF`qwFi!;i!Uktlk8kkqU zcFU0e2f8~7Gb=zu8q|aZC$3%ne9u15^3NC7>r6jU1S-L+{(*+HrcJ)DSEs6^q$6H$ z`K&jfk74R+aE6HAd7=FGd37bFa=rO0*KCg8|64&xX?b70+WPyo6M~Q1PX_0VIX|l} z|3CWl)wbk1v5Cs3XO@ai-#Xp+blBVU!fMyFoxi2i#eY{q`tFy_qviko`?zD){<(IQ zZ%$ZVuzR=M|60BBwn)CNi3y!w(L->Y0gZi>&tVw~vfRsXb;!pK&z)FOO7xDBiH# zAQ9yQY7D`8-d#KGW1|;(%ADA@)30#yDa#3U3zp`bTNLqI=+KI|Ij?25CUcqY6b}+@ zI_>ydZ6~}-nGZIntH?% zR=ir3+Yz-wt8ty~g+=WA%Y}P&eZ<v430p{^NOY%T`;936woR#o*~xk9KcQ`@HFx zN2KSOO>4e>3*KQMqkF(?>5+AIv(GIoOI~tA;pJj0UqzdV6GIk;LMk>d`RWe^R)+aa zv!}1q|M&0Bk%gBl&89xw_QLw&GocCj+~1xpeO;6reBAPE_?O}Z-lf-;o;tlLXUoG* zm7vyH4aW*hZNFUYNzqgP>9Liow@^3muJxS)-isyyPGO4`y%WC_KbWxV%g0>{SgclQ zx8}Cr-6DCETgLgZyUP(CiyhxwUEPEhgng6NKfUQ_s6g=7-7OMNFMF=+Njee~_I3N- z7SS&bS-T%+bF3@B-`NpU@w-rB!#Y;kJuJ%vl2+VOIO6(RL)q|KyY6Yegy|DzU$guY z+Ol`;@z3e%e6CAkx4yVt#V3|<6WV!pp1MzOd&&^)H|9|-0 zzjk_l@$2JW<{H1YZQg60AN}jrw^(=EDPprSO@F;I`Y_?h(yAvxP z&p-LPT;9dy$(4`(@3}wE=s5A+KTdDj=IMt0;{VhBt&chv7Bl_yx31qA*M45#|9A11 zucGy4`)9wM{q` z0Uhx=CXh6x(;iQjb9^3VZqHx(U{#v;%2}#M-vpmg*uBl^>VD>4v*5ds-bl>Llk0;x z{EyhKG4kBgVE5x(xb?J?%fr59xpZ)LIeWZ|eQ{eLGise@;a*LXoEjGG#x#!A>-fXO z4+W?$%h?-sNZ4})%Py8HCa&ezCz#F?pPbS9Ep*o#hGRv2*+P2-Ctqsx%Z!h4vWt_R|D5(^2)(~q54vU=Yo z*jj+I-q)|PKGv!-KeY7V#ifChQg3D)7WPzdnE0z<%h#t)^QA-YU73<4D)l&O(W>(8 zYtOs++;=L|pR~fMT`0R?me7k8>qLI>)nuD?iQo7h5T&!s{#EeZ)in!~llWh6-Pt4h z^s?v1NjfkJZN9V>k|R$COOA;_gw1D_*IfO z{bJ9uQa;PtJ2Wj0;+Yb8EQ%-D*~#$T~Dog zw^j2MILkSd-sM<(xE7JcFj)_hl$17=ew}lD`|~`zUsIn7Mdp2aYaV}of6cGS=Zikw zKDyq;rRC`({o~^MwA>p`>0aHt{@xyO!HJjE`*rK={``IXKYM=tpQiipcAv4hVCCO; z&2y~&ecb!u*IjnOiI?ufrfUjc{YVe`W-eG5bCWBH7BXeXtDD=a>o$e(Us*>2goWF;Twy*Bl*uw|6YG0TRu4eL% z3EY-)GP1rEGIyqD(`~)`F0X$YbZT8u-J=%Ee4)y&=ij=|mZ0X3xZuR>+2L8=cY90R z@=CfQWN~HpK3|T=X_iyJTyfzJ$eucNmdd%tb@S$0VX- z?cq{`q{XXPZq1T%tekiH+_!gs4&62tw7m7T;zNPe#P<2~c5nZB!@5J{kZ@-NQ|)=* z$Z03Hp4AqdhhwAOQl7eKXPu!eS9>a z+SRV~S={Vp&r4)=7qRdyS@ST6r(~*IkK4jmYC4#?3?U*hyR+lOwX4N(@dvj3d=Wm) zJ7wp|?e}YJ|9^VvZ+H4+zW9D2LBVO=`|qrH?ia<%vLYt)?(bJ;t1W9k-FWnT5j>YY z4oTl%_ubLO<=?OE^)iv~zJdpm)7}4xPh1L`9o zoOrQM_pW)?3ne(!63n9iNj-acT9mWaszZ9*bu*T`e`6@+ibyG0iCZ(pJ$`|72fSPwkaW(t3Vi>O1yrGrKc4#76(A%l+*2vaXynFuNfzePfyJgUWf{dOPpz zDcSq2vaHm)O>7x=)WTD)+#7s3E6W~j*z`+B%31yH(G}hgK;yWwDbrHD&!611=F9si zm+I-;G#j7EMCi00ExkH*-`ux%FTKsmInd#fccdirtJ;q0Y_s)Qq2DA!xWbJWg)WRe zAE>9gW7U?gic2-oitu#mQ>@lF!t-uFY@g=t(qP8 zxJo$-LwB*{ssy&qYN&~typrqghR&k+jH}#NBxdNk9d>)mSS-uZe{nx|B-i5k%*QoS zL|1KN>H1PE8@M}g&jCY|x($om3bXg7fd-{tZ1E<-H`bd-#DCukVr=Vsw{_omfc7w;HV=6*hP zWV_+!PrO|VLt>8^g)ZZ~%(wn*8ApKw$c^@Y{ybJ%lk|9L|3oDwnF)7~Yfn27D)9_o ze~f{_&}ZM@XRia#uQx9{Gj|DO1TDW9)CbD^vMhMJ%-bcOQNw?2S7R5US+cPflt}v@^O4;mh*c@`1%Xa^Md&B=uxtCqv{9VbFnfj}7 zpK3r#%;hPo>lAYjZSGY(F?FBR#jm^*n65JN1va6j?y|$j+&A>?dF^P(ZgSYcHxc;e}p&9UcG&GlpRZ8-~bE|%YV`XD`W+l;r7OKOfi*Hg^mGdLBo z;$hVVaQkFu<;$w4#c#CAdDeP|uZmt6T61oGV}2f;QFZi3F8{8uiLL>r?XwZ&3A!4w{@ApkJ(MikGJISUYo7zozPQjsQUJ1 z=i#HSDaE>*yqmQ_bB4={cOxb2y!d*PUwbCV{Q2DI))Uyh$)B_R*0-%^wI_qd-Q)9N zQ-&|z-v0g}Bo;LK;Csqz1u@7LCR*>fXIX!_)^ zqi?It)k@9P@u%Jr^Pk^lub(`lQ|Rr7+J&bC6&SW0GheEnU=}^kE#Ini_Qfd-%GyW+ zz?D6j1t`N`bN2c?$w)wqf2C~{lgzW)^=QkQw6dJdYd5w^Y+;dlw%}{!hgHumo7UU^ zKd*pDv%bbr;`5E4>}y$8$K)~nL(Vd>jIl~UYqn+ z=bU^~5h|efe8oCN&^+H;l_Q>Wm|rs$aJpAJFX?{4xQivklzsQxgNI+Hh&K29YKmRG zPv^9Y%bs(gzOT(*e~Hb{S~T53ymDT({${PEhNqq`3GnGyW|9`ERyptVS?}wv_quMi zb^De!tkb=I^gX}pw{3E(UoP1lAf0g6GB3TlJgzkE_2w%}FT4ydd-2l``(y7SqmWI5m5A7E{h;p;*>H<}aDA)_ZL~5dKBzg;38jP!eor%~-d2>fQsp z8aUR+ZOC)E%o!gX5PBw?=jPsk$_p`3i*Bv|yff%G(`%=gsUKW~GLCEs4S%}Mt3rbD zs=2A+hLB_Y$+vG;*}Q+f%Hu-ag1_}XrL~t9evzD|>!rL*^Nb$*w#>Pjdm2H@O1_jv zYG=6bd!u|!acZrX#g%RCyMD=UVY$VwGgJZ z2rUTN7l2q0;vHV}uc+L_T8?x6BOK8{Ie%K_W$3>(|b?v1-CBzWdDEJw|RQ? z-*<;69^aLh-vMf)$Lf^-O8LBmG57r3fTs2L{14=8vzdCY_GV?d%Q3;E4JX&%2lyFDqab9 z50>*T^IfU4+9_AcE9pz$9&d-pX(!)SoA3F)HS4aL#r4uhz<<|e# z!o9CfOX|3It}!>N_gRd^lgi7kUeAxHL@l(dIX^q~@1Au*R_bot7q)k02ZL51-qz$_ z*|K683!kK0v}Vf+H3z}}7pLi*22E~#%*_4OC7X1Gp?1|p#i*&Zosvo5v|ZVk-%&a5 zbnEu)OXbSTeAZmc5c=z2J!6+&1n$czScaok6QW{mmOQX4h~0zI3%l z=n9WN{Smk8xp(r{a_{=Qa_{4qZW=ZthtA(rIKz;Mav=B}!BU!qIQ z<^Ng<3a+f$!*#kfcAe1y$S`ZX-QSbx)jHx8mQ#(lKM-aLIrmcL$E&lykBQ3u`xkf3 z&Rz|^1}Pf z=1tcl?%ey^yV-r3b?v9eEJf?@?TP=BlU@C;MX2U$9jImT64bKj+yie}uxF&@cba~5 zd-L}8t3$@KoOrBQSKEH|dy`q=bgtNXNdjZrjFMGZSz*;lCmzlAsO|{-)u?z_+iscd zhD8aCvzW!L_H262vr~T|>-@TP>zkhmMa@hqm9jpvZf(F+=gi#0+IrH-LZ=q3nh^85 z(J^{q?F*qeN#Ue3lU_)LNlsPWkgqxA$hI(}1E9`jlIJz4M_z0ZuE`zRvI#eThD3L~ zk}|A4vFXiaeg5h4{tFWyd&P2Y`CaIIDM)DYs}tg@x9r-mt4!TTb5HGix3`8KdsWl~ z6M`kyEj_1xMd!5CVdEe^!>R^7_Pfs$#6b)EKHI$CvoFNCN9sX<>avXV>hCAYZmm;4 zDsXkXQ|!6fxy;&L8%w3uvTVz2xv_M^`p;?7nyo9=Nh>}QtJ`?%O|VxiPpImKUeM4@ z$BC_HwfFA7CgQc|l+cVW5wD~z49>kP{(Sh{-@MMgtm9Sm!qPJtr?*!B4WGYl#@fp( zLRa`}@`NVc?g|xJJfAtXDN{Ob-t$S%cfQHB1p^VQvIePk%`lEU# zp3)MT&ypK8dww!M!}2Zqv96kphN~}a41Ii1+E8)(b(iXw-H$+p!5g6&x11>|e{sQ||4yD(J{@Z7@6xwsb*ANK zo9RzaU5np7r*ch}{M-9yi@uwmle0{nYgd_5nBzY`W}eTwH!>05Ycvk;;pCigcG~jG zv;FR>eN;Md;?yDI7$+XN1v5*OkJW4jtl!dJl;tuIC5Wh4?YN zl%98XUb1;BoA33o6}uL0(R&!$8M!p9bxMr=hJK+N;T5S%H=M3Jx?~+o*~C=v3d{Vm z3@K@IZP5fs1n0azSH7*>+k4(Nfp9Gc<@2AH?$6{cRApYX$neNio(&apTQa!=LmSFo z7kbQc@_9BXFI0S?#4S*d&A(jf^rlP2al+Lnbe{&!R`E)RRY>hG)$CmM3zXg;TbWEb z@c3055H}1R_wrk}`Ke~7A@zJt%xpA3W`$9mIaI+IPCUvZP zes&wXY==R6EVhZKo-p@^g&V&6*`MHMwnZ+kJZ zAH(ML@#=3xqW%C+)b~1mrY0xnr4?{mPVU}&HY9DrO2{x_vHDHOFrnd@dqO$~k^<6{ zRw?o=nz`ueJdRB{ht?XlNG$cu;){}JU!P|e`+MW(|2)Mx#xq#CH=GxGZajJG-ZRZk z9k;yacO2qWxZRSj9kx35`*Lm5wHNGGon7~E=?X2C>va#j`cKa=4}SCNis>37ZPOWZ zQ)j-(d9OD$=`8Q;2f-V*)h_=YSqo|#gH{B^S#Hwb=$$039m?ct*Bb7Y>=`1w;BM8M zNA2_L*VM*DcW7ViIJxP~iTBsv#jdP1&UmH1O0zNcN#(n}J<}%5@ZnsOwWMUNRrk{K z?QN50xM*)#*_zq@;9@JIaXP^{mAgDFkGxR5pgLjh+q*wO{Szg5=Z)*lro>jh zh(4uin8o=;(>HQjC+EfR>p~ezB$Bo;+%7rOt;kn;Z_VVWj;!c~>#j5Yo)+~wB(=ep zF|@bxo9a@5(9nhZcFi%5T@o;Nl}@AGu{a@j`3D!0+9h-kC`NZJJjw$ymecU-0pE^= z%{_6gyI3xp?J;Yv>aczi^#O>4KAIZemR*u6+GdT9sUEdS7GkybTM!<*-v(T z{rZ~!Po5@Oeg;0yuF*87I=egY-bL=Y5WbWMd z+)v{mq%3~!GRN<(8gjBteAMp`UgqO+-(R!2Z&C}wl_FsX(0OStjKP|$5LUErp-5Nr_9qi?e%}%&cw|{ss;J4 zKPs)ro*gds{?CQGv-YjfY*ft9>12smpnGKM3a7I>-+$XH?RRzYvR5UkUJJ4-+NMpK z5yGjn&7@m4KYpL;dJkt$AEg7U^qPL|&Y#fz-2Tm;?33*hCCg9owRr#6J8g9G^Pa-L zvEDgzK`o1fc;{$mwsdd9BPy!`!c!eZ*Zc5DrgdG-+#+;i*_YJFZ5b0xboXmrU3n_c zCAcDT8fY9hA+}-4E{RoM3-os%;Ztm#v|GNsl@61oQhbdM{p4x0EwF^H*J!1%}t z#$Qto1*mS*6;67g09xvh5X7&)?1git+4O@s^RK2j6Pkta=lix_*DuDzhR z+E?8A_HNIx*`bO%zcDYK!+iUNP@L%Jw655>yV^qA4(43EyFu|l{UxbIv$VGK>YUy) zZ~ey7Rf|}*S^odomY$*AxmdTca-M$OIq*iThcT;6rn-o#Zs5NEfk#)>aHmkls?)0! z4a0L+2h47pBvEpF6`$Mv+CA$YiZ7K(`l4m>^@Gc$b$*M(7S^_zyA*5-S$+A1>IQvh zo~@!wBZMYrAGxy8J2Eoi+x<4Ji7Tfb$xz+k+i4gsGHcq|O*t1<^J`8$lCk!JKxo`; zy9KL3ElQyq%etg@EqtWiI>};|n~%;$=Md3^>7SoXNNSTXb(pVZxb@ZM6D<-}CqSj5 z#OFC?ihMUaJRODp9@z9R_Lb^7pPuTRNgNlJ8SYp#A-4NP=nDPo4ejrkmpR2k3#Nm- zSDkiOXPfaGZknuwFF8z~?3A~@^V=nH*7zbU1rmyo$0g7rt7o^fMT0Gix$P7k7&3Y* zg@p`e>-yg@ox96-vpZsm>)|8v^PvNonh}tJ%+C_b?s1AG7-|^jmak~e*k-y-V6jhA z4qw#t=5W*G^s8UiAPNch47a-_tM!66e%q>FQK&u zv{ER}Y;`FYW0%G&_A<^5r**Ev7UyhVz0>e;!nPNAHv}HoXYv|)nI8(j{p#`@_W36l zUjFR--S_u|8qPOT^Yn974SzD+KJ_^Uw0a`{x>xjqQj=DTTMVvt+w5*%5_*)uoD%^vNl#=dRW<|_!Fr0n;*MO^nyGcg zJ$ULat%t1QyDwY2y~CjmzmKsC-UV8A_vFgQr{9l#_m9)l4X;`E zclXbmI^U<;|4odmS>t!}+pBGlH_bWy?OpzywK4C$oP7H87-EiSo*!r|^2;O!*jQvk z+NE9zWBc2;eG;!t+x4MmnLzV4r?&@^&dd|4{_V^mmhiFRs0QQF+&;x48pSVWG2i^& zazbx&?C}-5+C_96iXZ&as0^K~Fnhl_>&@r;jx5V#@|Z5U#`TU@G8gZxb!!7coio=x z?Cf}x^WMtd96q$;wZOkYNeQ}*0JP%N>!sON|KqEcvHWUNSDN&EC#dhZ_54QX%Qtoe z3MS0{;Np@O7AmlvfkC|8)5S3)ZkHZtoV;e$@(thOD6sPHd z65%Vq3s2_Uu>RGi^B;PqIQuN(>MdPYJhiA)%JzWWVd+I*Bv-ac*dDM04OsC<&I;x! z3}KYA3siNEOo6%o}-dDKGL3CdMI=^{;$!>h}n{#%}j4lvi)C!OMzOU+YoQsR! ze4DZ-`OE#~qOaU6*PA~RG`;C3``_oxe7RE;zSFP& zb@)!5-_5eO`)}UXwtKcY=Hu2I`){8C^?%Q~y71Niui&|E_rHYa?c>ZF&mD^ItezLW zO$NHuH29V8^1R8F@G6%7!kK$Q*^hc}+}?h*HQl6DqU4=Q^{crzE`ONub9T}@XL(Tb zOQ(#}#6Fwf(93?qdr6zvXIEvDZfkOcF`d1VUn_H9(}6epO?{8w0;g@2V@tQ}v{7%} z$ue6?LvL+BDC4fMso+-2x>o{|->{dh-Bxq1G;&&J#|hX{YOkkdrs2!8ycYOZC@EFW zv$i*HopfVb7enpgZ0GEbKsCp27ftu-3DtfY}?Xeaf)GgTY3hE;Z8RndEIkn&-ZDqG|!pDxncb}r^^-@ z(wglvTtuZ7&A%SH>T2c}hTlB-9hX4uuosNqPJX|YAic6x!t}sGzHO(ccIX9h8P0U` zng7XVI%t2{>8Tytf--OPtm5L;ZJh+kKk4dx?DzfN2rOo^C|MntGUL@|jkJ!%x{V8u zav4@NI8Svlmb>*V%O%>-sYh->M|4MSliAt}6P(&-_?RDJ33(&EjdR`l)q=ULZ=zQ0 z()K;F_VfL3S$o_dY4-^v?cVyeBJZK%w-rYvR<3p`4B4P{T(5Q`dqeQ8>FX9vIWQyi z<&9MzAHZ8;A5R}UW>d!L^R^dpV+%^f3*#ins3|E$ho7&kWqkE)aeSoS#HVWa>pp+| zb9%Y`PaXCBv46Z=Tq5-T75P{%kYUui9)U=`+kX7q{r9=B;6&@`=g&aL9Bbrn*h{a! zUmNk~&-ML(lQ+)!8*%f~xAXVyD=h^Df1aNIZ^MoA!N={RZ_WF8cg^*j^E%-%%d#pr z@7;7gJf{Dz`Q|&Y4bk^c-+KO3_{dzlpD*9*J$JBfe^eCE!y*i77_2m(yUX{9E39E) zb09?Rt_M=#9L(vjvbOf6(hZqiN14S|v)uT7@q5b@LzkEA$80igF>7<#b(hAoZ^$xA znVuPueCE5I$W&#+Qscv;5S!lO(b*A1= zyKm~OSzZg4tuj;*WfY3r`h;0!Pn9RTN5$Dmd;(|PBol(UOMKkKA856z3ngl-^^&F2s~VSOIoRlQ>JOzYQflEqXRbQ z!_|%ivSqAs>R8NM5c;bbM7r=6T>X{f+!4pbxpDRj#x8**aZk|R6!5+*{}YkZ)}9IN zT(Zv1r1i^#hb51!E^Xp^*r^i26uWcEkx;Xf)=<|}&Lipv znSCAxJ8Hghjt3h6eqQPTn=c>NoPDMfdyMpfnTpZhigl<1Q{w^y_~$hsVC(bZ6z``MSB6OD(JG zp@Sx$PP{hmzkS~H_U8EQk?;N;ySeo3T)WCOpLUqd1us{OaKDL?PR{a*svHP;*0n;Y zn6DGOKsfV=ONjWq(~p%{R?NHglX<;eecbOKi#MnD+kEfV-zOkA@xQs`_00HK&ON{W zR3=wEtNix=*#7gA?PROH_pyYvrcU3m>Up^C@Asd7W_TriQr)1lV%3LNa@8Lnx#s`A zGJRRtvR`2tk)@I1ahHqZ_Eb!anGmYD@;&>s(0aXxrfV0kFP@i8exQ7y2QPcUSvVXOKi zj|Qcu&-dxQidv97sYGM%zBPD6wWZNh|GHRoZ80Gm*vXfxsyzna2jRJe>X>*q6CW z?}qCK2Ip@VyA=C2ib~lWn9wVB`$m(oTXd^f@7h-;i?%t1XP(H`xagg5Lg07=*J5@J zA-yAp^Vk=&M&z8i()yyxIR9qRHp6eMi{pALO1`bR+`Hr3<8^CqJlb_E;+Koi3a8U; zS6}J9`*kNQUpo4v@ua=_TTXnvl^A^Bj`%E&`NfYSR{Kw6Srs528?`c-&)>zf;hW{g zm(9B09)~{ceLC4w$&1Nkm6T0ry|(eP4=y66No`AZ3NBv7k|fEb_ECV|?Z zVIFviN#&cl4%Icu>x)=87}okGy=QPq+k7*~xyWj+RjRPcfp{~sNqH~l-M`=e%}tN{ zd;upBzA^mW7#qk$J&(XbMF1MLxTq>4(0If1jpa;lKZ0Uafnkf8=D&yc|Gm9GN6NJR z|L^I>E@51Pt)-PXPUM^d%%Nx3M$&c?B4oWU%+|?2u_UhC*;i%B7 zTI)j7HT*&|B(Ik*D=S^UeC4uN*)y&D7;ounZ~nMzl7(34!<|Mp_L z8`cKf47zzG7VD|pzjW2qWxdvxmHZj+Pe@1^rm~lHe2$B}zT`*6)#NxvvDN1v@%4BH zuY7TGqpofD`nqe*m+$-k*Q$EWKU;F|_p9$a zd0fT%Uv`V@YxNhWa_X3sua27gja$0f(`5a6%{^wlb9Mz?j~2C>=2sP-$|!pOgPrP@ zoQiXysST;T)9QatuaEq7`^L^~F13fW_c8hu0W@*NT_C=3C;OpeJ53dt30b zFHL!u-A-L;jVKGfSGqUV>3&e4*dl+;&wH01+R7MQBh1{fGF0?caH-U-RTmz`X71~q zqVnFWIM*(FzxLXf)0`&0`rYzo|Ms2QXP?Hrs12WWbEjI_=IV~!m%7UPdAG*>HoX3D zvsP*OQqk=X54XIqnGyS@=GH5==&f3PJu}P;ZZ_=WpU=&tcf4e!a=gs5mAk(epEu+YWbyzdZ68`7CpA%VlH}r`i%bjPRCcftN-WRU2 zD(|hp$31H==uJ)DuN7F+92Pn|cb$VsbDq_-y>S!cr*iLdzR#cB9+6Y|D<Fnho+_@+`wU;-65n@w{hqD+vYbo9pBxX9>Q-I&ipyp8@070do3}p==9qJ?@n}_@ zXw)*h-WmH=U5i_PB7CLRx3beEJg4+lmA$ZZm8RUG~ygQ0Xu0%c^$g zn|4>(GVVzphJL(j+y|Fy{&O3N&l$B6&*qcl8GOHgoW=q{Ec*(zQ z=h6#x%2Vemd%Gufty;yi+tIi!D1G&+U9Y4na!q1;Go+%jzrI|u>feH!jBmSQm3K#F zzcTvr@u&U#YZ=S^)q54!7?maHDI3K)t=2BoW80~n+rhmnw7j2p+7p~ zaT9aaeE%A^J8z#)n>%^cq0gBDCTp+P)uyg}x9V8T&5b2H=O5cHx{Kq&Q`w}pt3LNv ztm4d(XP6%SrE6i>UXJS6`hTD2KleR-b?y0ov-jS6RK9ekW7X%VZu5exQ;$3{T)W$F zO_sgutKHe&;w9bhCp|i!CUfPTvS@wYt(}j~e>?cGmNE5pPF3Ie;_mr(jqSsyF3~&O zow|PW%dPsXH@LRmSG4a}c8dG^oXCCiL+}W1S)pR;^-m={?bMjUi zNNb15r?THJFFCf{B*gHxPTbYJ;>`iUFG`);?7|<wX=!r)RmZ}+3;Z)XYV&=s zR%CqKxX|v!%PJr3^Nru;p76DPTEtg!@p@?OZWpot?>5!jU5$v3H&2o;pPw92$+za0 zLjBFN7OD2PPlxEnm(KgRbKbRbPu;fA8sBH8H6M?PuidxuLY)82DqrpW+i!El>Z$%L z`uT6lBiE<9QfKWFOzq#fYPD#Mz4}Ap9d|=lU#@!8|LCChznGf5*q54i-}e9iy?<%_ z`rG`|pYIH5{&sfTx4N*YNA~3x7~MAhZ@%}V7xS8`^r?51lmEXf`j=((a`!Zm-`n~g zS)KWQ(?s`u@rT9Qam7#y_2YE%K5MM5 zn*HwU#`)>@Sjs$CuQsY(6@1-p!O!T}&?u<|@2$_;8R)UQKS|cq%6|9`2Tju9;;8K){m?2rxecI zx37J3j!(YJ)PHWe`yqb|9AA7IPdpfoA+0* zdHbUFCf~P@rg6Tnb8kDXiC+KS>TO&6?8gB|Ury^#lGdHb(GDCSgY%yuS)|< z7l+-gs%?MJ_1AIw-?h6oWN&%J-W3vm;!W0yfA<1vcRjkxnR`9>=)L^nqlcewox=8_ zI`Txi!1b?{vHU&nPjsFx{(WQpvcK6Fc%8^H1Ad zZOxcwoIhdN*DqDkiPL}U)#`_At6V%qHs-#+nd#ohnE98t++7v@wKiM%dEcK4cf0Ji zJ-mG3yPyU*Ju5=!&@JAMDufqmZZ?3{xdDLzOQ`_9e8EUWVt<3xP3TO0d zg;2T8PHo3hzYz|P9))F~1aUU`Jvf!FJ33Z$%$d~l zX7LaEI3>-cuQp$=n|WZlfosFegJ<+_>kI#2+2^-_JNn{!CzoAw1*%R?Pp#Z{|NZyp zpZC4LS|4Snsk-L%tutTQmu?WaxcRuNOZ1`3pEENLopsK*ZKiX&NkFb!VB)ItyVfx< zgzo3q#qnhEhDK)&1tkWD9>G;rn{Gd@?sV|Vlu$YL!s}A-vEalnTGM83%$=?M@Wgeo zCu>cfB&}pU(HLz#IVejj@t<6>jqa_z%wjVg?%n!S%d(q!>%9;YE!)Fu*ebT@Ubta8 zdCRQL>U_794V#rKO|N^*^>d#TZsoMpdRC>IRYHA^*z#6Qr|Dn#=B{ZuwNnk<^9rc z@$x7Caqc+R*>NSy;l0_JglE}O36sTz*D+a~ZE#=zy0CtSX~HarrNXQ3-}729`GxMk a{c2Z5d6QEZm>C!t7(8A5T-G@yGywoNN7MWO literal 0 HcmV?d00001 diff --git a/Examples/resources/scarfy_run.gif b/Examples/resources/scarfy_run.gif new file mode 100644 index 0000000000000000000000000000000000000000..f0f712ccdc1fa1b8addc74f1fef5cf61b93c0ab7 GIT binary patch literal 21261 zcmZ?wbhEHbY+z_$_|Cu(FU$}x!Vn|G7{SjJ%)=bO#p2J!;?Kzvz{Tpv!{*M|n$O zX0tFbFf%YRFtRW(vNABSgAgMtn9Ih%%*(*c&%n&b#=+0SE5OGu2u4DD0z$k(LfoRl zTw)^ZlA@eaV%(BqLXr|9l9ECa(p+MS%)%-R!s-md8Vn*@45DBprp*9GjG|iXlG;KF zx{|88(%O0w`uc)K23)2FOqPZWR)!2#MqmVCLr92-F@vQklZ6$Vg%h`>uYgUMh+V9> zW3r@kniLpGJEzMyr^~pc%Yu=-OPaE4s;)<}rB9->U%W>^oM%9sXJDLrV4O=xynRH1 zMO>m@T8d&px>!XfcS8ITvSG*44LeqB+`eYhmQ9;C?B2Ti#E!*Rch7pTck-)4lRg}u z_VsAbk3+3L54Zd}-2D4+!g#O#* z^KYHQzqz{qdSw39^Zu)3|5wWNuY?hViW&bEGyW}N{FcM^I9lR*fa(P=oipwRCtZwB zxELLCG&*W;c*sU?zlFvg6XhKSa@+M~5lByFyPotm1d`gS2SQu)BsS=YtkL6Jp~tmE zk9DCQ(*ivP5YlCsugfrBk72$((>!DLSvJDcyi_MdS@)!Qb!7!~We0&!PEcny1O<0w zhqPvgHDyQCXGd3O$5-SgmE@;o7Z*g-HCwJ*r?-E<#_{8d7cOvp_`vY*9}4>ap8+>g z{Lk&@8WQa67~pE8XTZ$Jz`&sRlZBIq;TMDAKY91mywsw^lGGH1q)LVSyqwIuRCiCe zlKlLfV!h=2Tm~Hm1_lODPGaEr$MBU?#$&^RgUuYmaybV#EIiyU5UnxCW8X?L~4dnAoD;*I9bd1iR(P>H14y*(RP zZ?ARJT|Uugrr{Hl8P_wPor!vTNok6ww(3s9H>VG;`MxWUt9sL$C0mqlO}lAe9)I0! zciG<>tKP#5jEg+`#k8hqhc9+(WD>2}`f>A#qyIGC|J`YH>iNax;rr*+R+o3)Klr=! z-lLTQ_q^ndTkL9oZOMEc`}|bdRnd%u#tV+(@?oD1Q{?MBUtVXb|LgiWzTvuH#e+6Z zwha#gcmhK&MYvAbHYt$LvwUvj-^3SPjqCRW#g>`$1#U_`IMBFXA5^#Bb!;PC6D%IY{Iv!7! zbnQw?01*|hZqFH&>z{spb~EDda_Sc$%bd&-K6^Ng5(Wnk!9e zl`I#}%;w7qTo`mH@=@>}%~LD1`}ibZE}1ebbLFHNT&rHX{8Rb3MR-Dq)AzIH7g{UR z+hzY9SDjl@6q&qieOXwvx5}kmFSNHkl4@Az`DxaI^_LiSui0QC6s;3D&*^IPawoHD z?UKfOS?e~7d^6ALsm!pZM-s8wDnq4_*&zVn~8V6c+9i=yX=a=>)EE)!rE&cug5p@e!syuW$mYbH(4#0 zd^>HpT6u?6{_QoLYG(FpEYzCfZS$%w7@pm}@ne(rmVMj#{3`CWDZl&rsAu)#9h0Ry|o0merU8Z1sY@(G-9koj`Ho=0;5i#mq`o7RR# zj+zI{q7)A3RD5Wb`*T>{<^{Vw1CxNN&O(mxAMB!=3R>SJ9A`A;SkKq=p;<{~A!p!> zL*m;m@c*+DN#sfQINU6Ap_$=IlbFp47u9D5+)5=2xfgRB5uNv;#i?f@k8FfXvy36P zXH1gVtQD>5K_7dx=RA^M$I+p~H?ccbq=RW{hl{8|Aa~%2WDzF|Ck?iXtx-Hn#2h`2 zSiDN^OJaE-`*nxA{iloV4I(KDoSes%K!tJe2>Waa4BG2d;2|hdR3>yzTE8P2VA+sJ}YG zNoSqFj6-WwcqKbLMK2g~E1Xznw!P9zqw_bsuIY z@uYLvM)*ZNIxw^HUy3@@x0C1V(t;brk1TL7%s5xQt#Eqag>-xIU*{RvN)`xAUBL%`6Wz&Py{{?4qg_WnFb`>%B)}H&rylcT2Ie87x@KSI`jR=n%Gfp6Ob{ zudWHVPhH%8Oq8!U*Tv&)SNPtpsq0g8wcTnzUf%Za%i0-d-{>+$--xz(z!>eD+XG9D`WchbA|NH`&`pj9Gbc|y|enx9<}S@r%N4*r%Sc(ar!QCY3?1* zYVYQ6mlmweo%@zq#o_+1OVc;F@UD*$ULErJp3?Sx(fKKNf5-jMD&PHWtA3sJ?CXC& zZ5RBu2vzt^moSlBGeD_w~!b;f>VY^ptU-@^-hHAqaZeH~rhn@Z! zgS6FJZ__|v;W)h(v zas*sv&%gQa{dW86w`IxFEVu5NbRG8Lk$+w_Q?AE&=WFh_!QR!IZ=KEN^g7VoB>m?B zyMOsCQ`4*SZ&}a&ajt3K>DqU-%Vr$OnoN#TzQ^UZ2x23bRj4s{$W43*N zAXk3rYN-a9=Lfmc{EB-&FWAn*Updv?uX)n{4^JL1`*G^>--mk+&3SCH-tu6++|4=P zw_TWj?yIo(jN_fxzuytRo|ng-_j!hROoR0CpY7Ltc>er~X%7wmvhQX0*Jnx8pTfA;Iy z`+4rf`soMOv#!rOWWV4eoBy9j?0mmA6hE-O^0sX4S3V}TuIi=p?>?(sUd!F69mmS* zzw2H3`RYw#e?A`b-1{Uv{^vF24_`%l_nM`zfA?XM+;^krd%u*wuU@v;?$avu4bxQr zO>qD9Y~mu_^Etcz`}BPNcKO@Rsgf7&vG1jxG*LO_J(Rvnl`?=#ian`5PN}E+feuu>H6_hZ~NHBI4Oi3-~ z>Ja0((O~kSAyS}G_(fS(Y1rSEMyVBbG97vErVGgLC{e5s3r;xWBxL+alj8Bk|^y=I`t+TIwywTw5-tw-|M_2nW`hONiY55+Z&z z?VV`W7gitp9ZdEc+UzHkl(@z4PYB;#USRAl@Pbw+l21#9Ms zwp@?mfB7e3`3&0G!rPe(+vO*=U)bI*anMieS$Nuv1mpCMxCgAe%NusNw`(5|k2}$# zl-McO*4eb8GwVd#+#j8+2RmN6cX?&hDmrGXcog|Oh>|(#H~U4`yd6yE&1+56yE+Yv zy(_vVPVa6!(S7_u_u3t8>ot0|yZ4;9(XqHg;<`ttsA7$MS;qYv#Xc9}9KY4Az0rBO zqLu27A7ZaX!Gzh)ym=icr{A7#d${y{l zz8n)L2`kp#PnIU#E2)VQBhI}|MvW=bbnPE!h;mNru&W9GD+ zo73_ng|amTvL&bIX9|{BPJb0JJ$>eMcE{;em4bCE`5S*uFFQHCD04;&r%=Vr8IB7r zdoqRle$MzMAOLN^e${TpvK4`#=yYEz`zOau`)0+F)%POFfxNk1}0Gdm5CeF zQDxz0fFWi!K6Xw%P^XnokdI$TP)I~rL`+0XLR4HzR6?3pT#;2wjY(7!(K!``^iMSz z#8nt1lo;d{8BFv*9a3u}1}w;!(aMzB(t^#*mea(6+t^Xi$VS}IN>1NgMaM)#+fYkO zUt3F0S5wzOP196K*;ZQGU5MY0o86y_)t`$sfQtnU1#z*2@-jyYFvW{7B#JU5i85qK zFx1L3Ox9$W3+apLfqG;M^cWWDF)!EST%*UoNl$F6p42uy>Fs(l+x2C4=*#Xjl;3Tl ze9%hksDr@?C!>=tCZ}9Y&UzT!4U>PL%KIgo<7Yn0??R@ZMJ!(n*x3NtuAHE*oM1H6nH}7j9o&%}+>#kwl@?K$9F-Oq9TgrO91`On6zdxp>k}C3 z9T4jk5bF^T=jI>hUB)$C+C5#; zJ43=hLntPTJ1>W=Dvzl-pP>!f69@Ol3m7_45OY@nM^6ENU%uM3BD)0@3Cr@PZBE^` zJNf*f#K$KRKA(yIeLn8r1!NTe?*bA!p8)E8pG*FCHtpZ(?7t^U{v56ObGYHpp=K~@ z`E#h{Hw^tc*!tsO+m{2K@AmgT+duK%zG+wX&N;Pb(VktaHf`T9clYio`}TJqIoxvm zSnbJ^C1=m3UA!E8f-4IGyMP0@sHsxyaTHha{}CfRrZ<# z>A)Ii-8r!l+=1NmAoYYqi zu!i3PB|(LN6Q9*u1Qe}Go4mHJ3SD70u_)w}$?|{|LF>Dg&I(=awls$O(Vs7#tD?8h zGVv63+^%|C_#awwu zc5YU7pDR5z_05Gs^%DXr!D2O$#?Q7dw5;c|on_*@w1l(#Qf%a_lwFCYzvE&p%ib*B zmaoj97oPR`-oD24G~T-wxf~~_#@GJdC4Bq&-TVLeOn*&T#li2y>()8x?z~HH^LDf6 z$=!QfvD{*{lwW*7?QdrG5K)=5yMbmiE3A2q&q(M0`PALyBItUd>bT6~`MC^SGb9@i zdA2R^<1%`o&?5NCRG?LC)AZP+fQ>66^JWPw4;C}I7NOY8!=>EQ#22<=K_AyT^%N01 zzT(6pJ*j1iJ<~i*lY33>btq3b{H5YdzqOR5%Ea)jn{s}7VXCrCCPph3GzGMEJf6wD z@8IL+&_kAsroX+(yo@8cH&dP^bysEDtlU|Z%N7)iaXp_{`b@m3o4HLu+>hs*<#N$5 zhtA~Y{T`Z1bAm2)yqNyfa93i;EQeohOA3OrH2cEhtkReNi>#S-cWzgf#mtpF>#nD_ z?sAG;+$4IgGIPnsRT0k}woFUUR@<@fqIUBhw}-FSA7O`nq;@xvCe2 zDtn*as}9)L{Z3lOuXdff!9feDo(+ebjP@)&{KKt4ltbZ3XrTzFd`#Y{JU#nrLp?( z8>wkCE*G56{r<4x^;`D)kw0QLpS4mo+p)iOwk)sW4B?>iN7JHv-PhOOm;SkLvh&Wk z=QFeY%%9I?<}JVOr7ivOoO9VdBeNSU3#$dTh0is3(XW2@+djYRdDicCf4*03vVXSp zuES;zeWvXf<9>7e)5QR1-{+j!_y2rWaM#}ydXzoK_S>!E_3htfn)5%0Uv~E0cHzeA z`Fd{@H+N6|^Z9!I@9zf>&;PNiN%-|o#Y8p}1*Wyz53oe>*KsOv+El1(F5_=-b(MchmpGKx`N7KbGkO=#7*;>B&eNJMZ` z1M5G%f`k0A54+^q1jRN;C<#tKaYStwQ|H!>B;7I(VXe3&?TRLg1R@<=bd>@-{dgV= zCn_G*oVTDWeGdok#vji5Y7@KHaU_XHF5n975bX5PNfuelA#CTD(#ye+DBbv?N&iq_ zpX-vvvWGj4tG>F}ld+{Ei0!Adz%d0j%@ZkHdY(rVt_5}`@I2NE6Lj_3bf90)@u{<# zr>A#P(BwHii@EJMj|W&4PFcESiB@3J(O5N?2`w@z2ELL<0<}I)+M@DEV`j!F$6uFJ z_WCHhG)TH6N-db#VxlH9aV3|kUUBD*KM#$)h0YcVxoFDYD6( zN35;|&inSyMBQ>}X1~ljA;yro+{sFI8Sp%I!{N0fAyoyeZN}Lq(VJh1hmro zuI4Cn6z*_hs`#>)l_QhUrps6TlF_2yA{?%f2uz4=5$Bul=i?@cGu2gJRPd+I@2vf1U6bRk}3I z`_D2lW3OwQlEgTZ1pXb4OSX=_S;x3>qpMEb{*Szem>3v*{_19OCPj(6giTr9pqXvI z>gM%J-?-1$>g63>a9ioawe1I7n+n326c|>Dv`gB)D>SY;IjihD!^YICocYnWO?G_W zyY;MY(!~w8?`b{Sevp;H``qtXv9|Jc4{V-@o&J6C^D^;mukU78-+z7YM^O2`uSfIh zLW|=)! ze%qQYD|4S@ueZD{B38!V5gVQ2E_LO|zasu+zoQR3-QJXX@69g#->vr-Npg03{7|a< z{ko#K`d;_C?K|GDeNgkh@_t4~?taJL`RCZ~Y+}E+`rtqLGa;?|G7P-W*X_}jiM{>q z>LW2W(;4aam?ttTT$X>Xv!eOTXMxYs44lt%m4pOSrq9`aTlcv6nN+{ev&8>AV?0qA zpL_4K!hf3=_a&+>Mi-TIysx@$5N|SRH}8U|;$17Jtu320Ip*zsp|Te@Y)id%$$efQ zZu3S=!urVRbuTwR-nBkEzWO%fhi@)NVsC4`FT1I!!L8cAG4SHWV>O@lR}@L`=afx&$zzxS?rmg`W5pS7HrS*am!~6 zSbz7>v~4>&_#XsLJa_8{WB=Rzb6?%Npk%hh>t@53<@$dwy#L2=_gn3x#((FTA9Bz9 z!QxQ&BcCgSMSuUwv-*d+7WOl|3cml#NYeatE_ZgvWZUEXoE4FC=cn|$-T!-KI|Da^ z0+U7+`+}S=>Ctz~>ID|o`yZ{>epdgMz2UNY1IrGEkJn@Oho4h9Z4|q7z1hK{@sfIjxreaT zjaWIs7U2~wwSo=Y0WEq@1YBM;t9LN)6tqUnXpLIY8X(ciW!Ua?f>SxVZYp*!kLV7q=-idjA%3xYM}#^<67!i1 z=5-ORjXSzTXY{PCVEDhiJ8EOk*&jW%f$h^gdXH!HR%i4`7`FGU5WLlq_&l`v#)ik+0P?uvc)L^hQV6Zks@9$a}Ggz52Sz0oi+i;rNN*Gz_8yGn1YbEN*R_Y1$ z>vGJ}Wtp$bxImY2kuIojx=~+ryFR2#D!0P`fp!|m?=n){YpQz4TIZO9@d+o>6V7HQ zUChDA?4*m?DOb~T9){QaHJ`>xe#+qap3DBTfaP-m$EyOq>-jQ!^Ym8c+Dy;!?9cJ* z&I#-S%|L?srtP5V$Izy%u-eSXij3%@wAhTKgoOCSh^WM%@C5(R1mBQ&@8Ec^pm>kK zIQM`!7yo#BzXS`PL>-S5CD$}L@N_Y#C+Y&4J%%u(UD83wHC@sxLpUOnD=&w&DwnA# z57Zg$%xA!Y3K+Tyn7Ru&`bvZ*S1HbJ(qGhNy<&pHs!7hPr?{_~=CNj)*P7{mYi9(n zof)-mcFOv>r5hKuZdx{d%d#cgmu}v<=)}HRR}S?(I@dD~WxT$e_2Np-kJ}?V-T$FAT`jt>pGT;eAt%e+*a2=!P~ayG@zlx%t_d8T!R;J(-)+Ip3O>=K;&9&XzD!pl$W^nOKJzk%0>jMv`Xa>$Y`F(Nxfns)kwKtrGNsGHA&F@)E z{&fFzi(|0f?3vG=bd(Hknreq zf4->Z&rh$;Eq=akUTx?14`-ODp(roL*08Wr>fF|ARTC7aU61!TY9ikva7=tri}0!EaxLOX9ShX8&!tMH z=+6>*%wc>fBe})m+;(Mo+eg!Hsn36~wW;vf&WfdB*|i>vCw;ohv7p7@?dF{+p;kw) zO>@rb44tm3^D}i)%D!;*el@uWrm3-}S4FBKxV9~;Eo|ddZFsNS`Pj7Lna^S``Q zU$8du>#|w$dYu`I_>D9Z7hj(tdBW#dh$ZjJ4W^2~%6p4q#5>a4X~(Yg{Zmd##Q{qgF}yyQ>sR4455xoEw1 zuV38i_xl)s?anDq5LH<}b*<2>f?a$mYYLBTVY{(@)BjJRhK|CkcTTIA`DnGlHtGC3 zA8YvLX9W06nzw6OkJY+4xyrpcXLyVyKWpD#q?qnryn(H|y}0J*yE~hY*|WbjlyI@H zdE_HC|*PHm?39w1zY;X*p6rT6<=?w3>BhTiP@7wuUz){cg z>CEYUJD;zRXRLCbw=Js|AZ@|7!5zV%yB z!Tr5|o*cHT6WKYtzV6rS&HWR8-8|gzDebxK`C{qOT^SuRe8BT7Potv{XP^`%}Ml)Km0_JLXHZCl0>C8douUMY*19V57J z9rSBCzBoy=dq)CJaS1WE=%_FXbb7cNU79%I&ckQ(z8|qj z+cY6vM_DPf^O&X1rimvU+%#e>ygpa@*Dc?YtaY}+!)Dv3sna^1>exnjbNdN0v1%NU zDBtN7Uvz2m)D=ezwpXwO@m**+8IvXzvC}(UPH5Jpo+oKrP6((+6}0VCNm9CO(Qc$? zG<$ZGF_Q0e)ff)`K$|tWfK%~p2P@!*K}2k z6DqR%aM1HPr+?2n;Tac2(!AgQ43hg4xGZ8xra)}E)1+r5%b8UdN$&i4+Mg|Sn)B1{ zrw=;pU);-)SxxT(eSy>j3Bwy)jEXIDmEoYj!B zs&sm;ZH9@qV#ptpDeF3Vm4g)%j?8&w%H5s9z$|w2jIoz!f47WI;>N5SJFQGMvMfo| zlZ_5Lq2{n@%3D=^{nvg~eA_lWefaNHdZWY%!&O(;Oq`p;#Nf?P@#*U-j<;{pU%THv zdP}T-hwa+D%Gb9m)5-&Y*_oR5)YDQjBYv?#>lWa7Z^PJm0)76#- zpFdmk+vSbc`jx7>kNl^;j{P9!u;;Pvdew8QFI*8kQqk4h^z;F@W%IML1=H3(s?k31 zfX#Zx{uRI9v$4I{%XnXenJrAO>2kvDRaQR^iyoN$&9e6&gTNs+l?VSTuJzGa7Lh$@q2UnVXTAG^X{B}~{ z;tBuq1TTBUl|8a5l9t%w)R&u@n!Mt=(BINoy&i9v7{1)tmd3F;|G}HfUv};0X;?3K z+9H=}iQ$HMHfB$D)lSIWyxZ{pyLCI3%Dy?UX!m(hg_pazAH9n`%-Z^Zt$ea?RKD2f zt=FU@w(l2j`cTWx`haozn*;wNKO|X8J>+WlI4u6JfWyV+QPMZYo&TD3vzt~(GxOz_ zv#KnAwPD7G$Bg_xPyBq9G5NXmB29CSL(zUyrysVtBVM-ZMB`h}mIRrnGU-3gR_ck& zKfLE;%G{qez5713D90)tPpdj_`|qPq-_?_SzQ$K|r+;p=h$uSiS9N^;y_f4JhrQ8K z-+k+6>?__SwiW{4)g$Gzuhq)ivNrf16kS)49pe7w-Px1X%X@cxzx2)aWj+6&2i*4) zCMn6;-`JBc6=f5+Fa6+q^Y)krto}b%dHa=woiDxlu=fnphN+*^zSz9*wEA^&_Ptlj z`&V2O-v6UQ%5cy8bBB^0=l{Mnecn?SHp6>GGZTIviu$osb>G$A{hy!I|8PalEJ>ba~esOyL@?YOo<9|h4^X*&W`sBUke7E=Eaxa^=?`41ZKH{hN z#WT;$*uRzM@Jy>;y1jn7OVq97Uf)7XwAhol%2W7`)Gsj&U7cDG{JlY5tdVz{v$|VU z+E%|zlcMe=b<5Q}Sw1%Qi8hId2Jo#Ye%RimS>e?GH9|72>79BrbEA`KM3ni8M#~k= zCD)tNLR*%#Hj6&s=-W`0cO=0jqs5)0#dAkXv3d(1TkHJmjjRh>rl_}^zLpT7(a3bL z)$m2DmwKC)c~!&mnlz2pTVae@GnjI2v`HRJSiHO?JEFDZhd`M`MTJIt)sFU>h_0fsWUSx zv$CkLvM92%$#8K=^74ua2#5&@iHV4aiHV7ei-UVLQX&#E{Nf56Vk)qiJ8hJ{kgz6$ zkP3sCEQ7QdgRCeB$xATksWO=BgS#}~j*XQOgQYPeXep^VisV%p$oq(aOguaEm zuBocFk*1cumX@BDrk;+buAz#$t(26ju%H(|j~@?*KQ~(d7s}*a5I0LWA9JiAQ-UyK ziYQZoBx}7K#{?DbIU0ORwFK7bifq=C*sd=Fnwv9_-DN1h$3%Ibh1vld?Gw&gM;r|H zIhky8HlN|*l;{$y=8^(gxr#zMr^_NC&;nP;GFR~Q8K{fmk}d^Ck}m03kZZbxTZV*3 zhPYRTaB!wTY!+8;4r_HDQ*$0eJC1G)Xrir~_8m;r1 z9rD{;vfJD;+dVQnJTgGYGqb}lt1Tp_IU=tquAnigxFxfqBd@lntaVCb->jbLi>A+8 zws`sK)oa&p-n@CsmMvSiZr!$R+wL735A0fdbnl8&`VthY|)#e@Tt!0C?Jt8QZ#3iG(8^rrAELVl0(hI40}KcFpnJtmZjcBXW~TB2ya2 zOjFRhQ-c5n+o}Mq*i~0Ff|pq-^3Ao{`r_ge|Cv^2XH8xjvMRyVD5h)5>d4K}y_1w! zP1Z#BGM-4X_1>PTFeU4zS{Qab4)+pJ6&V^c<e9NW+4h-`RvXdQ2L*j}r553lumUhj>qFw0%FXy&@J z)w8ZV+p??mb?v7o-!DAyZfB2*{$r7O@2I3vxt_=`saN-V|Fs|QG~1c8_x!xv=XRH^ ze|%qZqWGxa_2OS&-`E?m#M{<}oSE zHf5)-NNlrW&6pMR%{F*pYe8;?QoBy&kHiX|rRGYVb&g9e_o%OZ7!2*IRdU z`FutJt>@mxpInwNj8%M<=C8GLOUB~Is>PMc2e%!99#&g41IjQYJ{j&{kLkH=3+Exju&8a=0A_y1G3 zpB~p)FHoeQJU!>r$!$|tuk@6)UiA5#Rj`xiaXTZEV-oC-%r|eDd2`LA!^%(2d_HH# z{C4RD7w5z;7d^~f7#LYg-hREtCUIfwc{}^RTe!4pddzOgCiAXzj;{V|%9z;iyX~gU z=C>dJ-O615U|W1_{@+8~hDQv3Jk&4gu{a;NzQOE~UA@xJC%ON1b04tq#92IQ)~>U7 zY;V1e>B$ryzn%A|Xz8V ztA4!X^}g!&`=qawK5X55uIl&&=lYr#SDgE;Rjri!%0BNG57_&~I3@n?SNZV$wcp$8 z`RyEIv;F=&=r5jEeZ1;4fA!5ilYjd@vrd@*-7VezcIUa`SJjOF|9s!iEW^Oa(INkE zLHjlqvA%lQ+4t`KGP9n)^L;H}%L10w;f`;81)8-lDEzaCIKcmluZg?kAyb39JyYKL zf~}urUdudS(9Kxr@Q?M8^rIQmtt3uOXwzSDpEr4e^G(itHk&Vs0{az?92UHo%QxjA zlZry4gwut_o44xKrznc=?O-rimry@NLRs}fnwv?T zBX?v%vNV6kuKkjM-Nknnv3OP3xt~~{9DO^ARqFw>{G7lEtW6Jo^JyN})0#B#TJaMW zMah$%VV@?=xU%H^qZLQPPJJljI`f#Nv~8R6jZYkrCz>>C4P52s1x{U=yi_}y(|i8O z{%Kk}o(eMTJRN8CU^<6}5>uP!S=IwVJz+5iRn#kw8NZvU z-}u3oz2$QMW0mDfhbt_S>&^*g{JX8Ds(tdbe#7T!E1oP=T(0S_cS(45v-2{$pEu9f z@7wIaWGkiqG}BjIZ(^;i^NSmxaIOR(#PvTz!b@&hups33@q0R=2nDZTYl%_2ECOST?C&+ZeWK4Tr^R z-OHl0U6fHcXtwg3r{#Y4zS8DCSs!bbMY;>**c{9`+ zZDZ;S)efs)4n37BMxpIlnDc<1TB!b_Tw0jVvfkX2 zhk0`vPL`z^O`hu`ZKQwTOrTq_$<>RA;p-~fimQzo8F)M#F5P6TyymdvyH47zBY`v5 ziJ1K1DtPuIDRSfFH&Hh#Rveqh6mfpiKI2v4drhy#{F*lN+mg=R*Is@+wqx^ck*$o2 zPFypd7j=GeoaxfvOb7Ml7!GagF_kr6o9t4x>2U07m88SdQl!*>-nwz_+qE}$vi6Bs zFF#?|v`O3`+t2;xP3|<~j#`6U=8)M(ZuWiOx_xhB9{bbV_xa3Q=Faut;$Ct=v+aG; zZn3QOM{g_NaND{!z3F|@>)reRd|R@gllO7fs%wWgu5Gx(H*aFg>&k};58ogDXSnvn zbG|4AyJeaOO3xNfR4BWlDERZlsGWwq{yW(h6%EVFo%=_)y&dD`R z+vXqGTI6kF|J0UYRl`{ZgX0}n6MciX-kxyQ>3MRWB%?=UGE*#Bn6)j#nTJk4ytvt}u^ROjALMX_hwI;ANY({I;j#jb8R_2GT zaVJ_u1>0URwE1ha$vd_&Eo{s15X?EzmiwYDeMeidM7vyKTbYNjA4hxTjn-<7Hgda2 z$DK@%JDDDJFgjqVwcA8_r-3|lzC>Scv%b_~ZNWALu3T}}6cMI4L8b^k<{)mCKrZOg zK|f9wH+E)A7A6BG23QxDX=($dq?1|uyHS5r$* zU0YvCS6|G~fY;QJ)xwYwv=b1#84!K#r7?q*DWjzYv$+kcxg)2AC!bZ2kWHkRU7UnN zl9Xc#sEg#1Aqztwu5$*M1a*`oT{1w(HABKZQ`{?4I5dkVBbT*0pRuI?+U4mgWB?)1 zbPkBoS;)~^EL~HfRa|9}Q|pjX?~+#Up4#A%T(cILBlQ(v5) z`09M$>kGYaF7&*;2tj>sE=_!MY3i%XvtC|V@cinE%Qv?~y}P3H#J*ta_IZvPe&y)k$0cK65k zWNXaVU)x)w{;Tx&mW{I3Ve*fsdPjVSwXaiS?wcj?=+XY(`CjXcZ+%L*TxyujcY5dM z!}DFcI@)|@eakph95{cOjCo1+b*{wI(@b_*==icn)tFV96I4Qhb&i2dK)R&42 zn-(PLGY9cK5q%UE)`92Zl=?$#Mk@QK<=y%$KGrG;?3Ns!+ktq^8&^_Urmh z|B$1{ZnPT+r5|tq#CuIPc19%IjTYfBi-)c2Hd?fFFF1PQMzYDZbmdx$Ukj5XY!>}k z+_jETLV8l)Sq;u)Cao8$^7bFO#ZY@fUOYRj@&FL^^Q&v$0)y1Kwsq-w>Sow{6cF+FkY zHx}2pX;pZLeGv9x10W*Ni4tL`_1vii+RhWTC|o%ibkziZF;SJ?sC2+ORaS~ zg;*YhtzpU%t#-L28Xa@_o8at4ArCcqSDu`wbvN>C*sq+(i|0b$?sQGCZrOIHOnbtF zhDAr^Lfax;pX_z*@qRf&Z$_rtl9Pv`SMEu@c$`K~o#dy=MR zt@^2te&*v*^TjzI)*2jt z6Mpn#Qm97MzF)J7cNBi)`FLXDzZ}hTa_q7uO9Pmz*2xsO^oAY)Shti8zOoxN6=M*g!b?z+^+@7_!=sZ~NJZgMA!IT_O3kUpELx z3TrO8!x#IhkNtSf`_ODv+ppK7=hq&(U0yGDzfb%8PJg2fx&QzESib(mm&@_`dp>*+ z?)bCF+5V5@g}Psl<$rDXZhtdVujbW>y9}k}`wb#KG$l(kvnG}wGb_{5&T7 z4qad0wc!Cvb%d?PJ^rq%*-0$lBpTK9Ha5G~C@Wb-9G$sjLT=NVc`~690xnvI`o0}r ztn}rCyL}wPgfsya#p^qq-8T956!tt~k?r){=6JEm`$;kjS3m=^OoCJW9t9c69}dkm zMqLYJmU4(x9#oq3sb|HW2Cc}OUJ-FY)3`%i6mNF8ia#>qxjb23Gq&P%!m6VFbtS1& zW@+qEV`8s4_axQCcIWAUvJJBmJ)W9w{^=!GXk2mc&peK0jQY z3K~m#)i`bF^!>Ax_2w?y%%iiYp0ZK{ikmr7V<@Me7o%4J)yjOptuUwMwuxn82Zc57d; zuG6}*Ca!pOY_59HX35K(y`syN3Z6ykt~gM*X!44~F|WK=2efh?HCgN~x;of?R`}j1 zgH;#aWOM6sMA#*jY-kPDGK}6Cx$IWy#)_>EG~+9d@puXKeP5ly(vcN%PUzdxLfd~i zZmd=dZSP&}>Xp?M@vT1QvMiKkRjRHJ^RK8IMP)0G+vep8uDWn{*0+sms&Bt8bH7rz zYt#GhWr)zmu+Q{HLmA%^Y(lY}K^`><%A-?5pF0 z?-ngle!HOZ?1EdA~ z-v!N%x)63QLgim6NA$wmj-{)NcF&l5AuKZI;;f>IsjL=79+U1&P;F=2C+{iz2Eg|DI=eO_G_E_Jo|SXpYrB|*FBl^|L10& zV^39|_jylxqFl+;y0leE+%}D7^({x`PHb6bem2v0`-b@~s%uIQp7J`x_G4*HnPJ3i zugvhMS;{lsZCPXfH&eJ}7uVu>*(+BUWJ!ORIhBL$_0-g!`;%9cT|05DWMTE)Tit#d z=fhs8tKM9hxI|BMsl~inRy)Aof8{P*t6Le_+eq&H}8LoO%@iLe-xU2pyiaI;m2}w!N+X*oX4O4ecP{X{iIUP z@}zGcn6>78$RS(be}r&a8WXm$%P@0i9b%l*D~CeOa)B)8*YXlKc+ z^S7QUv~Rw=mfh@ZGY`$oRl2qM-L>XJHB1MN z8EwB?Q^J2@@4MptIZlAg#VYxJ~_Eu=*?&%OTpIcWb5M`eYoBXSL2G&H}w64n0};4b^EKYaJqrT6^zy@hRXps(iI=X=){zDxhS{ontekDS|ob^8DPn!mR8-R$|V?mh2N zw|OY|A^iWZ1?Tq7aeYwHxx#_z}vDe?Yo}Uw3FIZUrb9=p(gV5`A z!FTPIkEU1s6-jU@tq(a~FMiSg*YrBYiw&Y58_btiTz7BOO>GQrZ{*8hlGdob&)zWe zc-3Du0X2zQiG_`R7g&4`G#-22#5kkMa0Q?74kjxLL9>o#=N--Z7ImQ)oL?Mol1glL z-yx=;(InJS>tj(G=ph*JqD40%Ls%mrw4k1~qAb#*MNOg9qN8cu!&cqa@+6P?loPF{ z6>ZuR+PHTJ%DrfOo6eNIf$g4q$cFH?D35kGjdt;g?Wg~3Z?FB)t}EKnX3?H8qq%=a zn{KNBhex}~kNSxk9i5IL(>m&BXtYe8(KOql%P?7BUPWffiu$aIF0P4P)f%O1D%#g+ z6t`$}Zsibu8s4$O>#$6g4Yi0C{$qvPU@9?63} zuN*>b4ls9f^uA8-y`mv_t)utWi}pJ=dhf63x;dja=Kx#nj=sN7dY{h_{5z%3W<%fG z6KwA_dOt=8-hELox`OHTb^q_|{l7W-o>la!a!i<~FoBCxklcMGG;8 z@*=u+ew-|RoGh;F%;uo39it8-s8a`7U8u~$tiZ}5$I2qf$}Ggp#LEcX4#$kXz!1U* zAL#F|r7-unVwr3UKj&I(XdtLZCjLun@bLFpH!Jql_qnj2MF~7|Dn; zNXapXse!tAh$V;cDK{-2}uNykdzdYk>=GkEOW&E$6IgAsTYA!PmFehbYbb_U0t zOinnPK@fQTpgDB?pr_%@AhpNQ(r=Q5K4tQJ%jNh{!1}$A^?f1N<3iyJ1&X`!43_2E z&B}3`nB&`<6VL@c#3ZOAJGdnb9c0-cgoIAZxu#2krr*-Vy)s0CGx?IU*-LVn>+={P8{2R!A%v_V(N`DN$4}I{D_Ej-ZF1DkGS<`Ev_fUAPJObGE?TA$n04Lltn`<> z_&w2zj$NML7snom=`DLY`(%Fdk(n&QdV37j9`EkccTPJK`YGm=sP5;Zdm?kw&dp`7 zXPYa``fC5I&u+71Jq_O8YEryA@64}-8CRC?WIJDS+wAR~U74TX?G5N6+#SSJr=gIBy}*= zNbwz67*U=3bVjoEk*^YU0+-AmwtV)DNNoLj&|I-gYMqB@LxUyT^>~wO<%{Jk`%Wm= z+Pz{-X}XiUiL=||;5GS#NT;Jo9YT61m-H$hQ+H?$^gB5xBP_`?to&cOl#nzBHeVT61n=58zrcJhvyZn4!eV)*Y1uaZzE22xyR=#LRH2d|! zH0zt#<=EWbUvt9)8&zNMC493yvHIVJeOb>Y1v+)jTe)V_GxG+d;@@D>XN7gn!iEY=I`5`cR?V{_mR>u06 z=pD}s(fO7Y+JAXg_Irg7XSb(sYu%eYtxH2gEI(r3HH-M2-*3%cFaBF;_q$4Iww{+q zE=av$J#-~(!N+4#r?up3dF^{H?9OWny|;t?y0*@7nd+GM6PkxqKCa5G%``mrOl=a! z@iPXCKOXw^?nM5khkP-HdpTd9*?eA<;p~PBe8**iPv+e0Fl%iO!a@2$!{TYjIf zRiKGOspsgA#}nN3EFYVNu_pU)>J>+Ry5WqzbWB+x^#^-RR2m z>(w>x{$C%1uI#J*v`t_B_dDY=0(+eJm@yJcTxwcGsK%UAW&`ozC0*X{pjzHemeVPaf*z$RQ~LJ6md$5W*nyV#i~)CQh# zU^8$yBy{S0-5!m2Hq#7dKC=(a@3|jxczYZY)_Y&4E#vWX$8q~NJI|N?GqGt9XxDI& zx|CnD$032OT*F#m-TC(Q+)9G&87?0b13NkIJkW_-zKd^_UstfqBgu0KY{mx|`MR|4 zKP~bQ&@C#gc*~qD!EfMZG|#ZtI6_%^tH%B}odJbPa~4aS_BdfQ?;;~#MT?UBK?}20 z&HZ9E59FN%JRIc~OlY-9Q4+s-LNv^;(ziyLQ@7I7xM$;pNaCo`rjksO!BNr zJuPdJ^>}xBv0HqeKINd2^9+fD{C!1TcWfR@2u7X_O4~I1K1;8KxYIe^ssOI%|5TE! z9&Yes-%>ntR!_Qx;fV>SAHJ*pmmJH{vcuo^)#q8et}IcO`FLjYnaQnDJqrwCmj|@1 z`qaZGt7NoZGq9^Ecpl5uL^<7Zmue@$4vByl+;I#+6Ezc$kQGM2NEY z+83AP7xlFmEzR`ZebdkA)t1>^Z&vD8Gh3RU%dh-c%&ObEJmBbA5$=c!Da=vBh3&p^b$#2`*~z-Ajn;RDRjevq_2Q&P4Et}lE1^Lh##^1$~M@Yk<0P5K6dk3*_Q2RbIr?NbA*4prv7=2 z%;O!~FJ>MqS*Vn%mMf_pr6{(!=crDar|8V617@nb%x2^Xua34^l5~CXtS5Qph20Dv z+RFFzYviXE8tt0Gwthok!yOgT@8>Q&+q!S5>+&*d@LEEHzp5wdO}nia(;XJF-h0=m zXzglUcKM*fy90M5wuheJOgXS-wx&7n9ftRhLOMFGED;1PSGA9DR$)tQl`nt9ef=w| z_O*iUm)=Jt$~PbLFgw&++wHjU-<7?drh4W1xBQf|g*YSJoO(B|31BmhRPak%?XY0Z zqw`6MTaNSU|MOdW=d4cl%wsWWbEii(JBX@1nRE2Zs+o~%(p)x4PV}5tI7jlhrQjLK zLz-KSoj&w1*n0mwaP-S&<}9uk4*XFpt#(`deQYLl^QG)qI%n+?)!JhU+)|u9$r~Oy z-ANTpZoSO(@ym?dl?j3Uw@xfGW11Fxw=uMN$JLI-4^=uX+`OzCI?_e-JtXI(w5_Nr zY%446YI&QSH2>g@6K&0#&Y#FpvEF_C!ZnkO3=gNDeAe4;*d37kz3OD%`G`C0m1gU= zI_L4s*nR88wwD{P^0JrJ?_f5YR_63X_HE(xfPJ4-H$Qr?I$!+tHO6;gn-1)^%|CTP z;NhK6i?Z*X9_{+#j~JF4?)fKr=t0-~rFSIUcN|NP%Bj6SOF>Zn=K*U5%bB}bo@$=| zQ9Ak7r*?0tXNvXTkBA3-I(~DHs)fI0S7lvpt+L89=l^CKHcl$((cgQ<_WiTV&3c8M zHn*?0uH1F5^~P0!ifcEbk8ipl`{=2EAg*Oe_lzMDDgeUR!ytHHwebC4Qy_?3+3B-}%Z;d-)%S-dlft*~=)Jd}-Fn`?as6 zWj?&Gw)^zNyk7C}3ZEH3yQ}prE-qPO4X@1` zWCI#FOB&{}rU*<;IMwFCvXG-oL4a>s`98C{=gN)qo`&3bQYc{|Aa$aV_gSN&K$4|P z+C0{<-&cy%GXw5a}U>rh|2td9;s3axzDK)CxYnXRRxi zSN45xbGe>Sw4$!$Mn&0)GKFus>yEQDonX= 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