|
@@ -26,6 +26,7 @@
|
|
|
#include "modelAPI.h"
|
|
#include "modelAPI.h"
|
|
|
#include "imageAPI.h"
|
|
#include "imageAPI.h"
|
|
|
#include "drawAPI.h"
|
|
#include "drawAPI.h"
|
|
|
|
|
+// TODO: Inline as much as possible from Model.h to modelAPI.cpp, to reduce call depth and make it easy to copy and modify the model implementation.
|
|
|
#include "../implementation/render/model/Model.h"
|
|
#include "../implementation/render/model/Model.h"
|
|
|
#include "../base/virtualStack.h"
|
|
#include "../base/virtualStack.h"
|
|
|
#include <limits>
|
|
#include <limits>
|
|
@@ -195,12 +196,12 @@ void model_setLightMapByName(Model& model, int partIndex, ResourcePool &pool, co
|
|
|
// Single-threaded rendering for the simple cases where you just want it to work
|
|
// Single-threaded rendering for the simple cases where you just want it to work
|
|
|
void model_render(const Model& model, const Transform3D &modelToWorldTransform, ImageRgbaU8& colorBuffer, ImageF32& depthBuffer, const Camera &camera) {
|
|
void model_render(const Model& model, const Transform3D &modelToWorldTransform, ImageRgbaU8& colorBuffer, ImageF32& depthBuffer, const Camera &camera) {
|
|
|
if (model.isNotNull()) {
|
|
if (model.isNotNull()) {
|
|
|
- model->render((CommandQueue*)nullptr, image_exists(colorBuffer) ? &colorBuffer : nullptr, image_exists(depthBuffer) ? &depthBuffer : nullptr, modelToWorldTransform, camera);
|
|
|
|
|
|
|
+ model->render((CommandQueue*)nullptr, colorBuffer, depthBuffer, modelToWorldTransform, camera);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
void model_renderDepth(const Model& model, const Transform3D &modelToWorldTransform, ImageF32& depthBuffer, const Camera &camera) {
|
|
void model_renderDepth(const Model& model, const Transform3D &modelToWorldTransform, ImageF32& depthBuffer, const Camera &camera) {
|
|
|
if (model.isNotNull()) {
|
|
if (model.isNotNull()) {
|
|
|
- model->renderDepth(image_exists(depthBuffer) ? &depthBuffer : nullptr, modelToWorldTransform, camera);
|
|
|
|
|
|
|
+ model->renderDepth(depthBuffer, modelToWorldTransform, camera);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -210,533 +211,73 @@ void model_getBoundingBox(const Model& model, FVector3D& minimum, FVector3D& max
|
|
|
maximum = model->maxBound;
|
|
maximum = model->maxBound;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-static const int cellSize = 16;
|
|
|
|
|
-
|
|
|
|
|
-struct DebugLine {
|
|
|
|
|
- int64_t x1, y1, x2, y2;
|
|
|
|
|
- ColorRgbaI32 color;
|
|
|
|
|
- DebugLine(int64_t x1, int64_t y1, int64_t x2, int64_t y2, const ColorRgbaI32& color)
|
|
|
|
|
- : x1(x1), y1(y1), x2(x2), y2(y2), color(color) {}
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// Context for multi-threaded rendering of triangles in a command queue
|
|
|
|
|
-struct RendererImpl {
|
|
|
|
|
- bool receiving = false; // Preventing version dependency by only allowing calls in the expected order
|
|
|
|
|
- ImageRgbaU8 colorBuffer; // The color image being rendered to
|
|
|
|
|
- ImageF32 depthBuffer; // Linear depth for isometric cameras, 1 / depth for perspective cameras
|
|
|
|
|
- ImageF32 depthGrid; // An occlusion grid of cellSize² cells representing the longest linear depth where something might be visible
|
|
|
|
|
- CommandQueue commandQueue; // Triangles to be drawn
|
|
|
|
|
- List<DebugLine> debugLines; // Additional lines to be drawn as an overlay for debugging occlusion
|
|
|
|
|
- int width = 0, height = 0, gridWidth = 0, gridHeight = 0;
|
|
|
|
|
- bool occluded = false;
|
|
|
|
|
- RendererImpl() {}
|
|
|
|
|
- void beginFrame(ImageRgbaU8& colorBuffer, ImageF32& depthBuffer) {
|
|
|
|
|
- if (this->receiving) {
|
|
|
|
|
- throwError("Called renderer_begin on the same renderer twice without ending the previous batch!\n");
|
|
|
|
|
- }
|
|
|
|
|
- this->receiving = true;
|
|
|
|
|
- this->colorBuffer = colorBuffer;
|
|
|
|
|
- this->depthBuffer = depthBuffer;
|
|
|
|
|
- if (image_exists(this->colorBuffer)) {
|
|
|
|
|
- this->width = image_getWidth(this->colorBuffer);
|
|
|
|
|
- this->height = image_getHeight(this->colorBuffer);
|
|
|
|
|
- } else if (image_exists(this->depthBuffer)) {
|
|
|
|
|
- this->width = image_getWidth(this->depthBuffer);
|
|
|
|
|
- this->height = image_getHeight(this->depthBuffer);
|
|
|
|
|
- }
|
|
|
|
|
- this->gridWidth = (this->width + (cellSize - 1)) / cellSize;
|
|
|
|
|
- this->gridHeight = (this->height + (cellSize - 1)) / cellSize;
|
|
|
|
|
- this->occluded = false;
|
|
|
|
|
- }
|
|
|
|
|
- bool pointInsideOfEdge(const LVector2D &edgeA, const LVector2D &edgeB, const LVector2D &point) {
|
|
|
|
|
- LVector2D edgeDirection = LVector2D(edgeB.y - edgeA.y, edgeA.x - edgeB.x);
|
|
|
|
|
- LVector2D relativePosition = point - edgeA;
|
|
|
|
|
- return (edgeDirection.x * relativePosition.x) + (edgeDirection.y * relativePosition.y) <= 0;
|
|
|
|
|
- }
|
|
|
|
|
- // Returns true iff the point is inside of the hull
|
|
|
|
|
- // convexHullCorners from 0 to cornerCount-1 must be sorted clockwise and may not include any concave corners
|
|
|
|
|
- bool pointInsideOfHull(const ProjectedPoint* convexHullCorners, int cornerCount, const LVector2D &point) {
|
|
|
|
|
- for (int c = 0; c < cornerCount; c++) {
|
|
|
|
|
- int nc = c + 1;
|
|
|
|
|
- if (nc == cornerCount) {
|
|
|
|
|
- nc = 0;
|
|
|
|
|
- }
|
|
|
|
|
- if (!pointInsideOfEdge(convexHullCorners[c].flat, convexHullCorners[nc].flat, point)) {
|
|
|
|
|
- // Outside of one edge, not inside
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- // Passed all edge tests
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
- // Returns true iff all corners of the rectangle are inside of the hull
|
|
|
|
|
- bool rectangleInsideOfHull(const ProjectedPoint* convexHullCorners, int cornerCount, const IRect &rectangle) {
|
|
|
|
|
- return pointInsideOfHull(convexHullCorners, cornerCount, LVector2D(rectangle.left(), rectangle.top()))
|
|
|
|
|
- && pointInsideOfHull(convexHullCorners, cornerCount, LVector2D(rectangle.right(), rectangle.top()))
|
|
|
|
|
- && pointInsideOfHull(convexHullCorners, cornerCount, LVector2D(rectangle.left(), rectangle.bottom()))
|
|
|
|
|
- && pointInsideOfHull(convexHullCorners, cornerCount, LVector2D(rectangle.right(), rectangle.bottom()));
|
|
|
|
|
- }
|
|
|
|
|
- IRect getOuterCellBound(const IRect &pixelBound) {
|
|
|
|
|
- int minCellX = pixelBound.left() / cellSize;
|
|
|
|
|
- int maxCellX = pixelBound.right() / cellSize + 1;
|
|
|
|
|
- int minCellY = pixelBound.top() / cellSize;
|
|
|
|
|
- int maxCellY = pixelBound.bottom() / cellSize + 1;
|
|
|
|
|
- if (minCellX < 0) { minCellX = 0; }
|
|
|
|
|
- if (minCellY < 0) { minCellY = 0; }
|
|
|
|
|
- if (maxCellX > this->gridWidth) { maxCellX = this->gridWidth; }
|
|
|
|
|
- if (maxCellY > this->gridHeight) { maxCellY = this->gridHeight; }
|
|
|
|
|
- return IRect(minCellX, minCellY, maxCellX - minCellX, maxCellY - minCellY);
|
|
|
|
|
- }
|
|
|
|
|
- // Called before occluding so that the grid is initialized once when used and skipped when not used
|
|
|
|
|
- void prepareForOcclusion() {
|
|
|
|
|
- if (!this->occluded) {
|
|
|
|
|
- // Allocate the grid if a sufficiently large one does not already exist
|
|
|
|
|
- if (!(image_exists(this->depthGrid) && image_getWidth(this->depthGrid) >= gridWidth && image_getHeight(this->depthGrid) >= gridHeight)) {
|
|
|
|
|
- this->depthGrid = image_create_F32(gridWidth, gridHeight);
|
|
|
|
|
- }
|
|
|
|
|
- // Use inifnite depth in camera space
|
|
|
|
|
- image_fill(this->depthGrid, std::numeric_limits<float>::infinity());
|
|
|
|
|
- }
|
|
|
|
|
- this->occluded = true;
|
|
|
|
|
- }
|
|
|
|
|
- // If any occluder has been used during this pass, all triangles in the buffer will be filtered based using depthGrid
|
|
|
|
|
- void completeOcclusion() {
|
|
|
|
|
- if (this->occluded) {
|
|
|
|
|
- for (int t = this->commandQueue.buffer.length() - 1; t >= 0; t--) {
|
|
|
|
|
- bool anyVisible = false;
|
|
|
|
|
- ITriangle2D triangle = this->commandQueue.buffer[t].triangle;
|
|
|
|
|
- IRect outerBound = getOuterCellBound(triangle.wholeBound);
|
|
|
|
|
- for (int cellY = outerBound.top(); cellY < outerBound.bottom(); cellY++) {
|
|
|
|
|
- for (int cellX = outerBound.left(); cellX < outerBound.right(); cellX++) {
|
|
|
|
|
- // TODO: Optimize access using SafePointer iteration
|
|
|
|
|
- float backgroundDepth = image_readPixel_clamp(this->depthGrid, cellX, cellY);
|
|
|
|
|
- float triangleDepth = triangle.position[0].cs.z;
|
|
|
|
|
- replaceWithSmaller(triangleDepth, triangle.position[1].cs.z);
|
|
|
|
|
- replaceWithSmaller(triangleDepth, triangle.position[2].cs.z);
|
|
|
|
|
- if (triangleDepth < backgroundDepth + 0.001) {
|
|
|
|
|
- anyVisible = true;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if (!anyVisible) {
|
|
|
|
|
- // TODO: Make triangle swapping work so that the list can be sorted
|
|
|
|
|
- this->commandQueue.buffer[t].occluded = true;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- void occludeFromSortedHull(const ProjectedPoint* convexHullCorners, int cornerCount, const IRect& pixelBound) {
|
|
|
|
|
- // Loop over the outer bound
|
|
|
|
|
- if (pixelBound.width() > cellSize && pixelBound.height() > cellSize) {
|
|
|
|
|
- float distance = 0.0f;
|
|
|
|
|
- for (int c = 0; c < cornerCount; c++) {
|
|
|
|
|
- replaceWithLarger(distance, convexHullCorners[c].cs.z);
|
|
|
|
|
- }
|
|
|
|
|
- // Loop over all cells within the bound
|
|
|
|
|
- IRect outerBound = getOuterCellBound(pixelBound);
|
|
|
|
|
- for (int cellY = outerBound.top(); cellY < outerBound.bottom(); cellY++) {
|
|
|
|
|
- for (int cellX = outerBound.left(); cellX < outerBound.right(); cellX++) {
|
|
|
|
|
- IRect pixelRegion = IRect(cellX * cellSize, cellY * cellSize, cellSize, cellSize);
|
|
|
|
|
- IRect subPixelRegion = pixelRegion * constants::unitsPerPixel;
|
|
|
|
|
- if (rectangleInsideOfHull(convexHullCorners, cornerCount, subPixelRegion)) {
|
|
|
|
|
- float oldDepth = image_readPixel_clamp(this->depthGrid, cellX, cellY);
|
|
|
|
|
- if (distance < oldDepth) {
|
|
|
|
|
- image_writePixel(this->depthGrid, cellX, cellY, distance);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- IRect getPixelBoundFromProjection(const ProjectedPoint* convexHullCorners, int cornerCount) {
|
|
|
|
|
- IRect result = IRect(convexHullCorners[0].flat.x / constants::unitsPerPixel, convexHullCorners[0].flat.y / constants::unitsPerPixel, 1, 1);
|
|
|
|
|
- for (int p = 1; p < cornerCount; p++) {
|
|
|
|
|
- result = IRect::merge(result, IRect(convexHullCorners[p].flat.x / constants::unitsPerPixel, convexHullCorners[p].flat.y / constants::unitsPerPixel, 1, 1));
|
|
|
|
|
- }
|
|
|
|
|
- return result;
|
|
|
|
|
- }
|
|
|
|
|
- void occludeFromSortedHull(const ProjectedPoint* convexHullCorners, int cornerCount) {
|
|
|
|
|
- occludeFromSortedHull(convexHullCorners, cornerCount, getPixelBoundFromProjection(convexHullCorners, cornerCount));
|
|
|
|
|
- }
|
|
|
|
|
- void occludeFromExistingTriangles() {
|
|
|
|
|
- if (!this->receiving) {
|
|
|
|
|
- throwError("Cannot call renderer_occludeFromExistingTriangles without first calling renderer_begin!\n");
|
|
|
|
|
- }
|
|
|
|
|
- prepareForOcclusion();
|
|
|
|
|
- // Generate a depth grid to remove many small triangles behind larger triangles
|
|
|
|
|
- // This will leave triangles along seams but at least begin to remove the worst unwanted drawing
|
|
|
|
|
- for (int t = 0; t < this->commandQueue.buffer.length(); t++) {
|
|
|
|
|
- // Get the current triangle from the queue
|
|
|
|
|
- Filter filter = this->commandQueue.buffer[t].filter;
|
|
|
|
|
- if (filter == Filter::Solid) {
|
|
|
|
|
- ITriangle2D triangle = this->commandQueue.buffer[t].triangle;
|
|
|
|
|
- occludeFromSortedHull(triangle.position, 3, triangle.wholeBound);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- bool counterClockwise(const ProjectedPoint& p, const ProjectedPoint& q, const ProjectedPoint& r) {
|
|
|
|
|
- return (q.flat.y - p.flat.y) * (r.flat.x - q.flat.x) - (q.flat.x - p.flat.x) * (r.flat.y - q.flat.y) < 0;
|
|
|
|
|
- }
|
|
|
|
|
- // outputHullCorners must be at least as big as inputHullCorners, so that it can hold the worst case output size.
|
|
|
|
|
- // Instead of not allowing less than three points, it copies the input as output when it happens to reduce pre-conditions.
|
|
|
|
|
- void jarvisConvexHullAlgorithm(ProjectedPoint* outputHullCorners, int& outputCornerCount, const ProjectedPoint* inputHullCorners, int inputCornerCount) {
|
|
|
|
|
- if (inputCornerCount < 3) {
|
|
|
|
|
- outputCornerCount = inputCornerCount;
|
|
|
|
|
- for (int p = 0; p < inputCornerCount; p++) {
|
|
|
|
|
- outputHullCorners[p] = inputHullCorners[p];
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- int l = 0;
|
|
|
|
|
- outputCornerCount = 0;
|
|
|
|
|
- for (int i = 1; i < inputCornerCount; i++) {
|
|
|
|
|
- if (inputHullCorners[i].flat.x < inputHullCorners[l].flat.x) {
|
|
|
|
|
- l = i;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- int p = l;
|
|
|
|
|
- do {
|
|
|
|
|
- if (outputCornerCount >= inputCornerCount) {
|
|
|
|
|
- // Prevent getting stuck in an infinite loop from overflow
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- outputHullCorners[outputCornerCount] = inputHullCorners[p]; outputCornerCount++;
|
|
|
|
|
- int q = (p + 1) % inputCornerCount;
|
|
|
|
|
- for (int i = 0; i < inputCornerCount; i++) {
|
|
|
|
|
- if (counterClockwise(inputHullCorners[p], inputHullCorners[i], inputHullCorners[q])) {
|
|
|
|
|
- q = i;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- p = q;
|
|
|
|
|
- } while (p != l);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- // Transform and project the corners of a hull, so that the output can be given to the convex hull algorithm and used for occluding
|
|
|
|
|
- // Returns true if occluder culling passed, which may skip occluders that could have been visible
|
|
|
|
|
- bool projectHull(ProjectedPoint* outputHullCorners, const FVector3D* inputHullCorners, int cornerCount, const Transform3D &modelToWorldTransform, const Camera &camera) {
|
|
|
|
|
- for (int p = 0; p < cornerCount; p++) {
|
|
|
|
|
- FVector3D worldPoint = modelToWorldTransform.transformPoint(inputHullCorners[p]);
|
|
|
|
|
- FVector3D cameraPoint = camera.worldToCamera(worldPoint);
|
|
|
|
|
- FVector3D narrowPoint = cameraPoint * FVector3D(0.5f, 0.5f, 1.0f);
|
|
|
|
|
- for (int s = 0; s < camera.cullFrustum.getPlaneCount(); s++) {
|
|
|
|
|
- FPlane3D plane = camera.cullFrustum.getPlane(s);
|
|
|
|
|
- if (!plane.inside(narrowPoint)) {
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- outputHullCorners[p] = camera.cameraToScreen(cameraPoint);
|
|
|
|
|
- }
|
|
|
|
|
- return true;
|
|
|
|
|
- }
|
|
|
|
|
- #define GENERATE_BOX_CORNERS(TARGET, MIN, MAX) \
|
|
|
|
|
- TARGET[0] = FVector3D(MIN.x, MIN.y, MIN.z); \
|
|
|
|
|
- TARGET[1] = FVector3D(MIN.x, MIN.y, MAX.z); \
|
|
|
|
|
- TARGET[2] = FVector3D(MIN.x, MAX.y, MIN.z); \
|
|
|
|
|
- TARGET[3] = FVector3D(MIN.x, MAX.y, MAX.z); \
|
|
|
|
|
- TARGET[4] = FVector3D(MAX.x, MIN.y, MIN.z); \
|
|
|
|
|
- TARGET[5] = FVector3D(MAX.x, MIN.y, MAX.z); \
|
|
|
|
|
- TARGET[6] = FVector3D(MAX.x, MAX.y, MIN.z); \
|
|
|
|
|
- TARGET[7] = FVector3D(MAX.x, MAX.y, MAX.z);
|
|
|
|
|
- // Fills the occlusion grid using the box, so that things behind it can skip rendering
|
|
|
|
|
- void occludeFromBox(const FVector3D& minimum, const FVector3D& maximum, const Transform3D &modelToWorldTransform, const Camera &camera, bool debugSilhouette) {
|
|
|
|
|
- if (!this->receiving) {
|
|
|
|
|
- throwError("Cannot call renderer_occludeFromBox without first calling renderer_begin!\n");
|
|
|
|
|
- }
|
|
|
|
|
- prepareForOcclusion();
|
|
|
|
|
- static const int pointCount = 8;
|
|
|
|
|
- FVector3D localPoints[pointCount];
|
|
|
|
|
- ProjectedPoint projections[pointCount];
|
|
|
|
|
- ProjectedPoint edgeCorners[pointCount];
|
|
|
|
|
- GENERATE_BOX_CORNERS(localPoints, minimum, maximum)
|
|
|
|
|
- if (projectHull(projections, localPoints, 8, modelToWorldTransform, camera)) {
|
|
|
|
|
- // Get a 2D convex hull from the projected corners
|
|
|
|
|
- int edgeCornerCount = 0;
|
|
|
|
|
- jarvisConvexHullAlgorithm(edgeCorners, edgeCornerCount, projections, 8);
|
|
|
|
|
- occludeFromSortedHull(edgeCorners, edgeCornerCount);
|
|
|
|
|
- // Allow saving the 2D silhouette for debugging
|
|
|
|
|
- if (debugSilhouette) {
|
|
|
|
|
- for (int p = 0; p < edgeCornerCount; p++) {
|
|
|
|
|
- int q = (p + 1) % edgeCornerCount;
|
|
|
|
|
- if (projections[p].cs.z > camera.nearClip) {
|
|
|
|
|
- this->debugLines.pushConstruct(
|
|
|
|
|
- edgeCorners[p].flat.x / constants::unitsPerPixel, edgeCorners[p].flat.y / constants::unitsPerPixel,
|
|
|
|
|
- edgeCorners[q].flat.x / constants::unitsPerPixel, edgeCorners[q].flat.y / constants::unitsPerPixel,
|
|
|
|
|
- ColorRgbaI32(0, 255, 255, 255)
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- // Occlusion test for whole model bounds.
|
|
|
|
|
- // Returns false if the convex hull of the corners has a chance to be seen from the camera.
|
|
|
|
|
- bool isHullOccluded(ProjectedPoint* outputHullCorners, const FVector3D* inputHullCorners, int cornerCount, const Transform3D &modelToWorldTransform, const Camera &camera) {
|
|
|
|
|
- VirtualStackAllocation<FVector3D> cameraPoints(cornerCount);
|
|
|
|
|
- for (int p = 0; p < cornerCount; p++) {
|
|
|
|
|
- cameraPoints[p] = camera.worldToCamera(modelToWorldTransform.transformPoint(inputHullCorners[p]));
|
|
|
|
|
- outputHullCorners[p] = camera.cameraToScreen(cameraPoints[p]);
|
|
|
|
|
- }
|
|
|
|
|
- // Culling test to see if all points are outside of the same plane of the view frustum.
|
|
|
|
|
- for (int s = 0; s < camera.cullFrustum.getPlaneCount(); s++) {
|
|
|
|
|
- bool allOutside = true; // True until prooven false.
|
|
|
|
|
- FPlane3D plane = camera.cullFrustum.getPlane(s);
|
|
|
|
|
- for (int p = 0; p < cornerCount; p++) {
|
|
|
|
|
- if (plane.inside(cameraPoints[p])) {
|
|
|
|
|
- // One point was inside of this plane, so it can not guarantee that all interpolated points between the corners are outside.
|
|
|
|
|
- allOutside = false;
|
|
|
|
|
- break;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- // If all points are outside of the same plane in the view frustum...
|
|
|
|
|
- if (allOutside) {
|
|
|
|
|
- // ...then we know that all interpolated points in between are also outside of this plane.
|
|
|
|
|
- return true; // Occluded due to failing culling test.
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- IRect pixelBound = getPixelBoundFromProjection(outputHullCorners, cornerCount);
|
|
|
|
|
- float closestDistance = std::numeric_limits<float>::infinity();
|
|
|
|
|
- for (int c = 0; c < cornerCount; c++) {
|
|
|
|
|
- replaceWithSmaller(closestDistance, outputHullCorners[c].cs.z);
|
|
|
|
|
- }
|
|
|
|
|
- // Loop over all cells within the bound
|
|
|
|
|
- IRect outerBound = getOuterCellBound(pixelBound);
|
|
|
|
|
- for (int cellY = outerBound.top(); cellY < outerBound.bottom(); cellY++) {
|
|
|
|
|
- for (int cellX = outerBound.left(); cellX < outerBound.right(); cellX++) {
|
|
|
|
|
- if (closestDistance < image_readPixel_clamp(this->depthGrid, cellX, cellY)) {
|
|
|
|
|
- return false; // Visible because one cell had a more distant maximum depth.
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return true; // Occluded, because none of the cells had a more distant depth.
|
|
|
|
|
- }
|
|
|
|
|
- // Checks if the box from minimum to maximum in object space is fully occluded when seen by the camera
|
|
|
|
|
- // Must be the same camera as when occluders filled the grid with occlusion depth
|
|
|
|
|
- bool isBoxOccluded(const FVector3D &minimum, const FVector3D &maximum, const Transform3D &modelToWorldTransform, const Camera &camera) {
|
|
|
|
|
- if (!this->receiving) {
|
|
|
|
|
- throwError("Cannot call renderer_isBoxVisible without first calling renderer_begin and giving occluder shapes to the pass!\n");
|
|
|
|
|
- }
|
|
|
|
|
- FVector3D corners[8];
|
|
|
|
|
- GENERATE_BOX_CORNERS(corners, minimum, maximum)
|
|
|
|
|
- ProjectedPoint projections[8];
|
|
|
|
|
- return isHullOccluded(projections, corners, 8, modelToWorldTransform, camera);
|
|
|
|
|
|
|
+void model_render_threaded(const Model& model, const Transform3D &modelToWorldTransform, Renderer& renderer, const Camera &camera) {
|
|
|
|
|
+ MUST_EXIST(renderer, renderer_giveTask);
|
|
|
|
|
+ // Skip rendering if we do not have any model.
|
|
|
|
|
+ if (model.isNull()) {
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
- void giveTask(const Model& model, const Transform3D &modelToWorldTransform, const Camera &camera) {
|
|
|
|
|
- if (!this->receiving) {
|
|
|
|
|
|
|
+ // Check the renderer's state.
|
|
|
|
|
+ #ifndef NDEBUG
|
|
|
|
|
+ if (!renderer_takesTriangles(renderer)) {
|
|
|
throwError("Cannot call renderer_giveTask before renderer_begin!\n");
|
|
throwError("Cannot call renderer_giveTask before renderer_begin!\n");
|
|
|
}
|
|
}
|
|
|
- // If occluders are present, check if the model's bound is visible
|
|
|
|
|
- if (this->occluded) {
|
|
|
|
|
- FVector3D minimum, maximum;
|
|
|
|
|
- model_getBoundingBox(model, minimum, maximum);
|
|
|
|
|
- if (isBoxOccluded(minimum, maximum, modelToWorldTransform, camera)) {
|
|
|
|
|
- // Skip projection of triangles if the whole bounding box is already behind occluders
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- // TODO: Make an algorithm for selecting if the model should be queued as an instance or triangulated at once
|
|
|
|
|
- // An extra argument may choose to force an instance directly into the command queue
|
|
|
|
|
- // Because the model is being borrowed for vertex animation
|
|
|
|
|
- // To prevent the command queue from getting full hold as much as possible in a sorted list of instances
|
|
|
|
|
- // When the command queue is full, the solid instances will be drawn front to back before filtered is drawn back to front
|
|
|
|
|
- model->render(&this->commandQueue, image_exists(this->colorBuffer) ? &(this->colorBuffer) : nullptr, image_exists(this->depthBuffer) ? &(this->depthBuffer) : nullptr, modelToWorldTransform, camera);
|
|
|
|
|
|
|
+ #endif
|
|
|
|
|
+ // Culling.
|
|
|
|
|
+ if (!camera.isBoxSeen(model->minBound, model->maxBound, modelToWorldTransform)) return;
|
|
|
|
|
+ // Occlusion.
|
|
|
|
|
+ if (renderer_hasOccluders(renderer)) {
|
|
|
|
|
+ FVector3D minimum, maximum;
|
|
|
|
|
+ model_getBoundingBox(model, minimum, maximum);
|
|
|
|
|
+ if (!renderer_isBoxVisible(renderer, minimum, maximum, modelToWorldTransform, camera)) return;
|
|
|
}
|
|
}
|
|
|
- void endFrame(bool debugWireframe) {
|
|
|
|
|
- if (!this->receiving) {
|
|
|
|
|
- throwError("Called renderer_end without renderer_begin!\n");
|
|
|
|
|
- }
|
|
|
|
|
- this->receiving = false;
|
|
|
|
|
- // Mark occluded triangles to prevent them from being rendered
|
|
|
|
|
- completeOcclusion();
|
|
|
|
|
- this->commandQueue.execute(IRect::FromSize(this->width, this->height));
|
|
|
|
|
- if (image_exists(this->colorBuffer)) {
|
|
|
|
|
- // Debug drawn triangles
|
|
|
|
|
- if (debugWireframe) {
|
|
|
|
|
- /*if (image_exists(this->depthGrid)) {
|
|
|
|
|
- for (int cellY = 0; cellY < this->gridHeight; cellY++) {
|
|
|
|
|
- for (int cellX = 0; cellX < this->gridWidth; cellX++) {
|
|
|
|
|
- float depth = image_readPixel_clamp(this->depthGrid, cellX, cellY);
|
|
|
|
|
- if (depth < std::numeric_limits<float>::infinity()) {
|
|
|
|
|
- int intensity = depth;
|
|
|
|
|
- draw_rectangle(this->colorBuffer, IRect(cellX * cellSize + 4, cellY * cellSize + 4, cellSize - 8, cellSize - 8), ColorRgbaI32(intensity, intensity, 0, 255));
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }*/
|
|
|
|
|
- for (int t = 0; t < this->commandQueue.buffer.length(); t++) {
|
|
|
|
|
- if (!this->commandQueue.buffer[t].occluded) {
|
|
|
|
|
- ITriangle2D *triangle = &(this->commandQueue.buffer[t].triangle);
|
|
|
|
|
- draw_line(this->colorBuffer,
|
|
|
|
|
- triangle->position[0].flat.x / constants::unitsPerPixel, triangle->position[0].flat.y / constants::unitsPerPixel,
|
|
|
|
|
- triangle->position[1].flat.x / constants::unitsPerPixel, triangle->position[1].flat.y / constants::unitsPerPixel,
|
|
|
|
|
- ColorRgbaI32(255, 255, 255, 255)
|
|
|
|
|
- );
|
|
|
|
|
- draw_line(this->colorBuffer,
|
|
|
|
|
- triangle->position[1].flat.x / constants::unitsPerPixel, triangle->position[1].flat.y / constants::unitsPerPixel,
|
|
|
|
|
- triangle->position[2].flat.x / constants::unitsPerPixel, triangle->position[2].flat.y / constants::unitsPerPixel,
|
|
|
|
|
- ColorRgbaI32(255, 255, 255, 255)
|
|
|
|
|
- );
|
|
|
|
|
- draw_line(this->colorBuffer,
|
|
|
|
|
- triangle->position[2].flat.x / constants::unitsPerPixel, triangle->position[2].flat.y / constants::unitsPerPixel,
|
|
|
|
|
- triangle->position[0].flat.x / constants::unitsPerPixel, triangle->position[0].flat.y / constants::unitsPerPixel,
|
|
|
|
|
- ColorRgbaI32(255, 255, 255, 255)
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- // Debug anything else added to debugLines
|
|
|
|
|
- for (int l = 0; l < this->debugLines.length(); l++) {
|
|
|
|
|
- draw_line(this->colorBuffer, this->debugLines[l].x1, this->debugLines[l].y1, this->debugLines[l].x2, this->debugLines[l].y2, this->debugLines[l].color);
|
|
|
|
|
- }
|
|
|
|
|
- this->debugLines.clear();
|
|
|
|
|
- }
|
|
|
|
|
- this->commandQueue.clear();
|
|
|
|
|
|
|
+ // Render the model by calling the renderer API for each triangle.
|
|
|
|
|
+ // Get the filter.
|
|
|
|
|
+ Filter filter = model->filter;
|
|
|
|
|
+ // Transform and project all vertices.
|
|
|
|
|
+ int positionCount = model->positionBuffer.length();
|
|
|
|
|
+ VirtualStackAllocation<ProjectedPoint> projected(positionCount, "Projected points in renderer_giveTask");
|
|
|
|
|
+ for (int vert = 0; vert < positionCount; vert++) {
|
|
|
|
|
+ projected[vert] = camera.worldToScreen(modelToWorldTransform.transformPoint(model->positionBuffer[vert]));
|
|
|
}
|
|
}
|
|
|
- void occludeFromTopRows(const Camera &camera) {
|
|
|
|
|
- // Make sure that the depth grid exists with the correct dimensions.
|
|
|
|
|
- this->prepareForOcclusion();
|
|
|
|
|
- if (!this->receiving) {
|
|
|
|
|
- throwError("Cannot call renderer_occludeFromTopRows without first calling renderer_begin!\n");
|
|
|
|
|
- }
|
|
|
|
|
- if (!image_exists(this->depthBuffer)) {
|
|
|
|
|
- throwError("Cannot call renderer_occludeFromTopRows without having given a depth buffer in renderer_begin!\n");
|
|
|
|
|
- }
|
|
|
|
|
- SafePointer<float> depthRow = image_getSafePointer(this->depthBuffer);
|
|
|
|
|
- int depthStride = image_getStride(this->depthBuffer);
|
|
|
|
|
- SafePointer<float> gridRow = image_getSafePointer(this->depthGrid);
|
|
|
|
|
- int gridStride = image_getStride(this->depthGrid);
|
|
|
|
|
- if (camera.perspective) {
|
|
|
|
|
- // Perspective case using 1/depth for the depth buffer.
|
|
|
|
|
- for (int y = 0; y < this->height; y += cellSize) {
|
|
|
|
|
- SafePointer<float> gridPixel = gridRow;
|
|
|
|
|
- SafePointer<float> depthPixel = depthRow;
|
|
|
|
|
- int x = 0;
|
|
|
|
|
- int right = cellSize - 1;
|
|
|
|
|
- float maxInvDistance;
|
|
|
|
|
- // Scan bottom row of whole cell width
|
|
|
|
|
- for (int gridX = 0; gridX < this->gridWidth; gridX++) {
|
|
|
|
|
- maxInvDistance = std::numeric_limits<float>::infinity();
|
|
|
|
|
- if (right >= this->width) { right = this->width; }
|
|
|
|
|
- while (x < right) {
|
|
|
|
|
- float newInvDistance = *depthPixel;
|
|
|
|
|
- if (newInvDistance < maxInvDistance) { maxInvDistance = newInvDistance; }
|
|
|
|
|
- depthPixel += 1;
|
|
|
|
|
- x += 1;
|
|
|
|
|
- }
|
|
|
|
|
- float maxDistance = 1.0f / maxInvDistance;
|
|
|
|
|
- float oldDistance = *gridPixel;
|
|
|
|
|
- if (maxDistance < oldDistance) {
|
|
|
|
|
- *gridPixel = maxDistance;
|
|
|
|
|
- }
|
|
|
|
|
- gridPixel += 1;
|
|
|
|
|
- right += cellSize;
|
|
|
|
|
- }
|
|
|
|
|
- // Go to the next grid row
|
|
|
|
|
- depthRow.increaseBytes(depthStride * cellSize);
|
|
|
|
|
- gridRow.increaseBytes(gridStride);
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- // Orthogonal case where linear depth is used for both grid and depth buffer.
|
|
|
|
|
- // TODO: Create test cases for many ways to use occlusion, even these strange cases like isometric occlusion where plain culling does not leave many occluded models.
|
|
|
|
|
- for (int y = 0; y < this->height; y += cellSize) {
|
|
|
|
|
- SafePointer<float> gridPixel = gridRow;
|
|
|
|
|
- SafePointer<float> depthPixel = depthRow;
|
|
|
|
|
- int x = 0;
|
|
|
|
|
- int right = cellSize - 1;
|
|
|
|
|
- float maxDistance;
|
|
|
|
|
- // Scan bottom row of whole cell width
|
|
|
|
|
- for (int gridX = 0; gridX < this->gridWidth; gridX++) {
|
|
|
|
|
- maxDistance = 0.0f;
|
|
|
|
|
- if (right >= this->width) { right = this->width; }
|
|
|
|
|
- while (x < right) {
|
|
|
|
|
- float newDistance = *depthPixel;
|
|
|
|
|
- if (newDistance > maxDistance) { maxDistance = newDistance; }
|
|
|
|
|
- depthPixel += 1;
|
|
|
|
|
- x += 1;
|
|
|
|
|
- }
|
|
|
|
|
- float oldDistance = *gridPixel;
|
|
|
|
|
- if (maxDistance < oldDistance) {
|
|
|
|
|
- *gridPixel = maxDistance;
|
|
|
|
|
- }
|
|
|
|
|
- gridPixel += 1;
|
|
|
|
|
- right += cellSize;
|
|
|
|
|
- }
|
|
|
|
|
- // Go to the next grid row
|
|
|
|
|
- depthRow.increaseBytes(depthStride * cellSize);
|
|
|
|
|
- gridRow.increaseBytes(gridStride);
|
|
|
|
|
|
|
+ for (int partIndex = 0; partIndex < model->partBuffer.length(); partIndex++) {
|
|
|
|
|
+ // Get a pointer to the current part.
|
|
|
|
|
+ Part *part = &(model->partBuffer[partIndex]);
|
|
|
|
|
+ // Get textures.
|
|
|
|
|
+ const TextureRgbaU8 diffuse = part->diffuseMap;
|
|
|
|
|
+ const TextureRgbaU8 light = part->lightMap;
|
|
|
|
|
+ for (int p = 0; p < part->polygonBuffer.length(); p++) {
|
|
|
|
|
+ Polygon polygon = part->polygonBuffer[p];
|
|
|
|
|
+ // Render first triangle in the polygon of indices 0, 1, 2.
|
|
|
|
|
+ renderer_giveTask_triangle(renderer,
|
|
|
|
|
+ projected[polygon.pointIndices[0]],
|
|
|
|
|
+ projected[polygon.pointIndices[1]],
|
|
|
|
|
+ projected[polygon.pointIndices[2]],
|
|
|
|
|
+ polygon.colors[0],
|
|
|
|
|
+ polygon.colors[1],
|
|
|
|
|
+ polygon.colors[2],
|
|
|
|
|
+ polygon.texCoords[0],
|
|
|
|
|
+ polygon.texCoords[1],
|
|
|
|
|
+ polygon.texCoords[2],
|
|
|
|
|
+ diffuse, light, filter, camera
|
|
|
|
|
+ );
|
|
|
|
|
+ if (polygon.pointIndices[3] != -1) {
|
|
|
|
|
+ // Render second triangle in the polygon of indices 0, 2, 3 to form a quad polygon.
|
|
|
|
|
+ renderer_giveTask_triangle(renderer,
|
|
|
|
|
+ projected[polygon.pointIndices[0]],
|
|
|
|
|
+ projected[polygon.pointIndices[2]],
|
|
|
|
|
+ projected[polygon.pointIndices[3]],
|
|
|
|
|
+ polygon.colors[0],
|
|
|
|
|
+ polygon.colors[2],
|
|
|
|
|
+ polygon.colors[3],
|
|
|
|
|
+ polygon.texCoords[0],
|
|
|
|
|
+ polygon.texCoords[2],
|
|
|
|
|
+ polygon.texCoords[3],
|
|
|
|
|
+ diffuse, light, filter, camera
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-Renderer renderer_create() {
|
|
|
|
|
- return handle_create<RendererImpl>().setName("Renderer");
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-bool renderer_exists(const Renderer& renderer) {
|
|
|
|
|
- return renderer.isNotNull();
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void renderer_begin(Renderer& renderer, ImageRgbaU8& colorBuffer, ImageF32& depthBuffer) {
|
|
|
|
|
- MUST_EXIST(renderer, renderer_begin);
|
|
|
|
|
- renderer->beginFrame(colorBuffer, depthBuffer);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// TODO: Synchronous setting
|
|
|
|
|
-// * Asynchronous (default)
|
|
|
|
|
-// Only works on models that are locked from further editing
|
|
|
|
|
-// Locked models can also be safely pooled for reuse then (ResourcePool)
|
|
|
|
|
-// * Synced (for animation)
|
|
|
|
|
-// Dispatch triangles directly to the command queue so that the current state of the model is captured
|
|
|
|
|
-// This allow rendering many instances using the same model at different times
|
|
|
|
|
-// Enabling vertex light, reflection maps and bone animation
|
|
|
|
|
-void renderer_giveTask(Renderer& renderer, const Model& model, const Transform3D &modelToWorldTransform, const Camera &camera) {
|
|
|
|
|
- MUST_EXIST(renderer, renderer_giveTask);
|
|
|
|
|
- if (model.isNotNull()) {
|
|
|
|
|
- renderer->giveTask(model, modelToWorldTransform, camera);
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-void renderer_giveTask_triangle(Renderer& renderer,
|
|
|
|
|
- const ProjectedPoint &posA, const ProjectedPoint &posB, const ProjectedPoint &posC,
|
|
|
|
|
- const FVector4D &colorA, const FVector4D &colorB, const FVector4D &colorC,
|
|
|
|
|
- const FVector4D &texCoordA, const FVector4D &texCoordB, const FVector4D &texCoordC,
|
|
|
|
|
- const TextureRgbaU8& diffuseMap, const TextureRgbaU8& lightMap,
|
|
|
|
|
- Filter filter, const Camera &camera) {
|
|
|
|
|
- #ifndef NDEBUG
|
|
|
|
|
- MUST_EXIST(renderer, renderer_addTriangle);
|
|
|
|
|
- #endif
|
|
|
|
|
- renderTriangleFromData(
|
|
|
|
|
- &(renderer->commandQueue), &(renderer->colorBuffer), &(renderer->depthBuffer), camera,
|
|
|
|
|
- posA, posB, posC,
|
|
|
|
|
- filter, &(diffuseMap), &(lightMap),
|
|
|
|
|
- TriangleTexCoords(texCoordA, texCoordB, texCoordC),
|
|
|
|
|
- TriangleColors(colorA, colorB, colorC)
|
|
|
|
|
- );
|
|
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-void renderer_occludeFromBox(Renderer& renderer, const FVector3D& minimum, const FVector3D& maximum, const Transform3D &modelToWorldTransform, const Camera &camera, bool debugSilhouette) {
|
|
|
|
|
- MUST_EXIST(renderer, renderer_occludeFromBox);
|
|
|
|
|
- renderer->occludeFromBox(minimum, maximum, modelToWorldTransform, camera, debugSilhouette);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void renderer_occludeFromExistingTriangles(Renderer& renderer) {
|
|
|
|
|
- MUST_EXIST(renderer, renderer_optimize);
|
|
|
|
|
- renderer->occludeFromExistingTriangles();
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void renderer_occludeFromTopRows(Renderer& renderer, const Camera &camera) {
|
|
|
|
|
- MUST_EXIST(renderer, renderer_occludeFromTopRows);
|
|
|
|
|
- renderer->occludeFromTopRows(camera);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-bool renderer_isBoxVisible(Renderer& renderer, const FVector3D &minimum, const FVector3D &maximum, const Transform3D &modelToWorldTransform, const Camera &camera) {
|
|
|
|
|
- MUST_EXIST(renderer, renderer_isBoxVisible);
|
|
|
|
|
- return !(renderer->isBoxOccluded(minimum, maximum, modelToWorldTransform, camera));
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-void renderer_end(Renderer& renderer, bool debugWireframe) {
|
|
|
|
|
- MUST_EXIST(renderer, renderer_end);
|
|
|
|
|
- renderer->endFrame(debugWireframe);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|