From 0fa69e80e099b7e49702a5b4d27948b3cadf779d Mon Sep 17 00:00:00 2001 From: Niels Martin Hansen Date: Sun, 1 Apr 2007 16:42:08 +0000 Subject: [PATCH] Mostly working raytracer, still missing a few details to be fully usable though Originally committed to SVN as r971. --- automation/demos/raytracer-test1.ass | 25 ++ automation/demos/raytracer.lua | 402 +++++++++++++++++++++++++++ 2 files changed, 427 insertions(+) create mode 100644 automation/demos/raytracer-test1.ass create mode 100644 automation/demos/raytracer.lua diff --git a/automation/demos/raytracer-test1.ass b/automation/demos/raytracer-test1.ass new file mode 100644 index 000000000..3cfc731c0 --- /dev/null +++ b/automation/demos/raytracer-test1.ass @@ -0,0 +1,25 @@ +[Script Info] +; Script generated by Aegisub v2.00 PRE-RELEASE (SVN r965, jfs) +; http://www.aegisub.net +Title: Default Aegisub file +ScriptType: v4.00+ +WrapStyle: 0 +PlayResX: 640 +PlayResY: 480 +Video Aspect Ratio: 0 +Video Zoom: 6 +Video Position: 0 +Automation Scripts: ~raytracer.lua + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Arial,20,&H00FFFFFF,&H0000FFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,0 + +[Events] +Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text +Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0000,0000,0000,,light 2 2 2 100 0 0 +Dialogue: 0,0:00:05.00,0:00:10.00,Default,,0000,0000,0000,,light -2 2 2 0 200 0 +Dialogue: 0,0:00:10.00,0:00:15.00,Default,,0000,0000,0000,,light 2 -2 2 0 0 100 +Dialogue: 0,0:00:15.00,0:00:20.00,Default,,0000,0000,0000,,tri -10 -3 10 10 -3 10 -10 -3 -10 1 1 1 +Dialogue: 0,0:00:20.00,0:00:25.00,Default,,0000,0000,0000,,tri 0 0 1 1 0 1 0 1 1 1 1 1 +Dialogue: 0,0:00:25.00,0:00:30.00,Default,,0000,0000,0000,, diff --git a/automation/demos/raytracer.lua b/automation/demos/raytracer.lua new file mode 100644 index 000000000..d8bc9816e --- /dev/null +++ b/automation/demos/raytracer.lua @@ -0,0 +1,402 @@ +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)