572 lines
14 KiB
C++
572 lines
14 KiB
C++
/* common/polylib.h */
|
|
|
|
#pragma once
|
|
|
|
#include <common/mathlib.hh>
|
|
#include <common/bspfile.hh>
|
|
#include <common/cmdlib.hh>
|
|
#include <variant>
|
|
#include <array>
|
|
#include <vector>
|
|
|
|
#include <type_traits>
|
|
#include <stdexcept>
|
|
#include <optional>
|
|
|
|
template<class T, class = void>
|
|
struct is_iterator : std::false_type
|
|
{
|
|
};
|
|
|
|
template<class T>
|
|
struct is_iterator<T,
|
|
std::void_t<typename std::iterator_traits<T>::difference_type,
|
|
typename std::iterator_traits<T>::pointer,
|
|
typename std::iterator_traits<T>::reference,
|
|
typename std::iterator_traits<T>::value_type,
|
|
typename std::iterator_traits<T>::iterator_category>> : std::true_type
|
|
{
|
|
};
|
|
|
|
template<class T>
|
|
constexpr bool is_iterator_v = is_iterator<T>::value;
|
|
|
|
namespace polylib
|
|
{
|
|
|
|
constexpr size_t MAX_POINTS_ON_WINDING = 64;
|
|
|
|
constexpr vec_t ON_EPSILON = 0.1;
|
|
constexpr vec_t DEFAULT_BOGUS_RANGE = 65536.0;
|
|
|
|
using winding_edges_t = std::vector<plane_t>;
|
|
|
|
inline bool PointInWindingEdges(const winding_edges_t &wi, const vec3_t &point)
|
|
{
|
|
/* edgeplane faces toward the center of the face */
|
|
for (auto &edgeplane : wi) {
|
|
if (DotProduct(point, edgeplane.normal) - edgeplane.dist < 0)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Polygonal winding; uses stack allocation for the first N
|
|
// points, and moves to a dynamic array after that.
|
|
template<size_t N>
|
|
struct winding_base_t
|
|
{
|
|
private:
|
|
using array_type = std::array<qvec3d, N>;
|
|
using vector_type = std::vector<qvec3d>;
|
|
using variant_type = std::variant<array_type, vector_type>;
|
|
size_t count = 0;
|
|
variant_type data;
|
|
|
|
public:
|
|
// default constructor
|
|
winding_base_t() { }
|
|
|
|
// construct winding with initial size; may allocate
|
|
// memory, and sets size, but does not initialize any
|
|
// of them.
|
|
winding_base_t(const size_t &initial_size) :
|
|
count(initial_size),
|
|
data(count > N ? variant_type(vector_type(initial_size)) : variant_type(array_type()))
|
|
{
|
|
}
|
|
|
|
// construct winding from range.
|
|
// iterators must have operator-.
|
|
template<typename Iter, std::enable_if_t<is_iterator_v<Iter>, int> = 0>
|
|
winding_base_t(Iter begin, Iter end) :
|
|
count(end - begin),
|
|
data(count > N ? variant_type(vector_type(begin, end)) : variant_type(array_type()))
|
|
{
|
|
if (!is_dynamic())
|
|
std::copy(begin, end, std::get<array_type>(data).begin());
|
|
}
|
|
|
|
// copy constructor
|
|
winding_base_t(const winding_base_t ©) :
|
|
count(copy.count),
|
|
data(copy.data)
|
|
{
|
|
}
|
|
|
|
// move constructor
|
|
winding_base_t(winding_base_t &&move) :
|
|
count(move.count),
|
|
data(std::move(move.data))
|
|
{
|
|
move.count = 0;
|
|
}
|
|
|
|
// assignment copy
|
|
inline winding_base_t &operator=(const winding_base_t ©)
|
|
{
|
|
count = copy.count;
|
|
data = copy.data;
|
|
|
|
return *this;
|
|
}
|
|
|
|
// assignment move
|
|
inline winding_base_t &operator=(winding_base_t &&move)
|
|
{
|
|
count = move.count;
|
|
data = std::move(move.data);
|
|
|
|
move.count = 0;
|
|
|
|
return *this;
|
|
}
|
|
|
|
inline bool is_dynamic() const
|
|
{
|
|
return std::holds_alternative<vector_type>(data);
|
|
}
|
|
|
|
inline const size_t &size() const
|
|
{
|
|
return count;
|
|
}
|
|
|
|
inline qvec3d &at(const size_t &index)
|
|
{
|
|
#ifdef _DEBUG
|
|
if (index >= count)
|
|
throw std::invalid_argument("index");
|
|
#endif
|
|
|
|
if (is_dynamic())
|
|
return std::get<vector_type>(data)[index];
|
|
return std::get<array_type>(data)[index];
|
|
}
|
|
|
|
inline const qvec3d &at(const size_t &index) const
|
|
{
|
|
#ifdef _DEBUG
|
|
if (index >= count)
|
|
throw std::invalid_argument("index");
|
|
#endif
|
|
|
|
if (is_dynamic())
|
|
return std::get<vector_type>(data)[index];
|
|
return std::get<array_type>(data)[index];
|
|
}
|
|
|
|
inline qvec3d &operator[](const size_t &index)
|
|
{
|
|
return at(index);
|
|
}
|
|
|
|
inline const qvec3d &operator[](const size_t &index) const
|
|
{
|
|
return at(index);
|
|
}
|
|
|
|
const qvec3d *begin() const
|
|
{
|
|
if (is_dynamic())
|
|
return &std::get<vector_type>(data)[0];
|
|
return &std::get<array_type>(data)[0];
|
|
}
|
|
|
|
const qvec3d *end() const
|
|
{
|
|
if (is_dynamic())
|
|
return &std::get<vector_type>(data)[count];
|
|
return &std::get<array_type>(data)[count];
|
|
}
|
|
|
|
void push_back(const qvec3d &vec)
|
|
{
|
|
// move us to dynamic
|
|
if (count == N && !is_dynamic())
|
|
data = vector_type(begin(), end());
|
|
|
|
if (is_dynamic())
|
|
std::get<vector_type>(data).push_back(vec);
|
|
else
|
|
std::get<array_type>(data)[count] = vec;
|
|
|
|
count++;
|
|
}
|
|
|
|
void clear()
|
|
{
|
|
if (is_dynamic())
|
|
std::get<vector_type>(data).clear();
|
|
|
|
count = 0;
|
|
data = array_type();
|
|
}
|
|
|
|
vec_t area() const
|
|
{
|
|
if (count < 3)
|
|
throw std::domain_error("count");
|
|
|
|
vec_t total = 0;
|
|
|
|
for (size_t i = 2; i < count; i++) {
|
|
qvec3d d1 = at(i - 1) - at(0);
|
|
qvec3d d2 = at(i) - at(0);
|
|
total += 0.5 * qv::length(qv::cross(d1, d2));
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
qvec3d center() const
|
|
{
|
|
qvec3d center { };
|
|
|
|
for (auto &point : *this)
|
|
center += point;
|
|
|
|
return center * (1.0 / count);
|
|
}
|
|
|
|
// TODO: qboundf/qboundd type
|
|
void bounds(vec3_t out_mins, vec3_t out_maxs) const
|
|
{
|
|
ClearBounds(out_mins, out_maxs);
|
|
|
|
for (auto &point : *this)
|
|
AddPointToBounds((const vec_t *) &point[0], out_mins, out_maxs);
|
|
}
|
|
|
|
/*
|
|
* ============
|
|
* RemoveColinearPoints
|
|
* ============
|
|
*/
|
|
void remove_colinear()
|
|
{
|
|
winding_base_t temp;
|
|
|
|
// CHECK: would this be more efficient to instead store a running
|
|
// list of indices that *are* collinear, so we can return sooner
|
|
// before copying points over?
|
|
for (size_t i = 0; i < count; i++) {
|
|
size_t j = (i + 1) % count;
|
|
size_t k = (i + count - 1) % count;
|
|
qvec3d v1 = qv::normalize(at(j) - at(i));
|
|
qvec3d v2 = qv::normalize(at(i) - at(k));
|
|
|
|
if (qv::dot(v1, v2) < 0.999)
|
|
temp.push_back(at(i));
|
|
}
|
|
|
|
if (count != temp.count)
|
|
*this = std::move(temp);
|
|
}
|
|
|
|
plane_t plane() const
|
|
{
|
|
plane_t p;
|
|
|
|
qvec3d v1 = at(0) - at(1);
|
|
qvec3d v2 = at(2) - at(1);
|
|
qvec3d normal = qv::normalize(qv::cross(v1, v2));
|
|
|
|
for (size_t i = 0; i < 3; i++)
|
|
p.normal[i] = normal[i];
|
|
|
|
p.dist = qv::dot(at(0), normal);
|
|
|
|
return p;
|
|
}
|
|
|
|
static winding_base_t from_plane(const vec3_t &normal, const vec_t &dist)
|
|
{
|
|
/* find the major axis */
|
|
vec_t max = -VECT_MAX;
|
|
int32_t x = -1;
|
|
for (size_t i = 0; i < 3; i++) {
|
|
vec_t v = fabs(normal[i]);
|
|
|
|
if (v > max) {
|
|
x = i;
|
|
max = v;
|
|
}
|
|
}
|
|
|
|
if (x == -1)
|
|
FError("no axis found");
|
|
|
|
qvec3d vup { };
|
|
|
|
switch (x) {
|
|
case 0:
|
|
case 1: vup[2] = 1; break;
|
|
case 2: vup[0] = 1; break;
|
|
}
|
|
|
|
vec_t v = DotProduct(&vup[0], normal);
|
|
vup += qvec3d { normal } * -v;
|
|
vup = qv::normalize(vup);
|
|
|
|
vec3_t org;
|
|
VectorScale(normal, dist, org);
|
|
|
|
qvec3d vright = qv::cross(vup, qvec3d(normal));
|
|
|
|
vup *= 10e6;
|
|
vright *= 10e6;
|
|
|
|
/* project a really big axis aligned box onto the plane */
|
|
winding_base_t w(4);
|
|
qvec3d vorg{org};
|
|
|
|
w[0] = vorg - vright + vup;
|
|
w[1] = vorg + vright + vup;
|
|
w[2] = vorg + vright - vup;
|
|
w[3] = vorg - vright - vup;
|
|
|
|
return w;
|
|
}
|
|
|
|
void check(const vec_t &bogus_range = DEFAULT_BOGUS_RANGE) const
|
|
{
|
|
vec3_t dir, edgenormal;
|
|
|
|
if (count < 3)
|
|
FError("{} points", count);
|
|
|
|
vec_t a = area();
|
|
if (a < 1)
|
|
FError("{} area", a);
|
|
|
|
plane_t face = plane();
|
|
|
|
for (size_t i = 0; i < count; i++) {
|
|
const qvec3d &p1 = at(i);
|
|
size_t j = 0;
|
|
|
|
for (; j < 3; j++)
|
|
if (p1[j] > bogus_range || p1[j] < -bogus_range)
|
|
FError("BOGUS_RANGE: {}", p1[j]);
|
|
|
|
/* check the point is on the face plane */
|
|
vec_t d = DotProduct(&p1[0], face.normal) - face.dist;
|
|
if (d < -ON_EPSILON || d > ON_EPSILON)
|
|
FError("point off plane");
|
|
|
|
/* check the edge isn't degenerate */
|
|
const qvec3d &p2 = get[(i + 1) % count];
|
|
qvec3d dir = p2 - p1;
|
|
|
|
if (qv::length(dir) < ON_EPSILON)
|
|
FError("degenerate edge");
|
|
|
|
qvec3d edgenormal = qv::normalize(qv::cross(face.normal, dir));
|
|
vec_t edgedist = qv::dot(p1, edgenormal) + ON_EPSILON;
|
|
|
|
/* all other points must be on front side */
|
|
for (size_t j = 0; j < count; j++) {
|
|
if (j == i)
|
|
continue;
|
|
d = qv::dot(at(j), edgenormal);
|
|
if (d > edgedist)
|
|
FError("non-convex");
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<qvec3f> glm_winding_points() const
|
|
{
|
|
std::vector<qvec3f> points;
|
|
points.resize(count);
|
|
std::copy(begin(), end(), points.begin());
|
|
return points;
|
|
}
|
|
|
|
static inline winding_base_t from_winding_points(const std::vector<qvec3f> &points)
|
|
{
|
|
return { points.begin(), points.end() };
|
|
}
|
|
|
|
winding_edges_t winding_edges() const
|
|
{
|
|
plane_t p = plane();
|
|
|
|
winding_edges_t result(count);
|
|
|
|
for (size_t i = 0; i < count; i++) {
|
|
plane_t &dest = result[i];
|
|
|
|
const qvec3d &v0 = at(i);
|
|
const qvec3d &v1 = get((i + 1) % count);
|
|
|
|
qvec3d edgevec = qv::normalize(v1 - v0);
|
|
qvec3d normal = qv::cross(edgevec, p.normal);
|
|
for (size_t i = 0; i < 3; i++)
|
|
dest.normal[i] = normal[i];
|
|
dest.dist = qv::dot(normal, v0);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::array<std::optional<winding_base_t>, 2> clip(const vec3_t &normal, const vec_t &dist) const
|
|
{
|
|
vec_t *dists = (vec_t *) alloca(sizeof(vec_t) * (count + 1));
|
|
side_t *sides = (side_t *) alloca(sizeof(side_t) * (count + 1));
|
|
int counts[3] { };
|
|
|
|
/* determine sides for each point */
|
|
size_t i;
|
|
|
|
for (i = 0; i < count; i++) {
|
|
vec_t dot = DotProduct(&at(i)[0], normal);
|
|
dot -= dist;
|
|
|
|
dists[i] = dot;
|
|
|
|
if (dot > ON_EPSILON)
|
|
sides[i] = SIDE_FRONT;
|
|
else if (dot < -ON_EPSILON)
|
|
sides[i] = SIDE_BACK;
|
|
else
|
|
sides[i] = SIDE_ON;
|
|
|
|
counts[sides[i]]++;
|
|
}
|
|
|
|
sides[i] = sides[0];
|
|
dists[i] = dists[0];
|
|
|
|
if (!counts[0])
|
|
return { std::nullopt, *this };
|
|
else if (!counts[1])
|
|
return { *this, std::nullopt };
|
|
|
|
/* can't use counts[0]+2 because of fp grouping errors */
|
|
std::array<winding_base_t, 2> results { };
|
|
|
|
for (i = 0; i < count; i++) {
|
|
const qvec3d &p1 = at(i);
|
|
|
|
if (sides[i] == SIDE_ON) {
|
|
results[0].push_back(p1);
|
|
results[1].push_back(p1);
|
|
continue;
|
|
} else if (sides[i] == SIDE_FRONT) {
|
|
results[0].push_back(p1);
|
|
} else if (sides[i] == SIDE_BACK) {
|
|
results[1].push_back(p1);
|
|
}
|
|
|
|
if (sides[i + 1] == SIDE_ON || sides[i + 1] == sides[i])
|
|
continue;
|
|
|
|
/* generate a split point */
|
|
const qvec3d &p2 = at((i + 1) % count);
|
|
|
|
vec_t dot = dists[i] / (dists[i] - dists[i + 1]);
|
|
qvec3d mid { };
|
|
|
|
for (size_t j = 0; j < 3; j++) { /* avoid round off error when possible */
|
|
if (normal[j] == 1)
|
|
mid[j] = dist;
|
|
else if (normal[j] == -1)
|
|
mid[j] = -dist;
|
|
else
|
|
mid[j] = p1[j] + dot * (p2[j] - p1[j]);
|
|
}
|
|
|
|
results[0].push_back(mid);
|
|
results[1].push_back(mid);
|
|
}
|
|
|
|
if (results[0].count > MAX_POINTS_ON_WINDING || results[1].count > MAX_POINTS_ON_WINDING)
|
|
FError("MAX_POINTS_ON_WINDING");
|
|
|
|
return { std::move(results[0]), std::move(results[1]) };
|
|
}
|
|
|
|
std::optional<winding_base_t> chop(const vec3_t &normal, const vec_t &dist)
|
|
{
|
|
auto clipped = clip(normal, dist);
|
|
|
|
clear();
|
|
|
|
return clipped[0];
|
|
}
|
|
|
|
using save_fn_t = void (*)(winding_base_t &w, void *userinfo);
|
|
|
|
void dice(vec_t subdiv, save_fn_t save_fn, void *userinfo)
|
|
{
|
|
vec3_t mins, maxs;
|
|
|
|
bounds(mins, maxs);
|
|
|
|
size_t i;
|
|
|
|
for (i = 0; i < 3; i++)
|
|
if (floor((mins[i] + 1) / subdiv) < floor((maxs[i] - 1) / subdiv))
|
|
break;
|
|
|
|
if (i == 3) {
|
|
// no splitting needed
|
|
save_fn(*this, userinfo);
|
|
return;
|
|
}
|
|
|
|
//
|
|
// split the winding
|
|
//
|
|
vec3_t split { };
|
|
split[i] = 1;
|
|
vec_t dist = subdiv * (1 + floor((mins[i] + 1) / subdiv));
|
|
auto clipped = clip(split, dist);
|
|
clear();
|
|
|
|
//
|
|
// create a new patch
|
|
//
|
|
for (auto &o : clipped)
|
|
if (o.has_value())
|
|
o->dice(subdiv, save_fn, userinfo);
|
|
}
|
|
|
|
static winding_base_t from_face(const mbsp_t *bsp, const bsp2_dface_t *f)
|
|
{
|
|
winding_base_t w(f->numedges);
|
|
|
|
for (size_t i = 0; i < f->numedges; i++) {
|
|
int32_t se = bsp->dsurfedges[f->firstedge + i];
|
|
uint32_t v;
|
|
|
|
if (se < 0)
|
|
v = bsp->dedges[-se].v[1];
|
|
else
|
|
v = bsp->dedges[se].v[0];
|
|
|
|
const dvertex_t *dv = &bsp->dvertexes[v];
|
|
|
|
for (size_t j = 0; j < 3; j++) {
|
|
w[i][j] = dv->point[j];
|
|
}
|
|
}
|
|
|
|
// CHECK: can we do the above + colinear checks
|
|
// in a single pass?
|
|
w.remove_colinear();
|
|
|
|
return w;
|
|
}
|
|
};
|
|
|
|
// the default amount of points to keep on stack
|
|
constexpr size_t STACK_POINTS_ON_WINDING = MAX_POINTS_ON_WINDING / 4;
|
|
|
|
using winding_t = winding_base_t<STACK_POINTS_ON_WINDING>;
|
|
|
|
}; // namespace polylib
|