|
| 1 | +--[[ |
| 2 | + Very basic raytracer, passing results (i.e. hit "pixels") to a callback. |
| 3 | +
|
| 4 | + Usage: |
| 5 | + local rt = require("raytracer").new() |
| 6 | + table.insert(rt.model, {0,0,0,16,16,16}) |
| 7 | + --rt.camera.position = {-20,20,0} |
| 8 | + --rt.camera.target = {8,8,8} |
| 9 | + --rt.camera.fov = 100 |
| 10 | + rt:render(width, height, function(hitX, hitY, box, normal) |
| 11 | + -- do stuff with the hit information, e.g. set pixel at hitX/hitY to boxes color |
| 12 | + end) |
| 13 | +
|
| 14 | + Shapes must at least have their min/max coordinates given as the first six |
| 15 | + integer indexed entries of the table, as {minX,minY,minZ,maxX,maxY,maxZ}. |
| 16 | + The returned normal is a sequence with the x/y/z components of the normal. |
| 17 | +
|
| 18 | + The camera can be configured as shown in the example above, i.e. it has a |
| 19 | + position, target and field of view (which is in degrees). |
| 20 | +
|
| 21 | + MIT Licensed, Copyright Sangar 2015 |
| 22 | +]] |
| 23 | +local M = {} |
| 24 | + |
| 25 | +-- vector math stuffs |
| 26 | + |
| 27 | +local function vadd(v1, v2) |
| 28 | + return {v1[1]+v2[1], v1[2]+v2[2], v1[3]+v2[3]} |
| 29 | +end |
| 30 | +local function vsub(v1, v2) |
| 31 | + return {v1[1]-v2[1], v1[2]-v2[2], v1[3]-v2[3]} |
| 32 | +end |
| 33 | +local function vmul(v1, v2) |
| 34 | + return {v1[1]*v2[1], v1[2]*v2[2], v1[3]*v2[3]} |
| 35 | +end |
| 36 | +local function vcross(v1, v2) |
| 37 | + return {v1[2]*v2[3]-v1[3]*v2[2], v1[3]*v2[1]-v1[1]*v2[3], v1[1]*v2[2]-v1[2]*v2[1]} |
| 38 | +end |
| 39 | +local function vmuls(v, s) |
| 40 | + return vmul(v, {s, s, s}) |
| 41 | +end |
| 42 | +local function vdot(v1, v2) |
| 43 | + return v1[1]*v2[1] + v1[2]*v2[2] + v1[3]*v2[3] |
| 44 | +end |
| 45 | +local function vnorm(v) |
| 46 | + return vdot(v, v) |
| 47 | +end |
| 48 | +local function vlen(v) |
| 49 | + return math.sqrt(vnorm(v)) |
| 50 | +end |
| 51 | +local function vnormalize(v) |
| 52 | + return vmuls(v, 1/vlen(v)) |
| 53 | +end |
| 54 | + |
| 55 | +-- collision stuffs |
| 56 | + |
| 57 | +-- http://tog.acm.org/resources/GraphicsGems/gems/RayBox.c |
| 58 | +-- adjusted version also returning the surface normal |
| 59 | +local function collideRayBox(box, origin, dir) |
| 60 | + local inside = true |
| 61 | + local quadrant = {0,0,0} |
| 62 | + local minB = {box[1],box[2],box[3]} |
| 63 | + local maxB = {box[4],box[5],box[6]} |
| 64 | + local maxT = {0,0,0} |
| 65 | + local candidatePlane = {0,0,0} |
| 66 | + local sign = 0 |
| 67 | + |
| 68 | + -- Find candidate planes; this loop can be avoided if |
| 69 | + -- rays cast all from the eye(assume perpsective view) |
| 70 | + for i=1,3 do |
| 71 | + if origin[i] < minB[i] then |
| 72 | + quadrant[i] = true |
| 73 | + candidatePlane[i] = minB[i] |
| 74 | + inside = false |
| 75 | + sign = -1 |
| 76 | + elseif origin[i] > maxB[i] then |
| 77 | + quadrant[i] = true |
| 78 | + candidatePlane[i] = maxB[i] |
| 79 | + inside = false |
| 80 | + sign = 1 |
| 81 | + else |
| 82 | + quadrant[i] = false |
| 83 | + end |
| 84 | + end |
| 85 | + |
| 86 | + -- Ray origin inside bounding box |
| 87 | + if inside then |
| 88 | + return nil |
| 89 | + end |
| 90 | + |
| 91 | + -- Calculate T distances to candidate planes |
| 92 | + for i=1,3 do |
| 93 | + if quadrant[i] and dir[i] ~= 0 then |
| 94 | + maxT[i] = (candidatePlane[i] - origin[i]) / dir[i] |
| 95 | + else |
| 96 | + maxT[i] = -1 |
| 97 | + end |
| 98 | + end |
| 99 | + |
| 100 | + -- Get largest of the maxT's for final choice of intersection |
| 101 | + local whichPlane = 1 |
| 102 | + for i=2,3 do |
| 103 | + if maxT[whichPlane] < maxT[i] then |
| 104 | + whichPlane = i |
| 105 | + end |
| 106 | + end |
| 107 | + |
| 108 | + -- Check final candidate actually inside box |
| 109 | + if maxT[whichPlane] < 0 then return nil end |
| 110 | + local coord,normal = {0,0,0},{0,0,0} |
| 111 | + for i=1,3 do |
| 112 | + if whichPlane ~= i then |
| 113 | + coord[i] = origin[i] + maxT[whichPlane] * dir[i] |
| 114 | + if coord[i] < minB[i] or coord[i] > maxB[i] then |
| 115 | + return nil |
| 116 | + end |
| 117 | + else |
| 118 | + coord[i] = candidatePlane[i] |
| 119 | + normal[i] = sign |
| 120 | + end |
| 121 | + end |
| 122 | + |
| 123 | + return coord, normal -- ray hits box |
| 124 | +end |
| 125 | + |
| 126 | +local function trace(model, origin, dir) |
| 127 | + local bestBox, bestNormal, bestDist = nil, nil, math.huge |
| 128 | + for _, box in ipairs(model) do |
| 129 | + local hit, normal = collideRayBox(box, origin, dir) |
| 130 | + if hit then |
| 131 | + local dist = vlen(vsub(hit, origin)) |
| 132 | + if dist < bestDist then |
| 133 | + bestBox = box |
| 134 | + bestNormal = normal |
| 135 | + bestDist = dist |
| 136 | + end |
| 137 | + end |
| 138 | + end |
| 139 | + return bestBox, bestNormal |
| 140 | +end |
| 141 | + |
| 142 | +-- public api |
| 143 | + |
| 144 | +function M.new() |
| 145 | + return setmetatable({model={},camera={position={-1,1,-1},target={0,0,0},fov=90}}, {__index=M}) |
| 146 | +end |
| 147 | + |
| 148 | +function M:render(w, h, f) |
| 149 | + if #self.model < 1 then return end |
| 150 | + -- overall model bounds, for quick empty space skipping |
| 151 | + local bounds = {self.model[1][1],self.model[1][2],self.model[1][3],self.model[1][4],self.model[1][5],self.model[1][6]} |
| 152 | + for _, shape in ipairs(self.model) do |
| 153 | + bounds[1] = math.min(bounds[1], shape[1]) |
| 154 | + bounds[2] = math.min(bounds[2], shape[2]) |
| 155 | + bounds[3] = math.min(bounds[3], shape[3]) |
| 156 | + bounds[4] = math.max(bounds[4], shape[4]) |
| 157 | + bounds[5] = math.max(bounds[5], shape[5]) |
| 158 | + bounds[6] = math.max(bounds[6], shape[6]) |
| 159 | + end |
| 160 | + bounds = {bounds} |
| 161 | + -- setup framework for ray generation |
| 162 | + local origin = self.camera.position |
| 163 | + local forward = vnormalize(vsub(self.camera.target, origin)) |
| 164 | + local plane = vadd(origin, forward) |
| 165 | + local side = vcross(forward, {0,1,0}) |
| 166 | + local up = vcross(forward, side) |
| 167 | + local lside = math.tan(self.camera.fov/2/180*math.pi) |
| 168 | + -- generate ray for each pixel, left-to-right, top-to-bottom |
| 169 | + local blanks = 0 |
| 170 | + for sy = 1, h do |
| 171 | + local ry = (sy/h - 0.5)*lside |
| 172 | + local py = vadd(plane, vmuls(up, ry)) |
| 173 | + for sx = 1, w do |
| 174 | + local rx = (sx/w - 0.5)*lside |
| 175 | + local px = vadd(py, vmuls(side, rx)) |
| 176 | + local dir = vnormalize(vsub(px, origin)) |
| 177 | + if trace(bounds, origin, dir) then |
| 178 | + local box, normal = trace(self.model, origin, dir) |
| 179 | + if box then |
| 180 | + blanks = 0 |
| 181 | + if f(sx, sy, box, normal) == false then |
| 182 | + return |
| 183 | + end |
| 184 | + else |
| 185 | + blanks = blanks + 1 |
| 186 | + end |
| 187 | + else |
| 188 | + blanks = blanks + 1 |
| 189 | + end |
| 190 | + if blanks > 50 then |
| 191 | + blanks = 0 |
| 192 | + os.sleep(0) -- avoid too long without yielding |
| 193 | + end |
| 194 | + end |
| 195 | + end |
| 196 | +end |
| 197 | + |
| 198 | +return M |
0 commit comments