From d1a3c95286486c9e2d5941c327bbc2a596ba3063 Mon Sep 17 00:00:00 2001 From: HybridDog Date: Mon, 17 Feb 2020 13:04:53 +0100 Subject: [PATCH 1/6] Remove the vector.cross and deprecate vector.scalar; those functions are in minetest now --- init.lua | 12 ------------ legacy.lua | 6 ++++++ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/init.lua b/init.lua index 52dcd89..3a621fd 100644 --- a/init.lua +++ b/init.lua @@ -255,18 +255,6 @@ function funcs.sort_positions(ps, preferred_coords) table.sort(ps, ps_sorting) end -function funcs.scalar(v1, v2) - return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z -end - -function funcs.cross(v1, v2) - return { - x = v1.y*v2.z - v1.z*v2.y, - y = v1.z*v2.x - v1.x*v2.z, - z = v1.x*v2.y - v1.y*v2.x - } -end - -- Tschebyschew norm function funcs.maxnorm(v) return math.max(math.max(math.abs(v.x), math.abs(v.y)), math.abs(v.z)) diff --git a/legacy.lua b/legacy.lua index 8f530d9..39dd7c0 100644 --- a/legacy.lua +++ b/legacy.lua @@ -1,5 +1,11 @@ local funcs = vector_extras_functions +function funcs.scalar(v1, v2) + minetest.log("deprecated", "[vector_extras] vector.scalar is " .. + "deprecated, use vector.dot instead.") + return vector.dot(v1, v2) +end + function funcs.get_data_from_pos(tab, z,y,x) minetest.log("deprecated", "[vector_extras] get_data_from_pos is " .. "deprecated, use the minetest pos hash function instead.") From f7dbb1a8845ed4071f32b4ca6f62519ce5e35dd6 Mon Sep 17 00:00:00 2001 From: HybridDog Date: Fri, 27 Mar 2020 18:17:25 +0100 Subject: [PATCH 2/6] Add (unoptimized) vector.triangle --- init.lua | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/init.lua b/init.lua index 3a621fd..b2cd9d4 100644 --- a/init.lua +++ b/init.lua @@ -6,7 +6,8 @@ function funcs.pos_to_string(pos) return "("..pos.x.."|"..pos.y.."|"..pos.z..")" end -local r_corr = 0.25 --remove a bit more nodes (if shooting diagonal) to let it look like a hole (sth like antialiasing) +local r_corr = 0.25 --remove a bit more nodes (if shooting diagonal) to let it +-- look like a hole (sth like antialiasing) -- this doesn't need to be calculated every time local f_1 = 0.5-r_corr @@ -1006,6 +1007,110 @@ function funcs.serialize(vec) return "{x=" .. vec.x .. ",y=" .. vec.y .. ",z=" .. vec.z .. "}" end +function funcs.triangle(pos1, pos2, pos3) + local normal = vector.cross(vector.subtract(pos2, pos1), + vector.subtract(pos3, pos1)) + -- Find the biggest absolute component of the normal vector + local dir = vector.get_max_coord({ + x = math.abs(normal.x), + y = math.abs(normal.y), + z = math.abs(normal.z), + }) + -- Find the other directions for the for loops + local all_other_dirs = { + x = {"z", "y"}, + y = {"z", "x"}, + z = {"y", "x"}, + } + local other_dirs = all_other_dirs[dir] + -- Sort the positions along the other directions + --[[ + local sorteds = {} + for i = 1,2 do + local odir = other_dirs[i] + local ps = {} + if pos1[odir] < pos2[odir] then + if pos1[odir] < pos3[odir] then + ps[1] = pos1 + if pos2[odir] < pos3[odir] then + ps[2] = pos2 + ps[3] = pos3 + else + ps[3] = pos2 + ps[2] = pos3 + end + else + ps[1] = pos3 + ps[2] = pos1 + ps[3] = pos2 + end + elseif pos1[odir] < pos3[odir] then + ps[1] = pos2 + ps[2] = pos1 + ps[3] = pos3 + else + ps[3] = pos1 + if pos2[odir] < pos3[odir] then + ps[1] = pos2 + ps[2] = pos3 + else + ps[1] = pos3 + ps[2] = pos2 + end + end + sorteds[i] = ps + end + --~ p1[i] = sorteds[odir][1][odir] + --~ p2[i] = sorteds[odir][3][odir] + --]] + -- The boundaries of the 2D AABB along other_dirs + local odir1, odir2 = other_dirs[1], other_dirs[2] + local pos1_2d = {pos1[odir1], pos1[odir2]} + local pos2_2d = {pos2[odir1], pos2[odir2]} + local pos3_2d = {pos3[odir1], pos3[odir2]} + local p1 = {} + local p2 = {} + for i = 1,2 do + p1[i] = math.floor(math.min(pos1_2d[i], pos2_2d[i], pos3_2d[i])) + p2[i] = math.ceil(math.max(pos1_2d[i], pos2_2d[i], pos3_2d[i])) + end + -- The lines along the triangle boundaries + --~ local lines = {} + --~ local ps2 = sorteds[2] + --~ local s = {ps2[1][other_dirs[1]], ps2[1][other_dirs[2]]} + --~ local e = {ps2[2][other_dirs[1]], ps2[2][other_dirs[2]]} + --~ local line_left = {s[1], s[2], e[1] - s[1], e[2] - s[2]} + + -- https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/rasterization-stage + local function edgefunc(p1, p2, pos) + return (pos[1] - p1[1]) * (p2[2] - p1[2]) + - (pos[2] - p1[2]) * (p2[1] - p1[1]) + end + local a_all_inv = 1.0 / edgefunc(pos1_2d, pos2_2d, pos3_2d) + -- Calculate the triangle points + local points = {} + local barycentric_coords = {} + local n = 0 + for v1 = p1[1], p2[1] do + for v2 = p1[2], p2[2] do + -- Not optimized + local p = {v1, v2} + local k3 = edgefunc(pos1_2d, pos2_2d, p) * a_all_inv + local k1 = edgefunc(pos2_2d, pos3_2d, p) * a_all_inv + local k2 = 1 - k1 - k3 + if k1 >= 0 and k2 >= 0 and k3 >= 0 then + -- On triangle + local h = math.floor(k1 * pos1[dir] + k2 * pos2[dir] + + k3 * pos3[dir] + 0.5) + n = n+1 + points[n] = {[odir1] = v1, [odir2] = v2, [dir] = h} + barycentric_coords[n] = {k1, k2, k3} + end + end + end + return points, n, barycentric_coords +end + vector_extras_functions = funcs From 275ec4af3b1d877eff6e3fefa5b0dfc4ac8c3008 Mon Sep 17 00:00:00 2001 From: HybridDog Date: Fri, 27 Mar 2020 18:30:19 +0100 Subject: [PATCH 3/6] Remove unused vector.triangle code and try to make it a bit faster --- init.lua | 66 +++++++++++--------------------------------------------- 1 file changed, 13 insertions(+), 53 deletions(-) diff --git a/init.lua b/init.lua index b2cd9d4..74a56bc 100644 --- a/init.lua +++ b/init.lua @@ -1023,63 +1023,18 @@ function funcs.triangle(pos1, pos2, pos3) z = {"y", "x"}, } local other_dirs = all_other_dirs[dir] - -- Sort the positions along the other directions - --[[ - local sorteds = {} - for i = 1,2 do - local odir = other_dirs[i] - local ps = {} - if pos1[odir] < pos2[odir] then - if pos1[odir] < pos3[odir] then - ps[1] = pos1 - if pos2[odir] < pos3[odir] then - ps[2] = pos2 - ps[3] = pos3 - else - ps[3] = pos2 - ps[2] = pos3 - end - else - ps[1] = pos3 - ps[2] = pos1 - ps[3] = pos2 - end - elseif pos1[odir] < pos3[odir] then - ps[1] = pos2 - ps[2] = pos1 - ps[3] = pos3 - else - ps[3] = pos1 - if pos2[odir] < pos3[odir] then - ps[1] = pos2 - ps[2] = pos3 - else - ps[1] = pos3 - ps[2] = pos2 - end - end - sorteds[i] = ps - end - --~ p1[i] = sorteds[odir][1][odir] - --~ p2[i] = sorteds[odir][3][odir] - --]] - -- The boundaries of the 2D AABB along other_dirs local odir1, odir2 = other_dirs[1], other_dirs[2] + local pos1_2d = {pos1[odir1], pos1[odir2]} local pos2_2d = {pos2[odir1], pos2[odir2]} local pos3_2d = {pos3[odir1], pos3[odir2]} + -- The boundaries of the 2D AABB along other_dirs local p1 = {} local p2 = {} for i = 1,2 do p1[i] = math.floor(math.min(pos1_2d[i], pos2_2d[i], pos3_2d[i])) p2[i] = math.ceil(math.max(pos1_2d[i], pos2_2d[i], pos3_2d[i])) end - -- The lines along the triangle boundaries - --~ local lines = {} - --~ local ps2 = sorteds[2] - --~ local s = {ps2[1][other_dirs[1]], ps2[1][other_dirs[2]]} - --~ local e = {ps2[2][other_dirs[1]], ps2[2][other_dirs[2]]} - --~ local line_left = {s[1], s[2], e[1] - s[1], e[2] - s[2]} -- https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/rasterization-stage local function edgefunc(p1, p2, pos) @@ -1087,25 +1042,30 @@ function funcs.triangle(pos1, pos2, pos3) - (pos[2] - p1[2]) * (p2[1] - p1[1]) end local a_all_inv = 1.0 / edgefunc(pos1_2d, pos2_2d, pos3_2d) + local step_k3 = - (pos2_2d[1] - pos1_2d[1]) * a_all_inv + local step_k1 = - (pos3_2d[1] - pos2_2d[1]) * a_all_inv -- Calculate the triangle points local points = {} local barycentric_coords = {} local n = 0 + -- It is possible to further optimize this for v1 = p1[1], p2[1] do - for v2 = p1[2], p2[2] do - -- Not optimized - local p = {v1, v2} - local k3 = edgefunc(pos1_2d, pos2_2d, p) * a_all_inv - local k1 = edgefunc(pos2_2d, pos3_2d, p) * a_all_inv + local p = {v1, p1[2]} + local k3 = edgefunc(pos1_2d, pos2_2d, p) * a_all_inv + local k1 = edgefunc(pos2_2d, pos3_2d, p) * a_all_inv + for _ = p1[2], p2[2] do local k2 = 1 - k1 - k3 if k1 >= 0 and k2 >= 0 and k3 >= 0 then -- On triangle local h = math.floor(k1 * pos1[dir] + k2 * pos2[dir] + k3 * pos3[dir] + 0.5) n = n+1 - points[n] = {[odir1] = v1, [odir2] = v2, [dir] = h} + points[n] = {[odir1] = v1, [odir2] = p[2], [dir] = h} barycentric_coords[n] = {k1, k2, k3} end + p[2] = p[2]+1 + k3 = k3 + step_k3 + k1 = k1 + step_k1 end end return points, n, barycentric_coords From 8ddb3879fbca8b52caac38ae569c016c53345406 Mon Sep 17 00:00:00 2001 From: HybridDog Date: Fri, 27 Mar 2020 18:40:04 +0100 Subject: [PATCH 4/6] Update README --- README.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.txt b/README.txt index fbae6ae..64cc727 100644 --- a/README.txt +++ b/README.txt @@ -1,2 +1,6 @@ TODO: -— maybe make the explosion table function return a perlin explosion table +* maybe make the explosion table function return a perlin explosion table +* add documentation here instead of + https://dev.minetest.net/User:Hybrid_Dog#vector_extras +* implement 3D scanline search +* add vector.hollowsphere, less positions than WorldEdit hollowsphere From bc08421e209f57ef6369c14c7150ce0320b54b6e Mon Sep 17 00:00:00 2001 From: HybridDog Date: Tue, 7 Apr 2020 20:47:07 +0200 Subject: [PATCH 5/6] Add documentation --- README.txt | 7 ++- doc.md | 154 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 doc.md diff --git a/README.txt b/README.txt index 64cc727..cac4cb9 100644 --- a/README.txt +++ b/README.txt @@ -1,6 +1,5 @@ TODO: * maybe make the explosion table function return a perlin explosion table -* add documentation here instead of - https://dev.minetest.net/User:Hybrid_Dog#vector_extras -* implement 3D scanline search -* add vector.hollowsphere, less positions than WorldEdit hollowsphere +* Figure out and implement 3D scanline search +* Add vector.hollowsphere, less positions than WorldEdit hollowsphere +* Add unit tests diff --git a/doc.md b/doc.md new file mode 100644 index 0000000..c154a24 --- /dev/null +++ b/doc.md @@ -0,0 +1,154 @@ + +# Vector helpers added by this mod + +## Helpers which return many positions for a shape, e.g. a line + +### Line functions + +These may be deprecated since raycasting has been added to minetest. +See e.g. `minetest.line_of_sight`. + +* `vector.line([pos, dir[, range][, alt]])`: returns a table of vectors + * `dir` is either a direction (when range is a number) or + the start position (when range is the end position). + * If alt is true, an old path calculation is used. +* `vector.twoline(x, y)`: can return e.g. `{{0,0}, {0,1}}` + * This is a lower-level function than `vector.line`; it can be used for + a 2D line. +* `vector.threeline(x, y, z)`: can return e.g. `{{0,0,0}, {0,1,0}}` + * Similar to `vector.twoline`; this one is for the 3D case. + * The parameters should be integers. +* `vector.rayIter(pos, dir)`: returns an iterator for a for loop + * `pos` can have non-integer values +* `vector.fine_line([pos, dir[, range], scale])`: returns a table of vectors + * Like `vector.line` but allows non-integer positions + * It uses `vector.rayIter`. + + +### Flood Fill + +* `vector.search_2d(go_test, x0, y0, allow_revisit, give_map)`: returns e.g. + `{{0,0}, {0,1}}` + * This function uses a Flood Fill algorithm, so it can be used to detect + positions connected to each other in 2D. + * `go_test(x, y)` should be a function which returns true iff the algorithm + can "fill" at the position `(x, y)`. + * `(x0, y0)` defines the start position. + * If `allow_revisit` is false (the default), the function + invokes `go_test` only once at every potential position. + * If `give_map` is true (default is false), the function returns the + marked table, whose indices are 2D vector indices, instead of a list of + 2D positions. +* `vector.search_3d(can_go, startpos, apply_move, moves)`: returns FIXME + * FIXME + + +### Other Shapes + +* `vector.explosion_table(r)`: returns e.g. `{{pos1}, {pos2, true}}` + * The returned list of positions and boolean represents a sphere; + if the boolean is true, the position is on the outer side of the sphere. + * It might be used for explosion calculations; but `vector.explosion_perlin` + should make more realistic holes. +* `vector.explosion_perlin(rmin, rmax[, nparams])`: returns e.g. + `{{pos1}, {pos2, true}}` + * This function is similar to `vector.explosion_table`; the positions + do not represent a sphere but a more complex hole which is calculated + with the help of perlin noise. + * `rmin` and `rmax` represent the minimum and maximum radius, + and `nparams` (which has a default value) are parameters for the perlin + noise. +* `vector.circle(r)`: returns a table of vectors + * The returned positions represent a circle of radius `r` along the x and z + directions; the y coordinates are all zero. +* `vector.ring(r)`: returns a table of vectors + * This function is similar to `vector.circle`; the positions are all + touching each other (i.e. they are connected on whole surfaces and not + only infinitely thin edges), so it is called `ring` instead of `circle` + * `r` can be a non-integer number. +* `vector.throw_parabola(pos, vel, gravity, point_count, time)` + * FIXME: should return positions along a parabola so that moving objects + collisions can be calculated +* `vector.triangle(pos1, pos2, pos3)`: returns a table of positions, a number + and a table with barycentric coordinates + * This function calculates integer positions for a triangle defined by + `pos1`, `pos2` and `pos3`, so it can be used to place polygons in + minetest. + * The returned number is the number of positions. + * The barycentric coordinates are specified in a table with three elements; + the first one corresponds to `pos1`, etc. + + +## Helpers for various vector calculations + +* `vector.sort_positions(ps[, preferred_coords])` + * Sorts a table of vectors `ps` along the coordinates specified in the + table `preferred_coords` in-place. + * If `preferred_coords` is omitted, it sorts along z, y and x in this order, + where z has the highest priority. +* `vector.maxnorm(v)`: returns the Tschebyshew norm of `v` +* `vector.sumnorm(v)`: returns the Manhattan norm of `v` +* `vector.pnorm(v, p)`: returns the `p` norm of `v` +* `vector.inside(pos, minp, maxp)`: returns a boolean + * Returns true iff `pos` is within the closed AABB defined by `minp` + and `maxp`. +* `vector.minmax(pos1, pos2)`: returns two vectors + * This does the same as `worldedit.sort_pos`. + * The components of the second returned vector are all bigger or equal to + those of the first one. +* `vector.move(pos1, pos2, length)`: returns a vector + * Go from `pos1` `length` metres to `pos2` and then round to the nearest + integer position. + * Made for rubenwardy +* `vector.from_number(i)`: returns `{x=i, y=i, z=i}` +* `vector.chunkcorner(pos)`: returns a vector + * Returns the mapblock position of the mapblock which contains + the integer position `pos` +* `vector.point_distance_minmax(p1, p2)`: returns two numbers + * Returns the minimum and maximum of the absolute component-wise distances +* `vector.collision(p1, p2)` FIXME +* `vector.update_minp_maxp(minp, maxp, pos)` + * Can change `minp` and `maxp` so that `pos` is within the AABB defined by + `minp` and `maxp` +* `vector.unpack(v)`: returns three numbers + * Returns `v.z, v.y, v.x` +* `vector.get_max_coord(v)`: returns a string + * Returns `"x"`, `"y"` or `"z"`, depending on which component has the + biggest value +* `vector.get_max_coords(v)`: returns three strings + * Similar to `vector.get_max_coord`; it returns the coordinates in the order + of their component values + * Example: `vector.get_max_coords{x=1, y=5, z=3}` returns `"y", "z", "x"` +* `vector.serialize(v)`: returns a string + * In comparison to `minetest.serialize`, this function uses a more compact + string for the serialization. + + +## Minetest-specific helper functions + +* `vector.straightdelay([length, vel[, acc]])`: returns a number + * Returns the time an object takes to move `length` if it has velocity `vel` + and acceleration `acc` +* `vector.sun_dir([time])`: returns a vector or nil + * Returns the vector which points to the sun + * If `time` is omitted, it uses the current time. + * This function does not yet support the moon; + at night it simply returns `nil`. + + +## Helpers which I don't recommend to use now + +* `vector.pos_to_string(pos)`: returns a string + * It is similar to `minetest.pos_to_string`; it uses a different format: + `"("..pos.x.."|"..pos.y.."|"..pos.z..")"` +* `vector.zero` + * The zero vector `{x=0, y=0, z=0}` +* `vector.quickadd(pos, [z],[y],[x])` + * Adds values to the vector components in-place + + +## Deprecated helpers + +* `vector.plane` + * should be removed soon; it should have done the same as vector.triangle + From 6f2bc919db534c1c0801af730a09900e8ef800ab Mon Sep 17 00:00:00 2001 From: HybridDog Date: Sat, 18 Apr 2020 14:16:35 +0200 Subject: [PATCH 6/6] vector.triangle: (try to) avoid holes in neighbouring triangles --- README.txt | 1 + init.lua | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.txt b/README.txt index cac4cb9..6cded1a 100644 --- a/README.txt +++ b/README.txt @@ -3,3 +3,4 @@ TODO: * Figure out and implement 3D scanline search * Add vector.hollowsphere, less positions than WorldEdit hollowsphere * Add unit tests +* Use %a string format for vector.serialize so that it is reversible diff --git a/init.lua b/init.lua index 74a56bc..61e6d18 100644 --- a/init.lua +++ b/init.lua @@ -1041,6 +1041,10 @@ function funcs.triangle(pos1, pos2, pos3) return (pos[1] - p1[1]) * (p2[2] - p1[2]) - (pos[2] - p1[2]) * (p2[1] - p1[1]) end + -- eps is used to prevend holes in neighbouring triangles + -- It should be smaller than the smallest possible barycentric value + -- FIXME: I'm not sure if it really does what it should. + local eps = 0.5 / math.max(p2[1] - p1[1], p2[2] - p1[2]) local a_all_inv = 1.0 / edgefunc(pos1_2d, pos2_2d, pos3_2d) local step_k3 = - (pos2_2d[1] - pos1_2d[1]) * a_all_inv local step_k1 = - (pos3_2d[1] - pos2_2d[1]) * a_all_inv @@ -1055,7 +1059,7 @@ function funcs.triangle(pos1, pos2, pos3) local k1 = edgefunc(pos2_2d, pos3_2d, p) * a_all_inv for _ = p1[2], p2[2] do local k2 = 1 - k1 - k3 - if k1 >= 0 and k2 >= 0 and k3 >= 0 then + if k1 >= -eps and k2 >= -eps and k3 >= -eps then -- On triangle local h = math.floor(k1 * pos1[dir] + k2 * pos2[dir] + k3 * pos3[dir] + 0.5)