Browse Source

Implemented dense isometric background models.

David Piuva 5 years ago
parent
commit
7c908edfc8

+ 2 - 3
Source/SDK/sandbox/sandbox.cpp

@@ -30,7 +30,6 @@ BUGS:
 			An optional triangle patch can be added along the open sides. (all for planes and excluding sides for cylinders)
 			An optional triangle patch can be added along the open sides. (all for planes and excluding sides for cylinders)
 
 
 VISUALS:
 VISUALS:
-	* Implement freely rotated background models in the sprite engine.
 	* Make a directed light source that casts light and shadows from a fixed direction but can fade like a point light.
 	* Make a directed light source that casts light and shadows from a fixed direction but can fade like a point light.
 		Useful for street-lights and sky-lights that want to avoid normalizing and projecting light directions per pixel.
 		Useful for street-lights and sky-lights that want to avoid normalizing and projecting light directions per pixel.
 		Can be used both with and without casting shadows.
 		Can be used both with and without casting shadows.
@@ -285,11 +284,11 @@ void sandbox_main() {
 	component_setMouseDownEvent(mainPanel, [](const MouseEvent& event) {
 	component_setMouseDownEvent(mainPanel, [](const MouseEvent& event) {
 		if (event.key == MouseKeyEnum::Left) {
 		if (event.key == MouseKeyEnum::Left) {
 			if (overlayMode == OverlayMode_Tools) {
 			if (overlayMode == OverlayMode_Tools) {
+				// Place a passive visual instance using the brush
 				if (tool == Tool_PlaceSprite) {
 				if (tool == Tool_PlaceSprite) {
-					// Place a new visual instance using the sprite brush
 					spriteWorld_addBackgroundSprite(world, spriteBrush);
 					spriteWorld_addBackgroundSprite(world, spriteBrush);
 				} else if (tool == Tool_PlaceModel) {
 				} else if (tool == Tool_PlaceModel) {
-					// TODO: Implement a way to place a background model with 3-dimensional location, 3-axis rotation and uniform scaling
+					spriteWorld_addBackgroundModel(world, modelBrush);
 				}
 				}
 			}
 			}
 		} else if (event.key == MouseKeyEnum::Right) {
 		} else if (event.key == MouseKeyEnum::Right) {

+ 120 - 56
Source/SDK/sandbox/sprite/spriteAPI.cpp

@@ -5,6 +5,7 @@
 #include "importer.h"
 #include "importer.h"
 #include "../../../DFPSR/render/ITriangle2D.h"
 #include "../../../DFPSR/render/ITriangle2D.h"
 #include "../../../DFPSR/base/endian.h"
 #include "../../../DFPSR/base/endian.h"
+#include "../../../DFPSR/math/scalar.h"
 
 
 // Comment out a flag to disable an optimization when debugging
 // Comment out a flag to disable an optimization when debugging
 #define DIRTY_RECTANGLE_OPTIMIZATION
 #define DIRTY_RECTANGLE_OPTIMIZATION
@@ -17,6 +18,14 @@ static IRect renderModel(const Model& model, OrthoView view, ImageF32 depthBuffe
 template <bool HIGH_QUALITY>
 template <bool HIGH_QUALITY>
 static IRect renderDenseModel(const DenseModel& model, OrthoView view, ImageF32 depthBuffer, ImageRgbaU8 diffuseTarget, ImageRgbaU8 normalTarget, const FVector2D& worldOrigin, const Transform3D& modelToWorldSpace);
 static IRect renderDenseModel(const DenseModel& model, OrthoView view, ImageF32 depthBuffer, ImageRgbaU8 diffuseTarget, ImageRgbaU8 normalTarget, const FVector2D& worldOrigin, const Transform3D& modelToWorldSpace);
 
 
+static Transform3D combineWorldToScreenTransform(const FMatrix3x3& worldSpaceToScreenDepth, const FVector2D& worldOrigin) {
+	return Transform3D(FVector3D(worldOrigin.x, worldOrigin.y, 0.0f), worldSpaceToScreenDepth);
+}
+
+static Transform3D combineModelToScreenTransform(const Transform3D& modelToWorldSpace, const FMatrix3x3& worldSpaceToScreenDepth, const FVector2D& worldOrigin) {
+	return modelToWorldSpace * combineWorldToScreenTransform(worldSpaceToScreenDepth, worldOrigin);
+}
+
 struct SpriteConfig {
 struct SpriteConfig {
 	int centerX, centerY; // The sprite's origin in pixels relative to the upper left corner
 	int centerX, centerY; // The sprite's origin in pixels relative to the upper left corner
 	int frameRows; // The atlas has one row for each frame
 	int frameRows; // The atlas has one row for each frame
@@ -212,6 +221,30 @@ public:
 	}
 	}
 };
 };
 
 
+struct DenseTriangle {
+public:
+	FVector3D colorA, colorB, colorC, posA, posB, posC, normalA, normalB, normalC;
+public:
+	DenseTriangle() {}
+	DenseTriangle(
+	  const FVector3D& colorA, const FVector3D& colorB, const FVector3D& colorC,
+	  const FVector3D& posA, const FVector3D& posB, const FVector3D& posC,
+	  const FVector3D& normalA, const FVector3D& normalB, const FVector3D& normalC)
+	  : colorA(colorA), colorB(colorB), colorC(colorC),
+	    posA(posA), posB(posB), posC(posC),
+	    normalA(normalA), normalB(normalB), normalC(normalC) {}
+};
+// The raw format for dense models using vertex colors instead of textures
+// Due to the high number of triangles, indexing positions would cause a lot of cache misses
+struct DenseModelImpl {
+public:
+	Array<DenseTriangle> triangles;
+	FVector3D minBound, maxBound;
+public:
+	// Optimize an existing model
+	DenseModelImpl(const Model& original);
+};
+
 struct ModelType {
 struct ModelType {
 public:
 public:
 	DenseModel visibleModel;
 	DenseModel visibleModel;
@@ -337,7 +370,17 @@ public:
 			}
 			}
 		}
 		}
 	}
 	}
-	void renderSpriteShadows(CubeMapF32& shadowTarget, Octree<SpriteInstance>& sprites, const FMatrix3x3& normalToWorld) const {
+	// Render shadows from passive models
+	void renderPassiveShadows(CubeMapF32& shadowTarget, Octree<ModelInstance>& models, const FMatrix3x3& normalToWorld) const {
+		IVector3D center = ortho_floatingTileToMini(this->position);
+		IVector3D minBound = center - ortho_floatingTileToMini(radius);
+		IVector3D maxBound = center + ortho_floatingTileToMini(radius);
+		models.map(minBound, maxBound, [this, shadowTarget, normalToWorld](ModelInstance& model, const IVector3D origin, const IVector3D minBound, const IVector3D maxBound) mutable {
+			this->renderModelShadow(shadowTarget, model, normalToWorld);
+		});
+	}
+	// Render shadows from passive sprites
+	void renderPassiveShadows(CubeMapF32& shadowTarget, Octree<SpriteInstance>& sprites, const FMatrix3x3& normalToWorld) const {
 		IVector3D center = ortho_floatingTileToMini(this->position);
 		IVector3D center = ortho_floatingTileToMini(this->position);
 		IVector3D minBound = center - ortho_floatingTileToMini(radius);
 		IVector3D minBound = center - ortho_floatingTileToMini(radius);
 		IVector3D maxBound = center + ortho_floatingTileToMini(radius);
 		IVector3D maxBound = center + ortho_floatingTileToMini(radius);
@@ -399,11 +442,10 @@ private:
 		);
 		);
 	}
 	}
 	// Pre-condition: diffuseBuffer must be cleared unless sprites cover the whole block
 	// Pre-condition: diffuseBuffer must be cleared unless sprites cover the whole block
