ericw-tools/common/mapfile.cc

1247 lines
38 KiB
C++

#include <common/mapfile.hh>
#include <common/log.hh>
#include <common/ostream.hh>
#include <common/imglib.hh>
#include <utility>
namespace mapfile {
/*static*/ bool brush_side_t::is_valid_texture_projection(const qvec3f &faceNormal, const qvec3f &s_vec, const qvec3f &t_vec)
{
// TODO: This doesn't match how light does it (TexSpaceToWorld)
const qvec3f tex_normal = qv::normalize(qv::cross(s_vec, t_vec));
for (size_t i = 0; i < 3; i++) {
if (std::isnan(tex_normal[i])) {
return false;
}
}
const float cosangle = qv::dot(tex_normal, faceNormal);
if (std::isnan(cosangle)) {
return false;
} else if (fabs(cosangle) < ZERO_EPSILON) {
return false;
}
return true;
}
void brush_side_t::validate_texture_projection()
{
if (!is_valid_texture_projection()) {
/*
if (qbsp_options.verbose.value()) {
logging::print("WARNING: {}: repairing invalid texture projection (\"{}\" near {} {} {})\n", mapface.line,
mapface.texname, (int)mapface.planepts[0][0], (int)mapface.planepts[0][1], (int)mapface.planepts[0][2]);
} else {
issue_stats.num_repaired++;
}
*/
// Reset texturing to sensible defaults
set_texinfo(texdef_quake_ed_t {
{ 0.0, 0.0 },
0,
{ 1.0, 1.0 }
});
Q_assert(is_valid_texture_projection());
}
}
/*static*/ texdef_bp_t brush_side_t::parse_bp(parser_t &parser)
{
qmat<double, 2, 3> texMat;
parser.parse_token(PARSE_SAMELINE);
if (parser.token != "(") {
goto parse_error;
}
for (size_t i = 0; i < 2; i++) {
parser.parse_token(PARSE_SAMELINE);
if (parser.token != "(") {
goto parse_error;
}
for (size_t j = 0; j < 3; j++) {
parser.parse_token(PARSE_SAMELINE);
texMat.at(i, j) = std::stod(parser.token);
}
parser.parse_token(PARSE_SAMELINE);
if (parser.token != ")") {
goto parse_error;
}
}
parser.parse_token(PARSE_SAMELINE);
if (parser.token != ")") {
goto parse_error;
}
return { texMat };
parse_error:
FError("{}: couldn't parse Brush Primitives texture info", parser.location);
}
/*static*/ texdef_valve_t brush_side_t::parse_valve_220(parser_t &parser)
{
qmat<double, 2, 3> axis;
qvec2d shift, scale;
double rotate;
for (size_t i = 0; i < 2; i++) {
parser.parse_token(PARSE_SAMELINE);
if (parser.token != "[") {
goto parse_error;
}
for (size_t j = 0; j < 3; j++) {
parser.parse_token(PARSE_SAMELINE);
axis.at(i, j) = std::stod(parser.token);
}
parser.parse_token(PARSE_SAMELINE);
shift[i] = std::stod(parser.token);
parser.parse_token(PARSE_SAMELINE);
if (parser.token != "]") {
goto parse_error;
}
}
parser.parse_token(PARSE_SAMELINE);
rotate = std::stod(parser.token);
parser.parse_token(PARSE_SAMELINE);
scale[0] = std::stod(parser.token);
parser.parse_token(PARSE_SAMELINE);
scale[1] = std::stod(parser.token);
return {
{
shift,
rotate,
scale
},
{
axis
}
};
parse_error:
FError("{}: couldn't parse Valve220 texture info", parser.location);
}
/*static*/ texdef_quake_ed_t brush_side_t::parse_quake_ed(parser_t &parser)
{
qvec2d shift, scale;
double rotate;
parser.parse_token(PARSE_SAMELINE);
shift[0] = std::stod(parser.token);
parser.parse_token(PARSE_SAMELINE);
shift[1] = std::stod(parser.token);
parser.parse_token(PARSE_SAMELINE);
rotate = std::stod(parser.token);
parser.parse_token(PARSE_SAMELINE);
scale[0] = std::stod(parser.token);
parser.parse_token(PARSE_SAMELINE);
scale[1] = std::stod(parser.token);
return {
shift,
rotate,
scale
};
}
bool brush_side_t::parse_quark_comment(parser_t &parser)
{
if (!parser.parse_token(PARSE_COMMENT | PARSE_OPTIONAL)) {
return false;
}
if (parser.token.length() < 5 || strncmp(parser.token.c_str(), "//TX", 4)) {
return false;
}
// QuArK TX modes can only exist on Quaked-style maps
Q_assert(std::holds_alternative<texdef_quake_ed_t>(raw));
if (parser.token[4] != '1' && parser.token[4] != '2') {
return false;
}
raw = texdef_etp_t { std::get<texdef_quake_ed_t>(raw), parser.token[4] == '2' };
return true;
}
void brush_side_t::parse_extended_texinfo(parser_t &parser)
{
if (!parse_quark_comment(parser)) {
// Parse extra Quake 2 surface info
if (parser.parse_token(PARSE_OPTIONAL)) {
texinfo_quake2_t q2_info;
q2_info.contents = std::stoi(parser.token);
if (parser.parse_token(PARSE_OPTIONAL)) {
q2_info.flags.native = std::stoi(parser.token);
}
if (parser.parse_token(PARSE_OPTIONAL)) {
q2_info.value = std::stoi(parser.token);
}
extended_info = q2_info;
parse_quark_comment(parser);
}
}
}
void brush_side_t::set_texinfo(const texdef_quake_ed_t &texdef)
{
texture_axis_t axis(plane);
qvec3d vectors[2] = {
axis.xv,
axis.yv
};
/* Rotate axis */
double ang = texdef.rotate / 180.0 * Q_PI;
double sinv = sin(ang);
double cosv = cos(ang);
size_t sv, tv;
if (vectors[0][0]) {
sv = 0;
} else if (vectors[0][1]) {
sv = 1;
} else {
sv = 2; // unreachable, due to TextureAxisFromPlane lookup table
}
if (vectors[1][0]) {
tv = 0; // unreachable, due to TextureAxisFromPlane lookup table
} else if (vectors[1][1]) {
tv = 1;
} else {
tv = 2;
}
for (size_t i = 0; i < 2; i++) {
double ns = cosv * vectors[i][sv] - sinv * vectors[i][tv];
double nt = sinv * vectors[i][sv] + cosv * vectors[i][tv];
vectors[i][sv] = ns;
vectors[i][tv] = nt;
}
for (size_t i = 0; i < 2; i++) {
for (size_t j = 0; j < 3; j++) {
/* Interpret zero scale as no scaling */
vecs.at(i, j) = vectors[i][j] / (texdef.scale[i] ? texdef.scale[i] : 1);
}
}
vecs.at(0, 3) = texdef.shift[0];
vecs.at(1, 3) = texdef.shift[1];
// TODO: move these self-tests somewhere else, do them for all types
#if 0
if (false) {
// Self-test of SetTexinfo_QuakeEd_New
texvecf check;
SetTexinfo_QuakeEd_New(plane, shift, rotate, scale, check);
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 4; j++) {
if (fabs(check.at(i, j) - out->vecs.at(i, j)) > 0.001) {
SetTexinfo_QuakeEd_New(plane, shift, rotate, scale, check);
FError("fail");
}
}
}
}
if (false) {
// Self-test of TexDef_BSPToQuakeEd
texdef_quake_ed_t reversed = TexDef_BSPToQuakeEd(plane, std::nullopt, out->vecs, planepts);
if (!EqualDegrees(reversed.rotate, rotate)) {
reversed.rotate += 180;
reversed.scale[0] *= -1;
reversed.scale[1] *= -1;
}
if (!EqualDegrees(reversed.rotate, rotate)) {
ewt::print("wrong rotate got {} expected {}\n", reversed.rotate, rotate);
}
if (fabs(reversed.scale[0] - scale[0]) > 0.001 || fabs(reversed.scale[1] - scale[1]) > 0.001) {
ewt::print("wrong scale, got {} {} exp {} {}\n", reversed.scale[0], reversed.scale[1], scale[0], scale[1]);
}
if (fabs(reversed.shift[0] - shift[0]) > 0.1 || fabs(reversed.shift[1] - shift[1]) > 0.1) {
ewt::print("wrong shift, got {} {} exp {} {}\n", reversed.shift[0], reversed.shift[1], shift[0], shift[1]);
}
}
#endif
}
void brush_side_t::set_texinfo(const texdef_valve_t &texdef)
{
for (size_t i = 0; i < 3; i++) {
vecs.at(0, i) = texdef.axis.at(0, i) / texdef.scale[0];
vecs.at(1, i) = texdef.axis.at(1, i) / texdef.scale[1];
}
vecs.at(0, 3) = texdef.shift[0];
vecs.at(1, 3) = texdef.shift[1];
}
void brush_side_t::set_texinfo(const texdef_etp_t &texdef)
{
qvec3d vectors[2];
/*
* Type 1 uses vecs[0] = (pt[2] - pt[0]) and vecs[1] = (pt[1] - pt[0])
* Type 2 reverses the order of the vecs
* 128 is the scaling factor assumed by QuArK.
*/
if (!texdef.tx2) {
vectors[0] = planepts[2] - planepts[0];
vectors[1] = planepts[1] - planepts[0];
} else {
vectors[0] = planepts[1] - planepts[0];
vectors[1] = planepts[2] - planepts[0];
}
vectors[0] *= 1.0 / 128.0;
vectors[1] *= 1.0 / 128.0;
double a = qv::dot(vectors[0], vectors[0]);
double b = qv::dot(vectors[0], vectors[1]);
double c = b; /* qv::dot(vectors[1], vectors[0]); */
double d = qv::dot(vectors[1], vectors[1]);
/*
* Want to solve for out->vecs:
*
* | a b | | out->vecs[0] | = | vecs[0] |
* | c d | | out->vecs[1] | | vecs[1] |
*
* => | out->vecs[0] | = __ 1.0__ | d -b | | vecs[0] |
* | out->vecs[1] | a*d - b*c | -c a | | vecs[1] |
*/
double determinant = a * d - b * c;
if (fabs(determinant) < ZERO_EPSILON) {
logging::print("WARNING: {}: Face with degenerate QuArK-style texture axes\n", location);
for (size_t i = 0; i < 3; i++) {
vecs.at(0, i) = vecs.at(1, i) = 0;
}
} else {
for (size_t i = 0; i < 3; i++) {
vecs.at(0, i) = (d * vectors[0][i] - b * vectors[1][i]) / determinant;
vecs.at(1, i) = -(a * vectors[1][i] - c * vectors[0][i]) / determinant;
}
}
/* Finally, the texture offset is indicated by planepts[0] */
for (size_t i = 0; i < 3; ++i) {
vectors[0][i] = vecs.at(0, i);
vectors[1][i] = vecs.at(1, i);
}
vecs.at(0, 3) = -qv::dot(vectors[0], planepts[0]);
vecs.at(1, 3) = -qv::dot(vectors[1], planepts[0]);
}
/*
ComputeAxisBase()
from q3map2
computes the base texture axis for brush primitive texturing
note: ComputeAxisBase here and in editor code must always BE THE SAME!
warning: special case behaviour of atan2( y, x ) <-> atan( y / x ) might not be the same everywhere when x == 0
rotation by (0,RotY,RotZ) assigns X to normal
*/
inline std::tuple<qvec3d, qvec3d> compute_axis_base(const qvec3d &normal_unsanitized)
{
double RotY, RotZ;
qvec3d normal = normal_unsanitized;
/* do some cleaning */
if (fabs(normal[0]) < 1e-6) {
normal[0] = 0.0f;
}
if (fabs(normal[1]) < 1e-6) {
normal[1] = 0.0f;
}
if (fabs(normal[2]) < 1e-6) {
normal[2] = 0.0f;
}
/* compute the two rotations around y and z to rotate x to normal */
RotY = -atan2(normal[2], sqrt(normal[1] * normal[1] + normal[0] * normal[0]));
RotZ = atan2(normal[1], normal[0]);
return {
/* rotate (0,1,0) and (0,0,1) to compute texX and texY */
{
-sin(RotZ),
cos(RotZ),
0
},
{
/* the texY vector is along -z (t texture coorinates axis) */
-sin(RotY) * cos(RotZ),
-sin(RotY) * sin(RotZ),
-cos(RotY)
}
};
}
void brush_side_t::set_texinfo(const texdef_bp_t &texdef)
{
// FIXME:
const int32_t texWidth = 64;
const int32_t texHeight = 64;
const auto [texX, texY] = compute_axis_base(plane.normal);
const auto texMat = texdef.axis;
/*
derivation of the conversion below:
classic BSP texture vecs to texture coordinates:
u = (dot(vert, out->vecs[0]) + out->vecs[3]) / texWidth
brush primitives: (starting with q3map2 code, then rearranging it to look like the classic formula)
u = (texMat[0][0] * dot(vert, texX)) + (texMat[0][1] * dot(vert, texY)) + texMat[0][2]
factor out vert:
u = (vert[0] * (texX[0] * texMat[0][0] + texY[0] * texMat[0][1]))
+ (vert[1] * (texX[1] * texMat[0][0] + texY[1] * texMat[0][1]))
+ (vert[2] * (texX[2] * texMat[0][0] + texY[2] * texMat[0][1]))
+ texMat[0][2];
multiplying that by 1 = (texWidth / texWidth) gives us something in the same shape as the classic formula,
so we can get out->vecs.
*/
vecs.at(0, 0) = texWidth * ((texX[0] * texMat.at(0, 0)) + (texY[0] * texMat.at(0, 1)));
vecs.at(0, 1) = texWidth * ((texX[1] * texMat.at(0, 0)) + (texY[1] * texMat.at(0, 1)));
vecs.at(0, 2) = texWidth * ((texX[2] * texMat.at(0, 0)) + (texY[2] * texMat.at(0, 1)));
vecs.at(0, 3) = texWidth * texMat.at(0, 2);
vecs.at(1, 0) = texHeight * ((texX[0] * texMat.at(1, 0)) + (texY[0] * texMat.at(1, 1)));
vecs.at(1, 1) = texHeight * ((texX[1] * texMat.at(1, 0)) + (texY[1] * texMat.at(1, 1)));
vecs.at(1, 2) = texHeight * ((texX[2] * texMat.at(1, 0)) + (texY[2] * texMat.at(1, 1)));
vecs.at(1, 3) = texHeight * texMat.at(1, 2);
}
void brush_side_t::parse_texture_def(parser_t &parser, texcoord_style_t base_format)
{
if (base_format == texcoord_style_t::brush_primitives) {
raw = parse_bp(parser);
parser.parse_token(PARSE_SAMELINE);
texture = std::move(parser.token);
} else if (base_format == texcoord_style_t::quaked) {
parser.parse_token(PARSE_SAMELINE);
texture = std::move(parser.token);
parser.parse_token(PARSE_SAMELINE | PARSE_PEEK);
if (parser.token == "[") {
raw = parse_valve_220(parser);
} else {
raw = parse_quake_ed(parser);
}
} else {
FError("{}: Bad brush format", parser.location);
}
// Read extra Q2 params and/or QuArK subtype
parse_extended_texinfo(parser);
std::visit([this](auto &&x) { set_texinfo(x); }, raw);
}
void brush_side_t::parse_plane_def(parser_t &parser)
{
for (size_t i = 0; i < 3; i++) {
if (i != 0) {
parser.parse_token();
}
if (parser.token != "(") {
goto parse_error;
}
for (size_t j = 0; j < 3; j++) {
parser.parse_token(PARSE_SAMELINE);
planepts[i][j] = std::stod(parser.token);
}
parser.parse_token(PARSE_SAMELINE);
if (parser.token != ")") {
goto parse_error;
}
}
return;
parse_error:
FError("{}: Invalid brush plane format", parser.location);
}
void brush_side_t::write_extended_info(std::ostream &stream)
{
if (extended_info) {
ewt::print(stream, " {} {} {}", extended_info->contents, extended_info->flags.native, extended_info->value);
}
}
void brush_side_t::write_texinfo(std::ostream &stream, const texdef_quake_ed_t &texdef)
{
ewt::print(stream, "{} {} {} {} {}", texdef.shift[0], texdef.shift[1], texdef.rotate, texdef.scale[0], texdef.scale[1]);
write_extended_info(stream);
}
void brush_side_t::write_texinfo(std::ostream &stream, const texdef_valve_t &texdef)
{
ewt::print(stream, "[ {} {} {} {} ] [ {} {} {} {} ] {} {} {}",
texdef.axis.at(0, 0), texdef.axis.at(0, 1), texdef.axis.at(0, 2), texdef.shift[0],
texdef.axis.at(1, 0), texdef.axis.at(1, 1), texdef.axis.at(1, 2), texdef.shift[1],
texdef.rotate, texdef.scale[0], texdef.scale[1]);
write_extended_info(stream);
}
void brush_side_t::write_texinfo(std::ostream &stream, const texdef_etp_t &texdef)
{
write_texinfo(stream, (const texdef_quake_ed_t &) texdef);
ewt::print(stream, "//TX{}", texdef.tx2 ? '2' : '1');
}
void brush_side_t::write_texinfo(std::ostream &stream, const texdef_bp_t &texdef)
{
FError("todo bp");
}
void brush_side_t::write(std::ostream &stream)
{
ewt::print(stream, "( {} {} {} ) ( {} {} {} ) ( {} {} {} ) {} ", planepts[0][0], planepts[0][1], planepts[0][2],
planepts[1][0], planepts[1][1], planepts[1][2], planepts[2][0], planepts[2][1], planepts[2][2],
texture);
std::visit([this, &stream](auto &&x) { write_texinfo(stream, x); }, raw);
}
namespace convert_to_quaked
{
static qmat2x2f rotation2x2_deg(float degrees)
{
float r = degrees * (Q_PI / 180.0);
float cosr = cos(r);
float sinr = sin(r);
// [ cosTh -sinTh ]
// [ sinTh cosTh ]
qmat2x2f M{cosr, sinr, // col 0
-sinr, cosr}; // col1
return M;
}
static float extractRotation(qmat2x2f m)
{
qvec2f point = m * qvec2f(1, 0); // choice of this matters if there's shearing
float rotation = atan2(point[1], point[0]) * 180.0 / Q_PI;
return rotation;
}
static std::pair<int, int> getSTAxes(const qvec3d &snapped_normal)
{
if (snapped_normal[0]) {
return std::make_pair(1, 2);
} else if (snapped_normal[1]) {
return std::make_pair(0, 2);
} else {
return std::make_pair(0, 1);
}
}
static qvec2f projectToAxisPlane(const qvec3d &snapped_normal, const qvec3d &point)
{
const std::pair<int, int> axes = getSTAxes(snapped_normal);
const qvec2f proj(point[axes.first], point[axes.second]);
return proj;
}
float clockwiseDegreesBetween(qvec2f start, qvec2f end)
{
start = qv::normalize(start);
end = qv::normalize(end);
const float cosAngle = std::max(-1.0f, std::min(1.0f, qv::dot(start, end)));
const float unsigned_degrees = acos(cosAngle) * (360.0 / (2.0 * Q_PI));
if (unsigned_degrees < ANGLEEPSILON)
return 0;
// get a normal for the rotation plane using the right-hand rule
// if this is pointing up (qvec3f(0,0,1)), it's counterclockwise rotation.
// if this is pointing down (qvec3f(0,0,-1)), it's clockwise rotation.
qvec3f rotationNormal = qv::normalize(qv::cross(qvec3f(start[0], start[1], 0.0f), qvec3f(end[0], end[1], 0.0f)));
const float normalsCosAngle = qv::dot(rotationNormal, qvec3f(0, 0, 1));
if (normalsCosAngle >= 0) {
// counterclockwise rotation
return -unsigned_degrees;
}
// clockwise rotation
return unsigned_degrees;
}
static texdef_quake_ed_t Reverse_QuakeEd(qmat2x2f M, const qplane3d &plane, bool preserveX)
{
// Check for shear, because we might tweak M to remove it
{
qvec2f Xvec = M.row(0);
qvec2f Yvec = M.row(1);
double cosAngle = qv::dot(qv::normalize(Xvec), qv::normalize(Yvec));
// const double oldXscale = sqrt(pow(M[0][0], 2.0) + pow(M[1][0], 2.0));
// const double oldYscale = sqrt(pow(M[0][1], 2.0) + pow(M[1][1], 2.0));
if (fabs(cosAngle) > 0.001) {
// Detected shear
if (preserveX) {
const float degreesToY = clockwiseDegreesBetween(Xvec, Yvec);
const bool CW = (degreesToY > 0);
// turn 90 degrees from Xvec
const qvec2f newYdir =
qv::normalize(qvec2f(qv::cross(qvec3f(0, 0, CW ? -1.0f : 1.0f), qvec3f(Xvec[0], Xvec[1], 0.0))));
// scalar projection of the old Yvec onto newYDir to get the new Yscale
const float newYscale = qv::dot(Yvec, newYdir);
Yvec = newYdir * static_cast<float>(newYscale);
} else {
// Preserve Y.
const float degreesToX = clockwiseDegreesBetween(Yvec, Xvec);
const bool CW = (degreesToX > 0);
// turn 90 degrees from Yvec
const qvec2f newXdir =
qv::normalize(qvec2f(qv::cross(qvec3f(0, 0, CW ? -1.0f : 1.0f), qvec3f(Yvec[0], Yvec[1], 0.0))));
// scalar projection of the old Xvec onto newXDir to get the new Xscale
const float newXscale = qv::dot(Xvec, newXdir);
Xvec = newXdir * static_cast<float>(newXscale);
}
// recheck
cosAngle = qv::dot(qv::normalize(Xvec), qv::normalize(Yvec));
if (fabs(cosAngle) > 0.001) {
FError("SHEAR correction failed\n");
}
// update M
M.at(0, 0) = Xvec[0];
M.at(0, 1) = Xvec[1];
M.at(1, 0) = Yvec[0];
M.at(1, 1) = Yvec[1];
}
}
// extract abs(scale)
const double absXscale = sqrt(pow(M.at(0, 0), 2.0) + pow(M.at(0, 1), 2.0));
const double absYscale = sqrt(pow(M.at(1, 0), 2.0) + pow(M.at(1, 1), 2.0));
const qmat2x2f applyAbsScaleM{static_cast<float>(absXscale), // col0
0,
0, // col1
static_cast<float>(absYscale)};
auto [ xv, yv, snapped_normal ] = texture_axis_t(plane);
const qvec2f sAxis = projectToAxisPlane(snapped_normal, xv);
const qvec2f tAxis = projectToAxisPlane(snapped_normal, yv);
// This is an identity matrix possibly with negative signs.
const qmat2x2f axisFlipsM{sAxis[0], tAxis[0], // col0
sAxis[1], tAxis[1]}; // col1
// N.B. this is how M is built in SetTexinfo_QuakeEd_New and guides how we
// strip off components of it later in this function.
//
// qmat2x2f M = scaleM * rotateM * axisFlipsM;
// strip off the magnitude component of the scale, and `axisFlipsM`.
const qmat2x2f flipRotate = qv::inverse(applyAbsScaleM) * M * qv::inverse(axisFlipsM);
// We don't know the signs on the scales, which will mess up figuring out the rotation, so try all 4 combinations
for (float xScaleSgn : std::vector<float>{-1.0, 1.0}) {
for (float yScaleSgn : std::vector<float>{-1.0, 1.0}) {
// "apply" - matrix constructed to apply a guessed value
// "guess" - this matrix might not be what we think
const qmat2x2f applyGuessedFlipM{xScaleSgn, // col0
0,
0, // col1
yScaleSgn};
const qmat2x2f rotateMGuess = qv::inverse(applyGuessedFlipM) * flipRotate;
const float angleGuess = extractRotation(rotateMGuess);
// const qmat2x2f Mident = rotateMGuess * rotation2x2_deg(-angleGuess);
const qmat2x2f applyAngleGuessM = rotation2x2_deg(angleGuess);
const qmat2x2f Mguess = applyGuessedFlipM * applyAbsScaleM * applyAngleGuessM * axisFlipsM;
if (fabs(M.at(0, 0) - Mguess.at(0, 0)) < 0.001 && fabs(M.at(1, 0) - Mguess.at(1, 0)) < 0.001 &&
fabs(M.at(0, 1) - Mguess.at(0, 1)) < 0.001 && fabs(M.at(1, 1) - Mguess.at(1, 1)) < 0.001) {
texdef_quake_ed_t reversed;
reversed.rotate = angleGuess;
reversed.scale[0] = xScaleSgn / absXscale;
reversed.scale[1] = yScaleSgn / absYscale;
return reversed;
}
}
}
// TODO: detect when we expect this to fail, i.e. invalid texture axes (0-length),
// and throw an error if it fails unexpectedly.
return {};
}
static qmat4x4f texVecsTo4x4Matrix(const qplane3d &faceplane, const texvecf &in_vecs)
{
// [s]
// T * vec = [t]
// [distOffPlane]
// [?]
qmat4x4f T{
in_vecs.at(0, 0), in_vecs.at(1, 0), static_cast<float>(faceplane.normal[0]), 0, // col 0
in_vecs.at(0, 1), in_vecs.at(1, 1), static_cast<float>(faceplane.normal[1]), 0, // col 1
in_vecs.at(0, 2), in_vecs.at(1, 2), static_cast<float>(faceplane.normal[2]), 0, // col 2
in_vecs.at(0, 3), in_vecs.at(1, 3), static_cast<float>(-faceplane.dist), 1 // col 3
};
return T;
}
static qvec2f evalTexDefAtPoint(const texdef_quake_ed_t &texdef, const qplane3d &faceplane, const qvec3f &point)
{
brush_side_t temp;
temp.set_texinfo(texdef_quake_ed_t { texdef.shift, texdef.rotate, texdef.scale });
const qmat4x4f worldToTexSpace_res = texVecsTo4x4Matrix(faceplane, temp.vecs);
const qvec2f uv = qvec2f(worldToTexSpace_res * qvec4f(point[0], point[1], point[2], 1.0f));
return uv;
}
static texdef_quake_ed_t addShift(const texdef_quake_ed_t &texdef, const qvec2f shift)
{
texdef_quake_ed_t res = texdef;
res.shift = shift;
return res;
}
qvec2f normalizeShift(const std::optional<img::texture_meta> &texture, const qvec2f &in)
{
if (!texture) {
return in; // can't do anything without knowing the texture size.
}
int fullWidthOffsets = static_cast<int>(in[0]) / texture->width;
int fullHeightOffsets = static_cast<int>(in[1]) / texture->height;
qvec2f result(in[0] - static_cast<float>(fullWidthOffsets * texture->width),
in[1] - static_cast<float>(fullHeightOffsets * texture->height));
return result;
}
/// `texture` is optional. If given, the "shift" values can be normalized
static texdef_quake_ed_t TexDef_BSPToQuakeEd(const qplane3d &faceplane,
const std::optional<img::texture_meta> &texture, const texvecf &in_vecs, const std::array<qvec3d, 3> &facepoints)
{
// First get the un-rotated, un-scaled unit texture vecs (based on the face plane).
texture_axis_t axis(faceplane);
qvec3d &snapped_normal = axis.snapped_normal;
const qmat4x4f worldToTexSpace = texVecsTo4x4Matrix(faceplane, in_vecs);
// Grab the UVs of the 3 reference points
qvec2f facepoints_uvs[3];
for (int i = 0; i < 3; i++) {
facepoints_uvs[i] = qvec2f(worldToTexSpace * qvec4f(facepoints[i][0], facepoints[i][1], facepoints[i][2], 1.0));
}
// Project the 3 reference points onto the axis plane. They are now 2d points.
qvec2f facepoints_projected[3];
for (int i = 0; i < 3; i++) {
facepoints_projected[i] = projectToAxisPlane(snapped_normal, facepoints[i]);
}
// Now make 2 vectors out of our 3 points (so we are ignoring translation for now)
const qvec2f p0p1 = facepoints_projected[1] - facepoints_projected[0];
const qvec2f p0p2 = facepoints_projected[2] - facepoints_projected[0];
const qvec2f p0p1_uv = facepoints_uvs[1] - facepoints_uvs[0];
const qvec2f p0p2_uv = facepoints_uvs[2] - facepoints_uvs[0];
/*
Find a 2x2 transformation matrix that maps p0p1 to p0p1_uv, and p0p2 to p0p2_uv
[ a b ] [ p0p1.x ] = [ p0p1_uv.x ]
[ c d ] [ p0p1.y ] [ p0p1_uv.y ]
[ a b ] [ p0p2.x ] = [ p0p1_uv.x ]
[ c d ] [ p0p2.y ] [ p0p2_uv.y ]
writing as a system of equations:
a * p0p1.x + b * p0p1.y = p0p1_uv.x
c * p0p1.x + d * p0p1.y = p0p1_uv.y
a * p0p2.x + b * p0p2.y = p0p2_uv.x
c * p0p2.x + d * p0p2.y = p0p2_uv.y
back to a matrix equation, with the unknowns in a column vector:
[ p0p1_uv.x ] [ p0p1.x p0p1.y 0 0 ] [ a ]
[ p0p1_uv.y ] = [ 0 0 p0p1.x p0p1.y ] [ b ]
[ p0p2_uv.x ] [ p0p2.x p0p2.y 0 0 ] [ c ]
[ p0p2_uv.y ] [ 0 0 p0p2.x p0p2.y ] [ d ]
*/
const qmat4x4f M{
p0p1[0], 0, p0p2[0], 0, // col 0
p0p1[1], 0, p0p2[1], 0, // col 1
0, p0p1[0], 0, p0p2[0], // col 2
0, p0p1[1], 0, p0p2[1] // col 3
};
const qmat4x4f Minv = qv::inverse(M);
const qvec4f abcd = Minv * qvec4f(p0p1_uv[0], p0p1_uv[1], p0p2_uv[0], p0p2_uv[1]);
const qmat2x2f texPlaneToUV{abcd[0], abcd[2], // col 0
abcd[1], abcd[3]}; // col 1
{
// self check
// qvec2f uv01_test = texPlaneToUV * p0p1;
// qvec2f uv02_test = texPlaneToUV * p0p2;
// these fail if one of the texture axes is 0 length.
// checkEq(uv01_test, p0p1_uv, 0.01);
// checkEq(uv02_test, p0p2_uv, 0.01);
}
const texdef_quake_ed_t res = Reverse_QuakeEd(texPlaneToUV, faceplane, false);
// figure out shift based on facepoints[0]
const qvec3f testpoint = facepoints[0];
qvec2f uv0_actual = evalTexDefAtPoint(addShift(res, qvec2f(0, 0)), faceplane, testpoint);
qvec2f uv0_desired = qvec2f(worldToTexSpace * qvec4f(testpoint[0], testpoint[1], testpoint[2], 1.0f));
qvec2f shift = uv0_desired - uv0_actual;
// sometime we have very large shift values, normalize them to be smaller
shift = normalizeShift(texture, shift);
const texdef_quake_ed_t res2 = addShift(res, shift);
return res2;
}
};
namespace convert_to_valve
{
static texdef_valve_t TexDef_BSPToValve(const texvecf &in_vecs)
{
texdef_valve_t res;
// From the valve -> bsp code,
//
// for (i = 0; i < 3; i++) {
// out->vecs[0][i] = axis[0][i] / scale[0];
// out->vecs[1][i] = axis[1][i] / scale[1];
// }
//
// We'll generate axis vectors of length 1 and pick the necessary scale
for (size_t i = 0; i < 2; i++) {
qvec3d axis = in_vecs.row(i).xyz();
const double length = qv::normalizeInPlace(axis);
// avoid division by 0
if (length != 0.0) {
res.scale[i] = 1.0 / length;
} else {
res.scale[i] = 0.0;
}
res.shift[i] = in_vecs.at(i, 3);
res.axis.set_row(i, axis);
}
return res;
}
};
namespace convert_to_bp
{
// From FaceToBrushPrimitFace in GtkRadiant
static texdef_bp_t TexDef_BSPToBrushPrimitives(
const qplane3d &plane, const img::texture_meta &texture, const texvecf &in_vecs)
{
auto [ texX, texY ] = compute_axis_base(plane.normal);
// compute projection vector
qvec3d proj = plane.normal * plane.dist;
// (0,0) in plane axis base is (0,0,0) in world coordinates + projection on the affine plane
// (1,0) in plane axis base is texX in world coordinates + projection on the affine plane
// (0,1) in plane axis base is texY in world coordinates + projection on the affine plane
// use old texture code to compute the ST coords of these points
qvec2d st[] = {
in_vecs.uvs(proj, texture.width, texture.height),
in_vecs.uvs(texX + proj, texture.width, texture.height),
in_vecs.uvs(texY + proj, texture.width, texture.height)
};
// compute texture matrix
texdef_bp_t res;
res.axis.set_col(2, st[0]);
res.axis.set_col(0, st[1] - st[0]);
res.axis.set_col(1, st[2] - st[0]);
return res;
}
};
void brush_side_t::convert_to(texcoord_style_t style, const gamedef_t *game, const settings::common_settings &options)
{
// we're already this style
switch (style) {
case texcoord_style_t::quaked:
if (std::holds_alternative<texdef_quake_ed_t>(raw)) {
return;
}
break;
case texcoord_style_t::etp:
if (std::holds_alternative<texdef_etp_t>(raw)) {
return;
}
break;
case texcoord_style_t::brush_primitives:
if (std::holds_alternative<texdef_bp_t>(raw)) {
return;
}
break;
case texcoord_style_t::valve_220:
if (std::holds_alternative<texdef_valve_t>(raw)) {
return;
}
break;
}
if (style == texcoord_style_t::quaked) {
std::optional<img::texture_meta> meta = std::nullopt;
if (game) {
meta = std::get<0>(img::load_texture_meta(texture, game, options));
}
raw = convert_to_quaked::TexDef_BSPToQuakeEd(plane, meta, vecs, planepts);
} else if (style == texcoord_style_t::valve_220) {
raw = convert_to_valve::TexDef_BSPToValve(vecs);
} else if (style == texcoord_style_t::brush_primitives) {
if (!game) {
FError("conversion to brush primitives requires a `--game` option to be set");
}
auto [ meta, result, data ] = img::load_texture_meta(texture, game, options);
if (!meta) {
FError("conversion to brush primitives requires texture to be loaded");
}
raw = convert_to_bp::TexDef_BSPToBrushPrimitives(plane, meta.value(), vecs);
} else {
FError("can't currently convert to this format!");
}
}
void brush_t::parse_brush_face(parser_t &parser, texcoord_style_t base_format)
{
brush_side_t side;
side.location = parser.location;
side.parse_plane_def(parser);
/* calculate the normal/dist plane equation */
qvec3d ab = side.planepts[0] - side.planepts[1];
qvec3d cb = side.planepts[2] - side.planepts[1];
double length;
qvec3d normal = qv::normalize(qv::cross(ab, cb), length);
double dist = qv::dot(side.planepts[1], normal);
side.plane = { normal, dist };
side.parse_texture_def(parser, base_format);
if (length < NORMAL_EPSILON) {
logging::print("WARNING: {}: Brush plane with no normal\n", parser.location);
return;
}
/* Check for duplicate planes */
for (auto &check : faces) {
if (qv::epsilonEqual(check.plane, side.plane) ||
qv::epsilonEqual(-check.plane, side.plane)) {
logging::print("{}: Brush with duplicate plane\n", parser.location);
return;
}
}
// ericw -- round texture vector values that are within ZERO_EPSILON of integers,
// to attempt to attempt to work around corrupted lightmap sizes in DarkPlaces
// (it uses 32 bit precision in CalcSurfaceExtents)
for (size_t i = 0; i < 2; i++) {
for (size_t j = 0; j < 4; j++) {
double r = Q_rint(side.vecs.at(i, j));
if (fabs(side.vecs.at(i, j) - r) < ZERO_EPSILON) {
side.vecs.at(i, j) = r;
}
}
}
side.validate_texture_projection();
faces.emplace_back(std::move(side));
}
void brush_t::write(std::ostream &stream)
{
stream << "{\n";
if (base_format == texcoord_style_t::brush_primitives) {
stream << "brushDef\n{\n";
}
for (auto &face : faces) {
face.write(stream);
stream << "\n";
}
if (base_format == texcoord_style_t::brush_primitives) {
stream << "}\n";
}
stream << "}\n";
}
void brush_t::convert_to(texcoord_style_t style, const gamedef_t *game, const settings::common_settings &options)
{
for (auto &face : faces) {
face.convert_to(style, game, options);
}
if (style == texcoord_style_t::brush_primitives) {
base_format = style;
} else {
base_format = texcoord_style_t::quaked;
}
}
// map file stuff
void map_entity_t::parse_entity_dict(parser_t &parser)
{
std::string key = std::move(parser.token);
// trim whitespace from start/end
while (std::isspace(key.front())) {
key.erase(key.begin());
}
while (std::isspace(key.back())) {
key.erase(key.end() - 1);
}
parser.parse_token(PARSE_SAMELINE);
epairs.set(key, parser.token);
}
void map_entity_t::parse_brush(parser_t &parser)
{
// ericw -- brush primitives
if (!parser.parse_token(PARSE_PEEK)) {
FError("{}: unexpected EOF after {{ beginning brush", parser.location);
}
brush_t brush;
if (parser.token == "(" || parser.token == "}") {
brush.base_format = texcoord_style_t::quaked;
} else {
parser.parse_token();
brush.base_format = texcoord_style_t::brush_primitives;
// optional
if (parser.token == "brushDef") {
if (!parser.parse_token()) {
FError("{}: Brush primitives: unexpected EOF (nothing after brushDef)", parser.location);
}
}
// mandatory
if (parser.token != "{") {
FError("{}: Brush primitives: expected second {{ at beginning of brush, got \"{}\"", parser.location, parser.token);
}
}
// ericw -- end brush primitives
while (parser.parse_token()) {
// set linenum after first parsed token
if (!brush.location) {
brush.location = parser.location;
}
if (parser.token == "}") {
break;
}
brush.parse_brush_face(parser, brush.base_format);
}
// ericw -- brush primitives - there should be another closing }
if (brush.base_format == texcoord_style_t::brush_primitives) {
if (!parser.parse_token()) {
FError("Brush primitives: unexpected EOF (no closing brace)");
} else if (parser.token != "}") {
FError("{}: Brush primitives: Expected }}, got: {}", parser.location, parser.token);
}
}
// ericw -- end brush primitives
if (brush.faces.size()) {
brushes.push_back(std::move(brush));
}
}
bool map_entity_t::parse(parser_t &parser)
{
location = parser.location;
if (!parser.parse_token()) {
return false;
}
if (parser.token != "{") {
FError("{}: Invalid entity format, {{ not found", parser.location);
}
do {
if (!parser.parse_token()) {
FError("Unexpected EOF (no closing brace)");
}
if (parser.token == "}") {
break;
} else if (parser.token == "{") {
parse_brush(parser);
} else {
parse_entity_dict(parser);
}
} while (1);
return true;
}
void map_entity_t::write(std::ostream &stream)
{
stream << "{\n";
for (auto &kvp : epairs) {
ewt::print(stream, "\"{}\" \"{}\"\n", kvp.first, kvp.second);
}
size_t brush_id = 0;
for (auto &brush : brushes) {
ewt::print(stream, "// brush {}\n", brush_id++);
brush.write(stream);
}
stream << "}\n";
}
void map_file_t::parse(parser_t &parser)
{
while (true) {
map_entity_t &entity = entities.emplace_back();
if (!entity.parse(parser)) {
break;
}
}
// Remove dummy entity inserted above
assert(!entities.back().epairs.size());
entities.pop_back();
}
void map_file_t::write(std::ostream &stream)
{
size_t ent_id = 0;
for (auto &entity : entities) {
ewt::print(stream, "// entity {}\n", ent_id++);
entity.write(stream);
}
}
void map_file_t::convert_to(texcoord_style_t style, const gamedef_t *game, const settings::common_settings &options)
{
for (auto &entity : entities) {
for (auto &brush : entity.brushes) {
brush.convert_to(style, game, options);
}
}
}
map_file_t parse(const std::string_view &view, parser_source_location base_location)
{
parser_t parser(view, base_location);
map_file_t result;
result.parse(parser);
return result;
}
} // namespace mapfile