Skip to content

Instantly share code, notes, and snippets.

@Beherith
Last active March 20, 2022 12:50
Show Gist options
  • Save Beherith/107f3a31371f6cbe78e62833a84fed3a to your computer and use it in GitHub Desktop.
Save Beherith/107f3a31371f6cbe78e62833a84fed3a to your computer and use it in GitHub Desktop.
better outlines
function widget:GetInfo()
return {
name = "Unit Outlines GL4",
desc = "An interesting way of doing unit outlines",
author = "Beherith",
date = "2022.03.05",
license = "Lua: GNU GPL, v2 or later, GLSL code: (c) Beherith, mysterme@gmail.com ",
layer = -50,
enabled = false
}
end
local myvisibleUnits = {} -- table of unitID : unitDefID
local resurrectionHalosVBO = nil
local resurrectionHalosShader = nil
local luaShaderDir = "LuaUI/Widgets/Include/"
local texture = 'LuaUI/Images/halo.dds'
local OPTIONS = {
haloSize = 0.8,
haloDistance = 2.0,
skipBuildings = true,
}
local unitConf = {}
for unitDefID, unitDef in pairs(UnitDefs) do
--if not OPTIONS.skipBuildings or (OPTIONS.skipBuildings and not (unitDef.isBuilding or unitDef.isFactory or unitDef.speed==0)) then
local xsize, zsize = unitDef.xsize, unitDef.zsize
local scale = math.max(xsize,zsize)
unitConf[unitDefID] = {scale=scale, iconSize=scale*OPTIONS.haloSize, height=unitDef.height}
--end
end
local shaderConfig = {
TRANSPARENCY = 0.2, -- transparency of the stuff drawn
HEIGHTOFFSET = 1, -- Additional height added to everything
ANIMATION = 1, -- set to 0 if you dont want animation
INITIALSIZE = 0.66, -- What size the stuff starts off at when spawned
GROWTHRATE = 4, -- How fast it grows to full size
BREATHERATE = 30.0, -- how fast it periodicly grows
BREATHESIZE = 0.05, -- how much it periodicly grows
TEAMCOLORIZATION = 1.0, -- not used yet
CLIPTOLERANCE = 1.1, -- At 1.0 it wont draw at units just outside of view (may pop in), 1.1 is a good safe amount
USETEXTURE = 1, -- 1 if you want to use textures (atlasses too!) , 0 if not
BILLBOARD = 1, -- 1 if you want camera facing billboards, 0 is flat on ground
POST_ANIM = " ", -- what you want to do in the animation post function (glsl snippet, see shader source)
POST_VERTEX = "v_color = v_color;", -- noop
POST_GEOMETRY = "gl_Position.z = (gl_Position.z) - 256.0 / (gl_Position.w);", --"g_uv.zw = dataIn[0].v_parameters.xy;", -- noop
POST_SHADING = "fragColor.rgba = fragColor.rgba;", -- noop
MAXVERTICES = 64, -- The max number of vertices we can emit, make sure this is consistent with what you are trying to draw (tris 3, quads 4, corneredrect 8, circle 64
USE_CIRCLES = 1, -- set to nil if you dont want circles
USE_CORNERRECT = 1, -- set to nil if you dont want cornerrect
USE_TRIANGLES = 1,
FULL_ROTATION = 0, -- the primitive is fully rotated in the units plane
DISCARD = 0, -- Enable alpha threshold to discard fragments below 0.01
}
---- GL4 Backend Stuff----
local DrawPrimitiveAtUnitVBO = nil
local DrawPrimitiveAtUnitShader = nil
local luaShaderDir = "LuaUI/Widgets/Include/"
local LuaShader = VFS.Include(luaShaderDir.."LuaShader.lua")
VFS.Include(luaShaderDir.."instancevbotable.lua")
local vsSrc = [[
#version 420
#extension GL_ARB_uniform_buffer_object : require
#extension GL_ARB_shader_storage_buffer_object : require
#extension GL_ARB_shading_language_420pack: require
#line 5000
layout (location = 0) in vec4 lengthwidthcornerheight;
layout (location = 1) in uint teamID;
layout (location = 2) in uint numvertices;
layout (location = 3) in vec4 parameters; // lifestart, ismine
layout (location = 4) in vec4 uvoffsets; // this is optional, for using an Atlas
layout (location = 5) in uvec4 instData;
//__ENGINEUNIFORMBUFFERDEFS__
//__DEFINES__
struct SUniformsBuffer {
uint composite; // u8 drawFlag; u8 unused1; u16 id;
uint unused2;
uint unused3;
uint unused4;
float maxHealth;
float health;
float unused5;
float unused6;
vec4 speed;
vec4[5] userDefined; //can't use float[20] because float in arrays occupies 4 * float space
};
layout(std140, binding=1) readonly buffer UniformsBuffer {
SUniformsBuffer uni[];
};
#line 10000
uniform float addRadius;
uniform float iconDistance;
out DataVS {
uint v_numvertices;
float v_rotationY;
vec4 v_color;
vec4 v_lengthwidthcornerheight;
vec4 v_centerpos;
vec4 v_uvoffsets;
vec4 v_parameters;
};
layout(std140, binding=0) readonly buffer MatrixBuffer {
mat4 UnitPieces[];
};
bool vertexClipped(vec4 clipspace, float tolerance) {
return any(lessThan(clipspace.xyz, -clipspace.www * tolerance)) ||
any(greaterThan(clipspace.xyz, clipspace.www * tolerance));
}
void main()
{
uint baseIndex = instData.x; // this tells us which unit matrix to find
mat4 modelMatrix = UnitPieces[baseIndex]; // This gives us the models world pos and rot matrix
gl_Position = cameraViewProj * vec4(modelMatrix[3].xyz, 1.0); // We transform this vertex into the center of the model
v_rotationY = atan(modelMatrix[0][2], modelMatrix[0][0]); // we can get the euler Y rot of the model from the model matrix
v_uvoffsets = uvoffsets;
v_parameters = parameters;
v_color = teamColor[teamID]; // We can lookup the teamcolor right here
v_centerpos = vec4( modelMatrix[3].xyz, 1.0); // We are going to pass the centerpoint to the GS
v_lengthwidthcornerheight = lengthwidthcornerheight;
v_numvertices = numvertices;
if (vertexClipped(gl_Position, CLIPTOLERANCE)) v_numvertices = 0; // Make no primitives on stuff outside of screen
// TODO: take into account size of primitive before clipping
// this sets the num prims to 0 for units further from cam than iconDistance
float cameraDistance = length((cameraViewInv[3]).xyz - v_centerpos.xyz);
if (cameraDistance > iconDistance) v_numvertices = 0;
if (dot(v_centerpos.xyz, v_centerpos.xyz) < 1.0) v_numvertices = 0; // if the center pos is at (0,0,0) then we probably dont have the matrix yet for this unit, because it entered LOS but has not been drawn yet.
v_centerpos.y += HEIGHTOFFSET; // Add some height to ensure above groundness
v_centerpos.y += lengthwidthcornerheight.w; // Add per-instance height offset
if ((uni[instData.y].composite & 0x00000003u) < 1u ) v_numvertices = 0u; // this checks the drawFlag of wether the unit is actually being drawn (this is ==1 when then unit is both visible and drawn as a full model (not icon))
// TODO: allow overriding this check, to draw things even if unit (like a building) is not drawn
POST_VERTEX
}
]]
local gsSrc = [[
#version 330
#extension GL_ARB_uniform_buffer_object : require
#extension GL_ARB_shading_language_420pack: require
//__ENGINEUNIFORMBUFFERDEFS__
//__DEFINES__
layout(points) in;
layout(triangle_strip, max_vertices = 4) out;
#line 20000
uniform float addRadius;
uniform float iconDistance;
in DataVS {
uint v_numvertices;
float v_rotationY;
vec4 v_color;
vec4 v_lengthwidthcornerheight;
vec4 v_centerpos;
vec4 v_uvoffsets;
vec4 v_parameters;
} dataIn[];
out DataGS {
vec4 g_color;
vec4 g_uv;
float camdistance;
};
mat3 rotY;
vec4 centerpos;
void offsetVertex4( float x, float y, float z, float u, float v){
g_uv.xy = vec2(u,v);
vec3 primitiveCoords = vec3(x,y,z);
vec3 vecnorm = normalize(primitiveCoords);
gl_Position = cameraViewProj * vec4(centerpos.xyz + rotY * ( addRadius * vecnorm + primitiveCoords ), 1.0);
g_uv.zw = dataIn[0].v_parameters.zw;
EmitVertex();
}
#line 22000
void main(){
uint numVertices = dataIn[0].v_numvertices;
centerpos = dataIn[0].v_centerpos;
camdistance = length((cameraViewInv[3]).xyz - centerpos.xyz);
rotY = mat3(cameraViewInv[0].xyz,cameraViewInv[2].xyz, cameraViewInv[1].xyz); // swizzle cause we use xz
g_color = dataIn[0].v_color;
float length = dataIn[0].v_lengthwidthcornerheight.x;
float width = dataIn[0].v_lengthwidthcornerheight.y;
if (numVertices == uint(4)){ // A quad
offsetVertex4( width * 0.5, 0.0, length * 0.5, 0.0, 1.0);
offsetVertex4( width * 0.5, 0.0, -length * 0.5, 0.0, 0.0);
offsetVertex4(-width * 0.5, 0.0, length * 0.5, 1.0, 1.0);
offsetVertex4(-width * 0.5, 0.0, -length * 0.5, 1.0, 0.0);
EndPrimitive();
}
}
]]
local fsSrc =
[[
#version 420
#extension GL_ARB_uniform_buffer_object : require
#extension GL_ARB_shading_language_420pack: require
//__ENGINEUNIFORMBUFFERDEFS__
//__DEFINES__
#line 30000
uniform float addRadius;
uniform float iconDistance;
in DataGS {
vec4 g_color;
vec4 g_uv;
float camdistance;
};
uniform sampler2D DrawPrimitiveAtUnitTexture;
uniform sampler2D mapDepths;
uniform sampler2D modelDepths;
uniform float stencilPass = 0.0; // 1 if we are stenciling
out vec4 fragColor;
#define RESOLUTION 3
void main(void)
{
vec4 texcolor = texture(DrawPrimitiveAtUnitTexture, g_uv.xy);
float sqrtdist = RESOLUTION * 1.4241;
vec2 screenUV = gl_FragCoord.xy/ viewGeometry.xy;
float mapdepth = texture(mapDepths, screenUV).x;
float modeldepth = texture(modelDepths, screenUV).x;
float fulldepth = min(mapdepth, modeldepth);
float deltadepth = max(mapdepth - modeldepth, 0.0);
float camdistancefact = clamp( camdistance/ 1000, 1.0, 3.0 );
if (deltadepth > 0.0) discard; // we hit a model, bail!
//if ((modeldepth < 1.0) && (mapdepth > modeldepth)) discard; // model occluded behind map
if (stencilPass > 0.5){
float nearest = RESOLUTION*20 + 1 ;
for (int x = -1 * RESOLUTION; x <= RESOLUTION; x++){
for (int y = -1* RESOLUTION; y <= RESOLUTION; y++){
//vec2 pixeloffset = vec2(float(x), float(y)) * sin(timeInfo.x * 0.1) * 2.0 ;
vec2 pixeloffset = vec2(float(x), float(y));
vec2 screendelta = pixeloffset / viewGeometry.xy;
float mapd = texture(mapDepths, screenUV+ screendelta).x;
float modd = texture(modelDepths, screenUV + screendelta).x;
float dd = max(mapd - modd, 0.0);
if (dd > 0 ) nearest = min(nearest, length(pixeloffset));
}
}
if (min(g_uv.x, g_uv.y) > 0.02){ // we are NOT the edges
//if (nearest > RESOLUTION*20 +1 ) discard;
//float stupidcolor =
fragColor.rgba = vec4(vec3(0.0), 1.0 - pow((nearest/(RESOLUTION * 1.4241 / camdistancefact)), 0.5));
//fragColor.rgba = vec4(vec2(fract(gl_FragCoord.xy*0.1 )),0.0, 0.3);
}else{
fragColor.rgba = vec4(vec3(fract(nearest/16)), 1.0);
fragColor.rgba = vec4(vec2(fract(gl_FragCoord.xy*0.1 )),0.0, 0.3);
}
}else{
fragColor.rgba = vec4(vec2(fract(gl_FragCoord.xy*0.1 )),0.0, 0.3);
}
}
]]
local function goodbye(reason)
Spring.Echo("DrawPrimitiveAtUnits GL4 widget exiting with reason: "..reason)
widgetHandler:RemoveWidget()
end
local function InitDrawPrimitiveAtUnit(shaderConfig, DPATname)
local engineUniformBufferDefs = LuaShader.GetEngineUniformBufferDefs()
vsSrc = vsSrc:gsub("//__ENGINEUNIFORMBUFFERDEFS__", engineUniformBufferDefs)
fsSrc = fsSrc:gsub("//__ENGINEUNIFORMBUFFERDEFS__", engineUniformBufferDefs)
gsSrc = gsSrc:gsub("//__ENGINEUNIFORMBUFFERDEFS__", engineUniformBufferDefs)
DrawPrimitiveAtUnitShader = LuaShader(
{
vertex = vsSrc:gsub("//__DEFINES__", LuaShader.CreateShaderDefinesString(shaderConfig)),
fragment = fsSrc:gsub("//__DEFINES__", LuaShader.CreateShaderDefinesString(shaderConfig)),
geometry = gsSrc:gsub("//__DEFINES__", LuaShader.CreateShaderDefinesString(shaderConfig)),
uniformInt = {
DrawPrimitiveAtUnitTexture = 0;
mapDepths = 1,
modelDepths = 2,
},
uniformFloat = {
addRadius = 1,
iconDistance = 1,
},
},
DPATname .. "Shader GL4"
)
local shaderCompiled = DrawPrimitiveAtUnitShader:Initialize()
if not shaderCompiled then goodbye("Failed to compile ".. DPATname .." GL4 ") end
DrawPrimitiveAtUnitVBO = makeInstanceVBOTable(
{
{id = 0, name = 'lengthwidthcorner', size = 4},
{id = 1, name = 'teamID', size = 1, type = GL.UNSIGNED_INT},
{id = 2, name = 'numvertices', size = 1, type = GL.UNSIGNED_INT},
{id = 3, name = 'parameters', size = 4},
{id = 4, name = 'uvoffsets', size = 4},
{id = 5, name = 'instData', size = 4, type = GL.UNSIGNED_INT},
},
64, -- maxelements
DPATname .. "VBO", -- name
5 -- unitIDattribID (instData)
)
if DrawPrimitiveAtUnitVBO == nil then goodbye("Failed to create DrawPrimitiveAtUnitVBO") end
local DrawPrimitiveAtUnitVAO = gl.GetVAO()
DrawPrimitiveAtUnitVAO:AttachVertexBuffer(DrawPrimitiveAtUnitVBO.instanceVBO)
DrawPrimitiveAtUnitVBO.VAO = DrawPrimitiveAtUnitVAO
return DrawPrimitiveAtUnitVBO, DrawPrimitiveAtUnitShader
end
local function initGL4()
--local DrawPrimitiveAtUnit = VFS.Include(luaShaderDir.."DrawPrimitiveAtUnit.lua")
--local InitDrawPrimitiveAtUnit = DrawPrimitiveAtUnit.InitDrawPrimitiveAtUnit
--local shaderConfig = DrawPrimitiveAtUnit.shaderConfig -- MAKE SURE YOU READ THE SHADERCONFIG TABLE in DrawPrimitiveAtUnit.lua
shaderConfig.TRANSPARENCY = 0.5
shaderConfig.ANIMATION = 1
shaderConfig.HEIGHTOFFSET = 3.99
shaderConfig.BREATHERATE = 15.0 -- how fast it periodicly grows
shaderConfig.BREATHESIZE = 0.075 -- how much it periodicly grows
shaderConfig.MAXVERTICES = 4 -- how much it periodicly grows
shaderConfig.INITIALSIZE = 0.5 -- What size the stuff starts off at when spawned
shaderConfig.GROWTHRATE = 15 -- How fast it grows to full size
shaderConfig.POST_SHADING = "fragColor.rgba = texcolor;"
resurrectionHalosVBO, resurrectionHalosShader = InitDrawPrimitiveAtUnit(shaderConfig, "ResurrectionHalos")
end
function widget:VisibleUnitAdded(unitID, unitDefID, unitTeam)
--if unitConf[unitDefID] == nil or Spring.GetUnitRulesParam(unitID, "resurrected") == nil then return end
local gf = Spring.GetGameFrame()
myvisibleUnits[unitID] = unitDefID
pushElementInstance(
resurrectionHalosVBO, -- push into this Instance VBO Table
{
--unitConf[unitDefID].iconSize, unitConf[unitDefID].iconSize, 8, unitConf[unitDefID].height , -- lengthwidthcornerheight
unitConf[unitDefID].scale * 16, unitConf[unitDefID].scale * 16, 8, unitConf[unitDefID].height*0.5 , -- lengthwidthcornerheight
0, -- teamID
4, -- how many trianges should we make (2 = cornerrect)
gf, 0, 0, 0, -- the gameFrame (for animations), and any other parameters one might want to add
0, 1, 0, 1, -- These are our default UV atlas tranformations
0, 0, 0, 0 -- these are just padding zeros, that will get filled in
},
unitID, -- this is the key inside the VBO TAble,
true, -- update existing element
nil, -- noupload, dont use unless you know what you are doing
unitID -- last one should be UNITID?
)
end
function widget:VisibleUnitsChanged(extVisibleUnits, extNumVisibleUnits)
clearInstanceTable(resurrectionHalosVBO)
for unitID, unitDefID in pairs(extVisibleUnits) do
widget:VisibleUnitAdded(unitID, unitDefID, Spring.GetUnitTeam(unitID))
end
end
function widget:VisibleUnitRemoved(unitID)
--Spring.Echo("widget:VisibleUnitRemoved",unitID)
if resurrectionHalosVBO.instanceIDtoIndex[unitID] then
popElementInstance(resurrectionHalosVBO, unitID)
myvisibleUnits[unitID] = nil
end
end
function widget:RecvLuaMsg(msg, playerID)
if msg:sub(1,18) == 'LobbyOverlayActive' then
chobbyInterface = (msg:sub(1,19) == 'LobbyOverlayActive1')
end
end
local GL_ALWAYS = GL.ALWAYS
local GL_EQUAL = GL.EQUAL
local GL_LINE_LOOP = GL.LINE_LOOP
local GL_KEEP = 0x1E00 --GL.KEEP
local GL_REPLACE = GL.REPLACE
local GL_DECR = 0x1E03
local useStencil = true
local STENCILOPPASS = GL_DECR -- KEEP OR DECR
function widget:DrawWorld()
if chobbyInterface then return end
if Spring.IsGUIHidden() then
return
end
if resurrectionHalosVBO.usedElements > 0 then
gl.Texture(0, texture)
gl.Texture(1, "$map_gbuffer_zvaltex")-- Texture file
gl.Texture(2, "$model_gbuffer_zvaltex")-- Texture file
resurrectionHalosShader:Activate()
resurrectionHalosShader:SetUniform("iconDistance", 99999) -- pass
resurrectionHalosShader:SetUniform("addRadius", 0)
if useStencil then -- https://learnopengl.com/Advanced-OpenGL/Stencil-testing
gl.DepthMask(false)
-- FIRST PASS:
gl.ColorMask(false, false, false, false) -- disable writing to all but stencil
gl.StencilTest(true) -- enable stencil test
gl.DepthTest(false) -- dont do depth testing either
gl.StencilFunc(GL_ALWAYS, 1, 1) -- Always Passes, 1 Bit Plane, 1 As Mask
gl.StencilOp(GL_KEEP, GL_KEEP, GL_REPLACE) -- Set The Stencil Buffer To 1 Where Draw Any Polygon
--glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)
resurrectionHalosShader:SetUniform("stencilPass", 0)
resurrectionHalosVBO.VAO:DrawArrays(GL.POINTS, resurrectionHalosVBO.usedElements)
-- SECOND PASS
gl.ColorMask(true, true, true, true)
gl.StencilFunc(GL_EQUAL, 1, 0)
gl.StencilOp(GL_KEEP, GL_KEEP, STENCILOPPASS )
resurrectionHalosShader:SetUniform("stencilPass", 1.0)
resurrectionHalosVBO.VAO:DrawArrays(GL.POINTS, resurrectionHalosVBO.usedElements)
else
gl.DepthTest(false) -- dont do depth testing either
gl.DepthMask(false)
resurrectionHalosShader:SetUniform("stencilPass", 1.0)
resurrectionHalosVBO.VAO:DrawArrays(GL.POINTS, resurrectionHalosVBO.usedElements)
end
resurrectionHalosShader:Deactivate()
--gl.TexRect(-1, -1, 1000, 1000 )
gl.Clear( GL.STENCIL_BUFFER_BIT)
gl.Texture(0, false)
gl.Texture(1, false)-- Texture file
gl.Texture(2, false)-- Texture file
end
end
function widget:Initialize()
initGL4()
if WG['unittrackerapi'] and WG['unittrackerapi'].visibleUnits then
local visibleUnits = WG['unittrackerapi'].visibleUnits
for unitID, unitDefID in pairs(visibleUnits) do
widget:VisibleUnitAdded(unitID, unitDefID)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment