Browse Source

Safety checks and two new methods for the occlusion system.

David Piuva 3 years ago
parent
commit
43d54b2c7d
2 changed files with 135 additions and 25 deletions
  1. 119 24
      Source/DFPSR/api/modelAPI.cpp
  2. 16 1
      Source/DFPSR/api/modelAPI.h

+ 119 - 24
Source/DFPSR/api/modelAPI.cpp

@@ -269,7 +269,7 @@ public:
 		// Passed all edge tests
 		return true;
 	}
-	// Returns true iff all cornets of the rectangle are inside of the hull
+	// 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()))
@@ -359,6 +359,9 @@ public:
 		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
@@ -424,20 +427,26 @@ public:
 		}
 		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];
-		localPoints[0] = FVector3D(minimum.x, minimum.y, minimum.z);
-		localPoints[1] = FVector3D(minimum.x, minimum.y, maximum.z);
-		localPoints[2] = FVector3D(minimum.x, maximum.y, minimum.z);
-		localPoints[3] = FVector3D(minimum.x, maximum.y, maximum.z);
-		localPoints[4] = FVector3D(maximum.x, minimum.y, minimum.z);
-		localPoints[5] = FVector3D(maximum.x, minimum.y, maximum.z);
-		localPoints[6] = FVector3D(maximum.x, maximum.y, minimum.z);
-		localPoints[7] = FVector3D(maximum.x, maximum.y, maximum.z);
+		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;
@@ -459,14 +468,14 @@ public:
 		}
 	}
 	// Occlusion test for whole model bounds
-	bool isHullVisible(ProjectedPoint* outputHullCorners, const FVector3D* inputHullCorners, int cornerCount, const Transform3D &modelToWorldTransform, const Camera &camera) {
+	//   Because outerBound gives negative regions when outside of the picture, it can also be used as a rough culling test
+	bool isHullOccluded(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);
 			outputHullCorners[p] = camera.cameraToScreen(cameraPoint);
 		}
 		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);
@@ -476,11 +485,22 @@ public:
 		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 true;
+					return false;
 				}
 			}
 		}
-		return false;
+		return true;
+	}
+	// 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 giveTask(const Model& model, const Transform3D &modelToWorldTransform, const Camera &camera) {
 		if (!this->receiving) {
@@ -490,17 +510,7 @@ public:
 		if (this->occluded) {
 			FVector3D minimum, maximum;
 			model_getBoundingBox(model, minimum, maximum);
-			FVector3D corners[8];
-			ProjectedPoint projections[8];
-			corners[0] = FVector3D(minimum.x, minimum.y, minimum.z);
-			corners[1] = FVector3D(minimum.x, minimum.y, maximum.z);
-			corners[2] = FVector3D(minimum.x, maximum.y, minimum.z);
-			corners[3] = FVector3D(minimum.x, maximum.y, maximum.z);
-			corners[4] = FVector3D(maximum.x, minimum.y, minimum.z);
-			corners[5] = FVector3D(maximum.x, minimum.y, maximum.z);
-			corners[6] = FVector3D(maximum.x, maximum.y, minimum.z);
-			corners[7] = FVector3D(maximum.x, maximum.y, maximum.z);
-			if (!isHullVisible(projections, corners, 8, modelToWorldTransform, camera)) {
+			if (isBoxOccluded(minimum, maximum, modelToWorldTransform, camera)) {
 				// Skip projection of triangles if the whole bounding box is already behind occluders
 				return;
 			}
@@ -563,6 +573,81 @@ public:
 		}
 		this->commandQueue.clear();
 	}
+	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);
+			}
+		}
+	}
 };
 
 Renderer renderer_create() {
@@ -603,6 +688,16 @@ void renderer_occludeFromExistingTriangles(Renderer& renderer) {
 	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);

+ 16 - 1
Source/DFPSR/api/modelAPI.h

@@ -303,13 +303,28 @@ namespace dsr {
 	// Occluders may only be placed within solid geometry, because otherwise it may affect the visual result.
 	// Should ideally be used before giving render tasks, so that optimizations can take advantage of early occlusion checks.
 	void renderer_occludeFromBox(Renderer& renderer, const FVector3D& minimum, const FVector3D& maximum, const Transform3D &modelToWorldTransform, const Camera &camera, bool debugSilhouette = false);
+	// If you have drawn the ground in a separate pass and know that lower pixels along the current depth buffer are never further away from the camera,
+	// you can fill the occlusion grid using the furthest distance in the top row of each cell sampled from the depth buffer and know the maximum distance of each cell for occluding models in the next pass.
+	// Make sure to call it after renderer_begin (so that you don't clear your result on start), but before renderer_giveTask (so that whole models can be occluded without filling the buffer with projected triangles).
+	// Pre-condition:
+	//   The renderer must have started a pass with a depth buffer using renderer_begin.
+	void renderer_occludeFromTopRows(Renderer& renderer, const Camera &camera);
+	// After having filled the occlusion grid (using renderer_occludeFromBox, renderer_occludeFromTopRows or renderer_occludeFromExistingTriangles), you can check if a bounding box is visible.
+	//   For a single model, you can use model_getBoundingBox to get the local bound and then provide its model to world transform that would be used to render the specific instance.
+	//   This is already applied automatically in renderer_giveTask, but you might want to know which model may potentially be visible ahead of time
+	//   to bake effects into textures, procedurally generate geometry, skip whole groups of models in a broad-phase or use your own custom rasterizer.
+	// Opposite to when filling the occlusion grid, the tested bound must include the whole drawn content.
+	//   This makes sure that renderer_isBoxVisible will only return false if it cannot be seen, with exception for near clipping and abused occluders.
+	//   False positives from having the bounding box seen is to be expected, because the purpose is to save time by doing less work.
+	bool renderer_isBoxVisible(Renderer& renderer, const FVector3D &minimum, const FVector3D &maximum, const Transform3D &modelToWorldTransform, const Camera &camera);
 	// Once an object passed game-specific occlusion tests, give it to the renderer using renderer_giveTask.
 	// The render job will be performed during the next call to renderer_end.
 	// Pre-condition: renderer must refer to an existing renderer.
 	// An empty model handle will be skipped silently, which can be used instead of an model with zero polygons.
 	// Side-effect: The visible triangles are queued up in the renderer.
 	void renderer_giveTask(Renderer& renderer, const Model& model, const Transform3D &modelToWorldTransform, const Camera &camera);
-	// Use already given triangles as occluders
+	// Use already given triangles as occluders.
+	//   Used after calls to renderer_giveTask have filled the buffer with triangles, but before they are drawn using renderer_end.
 	void renderer_occludeFromExistingTriangles(Renderer& renderer);
 	// Side-effect: Finishes all the jobs in the rendering context so that triangles are rasterized to the targets given to renderer_begin.
 	// Pre-condition: renderer must refer to an existing renderer.