forked from mia/Aegisub
403 lines
9.7 KiB
Lua
403 lines
9.7 KiB
Lua
|
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)
|