Instead of posting a .love file, I'll post code.
This is a very highly unoptimized version of my dynamic lighting + normal mapping shader as seen
here. I also just changed it from the ugly to the unoptimized version right now, so it may not work correctly.
Also it's the version without the matrix multiplication for proper lighting when the image is rotated. Maybe I'll add that to this later.
Code: Select all
const int numlights = 3;
const vec3 gamma = vec3(2.2);
const vec3 invgamma = 1.0/gamma;
const vec3 viewdir = vec3(0.0, 0.0, 1.0);
extern vec4 Lights[6]; // max n / 2 lights
extern Image normaltexture;
extern number specpower = 25.0;
extern number yres;
extern number z = 0.0;
vec4 pow(vec4 color, vec3 exp)
{
color.rgb = pow(color.rgb, exp);
return color;
}
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 pixel_coords)
{
vec3 coords = vec3(pixel_coords.x, yres - pixel_coords.y, z);
vec4 texcolor = pow(Texel(texture, texture_coords), gamma);
vec4 finalcolor = pow(vec4(0.0), gamma) * texcolor;
vec4 normal = Texel(normaltexture, texture_coords);
vec3 N = normalize(normal.xyz * 2.0 - 1.0);
float specvalue = normal.a; // the normal texture has the specular texture inside its alpha component
for (int i = 0; i < numlights * 2; i += 2)
{
vec3 L = normalize(Lights[i].xyz - coords);
number NdotL = max(dot(N, L), 0.0);
vec3 R = normalize(reflect(-L, N));
number specular = pow(max(dot(viewdir, R), 0.0), specpower);
finalcolor += (texcolor * Lights[i+1] * NdotL) + (Lights[i+1] * specvalue * specular);
}
finalcolor.a = texcolor.a;
return pow(finalcolor, invgamma);
}
EDIT: For reference, here is the optimized file I made for my space game. It's not very easy to understand.
Code: Select all
Effects.Lighting = class("Effects.Lighting")
function Effects.Lighting:initialize()
local mt = {
__index = function(self, k)
if type(k) == "number" then self[k] = {} return self[k] end
end,
}
self.shaders = setmetatable({numdirlights=0, numpointlights=0}, mt)
self.lights = {
point = {num=0},
dir = {num=0},
}
self.ambientcolor = {0, 0, 0, 0}
self.current = {
angle = 0,
specpower = 25,
rotationmatrix = RotationMatrix(0),
z = 0,
}
end
function Effects.Lighting:GetCurrentShaders()
return self.shaders[self.lights.point.num][self.lights.dir.num]
end
function Effects.Lighting:AddLight(lighttype, pos, color, radius)
local light = {
pos = pos or {0, 0, 0, radius or 0},
radius = radius or pos[4] or 0,
color = color or {1, 1, 1, 1},
type = lighttype,
}
pos[4] = radius or pos[4]
self.lights[lighttype][light] = true
self.lights[lighttype].num = self.lights[lighttype].num + 1
self:UpdateInfo()
return light
end
function Effects.Lighting:UpdateInfo()
local curshaders = self:GetCurrentShaders() or {}
local numpointlights = self.lights.point.num
local numdirlights = self.lights.dir.num
-- debug.debug()
if not next(curshaders) then
curshaders = {
standard = self:GenerateShader(false, false, numpointlights, numdirlights),
normals = self:GenerateShader(true, false, numpointlights, numdirlights),
specular = self:GenerateShader(true, true, numpointlights, numdirlights),
}
--print(curshaders, numpointlights, numdirlights)
else
--print("no new light", numpointlights, numdirlights)
end
for k,v in pairs(curshaders) do
v:send("rotationmatrix", self.current.rotationmatrix)
if k == "normals" or k == "specular" then
if k == "specular" then
v:send("specpower", self.current.specpower)
end
if self.current.normaltexture then v:send("normaltexture", self.current.normaltexture) end
end
end
self.shaders[numpointlights][numdirlights] = curshaders
end
function Effects.Lighting:RemoveLight(light)
if not self.lights[light.type][light] then return end
self.lights[light.type][light] = nil
self.lights[light.type].num = self.lights[light.type].num - 1
self:UpdateInfo()
end
function Effects.Lighting:SetMaterialInfo(angle, z, normaltexture, specpower)
local rotationmatrix = RotationMatrix(angle or 0)
for k,v in pairs(self:GetCurrentShaders() or {}) do
if self.current.angle ~= angle then
v:send("rotationmatrix", rotationmatrix)
end
if self.current.z ~= z then
v:send("z", z)
end
if (k == "normals" or k == "specular") and normaltexture then
v:send("normaltexture", normaltexture)
end
if k == "specular" and specpower and self.current.specpower ~= specpower then
v:send("specpower", specpower)
end
end
self.current.angle = angle
self.current.z = z
self.current.rotationmatrix = rotationmatrix
self.current.specpower = specpower
self.current.normaltexture = normaltexture
end
function Effects.Lighting:SetEffectType(etype)
local cureffects = self:GetCurrentShaders()
love.graphics.setPixelEffect(cureffects and cureffects[etype] or nil)
end
function Effects.Lighting:update(dt)
local dirlights, pointlights = {}, {}
for k in pairs(self.lights.dir) do
if k ~= "num" then
dirlights[#dirlights+1] = k.pos
dirlights[#dirlights+1] = k.color
end
end
for k in pairs(self.lights.point) do
if k ~= "num" then
pointlights[#pointlights+1] = k.pos
pointlights[#pointlights+1] = k.color
end
end
local cureffects = self:GetCurrentShaders() or {}
for k, v in pairs(cureffects) do
-- send lights
if #pointlights > 0 then v:send("pointlights", unpack(pointlights)) end
if #dirlights > 0 then v:send("dirlights", unpack(dirlights)) end
end
end
function Effects.Lighting:SetAmbientColor(r, g, b, a)
self.ambientcolor = {r, g, b, a}
for k,v in pairs(self:GetCurrentShaders()) do
v:send("ambientcolor", self.ambientcolor)
end
end
local phong_base = [[
const vec3 gamma = vec3(2.2);
const vec3 invgamma = 1.0/gamma;
vec3 viewdir = vec3(0.0, 0.0, 1.0);
/* pointlight_array */
/* dirlight_array */
/* normal_texture */
extern number yres = 720;
extern number z = 0.0;
extern number specpower = 25.0;
extern mat2 rotationmatrix;
extern vec4 ambientcolor = vec4(0.5);
vec4 pow(vec4 color, vec3 exp)
{
color.rgb = pow(color.rgb, exp);
return color;
}
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 pixel_coords)
{
viewdir.xy = rotationmatrix * viewdir.xy; // TODO: only do this if specular is on
vec3 coords = vec3(pixel_coords.x, yres - pixel_coords.y, z);
vec4 texcolor = pow(Texel(texture, texture_coords), gamma);
vec4 finalcolor = pow(ambientcolor, gamma) * texcolor;
// get normal
/* normal_texture_access */
// calculate point lights (if any)
/* pointlight_calc */
// calculate directional lights (if any)
/* dirlight_calc */
finalcolor.a = texcolor.a;
return pow(finalcolor, invgamma) * color;
}
]]
function Effects.Lighting:GenerateShader(use_normalmap, use_specularmap, num_pointlights, num_dirlights)
local str = string.format("%d;%d;%d;%d", use_normalmap and 1 or 0, use_specularmap and 1 or 0, num_pointlights, num_dirlights)
if self.shaders[str] then return self.shaders[str] end
use_specularmap = use_normalmap and use_specularmap or false
local shadertext = phong_base
local formats = {
pointlight_array = "extern vec4 pointlights[%d];",
dirlight_array = "extern vec4 dirlights[%d];",
normal_texture = "extern Image normaltexture;",
normal_texture_access = [[
vec4 normal = Texel(normaltexture, texture_coords);
vec3 N = normalize(normal.xyz * 2.0 - 1.0);
%s
]],
pointlight_calc = [[
number radius_PN_ = pointlights[_PN2_].w;
vec3 lightdir_PN_ = (pointlights[_PN2_].xyz - coords) / radius_PN_;
lightdir_PN_.xy = rotationmatrix * lightdir_PN_.xy;
vec3 L_PN_ = normalize(lightdir_PN_);
number atten_PN_ = clamp(1.0 - dot(lightdir_PN_, lightdir_PN_), 0.0, 1.0);
number NdotL_PN_ = max(dot(N, L_PN_), 0.0);
%s
finalcolor += (texcolor * pointlights[_PN3_] * NdotL_PN_ * atten_PN_)%s;
]],
}
local pointlight_specular = {[[
vec3 R_PN_ = normalize(reflect(-L_PN_, N));
number specular_PN_ = pow(max(dot(viewdir, R_PN_), 0.0), specpower);]],
[[ + (pointlights[_PN3_] * specvalue * specular_PN_ * atten_PN_)]],
}
formats.dirlight_calc = [[
vec3 lightdir_DN_ = dirlights[_DN2_].xyz;
lightdir_DN_.xy = rotationmatrix * lightdir_DN_.xy;
vec3 L_DN_ = normalize(lightdir_DN_);
number NdotL_DN_ = max(dot(N, L_DN_), 0.0);
%s
finalcolor += (texcolor * dirlights[_DN3_] * NdotL_DN_)%s;
]]
local dirlight_specular = {}
for i,v in ipairs(pointlight_specular) do
v = v:gsub("pointlights", "dirlights")
v = v:gsub("_PN", "_DN")
dirlight_specular[i] = v:gsub(" %* atten_DN_", "")
end
local t = {}
for k,v in pairs(formats) do t[k] = "" end
if use_normalmap then
t.normal_texture = formats.normal_texture
if use_specularmap then
t.normal_texture_access = formats.normal_texture_access:format("number specvalue = normal.a;")
t.pointlight_calc = formats.pointlight_calc:format(unpack(pointlight_specular))
t.dirlight_calc = formats.dirlight_calc:format(unpack(dirlight_specular))
else
t.normal_texture_access = formats.normal_texture_access:format("")
t.pointlight_calc = formats.pointlight_calc:format("", "")
t.dirlight_calc = formats.dirlight_calc:format("", "")
end
else
t.normal_texture_access = "vec3 N = vec3(0.0, 0.0, 1.0);"
t.pointlight_calc = formats.pointlight_calc:format("", "")
t.dirlight_calc = formats.dirlight_calc:format("", "")
end
local function add_lights(basestr, numlights)
local str = ""
for i=1, numlights do
local f = basestr
f = f:gsub("_(%a)N_", "_%1"..i.."_")
f = f:gsub("_(%a)N2_", (i-1)*2)
f = f:gsub("_(%a)N3_", (i-1)*2 + 1)
str = str..f
end
return str
end
t.pointlight_array = num_pointlights > 0 and formats.pointlight_array:format(num_pointlights * 2) or ""
t.pointlight_calc = add_lights(t.pointlight_calc, num_pointlights)
num_dlights = num_dirlights
-- debug.debug()
t.dirlight_array = num_dirlights > 0 and formats.dirlight_array:format(num_dirlights * 2) or ""
t.dirlight_calc = add_lights(t.dirlight_calc, num_dirlights)
shadertext = shadertext:gsub("\t*/%* (.-) %*/", t)
shadertext = shadertext:gsub("\n\t\t", "\n")
-- print(shadertext)
self.shaders[str] = love.graphics.newPixelEffect(shadertext)
return self.shaders[str]
end
This is a passthrough shader for graphics primitives (untextured things)
Code: Select all
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 pixel_coords)
{
return color;
}
This is a passthrough shader for images
Code: Select all
vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 pixel_coords)
{
return color * Texel(texture, texture_coords);
}
This is one way to turn a square image into a rotating sphere (both ways are shown
here)
Code: Select all
extern number time; // time in seconds
vec4 effect(vec4 color, Image texture, vec2 tc, vec2 pixel_coords)
{
vec2 p = -1.0 + 2.0 * tc;
number r = dot(p, p);
if (r > 1.0) discard;
number f = (1.0 - sqrt(1.0 - r)) / (r);
vec2 uv;
uv.x = 1.0*p.x*f + time;
uv.y = 1.0*p.y*f;
return vec4(Texel(texture, uv).xyz, 1.0) * color;
}
Here is the other way shown in the video
Code: Select all
const number pi = 3.14159265;
const number pi2 = 2.0 * pi;
extern number time;
vec4 effect(vec4 color, Image texture, vec2 tc, vec2 pixel_coords)
{
vec2 p = 2.0 * (tc - 0.5);
number r = sqrt(p.x*p.x + p.y*p.y);
if (r > 1.0) discard;
number d = r != 0.0 ? asin(r) / r : 0.0;
vec2 p2 = d * p;
number x3 = mod(p2.x / (pi2) + 0.5 + time, 1.0);
number y3 = p2.y / (pi2) + 0.5;
vec2 newCoord = vec2(x3, y3);
vec4 sphereColor = color * Texel(texture, newCoord);
return sphereColor;
}
There are a bunch of post-processing GLSL shaders available
here which can be ported to the LÖVE pixeleffect syntax pretty easily.