diff --git a/indra/llmath/llvolume.cpp b/indra/llmath/llvolume.cpp index 44cfcaec08..a7331aa24b 100644 --- a/indra/llmath/llvolume.cpp +++ b/indra/llmath/llvolume.cpp @@ -450,7 +450,7 @@ S32 LLProfile::getNumNGonPoints(const LLProfileParams& params, S32 sides, F32 of // What is the bevel parameter used for? - DJS 04/05/02 // Bevel parameter is currently unused but presumedly would support // filleted and chamfered corners -void LLProfile::genNGon(const LLProfileParams& params, S32 sides, F32 offset, F32 bevel, F32 ang_scale, S32 split) +bool LLProfile::genNGon(const LLProfileParams& params, S32 sides, F32 offset, F32 bevel, F32 ang_scale, S32 split) { // Generate an n-sided "circular" path. // 0 is (1,0), and we go counter-clockwise along a circular path from there. @@ -491,7 +491,8 @@ void LLProfile::genNGon(const LLProfileParams& params, S32 sides, F32 offset, F3 t_fraction = (begin - t_first)*sides; // Only use if it's not almost exactly on an edge. - if (t_fraction < 0.9999f) + const bool begin_point = t_fraction < 0.9999f; + if (begin_point) { LLVector4a new_pt; new_pt.setLerp(pt1, pt2, t_fraction); @@ -575,6 +576,7 @@ void LLProfile::genNGon(const LLProfileParams& params, S32 sides, F32 offset, F3 } mTotal = mProfile.size(); + return begin_point; } // Hollow is percent of the original bounding box, not of this particular @@ -588,7 +590,7 @@ LLProfile::Face* LLProfile::addHole(const LLProfileParams& params, bool flat, F3 mTotalOut = mTotal; // Why is the "bevel" parameter -1? DJS 04/05/02 - genNGon(params, llfloor(sides),offset,-1, ang_scale, split); + const bool begin_point = genNGon(params, llfloor(sides),offset,-1, ang_scale, split); Face *face = addFace(mTotalOut, mTotal-mTotalOut,0,LL_FACE_INNER_SIDE, flat); @@ -670,6 +672,19 @@ LLProfile::Face* LLProfile::addHole(const LLProfileParams& params, bool flat, F3 mProfile[i] = pt[j--]; } + // A flat hole's corners. genNGon laid the points out as its begin point, + // where it has one, then split points and a corner by turns, split + // points first, to its end; they run the other way here. A hole drawn + // in to its wall may fold at any point. + if (flat) + { + const S32 count = mTotal - mTotalOut; + const S32 step = mHoleTouchesWall ? 1 : split + 1; + const S32 first_laid = (begin_point ? 1 : 0) + split; + face->mCornerStep = step; + face->mFirstCorner = ((count - 1 - first_laid) % step + step) % step; + } + for (S32 i=0;i<(S32)mFaces.size();i++) { if (mFaces[i].mCap) @@ -2105,9 +2120,10 @@ bool LLVolume::generate() // stretched due to twisting or scaling on the path. S32 split = (S32) ((mDetail)*0.66f); - if (mParams.getPathParams().getCurveType() == LL_PCODE_PATH_LINE && - (mParams.getPathParams().getScale().mV[0] != 1.0f || - mParams.getPathParams().getScale().mV[1] != 1.0f) && + const LLPathParams& path_params = mParams.getPathParams(); + const bool tapered = path_params.getScale().mV[0] != 1.0f || path_params.getScale().mV[1] != 1.0f; + if (path_params.getCurveType() == LL_PCODE_PATH_LINE && + tapered && (mParams.getProfileParams().getCurveType() == LL_PCODE_PROFILE_SQUARE || mParams.getProfileParams().getCurveType() == LL_PCODE_PROFILE_ISOTRI || mParams.getProfileParams().getCurveType() == LL_PCODE_PROFILE_EQUALTRI || @@ -2116,6 +2132,19 @@ bool LLVolume::generate() split = 0; } + // A straight path that neither twists nor tapers sweeps every profile + // edge into a parallelogram, and every face is flat or ruled straight + // along the path: positions and UVs are affine over each quad, and the + // split points and the rings between the ends lie on the triangles the + // ends make. A hole drawn in to its wall is the exception: the wall + // draws in some of its points and not others, its edges no longer run + // straight between its corners, and its split points shape it. + const S32 lighting_split = split; + if (path_params.getCurveType() == LL_PCODE_PATH_LINE && !tapered && path_params.getTwistBegin() == path_params.getTwist()) + { + split = 0; + } + mLODScaleBias.setVec(0.5f, 0.5f, 0.5f); F32 profile_detail = mDetail; @@ -2138,6 +2167,11 @@ bool LLVolume::generate() bool regenPath = mPathp->generate(mParams.getPathParams(), path_detail, split); bool regenProf = mProfilep->generate(mParams.getProfileParams(), mPathp->isOpen(),profile_detail, split); + if (regenProf && split < lighting_split && mProfilep->holeTouchesWall()) + { + mProfilep->setDirty(); + mProfilep->generate(mParams.getProfileParams(), mPathp->isOpen(), profile_detail, lighting_split); + } if (regenPath || regenProf ) { @@ -2960,8 +2994,17 @@ void LLVolume::createVolumeFaces() { type_mask |= LLVolumeFace::INNER_MASK; if (face.mFlat && num_s > 2) - { //flat inner faces have to copy vert normals - num_s = num_s*2; + { + // A flat inner side has a column for each of its + // points and a second for each corner, where its + // normals turn. + for (S32 point = 1; point + 1 < face.mCount; ++point) + { + if (face.isCorner(point)) + { + ++num_s; + } + } if (num_s < 0) { LL_ERRS() << "Volume face corruption detected." << LL_ENDL; @@ -5859,6 +5902,116 @@ namespace return outer_shorter(); } + // A side's rows and columns that its parameters take to one point at + // every station: the first or last row where the path tapers to nothing + // at an end it reaches, and the first or last column where a half circle + // ends on the axis a circle path turns it about and the path keeps it + // there, a sphere's poles. A half circle's ends lie on that axis where + // no cut moves them; a circle of no radius, a hole size of 1 in y, with + // no skew and no taper along the axis keeps them there, and a twist + // keeps them only by whole half turns. An inner side runs the other way + // along its profile. + struct Collapse + { + bool first_row = false; + bool last_row = false; + bool first_column = false; + bool last_column = false; + }; + + Collapse collapse_of(const LLVolume* volume, bool inner) + { + Collapse collapse; + const LLPathParams& path = volume->getParams().getPathParams(); + const U8 curve = path.getCurveType(); + if (curve == LL_PCODE_PATH_LINE) + { + // A line's scale of 2 narrows its beginning to nothing, as + // LLPathParams::getBeginScale has it, and one of 0 its end. + const F32 scale_x = path.getScaleX(); + const F32 scale_y = path.getScaleY(); + collapse.first_row = scale_x == 2.f && scale_y == 2.f && path.getBegin() == 0.f; + collapse.last_row = scale_x == 0.f && scale_y == 0.f && path.getEnd() == 1.f; + return collapse; + } + if (curve != LL_PCODE_PATH_CIRCLE && curve != LL_PCODE_PATH_CIRCLE2) + { + return collapse; + } + + // LLPath::genNGon tapers the end by a positive taper and the + // beginning by a negative one. + collapse.first_row = path.getTaperX() == -1.f && path.getTaperY() == -1.f && path.getBegin() == 0.f; + collapse.last_row = path.getTaperX() == 1.f && path.getTaperY() == 1.f && path.getEnd() == 1.f; + + const LLProfileParams& profile = volume->getParams().getProfileParams(); + const F32 half_turns = 2.f * path.getTwist(); + if (curve == LL_PCODE_PATH_CIRCLE && (profile.getCurveType() & LL_PCODE_PROFILE_MASK) == LL_PCODE_PROFILE_CIRCLE_HALF && + path.getScaleY() == 1.f && path.getSkew() == 0.f && path.getTaperX() == 0.f && path.getTwistBegin() == path.getTwist() && + half_turns == floorf(half_turns)) + { + const bool begin_pole = profile.getBegin() == 0.f; + const bool end_pole = profile.getEnd() == 1.f; + collapse.first_column = inner ? end_pole : begin_pole; + collapse.last_column = inner ? begin_pole : end_pole; + } + return collapse; + } + + // A side's triangles, from the quads of its grid, vertex s + num_s * t, + // whose left columns are listed, or from every quad: bottom left, top + // right, top left and bottom left, bottom right, top right, but for + // those whose corners a collapsed row or column puts in one place. + // Returns the end of what it wrote. + template + U16* grid_indices(U16* out, S32 num_s, S32 num_t, const S32* lefts, S32 quads, const Collapse& collapse) + { + const auto upper = [&](S32 s, S32 t) + { + *out++ = (U16)(s + num_s*t); //bottom left + *out++ = (U16)(s+1 + num_s*(t+1)); //top right + *out++ = (U16)(s + num_s*(t+1)); //top left + }; + const auto lower = [&](S32 s, S32 t) + { + *out++ = (U16)(s + num_s*t); //bottom left + *out++ = (U16)(s+1 + num_s*t); //bottom right + *out++ = (U16)(s+1 + num_s*(t+1)); //top right + }; + // The last quad's column. + const S32 last = num_s - 2; + for (S32 t = 0; t < num_t - 1; t++) + { + const bool keep_upper = !COLLAPSED || !(collapse.last_row && t + 1 == num_t - 1); + const bool keep_lower = !COLLAPSED || !(collapse.first_row && t == 0); + if (!COLLAPSED || (keep_upper && keep_lower && !collapse.first_column && !collapse.last_column)) + { + for (S32 q = 0; q < quads; ++q) + { + const S32 s = LISTED ? lefts[q] : q; + upper(s, t); + lower(s, t); + } + continue; + } + // A collapsed column is the first quad's left or the last + // quad's right. + for (S32 q = 0; q < quads; ++q) + { + const S32 s = LISTED ? lefts[q] : q; + if (keep_upper && !(s == 0 && collapse.first_column)) + { + upper(s, t); + } + if (keep_lower && !(s == last && collapse.last_column)) + { + lower(s, t); + } + } + } + return out; + } + // The path station a cap closes: the last for a top cap, the first for a // bottom one. S32 cap_station(const LLVolume* volume, bool top) @@ -6614,9 +6767,10 @@ void LLVolumeFace::createTangents() std::fill(mTangents, mTangents + mNumVertices, tangent); } else if (!(mTypeMask & CAP_MASK) && mNumS > 0 && mNumT > 0 && mNumS * mNumT == mNumVertices && - mNumIndices == (mNumS - 1) * (mNumT - 1) * 6) + mNumIndices <= (mNumS - 1) * (mNumT - 1) * 6) { - // A side is createSide's grid. + // A side is createSide's grid. The quads and triangles its indices + // leave out have no area, and add nothing to a tangent. if (mFlexible) { // Along the rows, the way u runs across the face, and w as @@ -6886,37 +7040,18 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) S32 s, t; F32 ss, tt; - num_vertices = mNumS*mNumT; - num_indices = (mNumS-1)*(mNumT-1)*6; - - // createVolumeFaces builds partially only where the layout, and so the - // counts, are the last build's. - llassert(!partial_build || (num_vertices == mNumVertices && num_indices == mNumIndices)); - if (num_vertices != mNumVertices || num_indices != mNumIndices) - { - partial_build = false; - } - - if (!partial_build) - { - resizeVertices(num_vertices); - resizeIndices(num_indices); - } + // A flat inner side copies each of its hole's corners, so that its + // normals turn there: the quad between a corner and its copy has no + // width, and has no triangles. + const bool doubled = (mTypeMask & INNER_MASK) && (mTypeMask & FLAT_MASK) && mNumS > 2; + const LLProfile::Face* outline = doubled ? &volume->getProfile().mFaces[mID] : nullptr; + const S32 num_s = doubled ? outline->mCount : mNumS; - LLVector4a* pos = (LLVector4a*) mPositions; - LLVector2* tc = (LLVector2*) mTexCoords; F32 begin_stex = floorf(profile[mBeginS][2]); - S32 num_s = ((mTypeMask & INNER_MASK) && (mTypeMask & FLAT_MASK) && mNumS > 2) ? mNumS/2 : mNumS; - S32 cur_vertex = 0; - S32 end_t = mBeginT+mNumT; - bool test = (mTypeMask & INNER_MASK) && (mTypeMask & FLAT_MASK) && mNumS > 2; - - // Copy the vertices into the array. Every row takes the same columns of - // its mesh row with the same s texture coordinates, and the path's t for - // its own: the columns and their s are worked out once, so the rows are - // copies. - { LL_PROFILE_ZONE_NAMED_CATEGORY_VOLUME("llvfcs - copy verts"); + // Every row takes the same columns of its mesh row with the same s + // texture coordinates, and the path's t for its own: the columns and + // their s are worked out once, so the rows are copies. struct Column { S32 mesh; @@ -6926,12 +7061,22 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) Column stack_columns[STACK_COLUMNS]; std::vector heap_columns; Column* columns = stack_columns; + // A flat inner side's quads with area, by their left columns. + S32 stack_lefts[STACK_COLUMNS]; + std::vector heap_lefts; + S32* lefts = stack_lefts; if (mNumS > STACK_COLUMNS) { heap_columns.resize(mNumS); columns = heap_columns.data(); + if (doubled) + { + heap_lefts.resize(mNumS); + lefts = heap_lefts.data(); + } } S32 column_count = 0; + S32 quads = doubled ? 0 : mNumS - 1; for (s = 0; s < num_s; s++) { if (mTypeMask & END_MASK) @@ -6971,19 +7116,72 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) // A column past the profile's end wraps to its start. const S32 column = mBeginS + s >= max_s ? mBeginS + s - max_s : mBeginS + s; + if (doubled && s > 0) + { + lefts[quads++] = column_count - 1; + } columns[column_count++] = { column, ss }; - if (test && s > 0) + if (doubled && outline->isCorner(s)) { columns[column_count++] = { column, ss }; } } - if (test) + llassert(column_count == mNumS); + + const S32 rows = mNumT - 1; + + // On a shared volume, a row or column its parameters take to one point + // keeps its vertices, each with its own UV and normal, and has no + // triangles: those with two corners there have no area. Of a grid + // quad's two triangles, bottom left, top right, top left has two corners + // on its left column and on its top row, and bottom left, bottom right, + // top right on its right column and its bottom row. A face that would be + // left with none keeps them all. + Collapse collapse; + if (!volume->isUnique() && sculpt_stitching == LL_SCULPT_TYPE_NONE) + { + collapse = collapse_of(volume, mTypeMask & INNER_MASK); + if (mTypeMask & END_MASK) + { + collapse.first_column = false; + collapse.last_column = false; + } + } + const S32 upper_left_out = (collapse.first_column ? rows : 0) + (collapse.last_row ? quads : 0) - (collapse.first_column && collapse.last_row ? 1 : 0); + const S32 lower_left_out = (collapse.last_column ? rows : 0) + (collapse.first_row ? quads : 0) - (collapse.last_column && collapse.first_row ? 1 : 0); + S32 triangles = 2 * quads * rows - upper_left_out - lower_left_out; + if (triangles <= 0) { - s = (mTypeMask & OPEN_MASK) ? num_s - 1 : 0; - columns[column_count++] = { mBeginS + s, profile[mBeginS + s][2] - begin_stex }; + collapse = Collapse(); + triangles = 2 * quads * rows; } - llassert(column_count == mNumS); + const bool collapsed = collapse.first_row || collapse.last_row || collapse.first_column || collapse.last_column; + num_vertices = mNumS*mNumT; + num_indices = triangles*3; + + // createVolumeFaces builds partially only where the layout, and so the + // counts, are the last build's. + llassert(!partial_build || (num_vertices == mNumVertices && num_indices == mNumIndices)); + if (num_vertices != mNumVertices || num_indices != mNumIndices) + { + partial_build = false; + } + + if (!partial_build) + { + resizeVertices(num_vertices); + resizeIndices(num_indices); + } + + LLVector4a* pos = (LLVector4a*) mPositions; + LLVector2* tc = (LLVector2*) mTexCoords; + + S32 cur_vertex = 0; + S32 end_t = mBeginT+mNumT; + + // Copy the vertices into the array. + { LL_PROFILE_ZONE_NAMED_CATEGORY_VOLUME("llvfcs - copy verts"); const LLVector4a* mesh_points = mesh.data(); for (t = mBeginT; t < end_t; t++) { @@ -7044,18 +7242,19 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) LL_PROFILE_ZONE_NAMED_CATEGORY_VOLUME("llvfcs - generate indices"); // Now we generate the indices. - for (t = 0; t < (mNumT-1); t++) + U16* end_index = mIndices; + if (collapsed) { - for (s = 0; s < (mNumS-1); s++) - { - mIndices[cur_index++] = s + mNumS*t; //bottom left - mIndices[cur_index++] = s+1 + mNumS*(t+1); //top right - mIndices[cur_index++] = s + mNumS*(t+1); //top left - mIndices[cur_index++] = s + mNumS*t; //bottom left - mIndices[cur_index++] = s+1 + mNumS*t; //bottom right - mIndices[cur_index++] = s+1 + mNumS*(t+1); //top right - } + end_index = doubled ? grid_indices(mIndices, mNumS, mNumT, lefts, quads, collapse) + : grid_indices(mIndices, mNumS, mNumT, lefts, quads, collapse); + } + else + { + end_index = doubled ? grid_indices(mIndices, mNumS, mNumT, lefts, quads, collapse) + : grid_indices(mIndices, mNumS, mNumT, lefts, quads, collapse); } + cur_index = (S32)(end_index - mIndices); + llassert(cur_index == mNumIndices); } // The normals come from the grid's quads, not from the triangles the @@ -7094,12 +7293,12 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) // one seam, and across its first and last rows: a prim's where they are // one place in every row or column, as its own positions say; a // sculpt's where its stitching samples them from one column or row of - // the map. + // the map. A flat inner side's seam is one of its corners. if (!partial_build) { if (sculpt_stitching == LL_SCULPT_TYPE_NONE) { - mJoinsS = columns_meet(pos, mNumS, mNumT, 0, mNumS - 1); + mJoinsS = !doubled && columns_meet(pos, mNumS, mNumT, 0, mNumS - 1); mJoinsT = rows_meet(pos, mNumS, 0, mNumT - 1); } else @@ -7168,8 +7367,10 @@ bool LLVolumeFace::createSide(LLVolume* volume, bool partial_build) // On a sphere's side, a column that is one place in every row is a pole // on the axis the half circle sweeps about, and takes the axis. A cut - // end of a hollow sphere lies on the axis, and has no area. - if (sculpt_stitching == LL_SCULPT_TYPE_NONE && !(mTypeMask & END_MASK) && volume->getPathType() == LL_PCODE_PATH_CIRCLE && + // end of a hollow sphere lies on the axis, and has no area. A flat inner + // side is a cone at a pole, and its ends keep the normals of the strips + // they end. + if (sculpt_stitching == LL_SCULPT_TYPE_NONE && !(mTypeMask & END_MASK) && !doubled && volume->getPathType() == LL_PCODE_PATH_CIRCLE && ((volume->getProfileType() & LL_PCODE_PROFILE_MASK) == LL_PCODE_PROFILE_CIRCLE_HALF)) { if (column_is_point(pos, mNumS, mNumT, 0)) diff --git a/indra/llmath/llvolume.h b/indra/llmath/llvolume.h index d9fa739d9c..dcc6e41a12 100644 --- a/indra/llmath/llvolume.h +++ b/indra/llmath/llvolume.h @@ -721,6 +721,17 @@ class LLProfile bool mCap; bool mFlat; LLFaceID mFaceID; + // A flat hole's corners, where its side folds: of its points between + // its two ends, mFirstCorner and every mCornerStep-th after it. The + // rest are split points, on the straight edge between two corners. + // A step of 0 marks none. + S32 mFirstCorner = 0; + S32 mCornerStep = 0; + + bool isCorner(S32 point) const + { + return mCornerStep > 0 && point > 0 && point + 1 < mCount && point >= mFirstCorner && (point - mFirstCorner) % mCornerStep == 0; + } }; LLAlignedArray mProfile; @@ -736,7 +747,9 @@ class LLProfile ~LLProfile(); static S32 getNumNGonPoints(const LLProfileParams& params, S32 sides, F32 offset=0.0f, F32 bevel = 0.0f, F32 ang_scale = 1.f, S32 split = 0); - void genNGon(const LLProfileParams& params, S32 sides, F32 offset=0.0f, F32 bevel = 0.0f, F32 ang_scale = 1.f, S32 split = 0); + // Returns whether it began with the point of the begin cut, which it + // leaves out where the cut falls just short of a step. + bool genNGon(const LLProfileParams& params, S32 sides, F32 offset=0.0f, F32 bevel = 0.0f, F32 ang_scale = 1.f, S32 split = 0); Face* addHole(const LLProfileParams& params, bool flat, F32 sides, F32 offset, F32 box_hollow, F32 ang_scale, S32 split = 0); Face* addCap (S16 faceID); diff --git a/indra/llmath/tests/llvolume_test.cpp b/indra/llmath/tests/llvolume_test.cpp index 08745f32a4..64e9da832a 100644 --- a/indra/llmath/tests/llvolume_test.cpp +++ b/indra/llmath/tests/llvolume_test.cpp @@ -151,8 +151,8 @@ namespace return face.mTypeMask & LLVolumeFace::CAP_MASK; } - // A flat inner side copies every profile point, so that each is a hard - // edge: the quad between a point and its copy has no width. + // A flat inner side copies each of its hole's corners, so that each is a + // hard edge: the quad between a corner and its copy has no width. bool isDoubled(const LLVolumeFace& face) { return (face.mTypeMask & LLVolumeFace::INNER_MASK) && (face.mTypeMask & LLVolumeFace::FLAT_MASK) && face.mNumS > 2; @@ -666,10 +666,43 @@ namespace if (face.mNumVertices != face.mNumS * face.mNumT) { fail(fmt::format("{} vertices for a grid of {} by {}", face.mNumVertices, face.mNumS, face.mNumT)); + return true; } - if (face.mNumIndices != (face.mNumS - 1) * (face.mNumT - 1) * 6) + // Each triangle is one of the grid's, bottom left, top right, + // top left or bottom left, bottom right, top right, a quad, and + // none twice; a triangle of the grid's left out has no area. + std::map, bool> grid; + for (S32 t = 0; t + 1 < face.mNumT; ++t) + { + for (S32 s = 0; s + 1 < face.mNumS; ++s) + { + const U32 bl = gridIndex(face, s, t), br = bl + 1, tl = gridIndex(face, s, t + 1), tr = tl + 1; + grid[{ bl, tr, tl }] = false; + grid[{ bl, br, tr }] = false; + } + } + for (S32 i = 0; i + 2 < face.mNumIndices; i += 3) + { + const auto found = grid.find({ face.mIndices[i], face.mIndices[i + 1], face.mIndices[i + 2] }); + if (found == grid.end() || found->second) + { + fail(fmt::format("triangle {} ({}, {}, {}) is {} of the grid's", i / 3, face.mIndices[i], face.mIndices[i + 1], face.mIndices[i + 2], + found == grid.end() ? "none" : "a second")); + return true; + } + found->second = true; + } + for (const auto& [corners, used] : grid) { - fail(fmt::format("{} indices for a grid of {} by {}", face.mNumIndices, face.mNumS, face.mNumT)); + const LLVector3 a = v3(face.mPositions[corners[0]]); + const LLVector3 b = v3(face.mPositions[corners[1]]); + const LLVector3 c = v3(face.mPositions[corners[2]]); + const F32 scale = (b - a).lengthSquared() + (c - a).lengthSquared() + (c - b).lengthSquared(); + if (!used && ((b - a) % (c - a)).length() > 1e-5f * scale) + { + fail(fmt::format("the grid's triangle ({}, {}, {}), which has area, is left out", corners[0], corners[1], corners[2])); + return true; + } } } return true; @@ -928,10 +961,22 @@ namespace incident[groups[v]].push_back((U32)t); } } + // The oracle gives a normal only to a vertex a triangle uses: of the + // vertices at a point the parameters collapse, as an apex's or a + // pole's, the triangles of no area are left out, and some vertices + // with them. + std::vector used(face.mNumVertices, false); + for (const Triangle& tri : triangles) + { + for (U32 v : tri.v) + { + used[v] = true; + } + } for (S32 v = 0; v < face.mNumVertices; ++v) { const std::vector& around = incident[groups[v]]; - if (around.empty() || std::any_of(around.begin(), around.end(), [&](U32 t) { return triangles[t].degenerate; })) + if (!used[v] || around.empty() || std::any_of(around.begin(), around.end(), [&](U32 t) { return triangles[t].degenerate; })) { continue; } @@ -1001,17 +1046,27 @@ namespace } } - // A flat inner side is hard at every point. Its columns run p0, then - // each later point twice, then p0 again (or the last point, where the - // profile is cut), so the quads after even columns are the real ones - // and the quads after odd columns have no width: a vertex takes its - // normal from the one strip of real quads beside it, up and down the - // path, and from no other strip at its point. A vertex in no triangle - // with area, as the copies that close the side or an apex's copies, - // is beside no strip. - S32 stripOf(S32 column) + // A flat inner side is hard at every corner. Its columns run along its + // points, each corner twice, the copy one place with the corner in every + // row and with its UVs: the quad between them has no width. Each run of + // columns between copies is a strip, a facet along the path, and a + // vertex takes its normal from the triangles of its own strip at its + // point, and from no other strip there. A vertex in no triangle with + // area, as an apex's, is in no strip. + std::vector stripsOf(const LLVolumeFace& face) { - return column % 2 == 0 ? column : column - 1; + std::vector strip(face.mNumS, 0); + for (S32 s = 1; s < face.mNumS; ++s) + { + bool copy = true; + for (S32 t = 0; t < face.mNumT && copy; ++t) + { + copy = bitsOf(face.mPositions[gridIndex(face, s, t)]) == bitsOf(face.mPositions[gridIndex(face, s - 1, t)]) && + face.mTexCoords[gridIndex(face, s, t)] == face.mTexCoords[gridIndex(face, s - 1, t)]; + } + strip[s] = strip[s - 1] + (copy ? 1 : 0); + } + return strip; } void checkHardEdges(const LLVolumeFace& face, const std::vector& triangles, const std::vector& normals, Report& report, @@ -1023,6 +1078,7 @@ namespace } const std::vector lone = loneVertices(face, triangles); const std::vector groups = weldGroups(face); + const std::vector strips = stripsOf(face); std::map, std::vector> in_strip; for (size_t t = 0; t < triangles.size(); ++t) { @@ -1030,14 +1086,14 @@ namespace { continue; } - S32 strip = face.mNumS; + S32 column = face.mNumS; for (U32 v : triangles[t].v) { - strip = llmin(strip, (S32)(v % face.mNumS)); + column = llmin(column, (S32)(v % face.mNumS)); } for (U32 v : triangles[t].v) { - in_strip[{ groups[v], strip }].push_back((U32)t); + in_strip[{ groups[v], strips[column] }].push_back((U32)t); } } for (S32 v = 0; v < face.mNumVertices; ++v) @@ -1047,7 +1103,7 @@ namespace { continue; } - const auto found = in_strip.find({ groups[v], stripOf(column) }); + const auto found = in_strip.find({ groups[v], strips[column] }); if (found == in_strip.end()) { continue; @@ -2559,9 +2615,9 @@ namespace // Vertices transformed a triangle with a 16-entry FIFO cache, and // bytes fetched over the buffer's, per LOD. const Bound bounds[] = { - { "box", { 2.0000, 2.0000, 1.1250, 0.8889 }, { 1.2000, 1.2000, 1.0667, 1.0000 } }, - { "cylinder", { 1.2500, 1.1667, 1.0889, 1.0625 }, { 1.0133, 1.0286, 1.0146, 1.0211 } }, - { "sphere", { 0.7778, 0.6944, 0.6429, 1.0833 }, { 1.0286, 1.0240, 1.0000, 1.0043 } }, + { "box", { 2.0000, 2.0000, 2.0000, 2.0000 }, { 1.2000, 1.2000, 1.2000, 1.2000 } }, + { "cylinder", { 1.2500, 1.1667, 1.1000, 1.0833 }, { 1.0133, 1.0286, 1.0182, 1.0353 } }, + { "sphere", { 1.0833, 0.8889, 0.7000, 1.0909 }, { 1.0286, 1.0240, 1.0000, 1.0043 } }, { "torus", { 0.6806, 1.1111, 1.0667, 1.0417 }, { 1.0122, 1.0080, 1.0000, 1.0010 } }, { "sculpt sphere", { 0.6806, 1.1250, 1.0625, 1.0312 }, { 1.0122, 1.0074, 1.0021, 1.0006 } }, { "sculpt plane", { 0.6806, 1.1250, 1.0625, 1.0312 }, { 1.0122, 1.0074, 1.0021, 1.0006 } }, @@ -3529,4 +3585,439 @@ namespace INFO(report.text()); CHECK(report.failures == 0); } + + // ------------------------------------------------------------------ + // Vertex and triangle counts + + struct Counts + { + S32 vertices = 0; + S32 triangles = 0; + + bool operator==(const Counts& other) const { return vertices == other.vertices && triangles == other.triangles; } + }; + + Counts countsOf(const LLVolume& volume) + { + Counts counts; + counts.triangles = volume.getNumTriangles(&counts.vertices); + return counts; + } + + struct ExpectedCounts + { + Shape shape; + Counts lods[LOD_COUNT]; + }; + + void checkCounts(const ExpectedCounts& expected) + { + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + LLPointer volume = build(expected.shape, lod); + const Counts counts = countsOf(*volume); + INFO(fmt::format("{}: {} vertices and {} triangles, where {} and {} are expected", lodName(expected.shape, lod), counts.vertices, + counts.triangles, expected.lods[lod].vertices, expected.lods[lod].triangles)); + CHECK(counts == expected.lods[lod]); + } + } + + // A straight path that neither twists nor tapers sweeps every profile + // edge into a parallelogram, which two triangles map exactly: a flat + // side is one quad, an uncut box's cap is one, and the path has its two + // ends alone. A path that twists or tapers, a circle and a flexible path + // keep the points they lay out. + TEST_CASE("llvolume: a straight path that neither twists nor tapers is one quad a profile edge", "[llvolume]") + { + Shape twisted = box(); + twisted.name = "twisted box"; + twisted.twist_begin = -0.5f; + twisted.twist_end = 0.5f; + Shape tapered = cylinder(); + tapered.name = "tapered cylinder"; + tapered.scale_x = tapered.scale_y = 0.5f; + const ExpectedCounts table[] = { + { box(), { { 24, 12 }, { 24, 12 }, { 24, 12 }, { 24, 12 } } }, + { cylinder(), { { 30, 24 }, { 42, 36 }, { 66, 60 }, { 102, 96 } } }, + { twisted, { { 32, 20 }, { 48, 36 }, { 126, 144 }, { 256, 348 } } }, + { tapered, { { 30, 24 }, { 42, 36 }, { 82, 90 }, { 152, 192 } } }, + { torus(), { { 49, 72 }, { 100, 162 }, { 256, 450 }, { 625, 1152 } } }, + }; + for (const ExpectedCounts& expected : table) + { + checkCounts(expected); + } + + Report report; + for (const Shape& s : sweepShapes()) + { + if (s.path != LL_PCODE_PATH_LINE || s.twist_begin != s.twist_end || s.scale_x != 1.f || s.scale_y != 1.f) + { + continue; + } + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + LLPointer volume = build(s, lod); + const std::string name = lodName(s, lod); + if (volume->getPath().getPathLength() != 2) + { + report.fail(fmt::format("{}: {} path points", name, volume->getPath().getPathLength())); + continue; + } + // A hole drawn in to its wall is not straight between its + // corners, and keeps its split points; its outer sides + // keep theirs with it. + const bool drawn_in = volume->getProfile().holeTouchesWall(); + for (S32 i = 0; i < volume->getNumVolumeFaces(); ++i) + { + const LLVolumeFace& face = volume->getVolumeFace(i); + const LLProfile::Face& outline = volume->getProfile().mFaces[i]; + const bool flat_outer = (face.mTypeMask & LLVolumeFace::OUTER_MASK) && (face.mTypeMask & LLVolumeFace::FLAT_MASK); + if (flat_outer && !drawn_in && outline.mCount != 2) + { + report.fail(fmt::format("{}: {} has {} profile points", name, faceName(face), outline.mCount)); + } + if (isCap(face) && !(face.mTypeMask & (LLVolumeFace::HOLLOW_MASK | LLVolumeFace::OPEN_MASK)) && s.profile == LL_PCODE_PROFILE_SQUARE && + s.path_begin == 0.f && s.path_end == 1.f && face.mNumVertices != 4) + { + report.fail(fmt::format("{}: {} has {} vertices", name, faceName(face), face.mNumVertices)); + } + } + } + } + INFO(report.text()); + CHECK(report.failures == 0); + + // A square hole in a cylinder: its corners alone where it stays clear + // of the wall, and every split point where it is drawn in to it. + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + const S32 split = (S32)(LOD_DETAIL[lod] * 0.66f); + for (F32 hollow : { 0.5f, 0.85f }) + { + Shape s = cylinder(); + s.hole = LL_PCODE_HOLE_SQUARE; + s.hollow = hollow; + s.name = fmt::format("cylinder with a square hole at {}", hollow); + LLPointer volume = build(s, lod); + const LLProfile& outline = volume->getProfile(); + const S32 hole = outline.getTotal() - outline.getTotalOut(); + INFO(fmt::format("{}: {} hole points", lodName(s, lod), hole)); + CHECK(outline.holeTouchesWall() == (hollow > 0.75f)); + CHECK(hole == (outline.holeTouchesWall() ? 4 * (split + 1) + 1 : 5)); + CHECK(volume->getPath().getPathLength() == 2); + } + } + } + + + // A flat inner side's columns, against its hole's points: a column for + // each, and a copy after each corner, a point where the hole turns, or + // after every point between the ends where the hole is drawn in to its + // wall; no copy of an end or of a split point; and no triangle in the + // quad between a corner and its copy. + void checkCorners(const LLVolume& volume, const std::string& name, Report& report) + { + const LLProfile& outline = volume.getProfile(); + for (S32 i = 0; i < volume.getNumVolumeFaces(); ++i) + { + const LLVolumeFace& face = volume.getVolumeFace(i); + if (!isDoubled(face) || !isGrid(face)) + { + continue; + } + const std::string where = fmt::format("{}: {}", name, faceName(face)); + const S32 points = outline.mFaces[i].mCount; + const std::vector strips = stripsOf(face); + std::vector copies(points, 0); + S32 point = 0; + for (S32 s = 1; s < face.mNumS; ++s) + { + if (strips[s] != strips[s - 1]) + { + ++copies[point]; + } + else + { + ++point; + } + } + if (point != points - 1) + { + report.fail(fmt::format("{}: {} columns for {} points", where, face.mNumS, points)); + continue; + } + for (S32 k = 0; k < points; ++k) + { + bool corner = false; + if (k > 0 && k + 1 < points) + { + const LLVector4a& a = outline.mProfile[face.mBeginS + k - 1]; + const LLVector4a& b = outline.mProfile[face.mBeginS + k]; + const LLVector4a& c = outline.mProfile[face.mBeginS + k + 1]; + const F32 e1x = b[0] - a[0], e1y = b[1] - a[1], e2x = c[0] - b[0], e2y = c[1] - b[1]; + corner = fabsf(e1x * e2y - e1y * e2x) > 1e-4f * sqrtf((e1x * e1x + e1y * e1y) * (e2x * e2x + e2y * e2y)) || outline.holeTouchesWall(); + } + if (copies[k] != (corner ? 1 : 0)) + { + report.fail(fmt::format("{}: point {} of {}, {}, has {} copies", where, k, points, corner ? "a corner" : "no corner", copies[k])); + break; + } + } + for (S32 t = 0; t + 2 < face.mNumIndices; t += 3) + { + S32 low = face.mNumS, high = 0; + for (S32 k = 0; k < 3; ++k) + { + low = llmin(low, (S32)(face.mIndices[t + k] % face.mNumS)); + high = llmax(high, (S32)(face.mIndices[t + k] % face.mNumS)); + } + if (strips[low] != strips[high]) + { + report.fail(fmt::format("{}: triangle {} spans a corner and its copy", where, t / 3)); + break; + } + } + } + } + + // A flat inner side copies its hole's corners, where its normals turn, + // and no point between them, so that it is smooth from corner to corner + // as an outer side is; it has no triangles between a corner and its + // copy. A hollow box's inner side is one quad a side of its hole. + TEST_CASE("llvolume: a flat inner side copies its corners and nothing else", "[llvolume]") + { + Shape hollow_box = box(); + hollow_box.name = "hollow box"; + hollow_box.hole = LL_PCODE_HOLE_SQUARE; + hollow_box.hollow = 0.5f; + Shape hollow_prism = prism(); + hollow_prism.name = "hollow prism"; + hollow_prism.hollow = 0.5f; + Shape twisted = hollow_box; + twisted.name = "twisted hollow box"; + twisted.twist_begin = -0.5f; + twisted.twist_end = 0.5f; + Shape tube_hole = tube(); + tube_hole.name = "tube with a square hole"; + tube_hole.hole = LL_PCODE_HOLE_SQUARE; + tube_hole.hollow = 0.5f; + checkCounts({ hollow_box, { { 52, 32 }, { 52, 32 }, { 52, 32 }, { 52, 32 } } }); + checkCounts({ hollow_prism, { { 40, 24 }, { 40, 24 }, { 40, 24 }, { 40, 24 } } }); + checkCounts({ twisted, { { 68, 48 }, { 100, 80 }, { 252, 288 }, { 500, 672 } } }); + checkCounts({ tube_hole, { { 112, 96 }, { 160, 144 }, { 384, 480 }, { 800, 1152 } } }); + + // A flexible hollow box, as a frame lays it out: its path's sections + // keep their split points, and its inner side copies only corners. + struct Flexi + { + S32 sections; + S32 lod; + Counts counts; + }; + for (const Flexi& expected : { Flexi{ 4, 1, { 100, 80 } }, Flexi{ 4, 3, { 212, 240 } }, Flexi{ 16, 1, { 292, 272 } }, Flexi{ 16, 3, { 596, 816 } } }) + { + LLPointer volume = flexiVolume(LL_PCODE_PROFILE_SQUARE, LL_PCODE_HOLE_SQUARE, 0.5f, expected.lod); + bendPath(*volume, expected.sections, 1.1f); + volume->regen(); + const Counts counts = countsOf(*volume); + INFO(fmt::format("flexible hollow box, {} sections, LOD {}: {} vertices and {} triangles, where {} and {} are expected", expected.sections, + expected.lod, counts.vertices, counts.triangles, expected.counts.vertices, expected.counts.triangles)); + CHECK(counts == expected.counts); + } + + Report report; + S32 volumes = 0; + for (const Shape& s : sweepShapes()) + { + if (s.hole != LL_PCODE_HOLE_SQUARE && s.hole != LL_PCODE_HOLE_TRIANGLE && !(s.hole == LL_PCODE_HOLE_SAME && s.profile != LL_PCODE_PROFILE_CIRCLE && + s.profile != LL_PCODE_PROFILE_CIRCLE_HALF)) + { + continue; + } + if (s.hollow <= 0.f) + { + continue; + } + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + LLPointer volume = build(s, lod); + checkCorners(*volume, lodName(s, lod), report); + ++volumes; + } + } + for (S32 sections : { 4, 16 }) + { + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + LLPointer volume = flexiVolume(LL_PCODE_PROFILE_SQUARE, LL_PCODE_HOLE_SQUARE, 0.5f, lod); + bendPath(*volume, sections, 1.1f); + volume->regen(); + checkCorners(*volume, fmt::format("flexible hollow box, {} sections, LOD {}", sections, lod), report); + } + } + // A hollow sphere's flat inner side runs from pole to pole, a cone + // at each: both its ends take the normals the strips they end give + // them, not the axis. + for (U8 hole : { LL_PCODE_HOLE_SQUARE, LL_PCODE_HOLE_TRIANGLE }) + { + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + Shape s = sphere(); + s.hole = hole; + s.hollow = 0.75f; + s.name = hole == LL_PCODE_HOLE_SQUARE ? "sphere with a square hole" : "sphere with a triangle hole"; + LLPointer volume = build(s, lod); + for (S32 i = 0; i < volume->getNumVolumeFaces(); ++i) + { + const LLVolumeFace& face = volume->getVolumeFace(i); + if (!isDoubled(face) || !isGrid(face)) + { + continue; + } + for (S32 column : { 0, face.mNumS - 1 }) + { + for (S32 t = 0; t < face.mNumT; ++t) + { + const LLVector3 n = v3(face.mNormals[gridIndex(face, column, t)]); + if (degreesBetween(n, LLVector3(1.f, 0.f, 0.f)) < 1.f || degreesBetween(n, LLVector3(-1.f, 0.f, 0.f)) < 1.f) + { + report.fail(fmt::format("{}: {} column {} row {} takes the axis", lodName(s, lod), faceName(face), column, t)); + break; + } + } + } + } + } + } + INFO(fmt::format("{} volumes", volumes)); + INFO(report.text()); + CHECK(volumes > 0); + CHECK(report.failures == 0); + } + + + // A shared volume's faces against its unique twin's: the same vertices, + // and the same triangles but for some of no area, which it leaves out. + // Returns how many it left out. + S32 checkAgainstUnique(const Shape& s, S32 lod, Report& report) + { + LLPointer shared = build(s, lod); + LLPointer unique = build(s, lod, true); + const std::string name = lodName(s, lod); + if (shared->getNumVolumeFaces() != unique->getNumVolumeFaces()) + { + report.fail(fmt::format("{}: {} faces against {}", name, shared->getNumVolumeFaces(), unique->getNumVolumeFaces())); + return 0; + } + S32 left_out = 0; + for (S32 i = 0; i < shared->getNumVolumeFaces(); ++i) + { + const LLVolumeFace& a = shared->getVolumeFace(i); + const LLVolumeFace& b = unique->getVolumeFace(i); + const std::string where = fmt::format("{}: {}", name, faceName(a)); + if (a.mNumVertices != b.mNumVertices || std::memcmp(a.mPositions, b.mPositions, sizeof(LLVector4a) * a.mNumVertices) != 0 || + std::memcmp(a.mTexCoords, b.mTexCoords, sizeof(LLVector2) * a.mNumVertices) != 0) + { + report.fail(fmt::format("{}: its vertices are not its unique twin's", where)); + continue; + } + std::map, S32> kept; + for (S32 k = 0; k + 2 < a.mNumIndices; k += 3) + { + ++kept[{ a.mIndices[k], a.mIndices[k + 1], a.mIndices[k + 2] }]; + } + const std::vector triangles = trianglesOf(b); + for (const Triangle& tri : triangles) + { + const auto found = kept.find({ (U16)tri.v[0], (U16)tri.v[1], (U16)tri.v[2] }); + if (found != kept.end() && found->second > 0) + { + --found->second; + continue; + } + ++left_out; + if (!tri.degenerate) + { + report.fail(fmt::format("{}: triangle ({}, {}, {}), which has area, is left out", where, tri.v[0], tri.v[1], tri.v[2])); + return left_out; + } + } + // Every triangle it kept is one of the twin's. + for (const auto& [corners, count] : kept) + { + if (count != 0) + { + report.fail(fmt::format("{}: triangle ({}, {}, {}) is not its unique twin's", where, corners[0], corners[1], corners[2])); + return left_out; + } + } + } + return left_out; + } + + // On a shared volume, a row or column its parameters take to one point, + // the apex of a taper to a point or a sphere's pole, has no triangles: + // those with two corners there have no area. It keeps its vertices, + // each with its own UV and normal. The parameters decide it, so a + // unique volume, whose path a rebuild may move, keeps every triangle, + // and so does a sphere whose twist turns its poles off the axis. + TEST_CASE("llvolume: a shared volume has no triangles where its parameters collapse a row or column", "[llvolume]") + { + Shape pyramid = box(); + pyramid.name = "pyramid"; + pyramid.scale_x = pyramid.scale_y = 0.f; + Shape inverted = box(); + inverted.name = "box tapered to a point at its base"; + inverted.scale_x = inverted.scale_y = 2.f; + Shape cone = cylinder(); + cone.name = "cone"; + cone.scale_x = cone.scale_y = 0.f; + Shape half_turned = sphere(); + half_turned.name = "sphere turned half a turn"; + half_turned.twist_begin = half_turned.twist_end = 0.5f; + checkCounts({ sphere(), { { 28, 24 }, { 50, 54 }, { 128, 180 }, { 325, 528 } } }); + checkCounts({ half_turned, { { 28, 24 }, { 50, 54 }, { 128, 180 }, { 325, 528 } } }); + checkCounts({ pyramid, { { 24, 8 }, { 24, 8 }, { 24, 8 }, { 24, 8 } } }); + checkCounts({ inverted, { { 24, 8 }, { 24, 8 }, { 24, 8 }, { 24, 8 } } }); + checkCounts({ cone, { { 30, 18 }, { 42, 27 }, { 82, 75 }, { 152, 168 } } }); + + Report report; + Shape twisted = sphere(); + twisted.name = "twisted sphere"; + twisted.twist_begin = -0.5f; + twisted.twist_end = 0.75f; + Shape quarter_turned = sphere(); + quarter_turned.name = "sphere turned a quarter turn"; + quarter_turned.twist_begin = quarter_turned.twist_end = 0.25f; + for (const Shape& s : { twisted, quarter_turned }) + { + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + if (checkAgainstUnique(s, lod, report) != 0) + { + report.fail(fmt::format("{}: triangles left out where the poles leave the axis", lodName(s, lod))); + } + } + } + S32 left_out = 0; + for (const Shape& s : sweepShapes()) + { + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + left_out += checkAgainstUnique(s, lod, report); + } + } + for (const Shape& s : { pyramid, inverted, cone, half_turned }) + { + for (S32 lod = 0; lod < LOD_COUNT; ++lod) + { + left_out += checkAgainstUnique(s, lod, report); + } + } + INFO(fmt::format("{} triangles left out", left_out)); + INFO(report.text()); + CHECK(left_out > 0); + CHECK(report.failures == 0); + } }