Aegisub/automation/demos/raytracer.lua

408 lines
10 KiB
Lua

-- This script is given to the public domain
-- It was originally intended as an april fool's joke in 2007, though it never really caught on.
-- You're encouraged to experiment with this if you have plenty of time.
-- Even if it's made as a joke, it does show that "anything is possible with ASS, although some things are insane to try."
script_name = "Raytracer"
script_description = "Reads subtitles as a scene description and raytraces the scene"
script_author = "jfs"
script_version = tostring(math.pi)
include("utils.lua")
max_iter = 3
function raytrace(subs)
aegisub.progress.task("Reading scene...")
local lights, tris, camera, xres, yres = read_scene(subs)
aegisub.progress.task("Raytracing...")
local curp, totalp = 0, xres*yres
for y = 0, yres-1 do
aegisub.progress.task(string.format("Raytracing, line %d/%d...", y+1, yres))
for x = 0, xres-1 do
aegisub.progress.set(curp/totalp*100)
local l = trace_point(x, y, (x+0.5)/xres, (y+0.5)/yres, lights, tris, camera)
if l then
subs.append(l)
end
curp = curp + 1
end
end
aegisub.progress.task("Done.")
aegisub.progress.set(100)
end
function trace_point(px, py, x, y, lights, tris, camera)
-- fixme, assume a camera here ignoring defined one
local vec = vector.norm( { 2*x-1, 1-2*y, -1 } )
local r, g, b = trace_vec({0,0,-1}, vec, lights, tris, 0)
if not r then
return nil
end
r, g, b = clamp(r, 0, 255), clamp(g, 0, 255), clamp(b, 0, 255)
-- todo, make line
local l = {
class = "dialogue",
section = "Events",
comment = false,
layer = 0,
start_time = 0,
end_time = 3600*1000, -- one hour
style = "p",
actor = "",
margin_l = 0,
margin_r = 0,
margin_t = 0,
margin_b = 0,
effect = "",
text = string.format("{\\pos(%d,%d)\\1c&H%02x%02x%02x&\\p1}m 0 0 l 1 0 1 1 0 1", px, py, r, g, b)
}
return l
end
function trace_vec(org, vec, lights, tris, iter)
if iter > max_iter then
return 0, 0, 0
end
local hit = find_intersect(org, vec, tris)
if not hit then
return nil
end
-- got intersection, calculate lighting
local r, g, b = hit.t.c.r*10, hit.t.c.g*10, hit.t.c.b*10
local ray_cos_theta = vector.dot(hit.t.n, vec)
hit.p = hit.t.p[1]
hit.p = vector.add(hit.p, vector.scale(vector.sub(hit.t.p[2], hit.t.p[1]), hit.u))
hit.p = vector.add(hit.p, vector.scale(vector.sub(hit.t.p[3], hit.t.p[1]), hit.v))
for i, l in pairs(lights) do
-- shadow ray
local lvec = vector.sub(l.p, hit.p)
local shadow = find_intersect(hit.p, lvec, tris)
if not shadow or (shadow and (shadow.dist < 0 or shadow.dist > 1)) then
-- not in shadow
local lvecs = vector.len(lvec)
-- diffuse component
local light_cos_theta = math.abs(vector.dot(hit.t.n, lvec))
-- specular component
local cos_alpha = vector.dot(vector.sub(vector.scale(hit.t.n, 2*light_cos_theta), lvec), vec)
local cos_n_alpha = cos_alpha^3 -- arbitrary constant for now
-- add up
r = r + l.c.r*hit.t.c.r * (light_cos_theta*0.6 + cos_n_alpha*0.4) / math.max(lvecs,1)
g = g + l.c.g*hit.t.c.g * (light_cos_theta*0.6 + cos_n_alpha*0.4) / math.max(lvecs,1)
b = b + l.c.b*hit.t.c.b * (light_cos_theta*0.6 + cos_n_alpha*0.4) / math.max(lvecs,1)
end
end
-- reflection
local rvec = vector.sub(vector.scale(hit.t.n, 2*vector.dot(hit.t.n, vec)), vec)
local rr, rg, rb = trace_vec(hit.p, rvec, lights, tris, iter+1)
if not rr then
rr, rg, rb = 0, 0, 0
end
r = r*0.75 + rr*0.25
g = g*0.75 + rg*0.25
b = b*0.75 + rb*0.25
return r, g, b
end
function find_intersect(org, vec, tris)
local intersec = nil
-- find closest intersection
for i, t in pairs(tris) do
local dist, u, v = intersect_triangle(org, vec, t)
if dist and dist > 0 then
if not intersec or intersec.dist > dist then
intersec = {dist=dist, u=u, v=v, t=t}
end
end
end
return intersec
end
function intersect_triangle(org, vec, triangle)
-- taken from http://www.graphics.cornell.edu/pubs/1997/MT97.html
-- find vectors for two edges sharing point 1
local edge1, edge2 = vector.sub(triangle.p[2], triangle.p[1]), vector.sub(triangle.p[3], triangle.p[1])
-- begin calculating determinant - also used to calculate U parameter
local pvec = vector.cross(vec, edge2)
-- if determinant is near zero, ray lies in plane of triangle
local det = vector.dot(edge1, pvec)
if det > -0.00001 and det < 0.00001 then
-- parallel to plane
return nil
end
local inv_det = 1 / det
-- calculate distance from point 1 to ray origin
local tvec = vector.sub(org, triangle.p[1])
-- calculate U parameter and test bounds
local u = vector.dot(tvec, pvec) * inv_det
if u < 0 or u > 1 then
-- crosses plane but outside triangle
return nil
end
-- prepare to test V parameter
local qvec = vector.cross(tvec, edge1)
-- calculate V parameter and test bounds
local v = vector.dot(vec, qvec) * inv_det
if v < 0 or (u+v) > 1 then
-- crosses plane but outside triangle
return nil
end
-- calculate distance, ray intersects triangle
local dist = vector.dot(triangle.p[3], qvec)
return dist, u, v
end
function read_scene(subs)
local lights = {}
local tris = {}
local camera = { pos = {0,0,-1}, up = {0,1,0}, plane } -- fixme
local xres, yres = 384, 288
local style = {
class = "style",
section = "V4+ Styles",
name = "p",
fontname = "Arial",
fontsize = "20",
color1 = "&H00000000&",
color2 = "&H00000000&",
color3 = "&H00000000&",
color4 = "&H00000000&",
bold = false,
italic = false,
underline = false,
strikeout = false,
scale_x = 100,
scale_y = 100,
spacing = 0,
angle = 0,
borderstyle = 0,
outline = 0,
shadow = 0,
align = 5,
margin_l = 0,
margin_r = 0,
margin_t = 0,
margin_b = 0,
encoding = 0
}
local i, maxi = 1, #subs
local replaced_style = false
while i < maxi do
aegisub.progress.set(i / maxi * 100)
local l = subs[i]
if l.class == "dialogue" then
parse_line(l, lights, tris, camera)
subs.delete(i)
maxi = maxi - 1
elseif l.class == "style" then
if replaced_style then
subs.delete(i)
maxi = maxi - 1
else
style.section = l.section
subs[i] = style
replaced_style = true
i = i + 1
end
elseif l.class == "info" then
local k = l.key:lower()
if k == "playresx" then
xres = math.floor(l.value)
elseif k == "playresy" then
yres = math.floor(l.value)
end
i = i + 1
else
i = i + 1
end
end
return lights, tris, camera, xres, yres
end
function parse_line(line, lights, tris, camera)
local val, rest = string.headtail(line.text)
if val == "light" then
local pos, color = {}, {}
val, rest = string.headtail(rest)
pos[1] = tonumber(val)
val, rest = string.headtail(rest)
pos[2] = tonumber(val)
val, rest = string.headtail(rest)
pos[3] = tonumber(val)
-- these work as intensity values so they should probably be high
val, rest = string.headtail(rest)
color.r = tonumber(val) or 0
val, rest = string.headtail(rest)
color.g = tonumber(val) or 0
val, rest = string.headtail(rest)
color.b = tonumber(val) or 0
local light = {
p = pos,
c = color
}
table.insert(lights, light)
elseif val == "tri" then
local coord1, coord2, coord3, color = {}, {}, {}, {}
val, rest = string.headtail(rest)
coord1[1] = tonumber(val)
val, rest = string.headtail(rest)
coord1[2] = tonumber(val)
val, rest = string.headtail(rest)
coord1[3] = tonumber(val)
val, rest = string.headtail(rest)
coord2[1] = tonumber(val)
val, rest = string.headtail(rest)
coord2[2] = tonumber(val)
val, rest = string.headtail(rest)
coord2[3] = tonumber(val)
val, rest = string.headtail(rest)
coord3[1] = tonumber(val)
val, rest = string.headtail(rest)
coord3[2] = tonumber(val)
val, rest = string.headtail(rest)
coord3[3] = tonumber(val)
-- these work as reflectivity values so they should be in range 0..1
val, rest = string.headtail(rest)
color.r = tonumber(val) or 0
val, rest = string.headtail(rest)
color.g = tonumber(val) or 0
val, rest = string.headtail(rest)
color.b = tonumber(val) or 0
local t = {
p = {coord1, coord2, coord3},
n = vector.norm(vector.normal(coord1, coord2, coord3)),
c = color
}
table.insert(tris, t)
elseif val == "camera" then
-- fixme, redefine
val, rest = string.headtail(rest)
camera.pos[1] = tonumber(val)
val, rest = string.headtail(rest)
camera.pos[2] = tonumber(val)
val, rest = string.headtail(rest)
camera.pos[3] = tonumber(val)
val, rest = string.headtail(rest)
camera.plane[1][1] = tonumber(val)
val, rest = string.headtail(rest)
camera.plane[1][2] = tonumber(val)
val, rest = string.headtail(rest)
camera.plane[1][3] = tonumber(val)
val, rest = string.headtail(rest)
camera.plane[2][1] = tonumber(val)
val, rest = string.headtail(rest)
camera.plane[2][2] = tonumber(val)
val, rest = string.headtail(rest)
camera.plane[2][3] = tonumber(val)
camera.start_time = line.start_time
camera.end_time = line.end_time
else
-- unknown, ignore
end
end
vector = {}
vector.null = {0,0,0}
function vector.add(v1, v2)
local r = {}
r[1] = v1[1] + v2[1]
r[2] = v1[2] + v2[2]
r[3] = v1[3] + v2[3]
return r
end
function vector.sub(v1, v2) -- v1 minus v2
local r = {}
r[1] = v1[1] - v2[1]
r[2] = v1[2] - v2[2]
r[3] = v1[3] - v2[3]
return r
end
function vector.scale(v, s)
local r = {}
r[1] = v[1] * s
r[2] = v[2] * s
r[3] = v[3] * s
return r
end
function vector.len(v)
return math.sqrt(v[1]*v[1] + v[2]*v[2] + v[3]*v[3])
end
function vector.norm(v)
local r, il = {}, 1/vector.len(v)
r[1] = v[1]*il
r[2] = v[2]*il
r[3] = v[3]*il
return r
end
function vector.dot(v1, v2)
return v1[1]*v2[1] + v1[2]*v2[2] + v1[3]*v2[3]
end
function vector.cross(v1, v2)
local r = {}
r[1] = v1[2]*v2[3] - v1[3]*v2[2]
r[2] = v1[1]*v2[3] - v1[3]*v2[1]
r[3] = v1[1]*v2[2] - v1[2]*v2[1]
return r
end
function vector.normal(p1, p2, p3)
return vector.cross(vector.sub(p2, p1), vector.sub(p3, p1))
end
function raytrace_macro(subs)
raytrace(subs)
aegisub.set_undo_point("raytracing")
end
aegisub.register_macro("Raytrace!", "Raytrace the scene", raytrace_macro)
aegisub.register_filter("Raytrace", "Raytrace the scene", 2000, raytrace)