xurfer/xurfer/lovr/lib/lovr-iui/graphics.lua

400 lines
11 KiB
Lua

local iui --- @type IUILib
local colorClipShader --- @type Shader
local fontClipShader --- @type Shader
local imageClipShader --- @type Shader
local imageUnclippedShader --- @type Shader
local msdfImageClipShader --- @type Shader
local msdfImageUnclippedShader --- @type Shader
local nearestImageSampler --- @type Sampler
local linearImageSampler --- @type Sampler
local currentImageFilter = "linear" --- @type IUIImageFilter
local currentShader = nil --- @type Shader?
local isFilterDirty = false --- @type boolean
local isClipDirty = false --- @type boolean
local currentClip = nil --- @type number[]?
local currentWindow --- @type LovrIUIWorldWindow
local windowWidth --- @type number
local windowHeight --- @type number
--- @alias LovrIUIShaderName "color" | "font" | "image" | "msdf"
--- @class LovrIUIGraphics: IUIGraphicsBackend
--- @field pass Pass
local graphics = {}
--- @param shader LovrIUIShaderName
local function setShader(shader)
local targetShader = nil --- @type Shader?
if shader == "color" then
if currentClip then
targetShader = colorClipShader
end
elseif shader == "font" then
if currentClip then
targetShader = fontClipShader
end
elseif shader == "image" then
if currentClip then
targetShader = imageClipShader
else
targetShader = imageUnclippedShader
end
elseif shader == "msdf" then
if currentClip then
targetShader = msdfImageClipShader
else
targetShader = msdfImageUnclippedShader
end
end
local targetSampler = linearImageSampler
if currentImageFilter == "nearest" then
targetSampler = nearestImageSampler
end
if targetShader ~= currentShader then
graphics.pass:setShader(targetShader --[[@as any]])
currentShader = targetShader
if shader == "image" then
graphics.pass:send("useAAUV", currentImageFilter == "smooth")
graphics.pass:send("imageSampler", targetSampler)
isFilterDirty = false
elseif shader == "msdf" then
graphics.pass:send("msdfSampler", targetSampler)
end
end
if isFilterDirty then
if shader == "image" then
graphics.pass:send("useAAUV", currentImageFilter == "smooth")
graphics.pass:send("imageSampler", targetSampler)
elseif shader == "msdf" then
graphics.pass:send("msdfSampler", targetSampler)
end
isFilterDirty = false
end
if currentShader and currentClip and isClipDirty then
local windowCenter = currentWindow.center
isClipDirty = false
local left = currentClip[1]
local top = currentClip[2]
local right = left + currentClip[3]
local bottom = top + currentClip[4]
left = (left - (currentWindow.w / 2)) / currentWindow.ppm
top = -(top - (currentWindow.h / 2)) / currentWindow.ppm
right = (right - (currentWindow.w / 2)) / currentWindow.ppm
bottom = -(bottom - (currentWindow.h / 2)) / currentWindow.ppm
local q = currentWindow.rotation
local leftDir = vec3(1, 0, 0):rotate(q)
local topDir = vec3(0, -1, 0):rotate(q)
local rightDir = vec3(-1, 0, 0):rotate(q)
local bottomDir = vec3(0, 1, 0):rotate(q)
local topLeftVector = vec3(left, top, 0)
local bottomRightVector = vec3(right, bottom, 0)
topLeftVector = topLeftVector:rotate(q)
bottomRightVector = bottomRightVector:rotate(q)
topLeftVector = topLeftVector + windowCenter
bottomRightVector = bottomRightVector + windowCenter
graphics.pass:send("ClipPlanes", {
centers = {
topLeftVector,
topLeftVector,
bottomRightVector,
bottomRightVector,
},
directions = {
leftDir,
topDir,
rightDir,
bottomDir,
}
})
end
end
--- @param clip number[]?
local function setClip(clip)
if clip ~= nil then
if currentClip == nil then
isClipDirty = true
else
for i = 1, #clip do
if clip[i] ~= currentClip[i] then
isClipDirty = true
break
end
end
end
end
currentClip = clip
end
--- @param filter IUIImageFilter
local function setFilter(filter)
if filter ~= currentImageFilter then
currentImageFilter = filter
isFilterDirty = true
end
end
local function nineSliceMesh(nineSlice, x, y, w, h)
local image = nineSlice.image --- @type Texture
local iw, ih = image:getDimensions()
local l, t, r, b = nineSlice.l, nineSlice.t, nineSlice.r, nineSlice.b
local uvl, uvt, uvr, uvb = l / iw, t / ih, (iw - r) / iw, (ih - b) / ih
local wh = windowHeight
local mesh = lovr.graphics.newMesh(
{
{ "VertexPosition", "vec2" },
{ "VertexUV", "vec2" },
},
{
{ x, wh - (y), 0, 0 },
{ x + l, wh - (y), uvl, 0 },
{ x + w - r, wh - (y), uvr, 0 },
{ x + w, wh - (y), 1, 0 },
{ x, wh - (y + t), 0, uvt },
{ x + l, wh - (y + t), uvl, uvt },
{ x + w - r, wh - (y + t), uvr, uvt },
{ x + w, wh - (y + t), 1, uvt },
{ x, wh - (y + h - b), 0, uvb },
{ x + l, wh - (y + h - b), uvl, uvb },
{ x + w - r, wh - (y + h - b), uvr, uvb },
{ x + w, wh - (y + h - b), 1, uvb },
{ x, wh - (y + h), 0, 1 },
{ x + l, wh - (y + h), uvl, 1 },
{ x + w - r, wh - (y + h), uvr, 1 },
{ x + w, wh - (y + h), 1, 1 },
}
)
mesh:setIndices({
1, 5, 2, 2, 5, 6,
2, 6, 3, 3, 6, 7,
3, 7, 4, 4, 7, 8,
5, 9, 6, 6, 9, 10,
6, 10, 7, 7, 10, 11,
7, 11, 8, 8, 11, 12,
9, 13, 10, 10, 13, 14,
10, 14, 11, 11, 14, 15,
11, 15, 12, 12, 15, 16,
})
return image, mesh
end
--- @param lib IUILib
--- @param backend LovrIUIBackend
function graphics.load(lib, backend)
iui = lib
colorClipShader = lovr.graphics.newShader(
backend.resourcePath .. "shaders/ui-clip.glsl", "unlit"
)
fontClipShader = lovr.graphics.newShader(
backend.resourcePath .. "shaders/ui-clip.glsl", "font"
)
imageClipShader = lovr.graphics.newShader(
backend.resourcePath .. "shaders/ui-clip.glsl",
backend.resourcePath .. "shaders/ui-image.glsl"
)
imageUnclippedShader = lovr.graphics.newShader(
"unlit",
backend.resourcePath .. "shaders/ui-image.glsl"
)
msdfImageClipShader = lovr.graphics.newShader(
backend.resourcePath .. "shaders/ui-clip.glsl",
backend.resourcePath .. "shaders/ui-msdf.glsl"
)
msdfImageUnclippedShader = lovr.graphics.newShader(
"unlit",
backend.resourcePath .. "shaders/ui-msdf.glsl"
)
nearestImageSampler = lovr.graphics.newSampler {
wrap = { "clamp", "clamp", "clamp" },
filter = { "nearest", "nearest", "linear" },
}
linearImageSampler = lovr.graphics.newSampler {
wrap = { "clamp", "clamp", "clamp" },
}
end
--- @param window LovrIUIWorldWindow
function graphics.setWindow(window)
currentWindow = window
end
function graphics.beginDraw(width, height)
windowWidth = width
windowHeight = height
currentImageFilter = "linear"
currentShader = nil
currentClip = nil
isFilterDirty = false
isClipDirty = false
local pass = graphics.pass
pass:setShader()
pass:setMaterial()
if iui.idiom == "desktop" then
pass:push()
pass:setDepthTest()
pass:setViewPose(1, mat4():identity(), false)
pass:setProjection(1, mat4():orthographic(
0, windowWidth, windowHeight, 0, -10, 10)
)
end
end
function graphics.endDraw()
if iui.idiom == "desktop" then
graphics.pass:pop()
end
graphics.pass:setShader()
end
function graphics.newFont(size, hinting, dpiscale)
local baseFontSize = 32
local font = lovr.graphics.newFont(baseFontSize)
font:setPixelDensity(baseFontSize / size)
return font
end
--- @param image Texture
function graphics.getImageDimensions(image)
return image:getDimensions()
end
function graphics.clip(x, y, w, h)
if iui.idiom == "desktop" then
if x then
local wx, wy, ww, wh = 0, 0, windowWidth, windowHeight
local wmx, wmy = wx + ww, wy + wh
local mx, my = x + w, y + h
local dminX, dminY = math.max(x, wx), math.max(y, wy)
local dmaxX, dmaxY = math.min(mx, wmx), math.min(my, wmy)
local dx, dy, dw, dh = dminX, dminY, dmaxX - dminX, dmaxY - dminY
if dx < 0 or dy < 0 or dw < 0 or dh < 0 then
graphics.pass:setScissor(0, 0, 0, 0)
else
local dpi = lovr.system.getWindowDensity()
graphics.pass:setScissor(dx * dpi, dy * dpi, dw * dpi, dh * dpi)
end
else
graphics.pass:setScissor()
end
else
if x then
setClip({ x, y, w, h })
else
setClip(nil)
end
end
end
function graphics.setColor(r, g, b, a)
graphics.pass:setColor(r, g, b, a)
end
function graphics.rectangle(x, y, w, h, rx, ry)
setShader("color")
if (rx or 0) == 0 and (ry or 0) == 0 then
graphics.pass:setBlendMode()
end
graphics.pass:roundrect(x + w * 0.5, windowHeight - (y + h * 0.5), 0, w, h, 0, 0, 0, 0, 0, rx)
graphics.pass:setBlendMode("alpha", "alphamultiply")
end
function graphics.circle(x, y, r)
setShader("color")
graphics.pass:circle(x, windowHeight - y, 0, r)
end
function graphics.setFont(f)
graphics.pass:setFont(f)
end
function graphics.print(s, x, y)
setShader("font")
graphics.pass:text(s, x, windowHeight - y, 0, 1, 0, 0, 1, 0, 0, "left", "top")
end
--- @param image Texture
function graphics.image(image, filter, x, y, w, h)
setFilter(filter)
setShader("image")
graphics.pass:setMaterial(image)
graphics.pass:plane(x + w * 0.5, windowHeight - (y + h * 0.5), 0, w, h)
graphics.pass:setMaterial()
end
function graphics.nineSlice(nineSlice, filter, x, y, w, h)
local image, mesh = nineSliceMesh(nineSlice, x, y, w, h)
setFilter(filter)
setShader("image")
graphics.pass:setMaterial(image)
graphics.pass:draw(mesh)
graphics.pass:setMaterial()
end
function graphics.msdfImage(image, x, y, w, h)
setFilter("linear")
setShader("msdf")
graphics.pass:setMaterial(image)
graphics.pass:plane(x + w * 0.5, windowHeight - (y + h * 0.5), 0, w, h)
graphics.pass:setMaterial()
end
function graphics.msdfNineSlice(nineSlice, x, y, w, h)
local image, mesh = nineSliceMesh(nineSlice, x, y, w, h)
setFilter("linear")
setShader("msdf")
graphics.pass:setMaterial(image)
graphics.pass:draw(mesh)
graphics.pass:setMaterial()
end
return graphics