-	void draw(Octree<SpriteInstance>& sprites, const OrthoView& ortho) {
+	void draw(Octree<SpriteInstance>& sprites, Octree<ModelInstance>& models, const OrthoView& ortho) {
 		image_fill(this->normalBuffer, ColorRgbaI32(128));
 		image_fill(this->normalBuffer, ColorRgbaI32(128));
 		image_fill(this->heightBuffer, -std::numeric_limits<float>::max());
 		image_fill(this->heightBuffer, -std::numeric_limits<float>::max());
-		sprites.map(
-		[ortho,this](const IVector3D& minBound, const IVector3D& maxBound){
+		OcTreeFilter orthoCullingFilter = [ortho,this](const IVector3D& minBound, const IVector3D& maxBound){
 			IVector2D corners[8];
 			IVector2D corners[8];
 			for (int c = 0; c < 8; c++) {
 			for (int c = 0; c < 8; c++) {
 				corners[c] = ortho.miniTileOffsetToScreenPixel(getBoxCorner(minBound, maxBound, c));
 				corners[c] = ortho.miniTileOffsetToScreenPixel(getBoxCorner(minBound, maxBound, c));
@@ -449,24 +491,27 @@ private:
 				return false;
 				return false;
 			}
 			}
 			return true;
 			return true;
-		},
-		[this, ortho](SpriteInstance& sprite, const IVector3D origin, const IVector3D minBound, const IVector3D maxBound){
+		};
+		sprites.map(orthoCullingFilter, [this, ortho](SpriteInstance& sprite, const IVector3D origin, const IVector3D minBound, const IVector3D maxBound){
 			drawSprite(sprite, ortho, -this->worldRegion.upperLeft(), this->heightBuffer, this->diffuseBuffer, this->normalBuffer);
 			drawSprite(sprite, ortho, -this->worldRegion.upperLeft(), this->heightBuffer, this->diffuseBuffer, this->normalBuffer);
 		});
 		});
+		models.map(orthoCullingFilter, [this, ortho](ModelInstance& model, const IVector3D origin, const IVector3D minBound, const IVector3D maxBound){
+			drawModel(model, ortho, -this->worldRegion.upperLeft(), this->heightBuffer, this->diffuseBuffer, this->normalBuffer);
+		});
 	}
 	}
 public:
 public:
-	BackgroundBlock(Octree<SpriteInstance>& sprites, const IRect& worldRegion, const OrthoView& ortho)
+	BackgroundBlock(Octree<SpriteInstance>& sprites, Octree<ModelInstance>& models, const IRect& worldRegion, const OrthoView& ortho)
 	: worldRegion(worldRegion), cameraId(ortho.id), state(BlockState::Ready),
 	: worldRegion(worldRegion), cameraId(ortho.id), state(BlockState::Ready),
 	  diffuseBuffer(image_create_RgbaU8(blockSize, blockSize)),
 	  diffuseBuffer(image_create_RgbaU8(blockSize, blockSize)),
 	  normalBuffer(image_create_RgbaU8(blockSize, blockSize)),
 	  normalBuffer(image_create_RgbaU8(blockSize, blockSize)),
 	  heightBuffer(image_create_F32(blockSize, blockSize)) {
 	  heightBuffer(image_create_F32(blockSize, blockSize)) {
-		this->draw(sprites, ortho);
+		this->draw(sprites, models, ortho);
 	}
 	}
-	void update(Octree<SpriteInstance>& sprites, const IRect& worldRegion, const OrthoView& ortho) {
+	void update(Octree<SpriteInstance>& sprites, Octree<ModelInstance>& models, const IRect& worldRegion, const OrthoView& ortho) {
 		this->worldRegion = worldRegion;
 		this->worldRegion = worldRegion;
 		this->cameraId = ortho.id;
 		this->cameraId = ortho.id;
 		image_fill(this->diffuseBuffer, ColorRgbaI32(0));
 		image_fill(this->diffuseBuffer, ColorRgbaI32(0));
-		this->draw(sprites, ortho);
+		this->draw(sprites, models, ortho);
 		this->state = BlockState::Ready;
 		this->state = BlockState::Ready;
 	}
 	}
 	void draw(ImageRgbaU8& diffuseTarget, ImageRgbaU8& normalTarget, ImageF32& heightTarget, const IRect& seenRegion) const {
 	void draw(ImageRgbaU8& diffuseTarget, ImageRgbaU8& normalTarget, ImageF32& heightTarget, const IRect& seenRegion) const {
@@ -486,14 +531,17 @@ public:
 	}
 	}
 };
 };
 
 
-// TODO: A way to delete passive sprites using search criterias for bounding box and leaf content using a boolean lambda
+// TODO: A way to delete passive sprites and models using search criterias for bounding box and leaf content using a boolean lambda
 class SpriteWorldImpl {
 class SpriteWorldImpl {
 public:
 public:
 	// World
 	// World
 	OrthoSystem ortho;
 	OrthoSystem ortho;
-	// Sprites that rarely change and can be stored in a background image. (no animations allowed)
-	// TODO: Don't store the position twice, by keeping it separate from the SpriteInstance struct.
+	// Having one passive and one active collection per member type allow packing elements tighter to reduce cache misses.
+	//   It also allow executing rendering sorted by which code has to be fetched into the instruction cache.
+	// Sprites that rarely change and can be stored in a background image.
 	Octree<SpriteInstance> passiveSprites;
 	Octree<SpriteInstance> passiveSprites;
+	// Rarely moved models can be rendered using free rotation and uniform scaling to the background image.
+	Octree<ModelInstance> passiveModels;
 	// Temporary things are deleted when spriteWorld_clearTemporary is called
 	// Temporary things are deleted when spriteWorld_clearTemporary is called
 	List<SpriteInstance> temporarySprites;
 	List<SpriteInstance> temporarySprites;
 	List<ModelInstance> temporaryModels;
 	List<ModelInstance> temporaryModels;
@@ -518,7 +566,7 @@ private:
 	CubeMapF32 temporaryShadowMap;
 	CubeMapF32 temporaryShadowMap;
 public:
 public:
 	SpriteWorldImpl(const OrthoSystem &ortho, int shadowResolution)
 	SpriteWorldImpl(const OrthoSystem &ortho, int shadowResolution)
-	: ortho(ortho), passiveSprites(ortho_miniUnitsPerTile * 64), shadowResolution(shadowResolution), temporaryShadowMap(shadowResolution) {}
+	: ortho(ortho), passiveSprites(ortho_miniUnitsPerTile * 64), passiveModels(ortho_miniUnitsPerTile * 64), shadowResolution(shadowResolution), temporaryShadowMap(shadowResolution) {}
 public:
 public:
 	void updateBlockAt(const IRect& blockRegion, const IRect& seenRegion) {
 	void updateBlockAt(const IRect& blockRegion, const IRect& seenRegion) {
 		int unusedBlockIndex = -1;
 		int unusedBlockIndex = -1;
@@ -532,7 +580,7 @@ public:
 					if (currentBlockPtr->worldRegion.left() == blockRegion.left() && currentBlockPtr->worldRegion.top() == blockRegion.top()) {
 					if (currentBlockPtr->worldRegion.left() == blockRegion.left() && currentBlockPtr->worldRegion.top() == blockRegion.top()) {
 						// Update if needed
 						// Update if needed
 						if (currentBlockPtr->state == BlockState::Dirty) {
 						if (currentBlockPtr->state == BlockState::Dirty) {
-							currentBlockPtr->update(this->passiveSprites, blockRegion, this->ortho.view[this->cameraIndex]);
+							currentBlockPtr->update(this->passiveSprites, this->passiveModels, blockRegion, this->ortho.view[this->cameraIndex]);
 						}
 						}
 						// Use the block
 						// Use the block
 						return;
 						return;
@@ -559,10 +607,10 @@ public:
 		// If none of them matched, we should've passed by any unused block already
 		// If none of them matched, we should've passed by any unused block already
 		if (unusedBlockIndex > -1) {
 		if (unusedBlockIndex > -1) {
 			// We have a block to reuse
 			// We have a block to reuse
-			this->backgroundBlocks[unusedBlockIndex].update(this->passiveSprites, blockRegion, this->ortho.view[this->cameraIndex]);
+			this->backgroundBlocks[unusedBlockIndex].update(this->passiveSprites, this->passiveModels, blockRegion, this->ortho.view[this->cameraIndex]);
 		} else {
 		} else {
 			// Create a new block
 			// Create a new block
-			this->backgroundBlocks.pushConstruct(this->passiveSprites, blockRegion, this->ortho.view[this->cameraIndex]);
+			this->backgroundBlocks.pushConstruct(this->passiveSprites, this->passiveModels, blockRegion, this->ortho.view[this->cameraIndex]);
 		}
 		}
 	}
 	}
 	void invalidateBlockAt(int left, int top) {
 	void invalidateBlockAt(int left, int top) {
@@ -632,6 +680,7 @@ public:
 		}
 		}
 	}
 	}
 public:
 public:
+	// modifiedRegion is given in pixels relative to the world origin for the current camera angle
 	void updatePassiveRegion(const IRect& modifiedRegion) {
 	void updatePassiveRegion(const IRect& modifiedRegion) {
 		int64_t roundedLeft = roundDown(modifiedRegion.left(), BackgroundBlock::blockSize);
 		int64_t roundedLeft = roundDown(modifiedRegion.left(), BackgroundBlock::blockSize);
 		int64_t roundedTop = roundDown(modifiedRegion.top(), BackgroundBlock::blockSize);
 		int64_t roundedTop = roundDown(modifiedRegion.top(), BackgroundBlock::blockSize);
@@ -690,7 +739,8 @@ public:
 				startTime = time_getSeconds();
 				startTime = time_getSeconds();
 				this->temporaryShadowMap.clear();
 				this->temporaryShadowMap.clear();
 				// Shadows from background sprites
 				// Shadows from background sprites
-				currentLight->renderSpriteShadows(this->temporaryShadowMap, this->passiveSprites, ortho.view[this->cameraIndex].normalToWorldSpace);
+				currentLight->renderPassiveShadows(this->temporaryShadowMap, this->passiveSprites, ortho.view[this->cameraIndex].normalToWorldSpace);
+				currentLight->renderPassiveShadows(this->temporaryShadowMap, this->passiveModels, ortho.view[this->cameraIndex].normalToWorldSpace);
 				// Shadows from temporary sprites
 				// Shadows from temporary sprites
 				for (int s = 0; s < this->temporarySprites.length(); s++) {
 				for (int s = 0; s < this->temporarySprites.length(); s++) {
 					currentLight->renderSpriteShadow(this->temporaryShadowMap, this->temporarySprites[s], ortho.view[this->cameraIndex].normalToWorldSpace);
 					currentLight->renderSpriteShadow(this->temporaryShadowMap, this->temporarySprites[s], ortho.view[this->cameraIndex].normalToWorldSpace);
@@ -734,7 +784,6 @@ void spriteWorld_addBackgroundSprite(SpriteWorld& world, const SpriteInstance& s
 	IRect region = IRect(upperLeft.x, upperLeft.y, image_getWidth(frame->colorImage), image_getHeight(frame->colorImage));
 	IRect region = IRect(upperLeft.x, upperLeft.y, image_getWidth(frame->colorImage), image_getHeight(frame->colorImage));
 	world->updatePassiveRegion(region);
 	world->updatePassiveRegion(region);
 }
 }
-
 void spriteWorld_addTemporarySprite(SpriteWorld& world, const SpriteInstance& sprite) {
 void spriteWorld_addTemporarySprite(SpriteWorld& world, const SpriteInstance& sprite) {
 	MUST_EXIST(world, spriteWorld_addTemporarySprite);
 	MUST_EXIST(world, spriteWorld_addTemporarySprite);
 	if (sprite.typeIndex < 0 || sprite.typeIndex >= spriteTypes.length()) { throwError(U"Sprite type index ", sprite.typeIndex, " is out of bound!\n"); }
 	if (sprite.typeIndex < 0 || sprite.typeIndex >= spriteTypes.length()) { throwError(U"Sprite type index ", sprite.typeIndex, " is out of bound!\n"); }
@@ -742,6 +791,53 @@ void spriteWorld_addTemporarySprite(SpriteWorld& world, const SpriteInstance& sp
 	world->temporarySprites.push(sprite);
 	world->temporarySprites.push(sprite);
 }
 }
 
 
+static void transformCorners(const FVector3D& minBound, const FVector3D& maxBound, const Transform3D& transform, FVector3D* resultCorners) {
+	resultCorners[0] = transform.transformPoint(FVector3D(minBound.x, minBound.y, minBound.z));
+	resultCorners[1] = transform.transformPoint(FVector3D(maxBound.x, minBound.y, minBound.z));
+	resultCorners[2] = transform.transformPoint(FVector3D(minBound.x, maxBound.y, minBound.z));
+	resultCorners[3] = transform.transformPoint(FVector3D(maxBound.x, maxBound.y, minBound.z));
+	resultCorners[4] = transform.transformPoint(FVector3D(minBound.x, minBound.y, maxBound.z));
+	resultCorners[5] = transform.transformPoint(FVector3D(maxBound.x, minBound.y, maxBound.z));
+	resultCorners[6] = transform.transformPoint(FVector3D(minBound.x, maxBound.y, maxBound.z));
+	resultCorners[7] = transform.transformPoint(FVector3D(maxBound.x, maxBound.y, maxBound.z));
+}
+
+void spriteWorld_addBackgroundModel(SpriteWorld& world, const ModelInstance& instance) {
+	MUST_EXIST(world, spriteWorld_addBackgroundModel);
+	if (instance.typeIndex < 0 || instance.typeIndex >= modelTypes.length()) { throwError(U"Model type index ", instance.typeIndex, " is out of bound!\n"); }
+	// Get the origin and outer bounds
+	ModelType *type = &(modelTypes[instance.typeIndex]);
+	// Create a transform for global pixels
+	Transform3D worldToGlobalPixels = combineWorldToScreenTransform(world->ortho.view[world->cameraIndex].worldSpaceToScreenDepth, FVector2D());
+	FVector3D transformedCorners[8];
+	transformCorners(type->visibleModel->minBound, type->visibleModel->maxBound, instance.location, transformedCorners);
+	// World-space bound
+	IVector3D worldModelOrigin = ortho_floatingTileToMini(instance.location.position);
+	IVector3D worldMinBound = worldModelOrigin;
+	IVector3D worldMaxBound = worldModelOrigin;
+	// Screen bound
+	FVector3D globalPixelOrigin = worldToGlobalPixels.transformPoint(instance.location.position);
+	IVector2D globalPixelMinBound = IVector2D((int32_t)floor(globalPixelOrigin.x), (int32_t)floor(globalPixelOrigin.y));
+	IVector2D globalPixelMaxBound = globalPixelMinBound;
+	for (int c = 0; c < 8; c++) {
+		FVector3D miniSpaceCorner = transformedCorners[c] * (float)ortho_miniUnitsPerTile;
+		replaceWithSmaller(worldMinBound.x, (int32_t)floor(miniSpaceCorner.x));
+		replaceWithSmaller(worldMinBound.y, (int32_t)floor(miniSpaceCorner.y));
+		replaceWithSmaller(worldMinBound.z, (int32_t)floor(miniSpaceCorner.z));
+		replaceWithLarger(worldMaxBound.x, (int32_t)ceil(miniSpaceCorner.x));
+		replaceWithLarger(worldMaxBound.y, (int32_t)ceil(miniSpaceCorner.y));
+		replaceWithLarger(worldMaxBound.z, (int32_t)ceil(miniSpaceCorner.z));
+		FVector3D globalPixelSpaceCorner = worldToGlobalPixels.transformPoint(transformedCorners[c]);
+		replaceWithSmaller(globalPixelMinBound.x, (int32_t)floor(globalPixelSpaceCorner.x));
+		replaceWithSmaller(globalPixelMinBound.y, (int32_t)floor(globalPixelSpaceCorner.y));
+		replaceWithLarger(globalPixelMaxBound.x, (int32_t)ceil(globalPixelSpaceCorner.x));
+		replaceWithLarger(globalPixelMaxBound.y, (int32_t)ceil(globalPixelSpaceCorner.y));
+	}
+	// Add the passive sprite to the octree
+	world->passiveModels.insert(instance, worldModelOrigin, worldMinBound, worldMaxBound);
+	// Find the affected passive region and make it dirty
+	world->updatePassiveRegion(IRect(globalPixelMinBound.x, globalPixelMinBound.y, globalPixelMaxBound.x - globalPixelMinBound.x, globalPixelMaxBound.y - globalPixelMinBound.y));
+}
 void spriteWorld_addTemporaryModel(SpriteWorld& world, const ModelInstance& instance) {
 void spriteWorld_addTemporaryModel(SpriteWorld& world, const ModelInstance& instance) {
 	MUST_EXIST(world, spriteWorld_addTemporaryModel);
 	MUST_EXIST(world, spriteWorld_addTemporaryModel);
 	// Add the temporary model
 	// Add the temporary model
@@ -832,19 +928,11 @@ static IRect boundFromVertex(const FVector3D& screenProjection) {
 
 
 // Returns true iff the box might be seen using a pessimistic test
 // Returns true iff the box might be seen using a pessimistic test
 static IRect boundingBoxToRectangle(const FVector3D& minBound, const FVector3D& maxBound, const Transform3D& objectToScreenSpace) {
 static IRect boundingBoxToRectangle(const FVector3D& minBound, const FVector3D& maxBound, const Transform3D& objectToScreenSpace) {
-	FVector3D points[8] = {
-	  FVector3D(minBound.x, minBound.y, minBound.z),
-	  FVector3D(maxBound.x, minBound.y, minBound.z),
-	  FVector3D(minBound.x, maxBound.y, minBound.z),
-	  FVector3D(maxBound.x, maxBound.y, minBound.z),
-	  FVector3D(minBound.x, minBound.y, maxBound.z),
-	  FVector3D(maxBound.x, minBound.y, maxBound.z),
-	  FVector3D(minBound.x, maxBound.y, maxBound.z),
-	  FVector3D(maxBound.x, maxBound.y, maxBound.z)
-	};
-	IRect result = boundFromVertex(objectToScreenSpace.transformPoint(points[0]));
+	FVector3D points[8];
+	transformCorners(minBound, maxBound, objectToScreenSpace, points);
+	IRect result = boundFromVertex(points[0]);
 	for (int p = 1; p < 8; p++) {
 	for (int p = 1; p < 8; p++) {
-		result = IRect::merge(result, boundFromVertex(objectToScreenSpace.transformPoint(points[p])));
+		result = IRect::merge(result, boundFromVertex(points[p]));
 	}
 	}
 	return result;
 	return result;
 }
 }
@@ -880,30 +968,6 @@ static FVector3D getAverageNormal(const Model& model, int part, int poly) {
 	return normalize(normalSum);
 	return normalize(normalSum);
 }
 }
 
 
-struct DenseTriangle {
-public:
-	FVector3D colorA, colorB, colorC, posA, posB, posC, normalA, normalB, normalC;
-public:
-	DenseTriangle() {}
-	DenseTriangle(
-	  const FVector3D& colorA, const FVector3D& colorB, const FVector3D& colorC,
-	  const FVector3D& posA, const FVector3D& posB, const FVector3D& posC,
-	  const FVector3D& normalA, const FVector3D& normalB, const FVector3D& normalC)
-	  : colorA(colorA), colorB(colorB), colorC(colorC),
-	    posA(posA), posB(posB), posC(posC),
-	    normalA(normalA), normalB(normalB), normalC(normalC) {}
-};
-// The raw format for dense models using vertex colors instead of textures
-// Due to the high number of triangles, indexing positions would cause a lot of cache misses
-struct DenseModelImpl {
-public:
-	Array<DenseTriangle> triangles;
-	FVector3D minBound, maxBound;
-public:
-	// Optimize an existing model
-	DenseModelImpl(const Model& original);
-};
-
 DenseModel DenseModel_create(const Model& original) {
 DenseModel DenseModel_create(const Model& original) {
 	return std::make_shared<DenseModelImpl>(original);
 	return std::make_shared<DenseModelImpl>(original);
 }
 }
@@ -976,7 +1040,7 @@ DenseModelImpl::DenseModelImpl(const Model& original)
 template <bool HIGH_QUALITY>
 template <bool HIGH_QUALITY>
 static IRect renderDenseModel(const DenseModel& model, OrthoView view, ImageF32 depthBuffer, ImageRgbaU8 diffuseTarget, ImageRgbaU8 normalTarget, const FVector2D& worldOrigin, const Transform3D& modelToWorldSpace) {
 static IRect renderDenseModel(const DenseModel& model, OrthoView view, ImageF32 depthBuffer, ImageRgbaU8 diffuseTarget, ImageRgbaU8 normalTarget, const FVector2D& worldOrigin, const Transform3D& modelToWorldSpace) {
 	// Combine position transforms
 	// Combine position transforms
-	Transform3D objectToScreenSpace = modelToWorldSpace * Transform3D(FVector3D(worldOrigin.x, worldOrigin.y, 0.0f), view.worldSpaceToScreenDepth);
+	Transform3D objectToScreenSpace = combineModelToScreenTransform(modelToWorldSpace, view.worldSpaceToScreenDepth, worldOrigin);
 	// Create a pessimistic 2D bound from the 3D bounding box
 	// Create a pessimistic 2D bound from the 3D bounding box
 	IRect pessimisticBound = boundingBoxToRectangle(model->minBound, model->maxBound, objectToScreenSpace);
 	IRect pessimisticBound = boundingBoxToRectangle(model->minBound, model->maxBound, objectToScreenSpace);
 	// Get the target image bound
 	// Get the target image bound

+ 1 - 0
Source/SDK/sandbox/sprite/spriteAPI.h

@@ -62,6 +62,7 @@ int spriteWorld_getModelTypeCount();
 
 
 SpriteWorld spriteWorld_create(OrthoSystem ortho, int shadowResolution);
 SpriteWorld spriteWorld_create(OrthoSystem ortho, int shadowResolution);
 void spriteWorld_addBackgroundSprite(SpriteWorld& world, const SpriteInstance& sprite);
 void spriteWorld_addBackgroundSprite(SpriteWorld& world, const SpriteInstance& sprite);
+void spriteWorld_addBackgroundModel(SpriteWorld& world, const ModelInstance& instance);
 void spriteWorld_addTemporarySprite(SpriteWorld& world, const SpriteInstance& sprite);
 void spriteWorld_addTemporarySprite(SpriteWorld& world, const SpriteInstance& sprite);
 void spriteWorld_addTemporaryModel(SpriteWorld& world, const ModelInstance& instance);
 void spriteWorld_addTemporaryModel(SpriteWorld& world, const ModelInstance& instance);