-- lua-tikz3dtools-occlusion.lua --[[ We use homogeneous vector convention throughout {{x, y, z, w, 1}} ]] local eps = 0.0000001 --- The dot product. --- @param u table> a vector --- @param v table> another vector --- @return number the dot product local function dot_product(u, v) local a, b = u[1], v[1] return a[1]*b[1] + a[2]*b[2] + a[3]*b[3] end --- Scalar multiplication local function scalar_multiplication(a, v) local tmp = v[1] return { {a*tmp[1], a*tmp[2], a*tmp[3], 1} } end local function vector_subtraction(u, v) local U = u[1] local V = v[1] return { {U[1]-V[1], U[2]-V[2], U[3]-V[3], 1} } end local function vector_addition(u, v) local U = u[1] local V = v[1] return { {U[1]+V[1], U[2]+V[2], U[3]+V[3], 1} } end local function distance(P1, P2) local A = P1[1] local B = P2[1] return math.sqrt( (A[1]-B[1])^2 + (A[2]-B[2])^2 + (A[3]-B[3])^2 ) end --- The orthogonal vector projection. --- @param u table> the vector being projected onto --- @param v table> the vector being projected --- @return table> the resulting projection local function orthogonal_vector_projection(u, v) return scalar_multiplication( dot_product(v, u) / dot_product(u, u), u ) end local function length(v) local V = v[1] return math.sqrt((V[1])^2 + (V[2])^2 + (V[3])^2) end local function cross_product(u,v) local x = u[1][2]*v[1][3]-u[1][3]*v[1][2] local y = u[1][3]*v[1][1]-u[1][1]*v[1][3] local z = u[1][1]*v[1][2]-u[1][2]*v[1][1] local result = { {x, y, z, 1} } return result end local function sign(number) if number > -0.0000001 then return "positive" end return "negative" end --- Occlusion sorts two points. --- @param P1 table> a point --- @param P2 table> another point --- @return boolean or nil whether P1 is in front local function point_point_occlusion_sort(P1, P2) --[[ If the points are not the same point, then calculate their depth directly. ]] if distance(P1, P2) > 0.0000001 then if distance({{P1[1][1], P1[1][2], 0, 1}}, {{P2[1][1], P2[1][2], 0, 1}}) < 0.0000001 then return P1[1][3] > P2[1][3] end end return nil end -- Gauss-Jordan solver (column-major) -- AugCols: array of n+1 columns, each column is an array of n numbers. -- Options (optional): -- opts.copy (default true) -> if true, solver works on a copy and does not mutate AugCols -- opts.eps (default 1e-12) -> pivot tolerance -- Returns: x (array 1..n) on success, or nil, reason on failure. local function gauss_jordan_cols(AugCols, opts) opts = opts or {} local copy_input = (opts.copy == nil) and true or not not opts.copy local eps = 0.0000001 -- basic validation local m = #AugCols if m == 0 then return {}, nil end local n = #AugCols[1] if m ~= n + 1 then return nil, "bad_matrix_size (need n+1 columns for n rows)" end for c = 1, m do if #AugCols[c] ~= n then return nil, "bad_column_length" end end -- clone columns if requested local cols = AugCols if copy_input then cols = {} for c = 1, m do local col = {} for r = 1, n do col[r] = AugCols[c][r] end cols[c] = col end end -- Gauss-Jordan elimination for i = 1, n do -- find pivot row (max abs value in column i for rows i..n) local pivot_row = i local maxval = math.abs(cols[i][i]) for r = i+1, n do local v = math.abs(cols[i][r]) if v > maxval then maxval = v pivot_row = r end end if maxval < eps then return nil, "singular" end -- swap rows i and pivot_row across all columns if needed if pivot_row ~= i then for c = 1, m do cols[c][i], cols[c][pivot_row] = cols[c][pivot_row], cols[c][i] end end -- normalize pivot row so pivot becomes 1 local pivot = cols[i][i] for c = 1, m do cols[c][i] = cols[c][i] / pivot end -- eliminate all other rows at column i for r = 1, n do if r ~= i then local factor = cols[i][r] if factor ~= 0 then for c = 1, m do cols[c][r] = cols[c][r] - factor * cols[c][i] end -- numerical cleanup cols[i][r] = 0 end end end end -- solution is in the last column (RHS), rows 1..n local x = {} for i = 1, n do x[i] = cols[m][i] end return x end --[[ Example usage: local aug = { {2, 1}, -- col 1 (a11, a21) {1, -1}, -- col 2 (a12, a22) {3, 0} -- RHS (b1, b2) } local sol, err = gauss_jordan_cols(aug) -- sol -> {1, 1} (if system solvable) ]] --- point_line_segment_occlusion_sort --- @param P table> the point --- @param L table> the line --- @return true|false|nil depth relationship local function point_line_segment_occlusion_sort(P, L) local line_origin = { L[1] } local line_direction_vector = vector_subtraction( { L[2] }, { L[1] } ) -- The line has affine basis {line_origin, line_direction_vector} local point_from_the_lines_origin = vector_subtraction( P, { L[1] } ) local projection = orthogonal_vector_projection( line_direction_vector, point_from_the_lines_origin ) local true_projection = vector_addition( line_origin, projection ) -- We orthogonally project the point onto the line segment. --[[ If the xy-projections of the point and true_projection are close, then proceed. Otherwise, they do not occlude one another. ]] local F = {{P[1][1], P[1][2], 0, 1}} local G = {{true_projection[1][1], true_projection[1][2], 0, 1}} if distance(F, G) < 0.0000001 then local tmp if ( sign(projection[1][1]) == sign(line_direction_vector[1][1]) and sign(projection[1][2]) == sign(line_direction_vector[1][2]) and sign(projection[1][3]) == sign(line_direction_vector[1][3]) ) then tmp = 1 else tmp = -1 end local test = tmp * length(projection) / length(line_direction_vector) if (0-eps<=test and test<=1+eps) then return point_point_occlusion_sort(P, true_projection) end end return nil end --- point versus triangle occlusion comparator --- @param P table> the point --- @param T table> the triangle --- @return boolean|nil the result of the occlusion comparison local function point_triangle_occlusion_sort(P, T) --[[ First we project the point and the triangle onto the viewing plane (xy). We do this to determine if they occluse using a cross product test. If they do, then we vertically project the point onto the triangle and compare by that. ]] local point_projection = { {P[1][1], P[1][2], 0, 1} } local T1_projection = { {T[1][1], T[1][2], 0, 1} } local T2_projection = { {T[2][1], T[2][2], 0, 1} } local T3_projection = { {T[3][1], T[3][2], 0, 1} } local dist1 = distance(P, {T[1]}) local dist2 = distance(P, {T[2]}) local dist3 = distance(P, {T[3]}) --[[ For reasons of space, we will invoke smaller names for our points. ]] local P1 = point_projection local T1 = T1_projection local T2 = T2_projection local T3 = T3_projection local T1T2 = vector_subtraction(T2, T1) local T1P1 = vector_subtraction(P1, T1) local T1T2xT1P1 = cross_product(T1T2, T1P1) local sign_T1T2xT1P1 = sign(T1T2xT1P1[1][3]) local T2T3 = vector_subtraction(T3, T2) local T2P1 = vector_subtraction(P1, T2) local T2T3xT2P1 = cross_product(T2T3, T2P1) local sign_T2T3xT2P1 = sign(T2T3xT2P1[1][3]) local T3T1 = vector_subtraction(T1, T3) local T3P1 = vector_subtraction(P1, T3) local T3T1xT3P1 = cross_product(T3T1, T3P1) local sign_T3T1xT3P1 = sign(T3T1xT3P1[1][3]) local tmp = (dist1 > 0.0000001 and dist2 > 0.0000001 and dist3 > 0.0000001) --if not tmp then return false end if ( sign_T1T2xT1P1 == sign_T2T3xT2P1 and sign_T2T3xT2P1 == sign_T3T1xT3P1 and tmp ) then local o = {T[1]} local u = vector_subtraction({T[2]}, {T[1]}) local v = vector_subtraction({T[3]}, {T[1]}) local augmented_matrix = { {u[1][1], u[1][2]} -- t ,{v[1][1], v[1][2]} -- s ,{P[1][1]-o[1][1], P[1][2]-o[1][2]} -- t+s } local sol = gauss_jordan_cols(augmented_matrix) local t, s if sol then t = sol[1] s = sol[2] end if t == nil or s == nil then return nil end if 0-eps<=t and t<=1+eps and 0-eps<=s and s<=1+eps then local a = vector_addition(o, scalar_multiplication(t, u)) local b = vector_addition(a, scalar_multiplication(s, v)) return point_point_occlusion_sort(P, b) end end return nil end --- line segment versus line segment occlusion sort --- @param L1 table> the first line segment --- @param L2 table> the second line segment --- @return boolean|nil the result of the occlusion comparison local function line_segment_line_segment_occlusion_sort(L1, L2) local L1A, L1B = {{L1[1][1], L1[1][2], 0, 1}}, {{L1[2][1], L1[2][2], 0, 1}} local L2A, L2B = {{L2[1][1], L2[1][2], 0, 1}}, {{L2[2][1], L2[2][2], 0, 1}} local L1_dir = vector_subtraction(L1B, L1A) local L2_dir = vector_subtraction(L2B, L2A) if length(cross_product(L1_dir,L2_dir)) > 0 then --[=[ L1A + (t)*L1_dir = L2A + (s)*L2_dir -->... ...--> [(t)*L1_dir] - [(s)*L2_dir] = [L2A - L1A] ]=] local augmented_matrix = { {L1_dir[1][1], L1_dir[1][2]}, --> t {-L2_dir[1][1], -L2_dir[1][2]}, --> s {L2A[1][1]-L1A[1][1], L2A[1][2]-L1A[1][2]} --> t+s } local sol = gauss_jordan_cols(augmented_matrix) local t, s if sol then t = sol[1] s = sol[2] end if t == nil or s == nil then return nil end if (0-eps <= t and t<=1+eps and 0-eps <= s and s <= 1+eps) then local L1_inverse = vector_addition({L1[1]}, scalar_multiplication(t, vector_subtraction({L1[2]}, {L1[1]}))) local L2_inverse = vector_addition({L2[1]}, scalar_multiplication(s, vector_subtraction({L2[2]}, {L2[1]}))) return point_point_occlusion_sort(L1_inverse, L2_inverse) end else --return point_point_occlusion_sort({L1[1]}, {L2[1]}) local M1 = point_line_segment_occlusion_sort({L1[1]}, L2) local M2 = point_line_segment_occlusion_sort({L1[2]}, L2) local M3 = point_line_segment_occlusion_sort({L2[1]}, L1) if M3 ~= nil then M3 = not M3 end local M4 = point_line_segment_occlusion_sort({L2[2]}, L1) if M4 ~= nil then M4 = not M4 end if (M1 == true or M2 == true or M3 == true or M4 == true) then return true end if (M1 == false or M2 == false or M3 == false or M4 == false) then return false end end return nil end --- line segment versus triangle test --- @param L table> the line segment --- @param LT table> the triangle --- @return boolean|nil the result of the occlusion comparison local function line_segment_triangle_occlusion_sort(L, T) local A = point_triangle_occlusion_sort({L[1]}, T) local B = point_triangle_occlusion_sort({L[2]}, T) local T1, T2, T3 = {T[1], T[2]}, {T[2], T[3]}, {T[3], T[1]} local C = line_segment_line_segment_occlusion_sort(L, T1) local D = line_segment_line_segment_occlusion_sort(L, T2) local E = line_segment_line_segment_occlusion_sort(L, T3) if ( A == true or B == true or C == true or D == true or E == true ) then return true end if ( A == false or B == false or C == false or D == false or E == false ) then return false end return nil end --- triangle versus triangle occlusion sort --- @param L1 table> the first triangle --- @param L2 table> the second triangle --- @return boolean|nil the result of the occlusion comparison local function triangle_triangle_occlusion_sort(T1, T2) local T1AB, T1BC, T1CA = {T1[1],T1[2]}, {T1[2],T1[3]}, {T1[3],T1[1]} local A = line_segment_triangle_occlusion_sort(T1AB, T2) local B = line_segment_triangle_occlusion_sort(T1BC, T2) local C = line_segment_triangle_occlusion_sort(T1CA, T2) if ( A == true or B == true or C == true ) then return true end if ( A == false or B == false or C == false ) then return false end local D = point_triangle_occlusion_sort({T1[1]}, T2) local E = point_triangle_occlusion_sort({T1[2]}, T2) local F = point_triangle_occlusion_sort({T1[3]}, T2) local G = point_triangle_occlusion_sort({T2[1]}, T1) local H = point_triangle_occlusion_sort({T2[2]}, T1) local I = point_triangle_occlusion_sort({T2[3]}, T1) if ( D == true or E == true or F == true or G == false or H == false or I == false ) then return true end if ( D == false or E == false or F == false or G == true or H == true or I == true ) then return false end return nil end --- Occlusion sorts an arbitrary set of segments. --- @param S1 table> a segment --- @param S2 table> another segment --- @return boolean or nil local function occlusion_sort_segments(S1, S2) if (S1.type == "point" and S2.type == "point") then return point_point_occlusion_sort(S1.segment, S2.segment) elseif (S1.type == "point" and S2.type == "line segment") then return point_line_segment_occlusion_sort(S1.segment, S2.segment) elseif (S1.type == "point" and S2.type == "triangle") then return point_triangle_occlusion_sort(S1.segment, S2.segment) elseif (S1.type == "line segment" and S2.type == "point") then local ans = point_line_segment_occlusion_sort(S2.segment, S1.segment) if ans == nil then return nil end return not ans elseif (S1.type == "line segment" and S2.type == "line segment") then return line_segment_line_segment_occlusion_sort(S1.segment, S2.segment) elseif (S1.type == "line segment" and S2.type == "triangle") then return line_segment_triangle_occlusion_sort(S1.segment, S2.segment) elseif (S1.type == "triangle" and S2.type == "point") then local ans = point_triangle_occlusion_sort(S2.segment, S1.segment) if ans == nil then return nil end return not ans elseif (S1.type == "triangle" and S2.type == "line segment") then local ans = line_segment_triangle_occlusion_sort(S2.segment, S1.segment) if ans == nil then return nil end return not ans elseif (S1.type == "triangle" and S2.type == "triangle") then return triangle_triangle_occlusion_sort(S1.segment, S2.segment) end end return occlusion_sort_segments