diff --git a/README.txt b/README.txt index fbae6ae..6cded1a 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 +* 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/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 + diff --git a/init.lua b/init.lua index bc9c809..60d415d 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 @@ -255,18 +256,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)) @@ -1018,6 +1007,74 @@ 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] + 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 + + -- 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 + -- 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 + -- 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 + 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 >= -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) + n = n+1 + 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 +end + vector_extras_functions = funcs 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.")