DirectX::BoundingOrientedBox GetBoundsInOrientedSpace(_In_ bool findTightestBounds, _In_ function<bool(XMFLOAT3*)> vertGenerator) { // we find tight bounds by // 1. find the convex hull // 2. rotating calipers to find the ideal bounding box - http://en.wikipedia.org/wiki/Rotating_calipers // The idea behind rotating calipers is that we keep track of some extreme vertices, and slowly rotate our coordinate frame. // As we rotate, a vertex may no-longer be extreme in the new rotated coordinate frame, so we increment the index to the next vertex // in the convex hull that is now extreme. float zmin = FLT_MAX, zmax = -FLT_MAX; auto convexHull = FindConvexHull([&](XMFLOAT2 *planarVert, UINT32 *index) -> bool { *index = 0; // we dont' care about the index here - only useful when exposing the convex hull directly XMFLOAT3 vert; bool ret = vertGenerator(&vert); if (ret) { if (vert.z < zmin) { zmin = vert.z; } if (vert.z > zmax) { zmax = vert.z; } } *planarVert = { vert.x, vert.y }; return ret; }); // first we need to set up the calipers - extreme vertices that we will incrementally update as we rotate XMFLOAT2 maxv = convexHull[0].first; XMFLOAT2 minv = convexHull[0].first; struct RotatedBoundingBox { UINT32 maxx, maxy, minx, miny; // these represent the indices of the max/min x and y coordinates in a rotated coordated frame. float area; float minwidth; float angle; }; // find the initial orientation's bounds: RotatedBoundingBox best = { 0, 0, 0, 0, FLT_MAX, FLT_MAX, XM_2PI }; for (UINT32 i = 1; i < convexHull.size(); ++i) { const auto vertex = convexHull[i].first; if (vertex.x > maxv.x) { maxv.x = vertex.x; best.maxx = i; } else if (vertex.x < minv.x) { minv.x = vertex.x; best.minx = i; } if (vertex.y > maxv.y) { maxv.y = vertex.y; best.maxy = i; } else if (vertex.y < minv.y) { minv.y = vertex.y; best.miny = i; } } best.angle = 0; best.area = (maxv.x - minv.x) * (maxv.y - minv.y); best.minwidth = min(maxv.x - minv.x, maxv.y - minv.y); ASSERT(best.minx != best.maxx); // xmin and xmax indices should never be the same ASSERT(best.miny != best.maxy); ASSERT(best.minx <= best.maxy); // our vertices should be located around the convex hull xmin->ymax->xmax->ymin ASSERT(best.maxy <= best.maxx); ASSERT(best.maxx <= best.miny || best.minx == best.miny); ASSERT(best.miny <= best.minx + convexHull.size()); ASSERT(best.minx <= convexHull.size()); // all of the indices should be in the convex hull ASSERT(best.maxx <= convexHull.size()); ASSERT(best.miny <= convexHull.size()); ASSERT(best.maxy <= convexHull.size()); ASSERT(best.minx == 0); // we expect minx to be the first vertex in the convex hull // Helper to calculate the rotation if we move from the given vertex to the next vertex in the convex hull auto getDeltaVectorForIndex = [&](UINT32 vert) { // return the delta between the given vertex and the subsequent vertex, so we can determine the angle our bounding box // would have to rotate to be parallel with this edge auto start = convexHull[vert % convexHull.size()].first; auto next = convexHull[(vert + 1) % convexHull.size()].first; ASSERT(start.x != next.x || start.y != next.y); return XMFLOAT2({ next.x - start.x, next.y - start.y }); }; // once we have the extreme vertices, we slowly rotate our coordinate system and adjust them // we rotate in such a way that only one extreme vertex changes at a time float angle = 0; RotatedBoundingBox current = best; BoundingOrientedBox bestBoxInPlaneSpace; bestBoxInPlaneSpace.Center = { (maxv.x + minv.x) / 2, (maxv.y + minv.y) / 2, (zmax + zmin) / 2 }; bestBoxInPlaneSpace.Extents = { (maxv.x - minv.x) / 2, (maxv.y - minv.y) / 2, (zmax - zmin) / 2 }; bestBoxInPlaneSpace.Orientation = { 0, 0, 0, 1 }; if (findTightestBounds) { RotatedBoundingBox initial = best; // The tightest bounding box will share a side with the convex hull. We start with a candidate bounding box oriented along // the x/y axes and iterate through all orientations where the box is aligned with an edge of the convex hull. The maximum possible rotations // we need to consider is convexHull.size(), which would be a rotation of 90 degrees. // Each iteration through the loop, we pick the vertex from our extreme vertices that has the smallest incremental rotation along its outgoing edge. // A neat trick is that the other extreme vertices remain extreme in the new rotated orientation. while (angle <= XM_PIDIV2 + ROTATING_CALIPERS_EPSILON && current.minx <= initial.maxy && current.maxy <= initial.maxx && current.maxx <= initial.miny && current.miny <= convexHull.size()) { const auto vectForXmin = getDeltaVectorForIndex(current.minx); const auto vectForXmax = getDeltaVectorForIndex(current.maxx); const auto vectForYmin = getDeltaVectorForIndex(current.miny); const auto vectForYmax = getDeltaVectorForIndex(current.maxy); UINT32* boundIndices[4] = { ¤t.minx, ¤t.maxx, ¤t.miny, ¤t.maxy }; float angles[4] = { atan2(vectForXmin.x, vectForXmin.y), atan2(-vectForXmax.x, -vectForXmax.y), atan2(vectForYmin.y, -vectForYmin.x), atan2(-vectForYmax.y, vectForYmax.x) }; int index = 0; float minAngle = XM_PI * 4 + angle; for (int i = 0; i < 4; ++i) { if (angles[i] > -ROTATING_CALIPERS_EPSILON && angles[i] < 0) { // the vector between vertices are horizontal/vertical, so angle is close to 0, treat it as zero // this can only occur with a rounding error angles[i] = 0; } else if (angles[i] < 0) { angles[i] += XM_PI * 2; } if (angles[i] < minAngle) { minAngle = angles[i]; index = i; } } *(boundIndices[index]) = ((*(boundIndices[index])) + 1); ASSERT(current.minx <= current.maxy); // we should remain ordering of vertices xmin->ymax->xmax->ymin as we rotate ASSERT(current.maxy <= current.maxx); ASSERT(current.maxx <= current.miny || best.minx == best.miny); ASSERT(current.miny <= current.minx + convexHull.size()); ASSERT(current.minx != current.maxx); // and we shouldn't ever have min and max indices equal ASSERT(current.miny != current.maxy); // now update our box: angle = minAngle; current.angle = minAngle; if (angle < XM_PIDIV2 + ROTATING_CALIPERS_EPSILON) { XMVECTOR vertsInRotatedPlaneSpace[4]; const XMMATRIX rotationTransform = XMMatrixRotationZ(angle); for (int i = 0; i < 4; ++i) { vertsInRotatedPlaneSpace[i] = XMVector3TransformCoord(XMLoadFloat2(&convexHull[(*(boundIndices[i])) % convexHull.size()].first), rotationTransform); } BoundingOrientedBox xmBoundsInPlaneSpace; xmBoundsInPlaneSpace.Center = { XMVectorGetX(vertsInRotatedPlaneSpace[0] + vertsInRotatedPlaneSpace[1]) / 2, XMVectorGetY(vertsInRotatedPlaneSpace[2] + vertsInRotatedPlaneSpace[3]) / 2, (zmax + zmin) / 2 }; xmBoundsInPlaneSpace.Extents = { XMVectorGetX(vertsInRotatedPlaneSpace[1] - vertsInRotatedPlaneSpace[0]) / 2, XMVectorGetY(vertsInRotatedPlaneSpace[3] - vertsInRotatedPlaneSpace[2]) / 2, (zmax - zmin) / 2 }; xmBoundsInPlaneSpace.Orientation = { 0, 0, 0, 1 }; xmBoundsInPlaneSpace.Transform(xmBoundsInPlaneSpace, XMMatrixTranspose(rotationTransform)); // rotate back to plane space from rotated plane space const XMFLOAT2 size = { xmBoundsInPlaneSpace.Extents.x * 2, xmBoundsInPlaneSpace.Extents.y * 2 }; current.area = size.x * size.y; current.minwidth = min(size.x, size.y); if (current.area < best.area || (current.area == best.area && current.minwidth < best.minwidth)) { best = current; bestBoxInPlaneSpace = xmBoundsInPlaneSpace; } } } } return bestBoxInPlaneSpace; }
// recursively render meshes for all nodes // lambda-free version bool CompoundMesh::render( ID3D11DeviceContext *deviceContext, VanillaShaderClass *shader, DirectX::CXMMATRIX worldMatrix, DirectX::CXMMATRIX viewMatrix, DirectX::CXMMATRIX projectionMatrix, std::vector<Light> &lights, bool orthoProjection /*= false*/, double animationTick /*= 1.0*/, CompoundMeshNode *node /*= nullptr*/, DirectX::CXMMATRIX parentNodeTransform /*= XMMatrixIdentity()*/ ) { if (!node) { // top-level invocation; do some initialization and culling node = m_root.get(); if (m_animation.loaded()) { double ticksPerSec = m_aiScene->mAnimations[0]->mTicksPerSecond; if (ticksPerSec <= 0) ticksPerSec = 24; animationTick *= ticksPerSec; animationTick += 1; // time starts at 0 but ticks, alas, at 1? while (animationTick > m_animation.maxTick) animationTick -= m_animation.maxTick; updateNodeTransforms(animationTick); } // clip to view frustum BoundingSphere bSphere(m_bSphere); bSphere.Transform(bSphere, worldMatrix); //BoundingOrientedBox bBox; //m_bBox.Transform(bBox, worldMatrix); if (!orthoProjection) { BoundingFrustum frustum(projectionMatrix); // TODO this should be moved somewhere higher up if the engine ever becomes CPU-bound XMStoreFloat4(&frustum.Orientation, XMVectorSet(0, 0, 0, 1)); // identity quaternion frustum.Transform(frustum, XMMatrixInverse(nullptr, viewMatrix)); // move the frustum as though it was an object within the world; probably slow? if (!DirectX::Internal::XMQuaternionIsUnit(XMLoadFloat4(&frustum.Orientation))) { cerr << "Bad frustum quaternion! :( " << XMLoadFloat4(&frustum.Orientation) << endl; XMStoreFloat4(&frustum.Orientation, XMVectorSet(0, 0, 0, 1)); } try { if (!frustum.Intersects(bSphere)) return true; } catch (exception *e) { cerr << e->what() << endl; } } else { // make a bounding box from the orthographic projection matrix (is this weird? perhaps it's weird.) XMMATRIX unproj = XMMatrixInverse(nullptr, projectionMatrix); float x = unproj.r[0].m128_f32[0]; // half the width; for now, assuming projection centered at origin float y = unproj.r[1].m128_f32[1]; // half the height float near_plane = unproj.r[3].m128_f32[2]; float far_plane = unproj.r[2].m128_f32[2] + near_plane; XMFLOAT3 points[2]; points[0].x = -x; points[0].y = -y; points[0].z = near_plane; points[1].x = x; points[1].y = y; points[1].z = max(far_plane,10000); // /fp:fast workaround ಠ_ಠ //points[1].z = far_plane; // XXX XXX XXX Something about this code fails with /fp:fast XXX XXX XXX // one workaround is to set far_plane to 10000; we're probably not culling based on distance anyway so no harm done, at least? // another is, of course, to set /fp:precise ... this costs several FPS on the i7 laptop with the double-torus test scene! BoundingOrientedBox viewBox; viewBox.CreateFromPoints(viewBox, 2, points, sizeof(XMFLOAT3)); XMStoreFloat4(&viewBox.Orientation, XMVectorSet(0, 0, 0, 1)); // identity quaternion... strange that Orientation would be uninitialized though viewBox.Transform(viewBox, XMMatrixInverse(nullptr, viewMatrix)); if (!DirectX::Internal::XMQuaternionIsUnit(XMLoadFloat4(&viewBox.Orientation))) { cerr << "Bad viewBox quaternion! :( " << XMLoadFloat4(&viewBox.Orientation) << endl; XMStoreFloat4(&viewBox.Orientation, XMVectorSet(0, 0, 0, 1)); } // clip to viewbox try { if (!viewBox.Intersects(bSphere)) return true; } catch (exception *e) { cerr << e->what() << endl; } } // tell the vertex shader about the wonderful animation buffer we have for it: if (m_animation.loaded()) { m_animation.updateCurrentBoneKeys(deviceContext, animationTick); } } for (auto mesh = node->meshes.begin(), end = node->meshes.end(); mesh != end; ++mesh) { if (mesh->m_indexBuffer == nullptr || mesh->m_vertexBuffer == nullptr || !mesh->getIndexCount()) { cerr << "strange, empty mesh"; continue; } mesh->setBuffers(deviceContext); // point the GPU at the right geometry data SimpleMesh::Material &mat = mesh->m_material; bool useNormalMap = mat.normalMap.getTexture() ? true : false; bool useSpecularMap = mat.specularMap.getTexture() ? true : false; if (!shader->SetPSMaterial(deviceContext, mat.ambient, mat.diffuse, mat.shininess, mat.specular, useNormalMap, useSpecularMap)) { return false; } XMFLOAT4X4 *offsetMatrix = nullptr; if (m_animation.loaded()) { m_animation.updateBoneTransforms(deviceContext, animationTick, mesh->m_OffsetMatrix, mesh->m_name, [&](std::string nodeName) { return getNodeGlobalTransform(nodeName); } ); } XMMATRIX finalWorldMatrix; if (!m_animation.loaded() && node->name.size() == 0) { finalWorldMatrix = worldMatrix; } else { finalWorldMatrix = XMMatrixMultiply(getNodeGlobalTransform(node->name), worldMatrix); } if (!shader->Render(deviceContext, mesh->getIndexCount(), finalWorldMatrix, viewMatrix, projectionMatrix, mesh->m_material.normalMap.getTexture(), mesh->m_material.specularMap.getTexture(), &lights, mesh->m_material.diffuseTexture.getTexture(), 1, true, m_animation.loaded() ? animationTick : -1)) { return false; } } for (auto i : node->children) { if (!render(deviceContext, shader, worldMatrix, viewMatrix, projectionMatrix, lights, orthoProjection, animationTick, i.get(), XMMatrixIdentity())) return false; } return true; }