diff --git a/Prowl.Runtime/Assets/Defaults/Line.shader b/Prowl.Runtime/Assets/Defaults/Line.shader new file mode 100644 index 00000000..3c4f0655 --- /dev/null +++ b/Prowl.Runtime/Assets/Defaults/Line.shader @@ -0,0 +1,84 @@ +Shader "Default/Line" + +Properties +{ + _MainTex ("Texture", Texture2D) = "white" + _StartColor ("Start Color", Color) = (1.0, 1.0, 1.0, 1.0) + _EndColor ("End Color", Color) = (1.0, 1.0, 1.0, 1.0) +} + +Pass "Line" +{ + Tags { "RenderOrder" = "Transparent" } + Cull Off + Blend Alpha + + GLSLPROGRAM + Vertex + { + #include "Fragment" + #include "VertexAttributes" + + out vec2 texCoord0; + out vec3 worldPos; + out vec4 currentPos; + out vec4 previousPos; + out float fogCoord; + out vec4 vColor; + + void main() + { + gl_Position = PROWL_MATRIX_MVP * vec4(vertexPosition, 1.0); + fogCoord = gl_Position.z; + currentPos = gl_Position; + texCoord0 = vertexTexCoord0; + + vec4 prevWorldPos = PROWL_MATRIX_M_PREVIOUS * vec4(vertexPosition, 1.0); + previousPos = PROWL_MATRIX_VP_PREVIOUS * prevWorldPos; + + worldPos = (PROWL_MATRIX_M * vec4(vertexPosition, 1.0)).xyz; + vColor = vertexColor; + } + } + + Fragment + { + #include "Fragment" + + layout (location = 0) out vec4 gAlbedo; + layout (location = 1) out vec4 gMotionVector; + layout (location = 2) out vec4 gNormal; + layout (location = 3) out vec4 gSurface; + + in vec2 texCoord0; + in vec3 worldPos; + in vec4 currentPos; + in vec4 previousPos; + in float fogCoord; + in vec4 vColor; + + uniform sampler2D _MainTex; + + void main() + { + vec2 curNDC = (currentPos.xy / currentPos.w) - _CameraJitter; + vec2 prevNDC = (previousPos.xy / previousPos.w) - _CameraPreviousJitter; + gMotionVector = vec4((curNDC - prevNDC) * 0.5, 0.0, 1.0); + + vec4 albedo = texture(_MainTex, texCoord0) * vColor; + + // Lines don't have meaningful normals in billboarded mode + gNormal = vec4(0.0, 0.0, 1.0, 1.0); + + // Unlit surface properties + gSurface = vec4(1.0, 0.0, 0.0, 1.0); + + vec3 baseColor = albedo.rgb; + baseColor.rgb = GammaToLinearSpace(baseColor.rgb); + + gAlbedo = vec4(baseColor, albedo.a); + gAlbedo.rgb = ApplyFog(fogCoord, gAlbedo.rgb); + } + } + ENDGLSL +} diff --git a/Prowl.Runtime/Assets/Defaults/Unlit.shader b/Prowl.Runtime/Assets/Defaults/Unlit.shader new file mode 100644 index 00000000..440b117c --- /dev/null +++ b/Prowl.Runtime/Assets/Defaults/Unlit.shader @@ -0,0 +1,131 @@ +Shader "Default/Unlit" + +Properties +{ + _MainTex ("Texture", Texture2D) = "white" + _MainColor ("Tint", Color) = (1.0, 1.0, 1.0, 1.0) +} + +Pass "Unlit" +{ + Tags { "RenderOrder" = "Opaque" } + Cull Back + + GLSLPROGRAM + Vertex + { + #include "Fragment" + #include "VertexAttributes" + + out vec2 texCoord0; + out vec3 worldPos; + out vec4 currentPos; + out vec4 previousPos; + out float fogCoord; + out vec4 vColor; + out vec3 vNormal; + + void main() + { +#ifdef SKINNED + vec4 skinnedPos = GetSkinnedPosition(vertexPosition); + vec3 skinnedNormal = GetSkinnedNormal(vertexNormal); + + gl_Position = PROWL_MATRIX_MVP * skinnedPos; + fogCoord = gl_Position.z; + currentPos = gl_Position; + texCoord0 = vertexTexCoord0; + + vec4 prevWorldPos = PROWL_MATRIX_M_PREVIOUS * skinnedPos; + previousPos = PROWL_MATRIX_VP_PREVIOUS * prevWorldPos; + + worldPos = (PROWL_MATRIX_M * skinnedPos).xyz; + vColor = vertexColor; + vNormal = normalize(mat3(PROWL_MATRIX_M) * skinnedNormal); +#else + gl_Position = PROWL_MATRIX_MVP * vec4(vertexPosition, 1.0); + fogCoord = gl_Position.z; + currentPos = gl_Position; + texCoord0 = vertexTexCoord0; + + vec4 prevWorldPos = PROWL_MATRIX_M_PREVIOUS * vec4(vertexPosition, 1.0); + previousPos = PROWL_MATRIX_VP_PREVIOUS * prevWorldPos; + + worldPos = (PROWL_MATRIX_M * vec4(vertexPosition, 1.0)).xyz; + vColor = vertexColor; + vNormal = normalize(mat3(PROWL_MATRIX_M) * vertexNormal); +#endif + } + } + + Fragment + { + #include "Fragment" + + layout (location = 0) out vec4 gAlbedo; + layout (location = 1) out vec4 gMotionVector; + layout (location = 2) out vec4 gNormal; + layout (location = 3) out vec4 gSurface; + + in vec2 texCoord0; + in vec3 worldPos; + in vec4 currentPos; + in vec4 previousPos; + in float fogCoord; + in vec4 vColor; + in vec3 vNormal; + + uniform sampler2D _MainTex; + + void main() + { + vec2 curNDC = (currentPos.xy / currentPos.w) - _CameraJitter; + vec2 prevNDC = (previousPos.xy / previousPos.w) - _CameraPreviousJitter; + gMotionVector = vec4((curNDC - prevNDC) * 0.5, 0.0, 1.0); + + vec4 albedo = texture(_MainTex, texCoord0) * vColor; + + vec3 viewNormal = normalize(mat3(PROWL_MATRIX_V) * vNormal); + gNormal = vec4(viewNormal, 1.0); + + gSurface = vec4(1.0, 0.0, 0.0, 1.0); + + vec3 baseColor = albedo.rgb; + baseColor.rgb = GammaToLinearSpace(baseColor.rgb); + + gAlbedo = vec4(baseColor, albedo.a); + gAlbedo.rgb = ApplyFog(fogCoord, gAlbedo.rgb); + } + } + ENDGLSL +} + +Pass "UnlitMotionVector" +{ + Tags { "RenderOrder" = "DepthOnly" } + Cull Back + + GLSLPROGRAM + Vertex + { + #include "Fragment" + #include "VertexAttributes" + + void main() + { +#ifdef SKINNED + vec4 skinnedPos = GetSkinnedPosition(vertexPosition); + gl_Position = PROWL_MATRIX_MVP * skinnedPos; +#else + gl_Position = PROWL_MATRIX_MVP * vec4(vertexPosition, 1.0); +#endif + } + } + + Fragment + { + #include "Fragment" + void main() {} + } + ENDGLSL +} diff --git a/Prowl.Runtime/Components/LineRenderer.cs b/Prowl.Runtime/Components/LineRenderer.cs new file mode 100644 index 00000000..5b74982f --- /dev/null +++ b/Prowl.Runtime/Components/LineRenderer.cs @@ -0,0 +1,370 @@ +// This file is part of the Prowl Game Engine +// Licensed under the MIT License. See the LICENSE file in the project root for details. + +using System; +using System.Collections.Generic; +using Prowl.Runtime.Rendering; +using Prowl.Runtime.Resources; +using Prowl.Runtime.GraphicsBackend.Primitives; +using Prowl.Vector; +using Prowl.Vector.Geometry; + +namespace Prowl.Runtime; + +public class LineRenderer : MonoBehaviour, IRenderable +{ + public Material Material; + public float StartWidth = 0.1f; + public float EndWidth = 0.1f; + public List Points = new(); + public bool Loop = false; + public Color StartColor = Color.white; + public Color EndColor = Color.white; + public TextureWrapMode TextureMode = TextureWrapMode.Stretch; + public float TextureTiling = 1.0f; // Controls UV tiling for Tile mode + public bool RecalculateNormals = false; + + private Mesh? _cachedMesh; + private ViewerData _lastViewer; + private bool _meshGenerated; + private bool _isDirty = true; + private AABBD _bounds; + + // Cached state for change detection + private List _lastPoints; + private float _lastStartWidth; + private float _lastEndWidth; + private bool _lastLoop; + private Color _lastStartColor; + private Color _lastEndColor; + private TextureWrapMode _lastTextureMode; + private float _lastTextureTiling; + + public override void Awake() + { + _lastPoints = new List(); + _lastStartColor = StartColor; + _lastEndColor = EndColor; + _lastStartWidth = StartWidth; + _lastEndWidth = EndWidth; + _lastTextureMode = TextureMode; + _lastTextureTiling = TextureTiling; + } + + public override void Update() + { + if (Material != null && Points != null && Points.Count >= 2) + { + // Check if we need to regenerate + bool needsUpdate = _isDirty || + _cachedMesh == null || + StartWidth != _lastStartWidth || + EndWidth != _lastEndWidth || + Loop != _lastLoop || + !StartColor.Equals(_lastStartColor) || + !EndColor.Equals(_lastEndColor) || + TextureMode != _lastTextureMode || + Math.Abs(TextureTiling - _lastTextureTiling) > 0.001f || + !PointsEqual(_lastPoints, Points); + + if (needsUpdate) + { + // Update cached state + _lastPoints = new List(Points); + _lastStartWidth = StartWidth; + _lastEndWidth = EndWidth; + _lastLoop = Loop; + _lastStartColor = StartColor; + _lastEndColor = EndColor; + _lastTextureMode = TextureMode; + _lastTextureTiling = TextureTiling; + _isDirty = true; // Flag for next render + + CalculateBounds(); + } + + // Always push the renderable (IRenderable interface handles actual rendering) + GameObject.Scene.PushRenderable(this); + } + } + + private bool PointsEqual(List a, List b) + { + if (a.Count != b.Count) return false; + + for (int i = 0; i < a.Count; i++) + { + if (!a[i].Equals(b[i])) return false; + } + + return true; + } + + private void CalculateBounds() + { + if (Points == null || Points.Count == 0) + { + _bounds = new AABBD(); + return; + } + + // Transform points to world space for bounds calculation + Double3 min = Transform.TransformPoint(Points[0]); + Double3 max = min; + + foreach (var point in Points) + { + Double3 worldPoint = Transform.TransformPoint(point); + min = Maths.Min(min, worldPoint); + max = Maths.Max(max, worldPoint); + } + + // Expand bounds by maximum line width + double maxWidth = Math.Max(StartWidth, EndWidth); + Double3 expansion = new Double3(maxWidth, maxWidth, maxWidth); + min -= expansion; + max += expansion; + + _bounds = new AABBD(min, max); + } + + public void MarkDirty() + { + _isDirty = true; + } + + // Helper methods for point manipulation + public void SetPosition(int index, Double3 position) + { + if (index >= 0 && index < Points.Count) + { + Points[index] = position; + _isDirty = true; + } + } + + public Double3 GetPosition(int index) + { + if (index >= 0 && index < Points.Count) + return Points[index]; + return Double3.Zero; + } + + public void SetPositions(List positions) + { + Points = new List(positions); + _isDirty = true; + } + + public void SetPositions(Double3[] positions) + { + Points = new List(positions); + _isDirty = true; + } + + public override void OnDisable() + { + // Clean up the mesh when disabled + _cachedMesh?.OnDispose(); + _cachedMesh = null; + } + + #region IRenderable Implementation + + public Material GetMaterial() => Material; + public int GetLayer() => GameObject.layerIndex; + + public void GetRenderingData(ViewerData viewer, out PropertyState properties, out Mesh drawData, out Double4x4 model) + { + // Create mesh only once + if (_cachedMesh == null) + { + _cachedMesh = new Mesh(); + } + + // Always regenerate for smooth billboarding (billboard lines always face camera) + UpdateBillboardedMesh(_cachedMesh, viewer); + + _lastViewer = viewer; + _meshGenerated = true; + _isDirty = false; + + // Setup properties + properties = new PropertyState(); + properties.SetInt("_ObjectID", InstanceID); + properties.SetColor("_StartColor", StartColor); + properties.SetColor("_EndColor", EndColor); + + drawData = _cachedMesh!; + model = Double4x4.Identity; // Vertices are already in world space + } + + public void GetCullingData(out bool isRenderable, out AABBD bounds) + { + isRenderable = Points != null && Points.Count >= 2 && Material != null; + bounds = _bounds; + } + + #endregion + + private void UpdateBillboardedMesh(Mesh mesh, ViewerData viewer) + { + if (Points.Count < 2) + return; + + // Transform points to world space + List worldPoints = new List(Points.Count); + foreach (var point in Points) + { + worldPoints.Add(Transform.TransformPoint(point)); + } + + // Add loop point if needed + if (Loop && worldPoints.Count > 2) + { + worldPoints.Add(worldPoints[0]); + } + + int segmentCount = worldPoints.Count - 1; + int vertexCount = worldPoints.Count * 2; + int triangleCount = segmentCount * 2; + + Float3[] vertices = new Float3[vertexCount]; + Float2[] uvs = new Float2[vertexCount]; + Color[] colors = new Color[vertexCount]; + uint[] indices = new uint[triangleCount * 3]; + + // Calculate total line length for distance-based UV mapping + float totalLength = 0f; + float[] segmentLengths = new float[segmentCount]; + + if (TextureMode == TextureWrapMode.RepeatPerSegment || TextureMode == TextureWrapMode.Tile) + { + for (int i = 0; i < segmentCount; i++) + { + float length = (float)Maths.Distance(worldPoints[i], worldPoints[i + 1]); + segmentLengths[i] = length; + totalLength += length; + } + } + + // Generate vertices + float accumulatedLength = 0f; + + for (int i = 0; i < worldPoints.Count; i++) + { + Double3 point = worldPoints[i]; + + // Calculate line direction + Double3 lineDir; + if (i == 0) + { + lineDir = Maths.Normalize(worldPoints[i + 1] - point); + } + else if (i == worldPoints.Count - 1) + { + lineDir = Maths.Normalize(point - worldPoints[i - 1]); + } + else + { + lineDir = Maths.Normalize((worldPoints[i + 1] - worldPoints[i - 1]) * 0.5); + } + + // Calculate perpendicular vector (billboard direction towards camera) + Double3 toCamera = Maths.Normalize(viewer.Position - point); + Double3 right = Maths.Normalize(Maths.Cross(toCamera, lineDir)); + + // If cross product is near zero (line points at camera), use camera up vector + if (Maths.LengthSquared(right) < 0.001) + { + right = Maths.Normalize(Maths.Cross(viewer.Up, lineDir)); + } + + // Interpolate width along the line + float t = i / (float)(worldPoints.Count - 1); + float width = Maths.Lerp(StartWidth, EndWidth, t); + float halfWidth = width * 0.5f; + + // Create offset vertices + Double3 offset = right * halfWidth; + + vertices[i * 2] = (Float3)(point - offset); + vertices[i * 2 + 1] = (Float3)(point + offset); + + // Calculate U coordinate based on texture mode + float u = CalculateUCoordinate(i, worldPoints.Count, accumulatedLength, totalLength); + + uvs[i * 2] = new Float2(u, 0); + uvs[i * 2 + 1] = new Float2(u, 1); + + // Update accumulated length for next iteration + if (i < segmentCount) + { + accumulatedLength += segmentLengths[i]; + } + + // Colors (interpolate from start to end) + Color color = Maths.Lerp(StartColor, EndColor, t); + colors[i * 2] = color; + colors[i * 2 + 1] = color; + } + + // Generate triangles + int triIndex = 0; + for (int i = 0; i < segmentCount; i++) + { + uint baseVertex = (uint)(i * 2); + + // First triangle + indices[triIndex++] = baseVertex; + indices[triIndex++] = baseVertex + 2; + indices[triIndex++] = baseVertex + 1; + + // Second triangle + indices[triIndex++] = baseVertex + 1; + indices[triIndex++] = baseVertex + 2; + indices[triIndex++] = baseVertex + 3; + } + + // Update the existing mesh instead of creating a new one + mesh.Vertices = vertices; + mesh.UV = uvs; + mesh.Colors = colors; + mesh.Indices = indices; + + if (RecalculateNormals) + { + mesh.RecalculateNormals(); + } + + mesh.RecalculateBounds(); + } + + private float CalculateUCoordinate(int index, int totalPoints, float accumulatedLength, float totalLength) + { + float t = index / (float)(totalPoints - 1); + + return TextureMode switch + { + TextureWrapMode.Stretch => t, // 0 to 1 stretched across entire line + + TextureWrapMode.Tile => totalLength > 0 ? (accumulatedLength / totalLength) * TextureTiling : t, // Repeat based on world distance + + TextureWrapMode.RepeatPerSegment => index, // Each segment gets 0 to 1 + + _ => t + }; + } +} + +public enum TextureWrapMode +{ + /// Stretch texture from start to end (0 to 1) + Stretch, + + /// Tile texture based on world-space distance + Tile, + + /// Repeat texture for each point (creates 0,1,2,3... pattern) + RepeatPerSegment +} diff --git a/Prowl.Runtime/InputManagement/InputAction.cs b/Prowl.Runtime/InputManagement/InputAction.cs index 4820b9c5..a868884f 100644 --- a/Prowl.Runtime/InputManagement/InputAction.cs +++ b/Prowl.Runtime/InputManagement/InputAction.cs @@ -230,9 +230,6 @@ internal void UpdateState(IInputHandler inputHandler, double currentTime) // Read the current value from bindings with interaction logic object newValue = ReadValueFromBindings(inputHandler, currentTime); - // Apply processors - newValue = ApplyProcessors(newValue); - bool valueChanged = !newValue.Equals(_currentValue); _currentValue = newValue; @@ -278,7 +275,17 @@ private object ReadValueFromBindings(IInputHandler inputHandler, double currentT { object compositeValue = composite.ReadValue(inputHandler); if (IsValueActuated(compositeValue)) + { + // Apply processors from the composite + foreach (var processor in composite.Processors) + { + if (compositeValue is float floatValue) + compositeValue = processor.Process(floatValue); + else if (compositeValue is Float2 vectorValue) + compositeValue = processor.Process(vectorValue); + } return compositeValue; + } } // Then check regular bindings with interaction logic @@ -289,6 +296,16 @@ private object ReadValueFromBindings(IInputHandler inputHandler, double currentT _interactionStates[binding] = new InteractionState(); object rawValue = ReadBinding(binding, inputHandler); + + // Apply processors from THIS binding only + foreach (var processor in binding.Processors) + { + if (rawValue is float floatValue) + rawValue = processor.Process(floatValue); + else if (rawValue is Float2 vectorValue) + rawValue = processor.Process(vectorValue); + } + bool isActuated = IsValueActuated(rawValue); // Evaluate interaction and get the result @@ -485,17 +502,8 @@ private object ReadBinding(InputBinding binding, IInputHandler inputHandler) private object ApplyProcessors(object value) { - foreach (var binding in Bindings) - { - foreach (var processor in binding.Processors) - { - if (value is float floatValue) - value = processor.Process(floatValue); - else if (value is Float2 vectorValue) - value = processor.Process(vectorValue); - } - } - + // Don't apply processors from all bindings - this is handled per-binding + // Processors should be applied in ReadValueFromBindings instead return value; } diff --git a/Prowl.Runtime/Resources/DefaultAssets.cs b/Prowl.Runtime/Resources/DefaultAssets.cs index 3107559c..7c96a44a 100644 --- a/Prowl.Runtime/Resources/DefaultAssets.cs +++ b/Prowl.Runtime/Resources/DefaultAssets.cs @@ -9,6 +9,8 @@ namespace Prowl.Runtime.Resources public enum DefaultShader { Standard, + Unlit, + Line, Invalid, UI, Gizmos, diff --git a/Prowl.Runtime/Resources/Shader.cs b/Prowl.Runtime/Resources/Shader.cs index 040a8ebe..7ab8d9a9 100644 --- a/Prowl.Runtime/Resources/Shader.cs +++ b/Prowl.Runtime/Resources/Shader.cs @@ -131,6 +131,8 @@ public static Shader LoadDefault(DefaultShader shader) string fileName = shader switch { DefaultShader.Standard => "Standard.shader", + DefaultShader.Unlit => "Unlit.shader", + DefaultShader.Line => "Line.shader", DefaultShader.Invalid => "Invalid.shader", DefaultShader.UI => "UI.shader", DefaultShader.Gizmos => "Gizmos.shader", diff --git a/Prowl.sln b/Prowl.sln index d7f6c103..e9b5d72c 100644 --- a/Prowl.sln +++ b/Prowl.sln @@ -35,6 +35,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleCube", "Samples\Simpl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhysicsCubes", "Samples\PhysicsCubes\PhysicsCubes.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VoxelEngine", "Samples\VoxelEngine\VoxelEngine.csproj", "{FC5AF18E-169D-4BE0-ABE6-9F7D86EB03A0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -93,6 +95,10 @@ Global {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {FC5AF18E-169D-4BE0-ABE6-9F7D86EB03A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC5AF18E-169D-4BE0-ABE6-9F7D86EB03A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC5AF18E-169D-4BE0-ABE6-9F7D86EB03A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC5AF18E-169D-4BE0-ABE6-9F7D86EB03A0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -110,6 +116,7 @@ Global {DD6F9C77-3B7A-4604-89AD-6275C7A4CCE2} = {33029885-0D82-4603-AC99-8426B0DD3C66} {7EE7D3F1-DE8D-4532-A810-A093D423AEAB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {FC5AF18E-169D-4BE0-ABE6-9F7D86EB03A0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {40438D96-37C1-4E32-A936-74E5406FDE5B} diff --git a/Samples/SimpleCube/Program.cs b/Samples/SimpleCube/Program.cs index 7b3da190..6ae83a6c 100644 --- a/Samples/SimpleCube/Program.cs +++ b/Samples/SimpleCube/Program.cs @@ -2,17 +2,18 @@ // Licensed under the MIT License. See the LICENSE file in the project root for details. // -// SimpleCube Demo - Showcasing Prowl's new Input Action System +// LineRenderer Demo // -// This demo demonstrates: -// - Action-based input with multiple input sources -// - Full keyboard + mouse + gamepad support -// - Input processing (deadzone, normalization, scaling) -// - Seamless switching between input methods +// This demo demonstrates the LineRenderer component with: +// - Billboard rendering (always faces camera) +// - Animated lines with dynamic point updates +// - Color gradients and width variation +// - Looped lines +// - Different texture wrap modes // // Controls: // Movement: WASD / Arrow Keys / Gamepad Left Stick -// Look: Right Mouse + Drag / Gamepad Right Stick +// Look: Mouse Move (when RMB held) / Gamepad Right Stick // Fly Up: E / Gamepad A Button // Fly Down: Q / Gamepad B Button // Sprint: Left Shift / Gamepad Left Stick Click @@ -23,7 +24,7 @@ using Prowl.Runtime.Rendering; using Prowl.Runtime.Resources; -using Silk.NET.Input; +using MouseButton = Prowl.Runtime.MouseButton; namespace SimpleCube; @@ -31,7 +32,7 @@ internal class Program { static void Main(string[] args) { - new MyGame().Run("Demo", 1280, 720); + new MyGame().Run("LineRenderer Demo", 1280, 720); } } @@ -39,134 +40,194 @@ public sealed class MyGame : Game { private GameObject cameraGO; private Scene scene; - private ModelRenderer model; // Input Actions private InputActionMap cameraMap = null!; private InputAction moveAction = null!; private InputAction lookAction = null!; + private InputAction lookEnableAction = null!; private InputAction flyUpAction = null!; private InputAction flyDownAction = null!; private InputAction sprintAction = null!; + // Line Renderer Examples + private LineRenderer helix; + private LineRenderer sineWave; + private LineRenderer orbitalRing; + private float time = 0; + public override void Initialize() { scene = new Scene(); - - // Setup Input Actions SetupInputActions(); // Create directional light GameObject lightGO = new("Directional Light"); - var light = lightGO.AddComponent(); + lightGO.AddComponent(); lightGO.Transform.localEulerAngles = new Double3(-80, 5, 0); scene.Add(lightGO); // Create camera cameraGO = new("Main Camera"); cameraGO.tag = "Main Camera"; - cameraGO.Transform.position = new(0, 0, -10); + cameraGO.Transform.position = new(0, 2, -8); var camera = cameraGO.AddComponent(); camera.Depth = -1; camera.HDR = true; - camera.Effects = new List() { - //new ScreenSpaceReflectionEffect(), - //new KawaseBloomEffect(), - //new BokehDepthOfFieldEffect(), + new FXAAEffect(), + new KawaseBloomEffect(), new TonemapperEffect(), }; - scene.Add(cameraGO); - Mesh cube = Mesh.CreateCube(Double3.One); - Material mat = new Material(Shader.LoadDefault(DefaultShader.Standard)); - - GameObject cubeGO = new GameObject("Cube"); - var mr = cubeGO.AddComponent(); - mr.Mesh = cube; - mr.Material = mat; + // Create ground plane + GameObject groundGO = new("Ground"); + var mr = groundGO.AddComponent(); + mr.Mesh = Mesh.CreateCube(Double3.One); + mr.Material = new Material(Shader.LoadDefault(DefaultShader.Standard)); + groundGO.Transform.position = new(0, -3, 0); + groundGO.Transform.localScale = new(20, 1, 20); + scene.Add(groundGO); - cubeGO.Transform.position = new(0, -1, 0); - cubeGO.Transform.localScale = new(10, 1, 10); + // Create Line Renderer Examples + CreateLineExamples(); - scene.Add(cubeGO); - - //var m = Model.LoadFromFile("Banana Man\\scene.gltf"); - var m = Model.LoadFromFile("glTF-Sponza\\Sponza.gltf"); - model = new GameObject("Model").AddComponent(); - model.Model = m; - scene.Add(model.GameObject); Input.SetCursorVisible(false); } - private void SetupInputActions() - { - // Create input action map - cameraMap = new InputActionMap("Camera"); - // Movement action + private void CreateLineExamples() + { + Texture2D texture = Texture2D.LoadDefault(DefaultTexture.Grid); + Material lineMat = new Material(Shader.LoadDefault(DefaultShader.Line)); + lineMat.SetTexture("_MainTex", texture); + + // 1. Animated Helix with width variation + GameObject helixGO = new("Helix"); + helix = helixGO.AddComponent(); + helix.Material = lineMat; + helix.StartWidth = 0.05f; + helix.EndWidth = 0.15f; + helix.TextureTiling = 10f; + helix.StartColor = new Color(1, 0.2f, 0.2f, 1); // Red + helix.EndColor = new Color(1, 0.8f, 0.2f, 1); // Orange + helix.TextureMode = TextureWrapMode.Tile; + + helix.Points = new List(); + for (int i = 0; i <= 80; i++) { - moveAction = cameraMap.AddAction("Move", InputActionType.Value); - moveAction.ExpectedValueType = typeof(Float2); - - // Add WASD composite - var wasdComp = new Vector2CompositeBinding( - InputBinding.CreateKeyBinding(KeyCode.W), - InputBinding.CreateKeyBinding(KeyCode.S), - InputBinding.CreateKeyBinding(KeyCode.A), - InputBinding.CreateKeyBinding(KeyCode.D), - true // normalize - ); - moveAction.AddBinding(wasdComp); - - // Also add gamepad left stick with deadzone - var leftStick = InputBinding.CreateGamepadAxisBinding(0, deviceIndex: 0); - leftStick.Processors.Add(new DeadzoneProcessor(0.15f)); - leftStick.Processors.Add(new NormalizeProcessor()); - moveAction.AddBinding(leftStick); + float t = i / 80f; + float angle = t * MathF.PI * 4; + helix.Points.Add(new Double3( + MathF.Cos(angle) * 0.8f, + t * 3f - 1.5f, + MathF.Sin(angle) * 0.8f + )); } - - // Look action + helixGO.Transform.position = new Double3(-4, 0, 0); + scene.Add(helixGO); + + // 2. Animated Sine Wave + GameObject sineGO = new("SineWave"); + sineWave = sineGO.AddComponent(); + sineWave.Material = lineMat; + sineWave.StartWidth = 0.08f; + sineWave.EndWidth = 0.08f; + sineWave.StartColor = new Color(0.2f, 1, 0.5f, 1); // Green + sineWave.EndColor = new Color(0.2f, 0.5f, 1, 1); // Blue + sineWave.TextureMode = TextureWrapMode.Stretch; + + sineWave.Points = new List(); + for (int i = 0; i <= 60; i++) { - lookAction = cameraMap.AddAction("Look", InputActionType.Value); - lookAction.ExpectedValueType = typeof(Float2); - - var mouse = new DualAxisCompositeBinding(InputBinding.CreateMouseAxisBinding(0), InputBinding.CreateMouseAxisBinding(1)); - mouse.Processors.Add(new ScaleProcessor(0.25f)); // Decreased sensitivity for looking - lookAction.AddBinding(mouse); - - // Gamepad right stick for looking with higher sensitivity and deadzone - var rightStick = InputBinding.CreateGamepadAxisBinding(1, deviceIndex: 0); - rightStick.Processors.Add(new DeadzoneProcessor(0.15f)); - rightStick.Processors.Add(new ScaleProcessor(3.0f)); // Increase sensitivity for looking - lookAction.AddBinding(rightStick); + float t = i / 60f; + sineWave.Points.Add(new Double3( + t * 5f - 2.5f, + MathF.Sin(t * MathF.PI * 3) * 0.8f, + 0 + )); } - - // Fly Up (E key + Gamepad A button) + sineGO.Transform.position = new Double3(0, 1, 3); + scene.Add(sineGO); + + // 3. Looping Orbital Ring + GameObject ringGO = new("OrbitalRing"); + orbitalRing = ringGO.AddComponent(); + orbitalRing.Material = lineMat; + orbitalRing.StartWidth = 0.06f; + orbitalRing.EndWidth = 0.06f; + orbitalRing.Loop = true; + orbitalRing.StartColor = new Color(1, 0.3f, 1, 1); + orbitalRing.EndColor = new Color(1, 0.3f, 1, 1); + orbitalRing.TextureMode = TextureWrapMode.RepeatPerSegment; + + orbitalRing.Points = new List(); + for (int i = 0; i < 48; i++) { - flyUpAction = cameraMap.AddAction("FlyUp", InputActionType.Button); - flyUpAction.AddBinding(KeyCode.E); - flyUpAction.AddBinding(GamepadButton.A); + float angle = (i / 48f) * MathF.PI * 2; + orbitalRing.Points.Add(new Double3( + MathF.Cos(angle) * 1.2f, + MathF.Sin(angle) * 0.3f, + MathF.Sin(angle) * 1.2f + )); } + ringGO.Transform.position = new Double3(4, 1, 0); + ringGO.Transform.localEulerAngles = new Double3(30, 45, 0); + scene.Add(ringGO); + } - // Fly Down (Q key + Gamepad B button) - { - flyDownAction = cameraMap.AddAction("FlyDown", InputActionType.Button); - flyDownAction.AddBinding(KeyCode.Q); - flyDownAction.AddBinding(GamepadButton.B); - } + private void SetupInputActions() + { + cameraMap = new InputActionMap("Camera"); - // Sprint (Shift + Gamepad Left Stick Click) - { - sprintAction = cameraMap.AddAction("Sprint", InputActionType.Button); - sprintAction.AddBinding(KeyCode.ShiftLeft); - sprintAction.AddBinding(GamepadButton.LeftStick); - } + // Movement (WASD + Gamepad) + moveAction = cameraMap.AddAction("Move", InputActionType.Value); + moveAction.ExpectedValueType = typeof(Float2); + moveAction.AddBinding(new Vector2CompositeBinding( + InputBinding.CreateKeyBinding(KeyCode.W), + InputBinding.CreateKeyBinding(KeyCode.S), + InputBinding.CreateKeyBinding(KeyCode.A), + InputBinding.CreateKeyBinding(KeyCode.D), + true + )); + var leftStick = InputBinding.CreateGamepadAxisBinding(0, deviceIndex: 0); + leftStick.Processors.Add(new DeadzoneProcessor(0.15f)); + leftStick.Processors.Add(new NormalizeProcessor()); + moveAction.AddBinding(leftStick); + + // Look enable (RMB) + lookEnableAction = cameraMap.AddAction("LookEnable", InputActionType.Button); + lookEnableAction.AddBinding(MouseButton.Right); + + // Look (Mouse + Gamepad) + lookAction = cameraMap.AddAction("Look", InputActionType.Value); + lookAction.ExpectedValueType = typeof(Float2); + var mouse = new DualAxisCompositeBinding( + InputBinding.CreateMouseAxisBinding(0), + InputBinding.CreateMouseAxisBinding(1)); + mouse.Processors.Add(new ScaleProcessor(0.1f)); + lookAction.AddBinding(mouse); + var rightStick = InputBinding.CreateGamepadAxisBinding(1, deviceIndex: 0); + rightStick.Processors.Add(new DeadzoneProcessor(0.15f)); + rightStick.Processors.Add(new NormalizeProcessor()); + lookAction.AddBinding(rightStick); + + // Fly Up/Down + flyUpAction = cameraMap.AddAction("FlyUp", InputActionType.Button); + flyUpAction.AddBinding(KeyCode.E); + flyUpAction.AddBinding(GamepadButton.A); + flyDownAction = cameraMap.AddAction("FlyDown", InputActionType.Button); + flyDownAction.AddBinding(KeyCode.Q); + flyDownAction.AddBinding(GamepadButton.B); + + // Sprint + sprintAction = cameraMap.AddAction("Sprint", InputActionType.Button); + sprintAction.AddBinding(KeyCode.ShiftLeft); + sprintAction.AddBinding(GamepadButton.LeftStick); - // Register and enable Input.RegisterActionMap(cameraMap); cameraMap.Enable(); } @@ -184,32 +245,59 @@ public override void Render() public override void Update() { scene.Update(); + time += (float)Time.deltaTime; - // Read movement from action (works with WASD, arrows, and gamepad left stick) - Float2 movement = moveAction.ReadValue(); + // Animate helix rotation + if (helix != null) + { + helix.GameObject.Transform.localEulerAngles = new Double3(0, time * 25, 0); + } + + // Animate sine wave + if (sineWave != null) + { + sineWave.Points.Clear(); + for (int i = 0; i <= 60; i++) + { + float t = i / 60f; + sineWave.Points.Add(new Double3( + t * 5f - 2.5f, + MathF.Sin(t * MathF.PI * 3 + time * 2) * 0.8f, + 0 + )); + } + sineWave.MarkDirty(); + } + + // Animate orbital ring rotation + if (orbitalRing != null) + { + orbitalRing.GameObject.Transform.localEulerAngles += new Double3( + 10 * Time.deltaTime, + 20 * Time.deltaTime, + 15 * Time.deltaTime + ); + } - // Calculate speed multiplier (sprint makes you move faster) + // Camera controls + Float2 movement = moveAction.ReadValue(); float speedMultiplier = sprintAction.IsPressed() ? 2.5f : 1.0f; float moveSpeed = 5f * speedMultiplier * (float)Time.deltaTime; - // Apply movement cameraGO.Transform.position += cameraGO.Transform.forward * movement.Y * moveSpeed; cameraGO.Transform.position += cameraGO.Transform.right * movement.X * moveSpeed; - // Vertical movement (fly up/down) float upDown = 0; if (flyUpAction.IsPressed()) upDown += 1; if (flyDownAction.IsPressed()) upDown -= 1; cameraGO.Transform.position += Double3.UnitY * upDown * moveSpeed; - // Look/rotate camera Float2 lookInput = lookAction.ReadValue(); + if (lookEnableAction.IsPressed() || Math.Abs(lookInput.X) > 0.01f || Math.Abs(lookInput.Y) > 0.01f) + { + cameraGO.Transform.localEulerAngles += new Double3(lookInput.Y, lookInput.X, 0); + } - // Apply look rotation from gamepad right stick - float lookSpeed = 100f * (float)Time.deltaTime; - cameraGO.Transform.localEulerAngles += new Double3(lookInput.Y, lookInput.X, 0) * lookSpeed; - - // Unlock cursor on press Escape if (Input.GetKeyDown(KeyCode.Escape)) Input.SetCursorVisible(true); }