brillo-rendering-2.0.0: Brillo/Internals/Rendering/Shader.hs
{-# OPTIONS_HADDOCK hide #-}
-- | Shader-based rendering for anti-aliased shapes using signed distance fields.
module Brillo.Internals.Rendering.Shader (
ShaderState (..),
initShaderState,
renderCircleSDF,
renderArcSDF,
renderThickLineSDF,
) where
import Control.Monad (unless)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Graphics.Rendering.OpenGL (($=))
import Graphics.Rendering.OpenGL.GL qualified as GL
-- | State for shader-based rendering
data ShaderState = ShaderState
{ circleProgram :: !(IORef (Maybe GL.Program))
, arcProgram :: !(IORef (Maybe GL.Program))
, lineProgram :: !(IORef (Maybe GL.Program))
}
-- | Initialize shader state (programs are compiled lazily on first use)
initShaderState :: IO ShaderState
initShaderState = do
circleRef <- newIORef Nothing
arcRef <- newIORef Nothing
lineRef <- newIORef Nothing
return
ShaderState
{ circleProgram = circleRef
, arcProgram = arcRef
, lineProgram = lineRef
}
-- | Vertex shader for SDF shapes - uses texture coords for local position
vertexShaderSrc :: String
vertexShaderSrc =
unlines
[ "#version 120"
, "varying vec2 vLocalCoord;"
, "void main() {"
, " vLocalCoord = gl_MultiTexCoord0.xy;"
, " gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;"
, "}"
]
-- | Fragment shader for filled circle with SDF anti-aliasing
circleFragmentShaderSrc :: String
circleFragmentShaderSrc =
unlines
[ "#version 120"
, "varying vec2 vLocalCoord;"
, "uniform vec4 uColor;"
, "uniform float uOuterRadius;"
, "uniform float uInnerRadius;" -- 0 for solid circle
, "uniform float uPixelSize;" -- Size of a pixel in local coords for AA width
, ""
, "void main() {"
, " float dist = length(vLocalCoord);"
, " // Compute actual pixel size from screen-space derivatives"
, " float pixelSize = length(vec2(dFdx(dist), dFdy(dist)));"
, " // AA width: ~1 pixel, but capped at 25% of radius so small circles remain visible"
, " float aaWidth = min(pixelSize, uOuterRadius * 0.25);"
, ""
, " // Outer edge - AA extends outward from the edge to match aliased circle size"
, " float outerAlpha = 1.0 - smoothstep(uOuterRadius, uOuterRadius + aaWidth, dist);"
, ""
, " // Inner edge (for rings) - AA extends inward from the edge"
, " float innerAlpha = uInnerRadius > 0.0 ? smoothstep(uInnerRadius - aaWidth, uInnerRadius, dist) : 1.0;"
, ""
, " float alpha = outerAlpha * innerAlpha;"
, " if (alpha < 0.001) discard;"
, " gl_FragColor = vec4(uColor.rgb, uColor.a * alpha);"
, "}"
]
{-| Fragment shader for arc with SDF anti-aliasing
Arc is drawn counter-clockwise from start angle to end angle
-}
arcFragmentShaderSrc :: String
arcFragmentShaderSrc =
unlines
[ "#version 120"
, "varying vec2 vLocalCoord;"
, "uniform vec4 uColor;"
, "uniform float uOuterRadius;"
, "uniform float uInnerRadius;"
, "uniform float uStartAngle;" -- In radians
, "uniform float uSweep;" -- Arc sweep in radians (always positive, counter-clockwise)
, "uniform float uPixelSize;"
, ""
, "#define PI 3.14159265359"
, "#define TAU 6.28318530718"
, ""
, "void main() {"
, " float dist = length(vLocalCoord);"
, " float angle = atan(vLocalCoord.y, vLocalCoord.x);"
, " // Compute actual pixel size from screen-space derivatives"
, " float pixelSize = length(vec2(dFdx(dist), dFdy(dist)));"
, " // AA width: ~1 pixel, but capped at 25% of radius so small circles remain visible"
, " float aaWidth = min(pixelSize, uOuterRadius * 0.25);"
, ""
, " // Outer edge - AA extends outward from the edge to match aliased circle size"
, " float outerAlpha = 1.0 - smoothstep(uOuterRadius, uOuterRadius + aaWidth, dist);"
, ""
, " // Inner edge (for rings) - AA extends inward from the edge"
, " float innerAlpha = uInnerRadius > 0.0 ? smoothstep(uInnerRadius - aaWidth, uInnerRadius, dist) : 1.0;"
, ""
, " // Calculate angle relative to start, normalized to [0, 2*PI)"
, " float relAngle = angle - uStartAngle;"
, " // Normalize to [0, 2*PI) - add TAU twice to handle very negative values"
, " relAngle = relAngle - TAU * floor(relAngle / TAU);"
, ""
, " // Soft edges at arc boundaries using signed distance from boundaries"
, " float angularAA = aaWidth / max(dist, 0.001);"
, ""
, " // Distance from start edge (positive = inside arc)"
, " float startDist = relAngle;"
, " // Distance from end edge (positive = inside arc)"
, " float endDist = uSweep - relAngle;"
, ""
, " float startAlpha = smoothstep(-angularAA, angularAA, startDist);"
, " float endAlpha = smoothstep(-angularAA, angularAA, endDist);"
, ""
, " float alpha = outerAlpha * innerAlpha * startAlpha * endAlpha;"
, " if (alpha < 0.001) discard;"
, " gl_FragColor = vec4(uColor.rgb, uColor.a * alpha);"
, "}"
]
{-| Fragment shader for line segment (capsule) with SDF anti-aliasing.
A thick line segment is rendered as a capsule - all points within
a given distance from the line segment.
-}
lineFragmentShaderSrc :: String
lineFragmentShaderSrc =
"#version 120 \n\
\varying vec2 vLocalCoord; \
\uniform vec4 uColor; \
\uniform vec2 uPointA; \
\uniform vec2 uPointB; \
\uniform float uThickness; \
\uniform float uPixelSize; \
\void main() { \
\ vec2 pa = vLocalCoord - uPointA; \
\ vec2 ba = uPointB - uPointA; \
\ float len2 = dot(ba, ba); \
\ float h = len2 > 0.0 ? clamp(dot(pa, ba) / len2, 0.0, 1.0) : 0.0; \
\ float dist = length(pa - ba * h); \
\ float aaWidth = min(uPixelSize, uThickness * 0.1); \
\ float d = dist - uThickness; \
\ float alpha = 1.0 - smoothstep(-aaWidth, aaWidth, d); \
\ if (alpha < 0.001) discard; \
\ gl_FragColor = vec4(uColor.rgb, uColor.a * alpha); \
\}"
-- | Compile a shader from source
compileShader :: GL.ShaderType -> String -> IO GL.Shader
compileShader shaderType src = do
shader <- GL.createShader shaderType
GL.shaderSourceBS shader $= GL.packUtf8 src
GL.compileShader shader
ok <- GL.get (GL.compileStatus shader)
unless ok $ do
infoLog <- GL.get (GL.shaderInfoLog shader)
error $ "Shader compilation failed: " ++ infoLog
return shader
-- | Create and link a shader program
createProgram :: String -> String -> IO GL.Program
createProgram vertSrc fragSrc = do
vertShader <- compileShader GL.VertexShader vertSrc
fragShader <- compileShader GL.FragmentShader fragSrc
program <- GL.createProgram
GL.attachShader program vertShader
GL.attachShader program fragShader
GL.linkProgram program
ok <- GL.get (GL.linkStatus program)
unless ok $ do
infoLog <- GL.get (GL.programInfoLog program)
error $ "Program linking failed: " ++ infoLog
-- Clean up shaders (they're copied into the program)
GL.deleteObjectName vertShader
GL.deleteObjectName fragShader
return program
-- | Get or create the circle shader program
getCircleProgram :: ShaderState -> IO GL.Program
getCircleProgram state = do
maybeProgram <- readIORef (circleProgram state)
case maybeProgram of
Just prog -> return prog
Nothing -> do
prog <- createProgram vertexShaderSrc circleFragmentShaderSrc
writeIORef (circleProgram state) (Just prog)
return prog
-- | Get or create the arc shader program
getArcProgram :: ShaderState -> IO GL.Program
getArcProgram state = do
maybeProgram <- readIORef (arcProgram state)
case maybeProgram of
Just prog -> return prog
Nothing -> do
prog <- createProgram vertexShaderSrc arcFragmentShaderSrc
writeIORef (arcProgram state) (Just prog)
return prog
-- | Get or create the line shader program
getLineProgram :: ShaderState -> IO GL.Program
getLineProgram state = do
maybeProgram <- readIORef (lineProgram state)
case maybeProgram of
Just prog -> return prog
Nothing -> do
prog <- createProgram vertexShaderSrc lineFragmentShaderSrc
writeIORef (lineProgram state) (Just prog)
return prog
-- | Render a circle/ring using SDF shader
renderCircleSDF ::
ShaderState ->
Float -> -- posX
Float -> -- posY
Float -> -- scaleFactor (pixels per unit)
Float -> -- outer radius
Float -> -- inner radius (0 for solid)
GL.Color4 GL.GLfloat -> -- color
IO ()
renderCircleSDF state posX posY scaleFactor outerR innerR color = do
program <- getCircleProgram state
-- Save current program
oldProgram <- GL.get GL.currentProgram
GL.currentProgram $= Just program
-- Set uniforms
uColorLoc <- GL.get (GL.uniformLocation program "uColor")
uOuterLoc <- GL.get (GL.uniformLocation program "uOuterRadius")
uInnerLoc <- GL.get (GL.uniformLocation program "uInnerRadius")
uPixelLoc <- GL.get (GL.uniformLocation program "uPixelSize")
GL.uniform uColorLoc $= color
GL.uniform uOuterLoc $= (realToFrac outerR :: GL.GLfloat)
GL.uniform uInnerLoc $= (realToFrac innerR :: GL.GLfloat)
GL.uniform uPixelLoc $= (realToFrac (1.0 / scaleFactor) :: GL.GLfloat)
-- Draw a quad that covers the circle
let r = outerR + 2.0 / scaleFactor -- Add padding for AA
x1 = posX - r
x2 = posX + r
y1 = posY - r
y2 = posY + r
-- Local coords relative to circle center
l1 = -r
l2 = r
-- Draw quad with position (vertex) and local coords (texCoord)
GL.renderPrimitive GL.Quads $ do
GL.texCoord $
GL.TexCoord2 (realToFrac l1 :: GL.GLfloat) (realToFrac l1 :: GL.GLfloat)
GL.vertex $
GL.Vertex2 (realToFrac x1 :: GL.GLfloat) (realToFrac y1 :: GL.GLfloat)
GL.texCoord $
GL.TexCoord2 (realToFrac l2 :: GL.GLfloat) (realToFrac l1 :: GL.GLfloat)
GL.vertex $
GL.Vertex2 (realToFrac x2 :: GL.GLfloat) (realToFrac y1 :: GL.GLfloat)
GL.texCoord $
GL.TexCoord2 (realToFrac l2 :: GL.GLfloat) (realToFrac l2 :: GL.GLfloat)
GL.vertex $
GL.Vertex2 (realToFrac x2 :: GL.GLfloat) (realToFrac y2 :: GL.GLfloat)
GL.texCoord $
GL.TexCoord2 (realToFrac l1 :: GL.GLfloat) (realToFrac l2 :: GL.GLfloat)
GL.vertex $
GL.Vertex2 (realToFrac x1 :: GL.GLfloat) (realToFrac y2 :: GL.GLfloat)
GL.currentProgram $= oldProgram
-- | Render an arc using SDF shader
renderArcSDF ::
ShaderState ->
Float -> -- posX
Float -> -- posY
Float -> -- scaleFactor
Float -> -- outer radius
Float -> -- inner radius
Float -> -- start angle (degrees)
Float -> -- end angle (degrees)
GL.Color4 GL.GLfloat -> -- color
IO ()
renderArcSDF state posX posY scaleFactor outerR innerR startDeg endDeg color = do
program <- getArcProgram state
oldProgram <- GL.get GL.currentProgram
GL.currentProgram $= Just program
-- Set uniforms
uColorLoc <- GL.get (GL.uniformLocation program "uColor")
uOuterLoc <- GL.get (GL.uniformLocation program "uOuterRadius")
uInnerLoc <- GL.get (GL.uniformLocation program "uInnerRadius")
uStartLoc <- GL.get (GL.uniformLocation program "uStartAngle")
uSweepLoc <- GL.get (GL.uniformLocation program "uSweep")
uPixelLoc <- GL.get (GL.uniformLocation program "uPixelSize")
-- Convert to radians and calculate sweep (counter-clockwise from start to end)
let startRad = startDeg * pi / 180.0
endRad = endDeg * pi / 180.0
-- Sweep is how far we go counter-clockwise from start to end
-- If start >= end, we wrap around through 360 degrees
sweep =
if startDeg >= endDeg
then (endRad + 2 * pi) - startRad
else endRad - startRad
GL.uniform uColorLoc $= color
GL.uniform uOuterLoc $= (realToFrac outerR :: GL.GLfloat)
GL.uniform uInnerLoc $= (realToFrac innerR :: GL.GLfloat)
GL.uniform uStartLoc $= (realToFrac startRad :: GL.GLfloat)
GL.uniform uSweepLoc $= (realToFrac sweep :: GL.GLfloat)
GL.uniform uPixelLoc $= (realToFrac (1.0 / scaleFactor) :: GL.GLfloat)
-- Draw a quad that covers the arc
let r = outerR + 2.0 / scaleFactor
x1 = posX - r
x2 = posX + r
y1 = posY - r
y2 = posY + r
l1 = -r
l2 = r
GL.renderPrimitive GL.Quads $ do
GL.texCoord $
GL.TexCoord2 (realToFrac l1 :: GL.GLfloat) (realToFrac l1 :: GL.GLfloat)
GL.vertex $
GL.Vertex2 (realToFrac x1 :: GL.GLfloat) (realToFrac y1 :: GL.GLfloat)
GL.texCoord $
GL.TexCoord2 (realToFrac l2 :: GL.GLfloat) (realToFrac l1 :: GL.GLfloat)
GL.vertex $
GL.Vertex2 (realToFrac x2 :: GL.GLfloat) (realToFrac y1 :: GL.GLfloat)
GL.texCoord $
GL.TexCoord2 (realToFrac l2 :: GL.GLfloat) (realToFrac l2 :: GL.GLfloat)
GL.vertex $
GL.Vertex2 (realToFrac x2 :: GL.GLfloat) (realToFrac y2 :: GL.GLfloat)
GL.texCoord $
GL.TexCoord2 (realToFrac l1 :: GL.GLfloat) (realToFrac l2 :: GL.GLfloat)
GL.vertex $
GL.Vertex2 (realToFrac x1 :: GL.GLfloat) (realToFrac y2 :: GL.GLfloat)
GL.currentProgram $= oldProgram
-- | Render a single line segment using SDF shader (capsule shape)
renderLineSegmentSDF ::
ShaderState ->
Float -> -- ax
Float -> -- ay
Float -> -- bx
Float -> -- by
Float -> -- scaleFactor (pixels per unit)
Float -> -- thickness (diameter)
GL.Color4 GL.GLfloat -> -- color
IO ()
renderLineSegmentSDF state ax ay bx by scaleFactor thickness color = do
program <- getLineProgram state
oldProgram <- GL.get GL.currentProgram
GL.currentProgram $= Just program
-- Set uniforms
uColorLoc <- GL.get (GL.uniformLocation program "uColor")
uPointALoc <- GL.get (GL.uniformLocation program "uPointA")
uPointBLoc <- GL.get (GL.uniformLocation program "uPointB")
uThicknessLoc <- GL.get (GL.uniformLocation program "uThickness")
uPixelLoc <- GL.get (GL.uniformLocation program "uPixelSize")
let halfThick = thickness / 2.0
GL.uniform uColorLoc $= color
GL.uniform uPointALoc
$= GL.Vector2 (realToFrac ax :: GL.GLfloat) (realToFrac ay)
GL.uniform uPointBLoc
$= GL.Vector2 (realToFrac bx :: GL.GLfloat) (realToFrac by)
GL.uniform uThicknessLoc $= (realToFrac halfThick :: GL.GLfloat)
GL.uniform uPixelLoc $= (realToFrac (1.0 / scaleFactor) :: GL.GLfloat)
-- Compute bounding box with padding for AA
let padding = 2.0 / scaleFactor
r = halfThick + padding
minX = min ax bx - r
maxX = max ax bx + r
minY = min ay by - r
maxY = max ay by + r
-- Draw quad covering the capsule
-- vLocalCoord receives the vertex position (world space)
GL.renderPrimitive GL.Quads $ do
GL.texCoord $
GL.TexCoord2 (realToFrac minX :: GL.GLfloat) (realToFrac minY :: GL.GLfloat)
GL.vertex $
GL.Vertex2 (realToFrac minX :: GL.GLfloat) (realToFrac minY :: GL.GLfloat)
GL.texCoord $
GL.TexCoord2 (realToFrac maxX :: GL.GLfloat) (realToFrac minY :: GL.GLfloat)
GL.vertex $
GL.Vertex2 (realToFrac maxX :: GL.GLfloat) (realToFrac minY :: GL.GLfloat)
GL.texCoord $
GL.TexCoord2 (realToFrac maxX :: GL.GLfloat) (realToFrac maxY :: GL.GLfloat)
GL.vertex $
GL.Vertex2 (realToFrac maxX :: GL.GLfloat) (realToFrac maxY :: GL.GLfloat)
GL.texCoord $
GL.TexCoord2 (realToFrac minX :: GL.GLfloat) (realToFrac maxY :: GL.GLfloat)
GL.vertex $
GL.Vertex2 (realToFrac minX :: GL.GLfloat) (realToFrac maxY :: GL.GLfloat)
GL.currentProgram $= oldProgram
-- | Render an entire path as connected thick line segments using SDF
renderThickLineSDF ::
ShaderState ->
[(Float, Float)] -> -- Path points
Float -> -- scaleFactor
Float -> -- thickness
GL.Color4 GL.GLfloat -> -- color
IO ()
renderThickLineSDF _state [] _ _ _ = return ()
renderThickLineSDF _state [_] _ _ _ = return () -- Single point, nothing to draw
renderThickLineSDF state path scaleFactor thickness color = do
-- Render each segment as a capsule
-- Overlapping capsules at joins naturally create rounded joins
let segments = zip path (drop 1 path)
mapM_ renderSegment segments
where
renderSegment ((ax, ay), (bx, by)) =
renderLineSegmentSDF state ax ay bx by scaleFactor thickness color