Created
July 5, 2025 17:03
-
-
Save ZedDevStuff/3d3e15fac1f29915679aa45c8b3e19c8 to your computer and use it in GitHub Desktop.
Working SFML.Net ICanvasRenderer for Prowl.Paper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Prowl.PaperUI; | |
using Prowl.Quill; | |
using Prowl.Vector; | |
using SFML.Graphics; | |
using SFML.System; | |
using System; | |
using System.Collections.Generic; | |
using System.Drawing; | |
using SFMLVertex = SFML.Graphics.Vertex; | |
using QuillVertex = Prowl.Quill.Vertex; | |
using ProwlIntRect = Prowl.Vector.IntRect; | |
using SFML.Graphics.Glsl; | |
using System.IO; | |
using System.Text; | |
namespace PaperTest | |
{ | |
public class SFMLCanvasRenderer : ICanvasRenderer, IDisposable | |
{ | |
private readonly RenderWindow _window; | |
private readonly Texture _whiteTexture; | |
private RenderStates _renderStates = RenderStates.Default; | |
private const string Stroke_FragmentShaderString = """ | |
varying vec2 fragTexCoord; | |
varying vec4 fragColor; | |
varying vec2 fragPos; | |
uniform sampler2D texture; | |
uniform mat4 scissorMat; | |
uniform vec2 scissorExt; | |
uniform mat4 brushMat; | |
uniform int brushType; // 0=none, 1=linear, 2=radial, 3=box | |
uniform vec4 brushColor1; // Start color | |
uniform vec4 brushColor2; // End color | |
uniform vec4 brushParams; // x,y = start point, z,w = end point (or center+radius for radial) | |
uniform vec2 brushParams2; // x = Box radius, y = Box Feather | |
float calculateBrushFactor() { | |
// No brush | |
if (brushType == 0) return 0.0; | |
vec2 transformedPoint = (brushMat * vec4(fragPos, 0.0, 1.0)).xy; | |
// Linear brush - projects position onto the line between start and end | |
if (brushType == 1) { | |
vec2 startPoint = brushParams.xy; | |
vec2 endPoint = brushParams.zw; | |
vec2 line = endPoint - startPoint; | |
float lineLength = length(line); | |
if (lineLength < 0.001) return 0.0; | |
vec2 posToStart = transformedPoint - startPoint; | |
float projection = dot(posToStart, line) / (lineLength * lineLength); | |
return clamp(projection, 0.0, 1.0); | |
} | |
// Radial brush - based on distance from center | |
if (brushType == 2) { | |
vec2 center = brushParams.xy; | |
float innerRadius = brushParams.z; | |
float outerRadius = brushParams.w; | |
if (outerRadius < 0.001) return 0.0; | |
float distance = smoothstep(innerRadius, outerRadius, length(transformedPoint - center)); | |
return clamp(distance, 0.0, 1.0); | |
} | |
// Box brush - like radial but uses max distance in x or y direction | |
if (brushType == 3) { | |
vec2 center = brushParams.xy; | |
vec2 halfSize = brushParams.zw; | |
float radius = brushParams2.x; | |
float feather = brushParams2.y; | |
if (halfSize.x < 0.001 || halfSize.y < 0.001) return 0.0; | |
// Calculate distance from center (normalized by half-size) | |
vec2 q = abs(transformedPoint - center) - (halfSize - vec2(radius)); | |
// Distance field calculation for rounded rectangle | |
//float dist = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius; | |
float dist = min(max(q.x,q.y),0.0) + length(max(q,0.0)) - radius; | |
return clamp((dist + feather * 0.5) / feather, 0.0, 1.0); | |
} | |
return 0.0; | |
} | |
float scissorMask(vec2 p) { | |
// Early exit if scissoring is disabled (when scissorExt.x is negative or zero) | |
if(scissorExt.x <= 0.0) return 1.0; | |
// Transform point to scissor space | |
vec2 transformedPoint = (scissorMat * vec4(p, 0.0, 1.0)).xy; | |
// Calculate signed distance from scissor edges (negative inside, positive outside) | |
vec2 distanceFromEdges = abs(transformedPoint) - scissorExt; | |
// Apply offset for smooth edge transition (0.5 creates half-pixel anti-aliased edges) | |
vec2 smoothEdges = vec2(0.5, 0.5) - distanceFromEdges; | |
// Clamp each component and multiply to get final mask value | |
// Result is 1.0 inside, 0.0 outside, with smooth transition at edges | |
return clamp(smoothEdges.x, 0.0, 1.0) * clamp(smoothEdges.y, 0.0, 1.0); | |
} | |
vec4 textureNice(sampler2D sam, vec2 uv) | |
{ | |
float textureResolution = float(textureSize(sam,0).x); | |
uv = uv * textureResolution + 0.5; | |
vec2 iuv = floor(uv); | |
vec2 fuv = fract(uv); | |
uv = iuv + fuv * fuv * (3.0 - 2.0 * fuv); | |
uv = (uv - 0.5) / textureResolution; | |
return texture2D(sam, uv); | |
} | |
void main() | |
{ | |
vec2 pixelSize = fwidth(fragTexCoord); | |
vec2 edgeDistance = min(fragTexCoord, 1.0 - fragTexCoord); | |
float edgeAlpha = smoothstep(0.0, pixelSize.x, edgeDistance.x) * smoothstep(0.0, pixelSize.y, edgeDistance.y); | |
edgeAlpha = clamp(edgeAlpha, 0.0, 1.0); | |
float mask = scissorMask(fragPos); | |
vec4 color = fragColor; | |
// Apply brush if active | |
if (brushType > 0) { | |
float factor = calculateBrushFactor(); | |
color = mix(brushColor1, brushColor2, factor); | |
} | |
vec4 textureColor = textureNice(texture, fragTexCoord); | |
color *= textureColor.xyzw; | |
color *= edgeAlpha * mask; | |
gl_FragColor = color; | |
} | |
"""; | |
private const string Vertex_VertexShaderString = """ | |
uniform mat4 mvp; | |
varying vec2 fragTexCoord; | |
varying vec4 fragColor; | |
varying vec2 fragPos; | |
void main() | |
{ | |
fragTexCoord = gl_MultiTexCoord0; | |
fragColor = gl_Color; | |
fragPos = gl_Vertex.xy; | |
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; | |
} | |
"""; | |
private Shader? _shader; | |
private Mat4 _scissorMat | |
{ | |
set => _shader?.SetUniform("scissorMat", value); | |
} | |
private Vector2f _scissorExt | |
{ | |
set => _shader?.SetUniform("scissorExt", value); | |
} | |
private int _brushType | |
{ | |
set => _shader?.SetUniform("brushType", value); | |
} | |
private Mat4 _brushMat | |
{ | |
set => _shader?.SetUniform("brushMat", value); | |
} | |
private Vec4 _brushColor1 | |
{ | |
set => _shader?.SetUniform("brushColor1", value); | |
} | |
private Vec4 _brushColor2 | |
{ | |
set => _shader?.SetUniform("brushColor2", value); | |
} | |
private Vec4 _brushParams | |
{ | |
set => _shader?.SetUniform("brushParams", value); | |
} | |
private Vec2 _brushParams2 | |
{ | |
set => _shader?.SetUniform("brushParams2", value); | |
} | |
private List<VertexArray> _triangles = new List<VertexArray>(); | |
public SFMLCanvasRenderer(RenderWindow window) | |
{ | |
_window = window ?? throw new ArgumentNullException(nameof(window)); | |
_shader = new Shader( | |
new MemoryStream(Encoding.UTF8.GetBytes(Vertex_VertexShaderString)), | |
null, | |
new MemoryStream(Encoding.UTF8.GetBytes(Stroke_FragmentShaderString))); | |
var whiteImage = new Image(1024, 1024, SFML.Graphics.Color.White); | |
_whiteTexture = new Texture(whiteImage); | |
} | |
public object CreateTexture(uint width, uint height) | |
{ | |
var img = new Image(width, height, SFML.Graphics.Color.Transparent); | |
var tex = new Texture(img) | |
{ | |
Smooth = true | |
}; | |
return tex; | |
} | |
public Vector2Int GetTextureSize(object texture) | |
{ | |
if (texture is not Texture tex) | |
throw new ArgumentException("Invalid texture type", nameof(texture)); | |
return new Vector2Int((int)tex.Size.X, (int)tex.Size.Y); | |
} | |
public void SetTextureData(object texture, ProwlIntRect bounds, byte[] data) | |
{ | |
if (texture is not Texture tex) | |
throw new ArgumentException("Invalid texture type", nameof(texture)); | |
if (data == null || data.Length == 0) | |
throw new ArgumentNullException(nameof(data)); | |
tex.Update(data, (uint)bounds.width, (uint)bounds.height, (uint)bounds.x, (uint)bounds.y); | |
} | |
private void SetUniforms(DrawCall drawCall) | |
{ | |
Texture textureToUse = _whiteTexture; | |
if (drawCall.Texture is Texture tex) | |
textureToUse = tex; | |
_renderStates.Texture = textureToUse; | |
_renderStates.Shader = _shader; | |
drawCall.GetScissor(out Matrix4x4 scissor, out Vector2 scissorExt); | |
scissor = Matrix4x4.Transpose(scissor); | |
_scissorMat = ToFloat(scissor); | |
_scissorExt = new Vector2f((float)scissorExt.x, (float)scissorExt.y); | |
_brushType = (int)drawCall.Brush.Type; | |
if (drawCall.Brush.Type != BrushType.None) | |
{ | |
var brushMat = Matrix4x4.Transpose(drawCall.Brush.BrushMatrix); | |
_shader?.SetUniform("brushMat", ToFloat(brushMat)); | |
_brushMat = ToFloat(brushMat); | |
_brushColor1 = new Vec4(drawCall.Brush.Color1.R, drawCall.Brush.Color1.G, drawCall.Brush.Color1.B, drawCall.Brush.Color1.A); | |
_brushColor2 = new Vec4(drawCall.Brush.Color2.R, drawCall.Brush.Color2.G, drawCall.Brush.Color2.B, drawCall.Brush.Color2.A); | |
_brushParams = new Vec4((float)drawCall.Brush.Point1.x, (float)drawCall.Brush.Point1.y, (float)drawCall.Brush.Point2.x, (float)drawCall.Brush.Point2.y); | |
_brushParams2 = new Vec2((float)drawCall.Brush.CornerRadii, (float)drawCall.Brush.Feather); | |
} | |
} | |
public void RenderCalls(Canvas canvas, IReadOnlyList<DrawCall> drawCalls) | |
{ | |
int index = 0; | |
foreach (var drawCall in drawCalls) | |
{ | |
SetUniforms(drawCall); | |
_renderStates.BlendMode = new BlendMode( | |
BlendMode.Factor.SrcAlpha, | |
BlendMode.Factor.OneMinusSrcAlpha, | |
BlendMode.Equation.Add); | |
// Create vertex array with the correct size for all triangles in this draw call | |
var vertices = new VertexArray(PrimitiveType.Triangles, (uint)drawCall.ElementCount); | |
uint vertexIndex = 0; | |
for (int i = 0; i < drawCall.ElementCount; i += 3) | |
{ | |
var a = canvas.Vertices[(int)canvas.Indices[index]]; | |
var b = canvas.Vertices[(int)canvas.Indices[index + 1]]; | |
var c = canvas.Vertices[(int)canvas.Indices[index + 2]]; | |
vertices[vertexIndex++] = new SFMLVertex( | |
new Vector2f(a.x, a.y), | |
new SFML.Graphics.Color(a.r, a.g, a.b, a.a), | |
new Vector2f(a.u, a.v)); | |
vertices[vertexIndex++] = new SFMLVertex( | |
new Vector2f(b.x, b.y), | |
new SFML.Graphics.Color(b.r, b.g, b.b, b.a), | |
new Vector2f(b.u, b.v)); | |
vertices[vertexIndex++] = new SFMLVertex( | |
new Vector2f(c.x, c.y), | |
new SFML.Graphics.Color(c.r, c.g, c.b, c.a), | |
new Vector2f(c.u, c.v)); | |
index += 3; | |
} | |
_window.Draw(vertices, _renderStates); | |
} | |
} | |
public void Dispose() | |
{ | |
_shader?.Dispose(); | |
_whiteTexture?.Dispose(); | |
} | |
private static Mat4 ToFloat(Matrix4x4 matrix) | |
{ | |
return new Mat4( | |
(float)matrix.M11, (float)matrix.M12, (float)matrix.M13, (float)matrix.M14, | |
(float)matrix.M21, (float)matrix.M22, (float)matrix.M23, (float)matrix.M24, | |
(float)matrix.M31, (float)matrix.M32, (float)matrix.M33, (float)matrix.M34, | |
(float)matrix.M41, (float)matrix.M42, (float)matrix.M43, (float)matrix.M44 | |
); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment