Selaa lähdekoodia

Made it possible to mix deep sprites with freely rotated dynamic models.

David Piuva 5 vuotta sitten
vanhempi
sitoutus
7a067f7118

+ 25 - 0
Source/SDK/sandbox/sandbox.cpp

@@ -115,6 +115,7 @@ LATER:
 
 #include "../../DFPSR/includeFramework.h"
 #include "sprite/spriteAPI.h"
+#include "sprite/importer.h"
 #include "../../DFPSR/image/PackOrder.h"
 #include <assert.h>
 #include <limits>
@@ -123,6 +124,7 @@ using namespace dsr;
 
 static const String mediaPath = string_combine(U"media", file_separator());
 static const String imagePath = string_combine(mediaPath, U"images", file_separator());
+static const String modelPath = string_combine(mediaPath, U"models", file_separator());
 
 // Variables
 static bool running = true;
@@ -313,7 +315,13 @@ void sandbox_main() {
 	float profileFrameRate = 0.0f;
 	double maxFrameTime = 0.0, lastMaxFrameTime = 0.0; // Peak per second
 
+	// Load models
+	Model barrelVisible = importer_loadModel(modelPath + U"Barrel.ply", true, Transform3D());
+	importer_generateNormalsIntoTextureCoordinates(barrelVisible);
+	Model barrelShadow = importer_loadModel(modelPath + U"Barrel_Shadow.ply", true, Transform3D());
+
 	while(running) {
+		double timer = time_getSeconds();
 		double startTime;
 
 		// Execute actions
@@ -374,6 +382,23 @@ void sandbox_main() {
 		// Show the brush
 		spriteWorld_addTemporarySprite(world, brush);
 
+		// Test freely rotated models
+		/*Transform3D testLocation = Transform3D(
+		  FVector3D(0.0f, sin(timer) * 0.1f, 0.0f),
+		  FMatrix3x3(
+		    FVector3D(1.0f, 0.0f, 0.0f),
+		    FVector3D(0.0f, 1.0f, 0.0f),
+		    FVector3D(0.0f, 0.0f, 1.0f))
+		);*/
+		Transform3D testLocation = Transform3D(
+		  FVector3D(0.0f, sin(timer) * 0.1f, 0.0f),
+		  FMatrix3x3(
+		    FVector3D(cos(timer), 0.0f, sin(timer)),
+		    FVector3D(0.0f, 1.0f, 0.0f),
+		    FVector3D(-sin(timer), 0.0f, cos(timer)))
+		);
+		spriteWorld_addTemporaryModel(world, ModelInstance(barrelVisible, barrelShadow, testLocation));
+
 		// Draw the world
 		spriteWorld_draw(world, colorBuffer);
 

+ 346 - 0
Source/SDK/sandbox/sprite/importer.cpp

@@ -0,0 +1,346 @@
+
+#include "importer.h"
+
+namespace dsr {
+
+struct PlyProperty {
+	String name;
+	bool list;
+	int scale = 1; // 1 for normalized input, 255 for uchar
+	// Single property
+	PlyProperty(String name, ReadableString typeName) : name(name), list(false) {
+		if (string_caseInsensitiveMatch(typeName, U"UCHAR")) {
+			this->scale = 255;
+		} else {
+			this->scale = 1;
+		}
+	}
+	// List of properties
+	PlyProperty(String name, ReadableString typeName, ReadableString lengthTypeName) : name(name), list(true) {
+		if (string_caseInsensitiveMatch(typeName, U"UCHAR")) {
+			this->scale = 255;
+		} else {
+			this->scale = 1;
+		}
+		if (string_caseInsensitiveMatch(lengthTypeName, U"FLOAT")) {
+			printText("loadPlyModel: Using floating-point numbers to describe the length of a list is nonsense!\n");
+		}
+	}
+};
+struct PlyElement {
+	String name; // Name of the collection
+	int count; // Size of the collection
+	List<PlyProperty> properties; // Properties on each line (list properties consume additional tokens)
+	PlyElement(const String &name, int count) : name(name), count(count) {}
+};
+enum class PlyDataInput {
+	Ignore, Vertex, Face
+};
+static PlyDataInput PlyDataInputFromName(const ReadableString& name) {
+	if (string_caseInsensitiveMatch(name, U"VERTEX")) {
+		return PlyDataInput::Vertex;
+	} else if (string_caseInsensitiveMatch(name, U"FACE")) {
+		return PlyDataInput::Face;
+	} else {
+		return PlyDataInput::Ignore;
+	}
+}
+struct PlyVertex {
+	FVector3D position = FVector3D(0.0f, 0.0f, 0.0f);
+	FVector4D color = FVector4D(1.0f, 1.0f, 1.0f, 1.0f);
+};
+// When exporting PLY to this tool:
+//   +X is right
+//   +Y is up
+//   +Z is forward
+//   This coordinate system is left handed, which makes more sense when working with depth buffers.
+// If exporting from a right-handed editor, setting Y as up and Z as forward might flip the X axis to the left side.
+//   In that case, flip the X axis when calling this function.
+static void loadPlyModel(Model& targetModel, int targetPart, const ReadableString& content, bool flipX, Transform3D axisConversion) {
+	//printText("loadPlyModel:\n", content, "\n");
+	// Find the target model
+	int startPointIndex = model_getNumberOfPoints(targetModel);
+	// Split lines
+	List<String> lines = string_split(content, U'\n', true);
+	List<PlyElement> elements;
+	bool readingContent = false; // True after passing end_header
+	int elementIndex = -1; // current member of elements
+	int memberIndex = 0; // current data line within the content of the current element
+	PlyDataInput inputMode = PlyDataInput::Ignore;
+	// Temporary geometry
+	List<PlyVertex> vertices;
+	if (lines.length() < 2) {
+		printText("loadPlyModel: Failed to identify line-breaks in the PLY file!\n");
+		return;
+	} else if (!string_caseInsensitiveMatch(string_removeOuterWhiteSpace(lines[0]), U"PLY")) {
+		printText("loadPlyModel: Failed to identify the file as PLY!\n");
+		return;
+	} else if (!string_caseInsensitiveMatch(string_removeOuterWhiteSpace(lines[1]), U"FORMAT ASCII 1.0")) {
+		printText("loadPlyModel: Only supporting the ascii 1.0 format!\n");
+		return;
+	}
+	for (int l = 0; l < lines.length(); l++) {
+		// Tokenize the current line
+		List<String> tokens = string_split(lines[l], U' ');
+		if (tokens.length() > 0 && !string_caseInsensitiveMatch(tokens[0], U"COMMENT")) {
+			if (readingContent) {
+				// Parse geometry
+				if (inputMode == PlyDataInput::Vertex || inputMode == PlyDataInput::Face) {
+					// Create new vertex with default properties
+					if (inputMode == PlyDataInput::Vertex) {
+						vertices.push(PlyVertex());
+					}
+					PlyElement *currentElement = &(elements[elementIndex]);
+					int tokenIndex = 0;
+					for (int propertyIndex = 0; propertyIndex < currentElement->properties.length(); propertyIndex++) {
+						if (tokenIndex >= tokens.length()) {
+							printText("loadPlyModel: Undeclared properties given to ", currentElement->name, " in the data!\n");
+							break;
+						}
+						PlyProperty *currentProperty = &(currentElement->properties[propertyIndex]);
+						if (currentProperty->list) {
+							int listLength = string_toInteger(tokens[tokenIndex]);
+							tokenIndex++;
+							// Detect polygons
+							if (inputMode == PlyDataInput::Face && string_caseInsensitiveMatch(currentProperty->name, U"VERTEX_INDICES")) {
+								if (vertices.length() == 0) {
+									printText("loadPlyModel: This ply importer does not support feeding polygons before vertices! Using vertices before defining them would require an additional intermediate representation.\n");
+								}
+								bool flipSides = flipX;
+								if (listLength == 4) {
+									// Use a quad to save memory
+									int indexA = string_toInteger(tokens[tokenIndex]);
+									int indexB = string_toInteger(tokens[tokenIndex + 1]);
+									int indexC = string_toInteger(tokens[tokenIndex + 2]);
+									int indexD = string_toInteger(tokens[tokenIndex + 3]);
+									FVector4D colorA = vertices[indexA].color;
+									FVector4D colorB = vertices[indexB].color;
+									FVector4D colorC = vertices[indexC].color;
+									FVector4D colorD = vertices[indexD].color;
+									if (flipSides) {
+										int polygon = model_addQuad(targetModel, targetPart,
+										  startPointIndex + indexD,
+										  startPointIndex + indexC,
+										  startPointIndex + indexB,
+										  startPointIndex + indexA
+										);
+										model_setVertexColor(targetModel, targetPart, polygon, 0, colorD);
+										model_setVertexColor(targetModel, targetPart, polygon, 1, colorC);
+										model_setVertexColor(targetModel, targetPart, polygon, 2, colorB);
+										model_setVertexColor(targetModel, targetPart, polygon, 3, colorA);
+									} else {
+										int polygon = model_addQuad(targetModel, targetPart,
+										  startPointIndex + indexA,
+										  startPointIndex + indexB,
+										  startPointIndex + indexC,
+										  startPointIndex + indexD
+										);
+										model_setVertexColor(targetModel, targetPart, polygon, 0, colorA);
+										model_setVertexColor(targetModel, targetPart, polygon, 1, colorB);
+										model_setVertexColor(targetModel, targetPart, polygon, 2, colorC);
+										model_setVertexColor(targetModel, targetPart, polygon, 3, colorD);
+									}
+								} else {
+									// Polygon generating a triangle fan
+									int indexA = string_toInteger(tokens[tokenIndex]);
+									int indexB = string_toInteger(tokens[tokenIndex + 1]);
+									FVector4D colorA = vertices[indexA].color;
+									FVector4D colorB = vertices[indexB].color;
+									for (int i = 2; i < listLength; i++) {
+										int indexC = string_toInteger(tokens[tokenIndex + i]);
+										FVector4D colorC = vertices[indexC].color;
+										// Create a triangle
+										if (flipSides) {
+											int polygon = model_addTriangle(targetModel, targetPart,
+											  startPointIndex + indexC,
+											  startPointIndex + indexB,
+											  startPointIndex + indexA
+											);
+											model_setVertexColor(targetModel, targetPart, polygon, 0, colorC);
+											model_setVertexColor(targetModel, targetPart, polygon, 1, colorB);
+											model_setVertexColor(targetModel, targetPart, polygon, 2, colorA);
+										} else {
+											int polygon = model_addTriangle(targetModel, targetPart,
+											  startPointIndex + indexA,
+											  startPointIndex + indexB,
+											  startPointIndex + indexC
+											);
+											model_setVertexColor(targetModel, targetPart, polygon, 0, colorA);
+											model_setVertexColor(targetModel, targetPart, polygon, 1, colorB);
+											model_setVertexColor(targetModel, targetPart, polygon, 2, colorC);
+										}
+										// Iterate the triangle fan
+										indexB = indexC;
+										colorB = colorC;
+									}
+								}
+							}
+							tokenIndex += listLength;
+						} else {
+							// Detect vertex data
+							if (inputMode == PlyDataInput::Vertex) {
+								float value = string_toDouble(tokens[tokenIndex]) / (double)currentProperty->scale;
+								// Swap X, Y and Z to convert from PLY coordinates
+								if (string_caseInsensitiveMatch(currentProperty->name, U"X")) {
+									if (flipX) {
+										value = -value; // Right-handed to left-handed conversion
+									}
+									vertices[vertices.length() - 1].position.x = value;
+								} else if (string_caseInsensitiveMatch(currentProperty->name, U"Y")) {
+									vertices[vertices.length() - 1].position.y = value;
+								} else if (string_caseInsensitiveMatch(currentProperty->name, U"Z")) {
+									vertices[vertices.length() - 1].position.z = value;
+								} else if (string_caseInsensitiveMatch(currentProperty->name, U"RED")) {
+									vertices[vertices.length() - 1].color.x = value;
+								} else if (string_caseInsensitiveMatch(currentProperty->name, U"GREEN")) {
+									vertices[vertices.length() - 1].color.y = value;
+								} else if (string_caseInsensitiveMatch(currentProperty->name, U"BLUE")) {
+									vertices[vertices.length() - 1].color.z = value;
+								} else if (string_caseInsensitiveMatch(currentProperty->name, U"ALPHA")) {
+									vertices[vertices.length() - 1].color.w = value;
+								}
+							}
+						}
+						// Count one for a list size or single property
+						tokenIndex++;
+					}
+					// Complete the vertex
+					if (inputMode == PlyDataInput::Vertex) {
+						FVector3D localPosition = vertices[vertices.length() - 1].position;
+						model_addPoint(targetModel, axisConversion.transformPoint(localPosition));
+					}
+				}
+				memberIndex++;
+				if (memberIndex >= elements[elementIndex].count) {
+					// Done with the element
+					elementIndex++;
+					memberIndex = 0;
+					if (elementIndex >= elements.length()) {
+						// Done with the file
+						if (l < lines.length() - 1) {
+							// Remaining lines will be ignored with a warning
+							printText("loadPlyModel: Ignored ", (lines.length() - 1) - l, " undeclared lines at file end!\n");
+						}
+						return;
+					} else {
+						// Identify the next element by name
+						inputMode = PlyDataInputFromName(elements[elementIndex].name);
+					}
+				}
+			} else {
+				if (tokens.length() == 1) {
+					if (string_caseInsensitiveMatch(tokens[0], U"END_HEADER")) {
+						readingContent = true;
+						elementIndex = 0;
+						memberIndex = 0;
+						if (elements.length() < 2) {
+							printText("loadPlyModel: Need at least two elements to defined faces and vertices in the model!\n");
+							return;
+						}
+						// Identify the first element by name
+						inputMode = PlyDataInputFromName(elements[elementIndex].name);
+					}
+				} else if (tokens.length() >= 3) {
+					if (string_caseInsensitiveMatch(tokens[0], U"ELEMENT")) {
+						elements.push(PlyElement(tokens[1], string_toInteger(tokens[2])));
+						elementIndex = elements.length() - 1;
+					} else if (string_caseInsensitiveMatch(tokens[0], U"PROPERTY")) {
+						if (elementIndex < 0) {
+							printText("loadPlyModel: Cannot declare a property without an element!\n");
+						} else if (readingContent) {
+							printText("loadPlyModel: Cannot declare a property outside of the header!\n");
+						} else {
+							if (tokens.length() == 3) {
+								// Single property
+								elements[elementIndex].properties.push(PlyProperty(tokens[2], tokens[1]));
+							} else if (tokens.length() == 5 && string_caseInsensitiveMatch(tokens[1], U"LIST")) {
+								// Integer followed by that number of properties as a list
+								elements[elementIndex].properties.push(PlyProperty(tokens[4], tokens[3], tokens[2]));
+							} else {
+								printText("loadPlyModel: Unable to parse property!\n");
+								return;
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+}
+
+void importer_loadModel(Model& targetModel, int part, const ReadableString& filename, bool flipX, Transform3D axisConversion) {
+	int lastDotIndex = string_findLast(filename, U'.');
+	if (lastDotIndex == -1) {
+		printText("The model's filename ", filename, " does not have an extension!\n");
+	} else {
+		ReadableString extension = string_after(filename, lastDotIndex);
+		if (string_caseInsensitiveMatch(extension, U"PLY")) {
+			// Store the whole model file in a string for fast reading
+			String content = string_load(filename);
+			// Parse the file from the string
+			loadPlyModel(targetModel, part, content, flipX, axisConversion);
+		} else {
+			printText("The extension ", extension, " in ", filename, " is not yet supported! You can implement an importer and call it from the loadModel function in tool.cpp.\n");
+		}
+	}
+}
+
+Model importer_loadModel(const ReadableString& filename, bool flipX, Transform3D axisConversion) {
+	Model result = model_create();
+	model_addEmptyPart(result, U"Imported");
+	importer_loadModel(result, 0, filename, flipX, axisConversion);
+	return result;
+}
+
+static FVector3D normalFromPoints(const FVector3D& A, const FVector3D& B, const FVector3D& C) {
+    return normalize(crossProduct(B - A, C - A));
+}
+
+static FVector3D getAverageNormal(const Model& model, int part, int poly) {
+	int vertexCount = model_getPolygonVertexCount(model, part, poly);
+	FVector3D normalSum;
+	for (int t = 0; t < vertexCount - 2; t++) {
+		normalSum = normalSum + normalFromPoints(
+		  model_getVertexPosition(model, part, poly, 0),
+		  model_getVertexPosition(model, part, poly, t + 1),
+		  model_getVertexPosition(model, part, poly, t + 2)
+		);
+	}
+	return normalize(normalSum);
+}
+
+// TODO: Create a compact model format for dense vertex models where positions are stored as 16.16 fixed precision aligned with vertex data to avoid random access
+// TODO: Create a triangle rasterizer optimized for many small triangles by just adding edge offsets, normals and colors.
+// TODO: Allow creating freely rotated and scaled 3D models as a part of the passively drawn background.
+// TODO: Allow creating freely rotated and scaled 3D models as dynamic items of a slightly lower detail level.
+
+void importer_generateNormalsIntoTextureCoordinates(Model model) {
+	int pointCount = model_getNumberOfPoints(model);
+	Array<FVector3D> normalPoints(pointCount, FVector3D());
+	// Calculate smooth normals in object-space, by adding each polygon's normal to each child vertex
+	for (int part = 0; part < model_getNumberOfParts(model); part++) {
+		for (int poly = 0; poly < model_getNumberOfPolygons(model, part); poly++) {
+			FVector3D polygonNormal = getAverageNormal(model, part, poly);
+			for (int vert = 0; vert < model_getPolygonVertexCount(model, part, poly); vert++) {
+				int point = model_getVertexPointIndex(model, part, poly, vert);
+				normalPoints[point] = normalPoints[point] + polygonNormal;
+			}
+		}
+	}
+	// Normalize the result per vertex, to avoid having unbalanced weights when normalizing per pixel
+	for (int point = 0; point < pointCount; point++) {
+		normalPoints[point] = normalize(normalPoints[point]);
+	}
+	// Store the resulting normals packed as texture coordinates
+	for (int part = 0; part < model_getNumberOfParts(model); part++) {
+		for (int poly = 0; poly < model_getNumberOfPolygons(model, part); poly++) {
+			for (int vert = 0; vert < model_getPolygonVertexCount(model, part, poly); vert++) {
+				int point = model_getVertexPointIndex(model, part, poly, vert);
+				FVector3D vertexNormal = normalPoints[point];
+				model_setTexCoord(model, part, poly, vert, FVector4D(vertexNormal.x, vertexNormal.y, vertexNormal.z, 0.0f));
+			}
+		}
+	}
+}
+
+}

+ 21 - 0
Source/SDK/sandbox/sprite/importer.h

@@ -0,0 +1,21 @@
+
+#ifndef DFPSR_IMPORTER
+#define DFPSR_IMPORTER
+
+#include "../../../DFPSR/includeFramework.h"
+
+namespace dsr {
+
+// Returning a new model
+Model importer_loadModel(const ReadableString& filename, bool flipX, Transform3D axisConversion);
+
+// In-place loading of a new part
+void importer_loadModel(Model& targetModel, int part, const ReadableString& filename, bool flipX, Transform3D axisConversion);
+
+// To be applied to visible models after importing to save space in the files
+// Side-effects: Generating smooth normals from polygon positions in model and packing the resulting (NX, NY, NZ) into (U1, V1, U2) texture coordinates
+void importer_generateNormalsIntoTextureCoordinates(Model model);
+
+}
+
+#endif

+ 69 - 74
Source/SDK/sandbox/sprite/spriteAPI.cpp

@@ -2,12 +2,15 @@
 #include "spriteAPI.h"
 #include "Octree.h"
 #include "DirtyRectangles.h"
+#include "importer.h"
 #include "../../../DFPSR/render/ITriangle2D.h"
 
 // Comment out a flag to disable an optimization when debugging
 #define DIRTY_RECTANGLE_OPTIMIZATION
 
 namespace dsr {
+	
+static IRect renderModel(Model model, OrthoView view, ImageF32 depthBuffer, ImageRgbaU8 diffuseTarget, ImageRgbaU8 normalTarget, FVector2D worldOrigin, Transform3D modelToWorldSpace);
 
 struct SpriteConfig {
 	int centerX, centerY; // The sprite's origin in pixels relative to the upper left corner
@@ -212,27 +215,20 @@ static int getSpriteFrameIndex(const Sprite& sprite, OrthoView view) {
 }
 
 // Returns a 2D bounding box of affected target pixels
-IRect drawSprite(const Sprite& sprite, const OrthoView& ortho, const IVector2D& worldCenter, ImageF32 targetHeight, ImageRgbaU8 targetColor, ImageRgbaU8 targetNormal) {
+static IRect drawSprite(const Sprite& sprite, const OrthoView& ortho, const IVector2D& worldCenter, ImageF32 targetHeight, ImageRgbaU8 targetColor, ImageRgbaU8 targetNormal) {
 	int frameIndex = getSpriteFrameIndex(sprite, ortho);
 	const SpriteFrame* frame = &types[sprite.typeIndex].frames[frameIndex];
 	IVector2D screenSpace = ortho.miniTilePositionToScreenPixel(sprite.location, worldCenter) - frame->centerPoint;
 	float heightOffset = sprite.location.y * ortho_tilesPerMiniUnit;
-	if (image_exists(targetColor)) {
-		if (image_exists(targetNormal)) {
-			draw_higher(targetHeight, frame->heightImage, targetColor, frame->colorImage, targetNormal, frame->normalImage, screenSpace.x, screenSpace.y, heightOffset);
-		} else {
-			draw_higher(targetHeight, frame->heightImage, targetColor, frame->colorImage, screenSpace.x, screenSpace.y, heightOffset);
-		}
-	} else {
-		if (image_exists(targetNormal)) {
-			draw_higher(targetHeight, frame->heightImage, targetNormal, frame->normalImage, screenSpace.x, screenSpace.y, heightOffset);
-		} else {
-			draw_higher(targetHeight, frame->heightImage, screenSpace.x, screenSpace.y, heightOffset);
-		}
-	}
+	draw_higher(targetHeight, frame->heightImage, targetColor, frame->colorImage, targetNormal, frame->normalImage, screenSpace.x, screenSpace.y, heightOffset);
 	return IRect(screenSpace.x, screenSpace.y, image_getWidth(frame->colorImage), image_getHeight(frame->colorImage));
 }
 
+static IRect drawModel(const ModelInstance& instance, const OrthoView& ortho, const IVector2D& worldCenter, ImageF32 targetHeight, ImageRgbaU8 targetColor, ImageRgbaU8 targetNormal) {
+	// TODO: Get the model's bounding box to get a faster camera culling before letting renderModel do the more precise test using transformed geometry data
+	return renderModel(instance.visibleModel, ortho, targetHeight, targetColor, targetNormal, FVector2D(worldCenter.x, worldCenter.y), instance.location);
+}
+
 // The camera transform for each direction
 FMatrix3x3 ShadowCubeMapSides[6] = {
 	FMatrix3x3::makeAxisSystem(FVector3D( 1.0f, 0.0f, 0.0f), FVector3D(0.0f, 1.0f, 0.0f)),
@@ -281,6 +277,18 @@ public:
 	PointLight(FVector3D position, float radius, float intensity, ColorRgbI32 color, bool shadowCasting)
 	: position(position), radius(radius), intensity(intensity), color(color), shadowCasting(shadowCasting) {}
 public:
+	void renderModelShadow(CubeMapF32& shadowTarget, const ModelInstance& instance, const FMatrix3x3& normalToWorld) const {
+		Model model = instance.shadowModel;
+		if (model_exists(model)) {
+			// Place the model relative to the light source's position, to make rendering in light-space easier
+			Transform3D modelToWorldTransform = instance.location;
+			modelToWorldTransform.position = modelToWorldTransform.position - this->position;
+			for (int s = 0; s < 6; s++) {
+				Camera camera = Camera::createPerspective(Transform3D(FVector3D(), ShadowCubeMapSides[s] * normalToWorld), shadowTarget.resolution, shadowTarget.resolution);
+				model_renderDepth(model, modelToWorldTransform, shadowTarget.cubeMapViews[s], camera);
+			}
+		}
+	}
 	void renderSpriteShadow(CubeMapF32& shadowTarget, const Sprite& sprite, const FMatrix3x3& normalToWorld) const {
 		if (sprite.shadowCasting) {
 			Model model = types[sprite.typeIndex].shadowModel;
@@ -453,6 +461,7 @@ public:
 	Octree<Sprite> passiveSprites;
 	// Temporary things are deleted when spriteWorld_clearTemporary is called
 	List<Sprite> temporarySprites;
+	List<ModelInstance> temporaryModels;
 	List<PointLight> temporaryPointLights;
 	List<DirectedLight> temporaryDirectedLights;
 	// View
@@ -582,6 +591,10 @@ public:
 			IRect drawnRegion = drawSprite(this->temporarySprites[s], this->ortho.view[this->cameraIndex], -seenRegion.upperLeft(), heightTarget, diffuseTarget, normalTarget);
 			this->dirtyBackground.makeRegionDirty(drawnRegion);
 		}
+		for (int s = 0; s < this->temporaryModels.length(); s++) {
+			IRect drawnRegion = drawModel(this->temporaryModels[s], this->ortho.view[this->cameraIndex], -seenRegion.upperLeft(), heightTarget, diffuseTarget, normalTarget);
+			this->dirtyBackground.makeRegionDirty(drawnRegion);
+		}
 	}
 public:
 	void updatePassiveRegion(const IRect& modifiedRegion) {
@@ -641,10 +654,16 @@ public:
 			if (currentLight->shadowCasting) {
 				startTime = time_getSeconds();
 				this->temporaryShadowMap.clear();
+				// Shadows from background sprites
 				currentLight->renderSpriteShadows(this->temporaryShadowMap, this->passiveSprites, ortho.view[this->cameraIndex].normalToWorldSpace);
+				// Shadows from temporary sprites
 				for (int s = 0; s < this->temporarySprites.length(); s++) {
 					currentLight->renderSpriteShadow(this->temporaryShadowMap, this->temporarySprites[s], ortho.view[this->cameraIndex].normalToWorldSpace);
 				}
+				// Shadows from temporary models
+				for (int s = 0; s < this->temporaryModels.length(); s++) {
+					currentLight->renderModelShadow(this->temporaryShadowMap, this->temporaryModels[s], ortho.view[this->cameraIndex].normalToWorldSpace);
+				}
 				debugText("Cast point-light shadows: ", (time_getSeconds() - startTime) * 1000.0, " ms\n");
 			}
 			startTime = time_getSeconds();
@@ -697,6 +716,12 @@ void spriteWorld_addTemporarySprite(SpriteWorld& world, const Sprite& sprite) {
 	world->temporarySprites.push(sprite);
 }
 
+void spriteWorld_addTemporaryModel(SpriteWorld& world, const ModelInstance& instance) {
+	MUST_EXIST(world, spriteWorld_addTemporaryModel);
+	// Add the temporary model
+	world->temporaryModels.push(instance);
+}
+
 void spriteWorld_createTemporary_pointLight(SpriteWorld& world, const FVector3D position, float radius, float intensity, ColorRgbI32 color, bool shadowCasting) {
 	MUST_EXIST(world, spriteWorld_createTemporary_pointLight);
 	world->temporaryPointLights.pushConstruct(position, radius, intensity, color, shadowCasting);
@@ -710,6 +735,7 @@ void spriteWorld_createTemporary_directedLight(SpriteWorld& world, const FVector
 void spriteWorld_clearTemporary(SpriteWorld& world) {
 	MUST_EXIST(world, spriteWorld_clearTemporary);
 	world->temporarySprites.clear();
+	world->temporaryModels.clear();
 	world->temporaryPointLights.clear();
 	world->temporaryDirectedLights.clear();
 }
@@ -765,58 +791,6 @@ void spriteWorld_setCameraDirectionIndex(SpriteWorld& world, int index) {
 	}
 }
 
-static FVector3D normalFromPoints(const FVector3D& A, const FVector3D& B, const FVector3D& C) {
-    return normalize(crossProduct(B - A, C - A));
-}
-
-static FVector3D getAverageNormal(const Model& model, int part, int poly) {
-	int vertexCount = model_getPolygonVertexCount(model, part, poly);
-	FVector3D normalSum;
-	for (int t = 0; t < vertexCount - 2; t++) {
-		normalSum = normalSum + normalFromPoints(
-		  model_getVertexPosition(model, part, poly, 0),
-		  model_getVertexPosition(model, part, poly, t + 1),
-		  model_getVertexPosition(model, part, poly, t + 2)
-		);
-	}
-	return normalize(normalSum);
-}
-
-// TODO: Create a compact model format for dense vertex models where positions are stored as 16.16 fixed precision aligned with vertex data to avoid random access
-// TODO: Create a triangle rasterizer optimized for many small triangles by just adding edge offsets, normals and colors.
-// TODO: Allow creating freely rotated and scaled 3D models as a part of the passively drawn background.
-// TODO: Allow creating freely rotated and scaled 3D models as dynamic items of a slightly lower detail level.
-
-// Side-effects: Packing normals into texture coordinates of model
-static void computeObjectSpaceNormals(Model model) {
-	int pointCount = model_getNumberOfPoints(model);
-	Array<FVector3D> normalPoints(pointCount, FVector3D());
-	// Calculate smooth normals in object-space, by adding each polygon's normal to each child vertex
-	for (int part = 0; part < model_getNumberOfParts(model); part++) {
-		for (int poly = 0; poly < model_getNumberOfPolygons(model, part); poly++) {
-			FVector3D polygonNormal = getAverageNormal(model, part, poly);
-			for (int vert = 0; vert < model_getPolygonVertexCount(model, part, poly); vert++) {
-				int point = model_getVertexPointIndex(model, part, poly, vert);
-				normalPoints[point] = normalPoints[point] + polygonNormal;
-			}
-		}
-	}
-	// Normalize the result per vertex, to avoid having unbalanced weights when normalizing per pixel
-	for (int point = 0; point < pointCount; point++) {
-		normalPoints[point] = normalize(normalPoints[point]);
-	}
-	// Store the resulting normals packed as texture coordinates
-	for (int part = 0; part < model_getNumberOfParts(model); part++) {
-		for (int poly = 0; poly < model_getNumberOfPolygons(model, part); poly++) {
-			for (int vert = 0; vert < model_getPolygonVertexCount(model, part, poly); vert++) {
-				int point = model_getVertexPointIndex(model, part, poly, vert);
-				FVector3D vertexNormal = normalPoints[point];
-				model_setTexCoord(model, part, poly, vert, FVector4D(vertexNormal.x, vertexNormal.y, vertexNormal.z, 0.0f));
-			}
-		}
-	}
-}
-
 // Only because normals are stored as texture coordinates
 static FVector3D unpackNormals(FVector4D packedNormals) {
 	return FVector3D(packedNormals.x, packedNormals.y, packedNormals.z);
@@ -825,24 +799,38 @@ static FVector3D unpackNormals(FVector4D packedNormals) {
 // Pre-conditions:
 //   * All images must exist and have the same dimensions
 //   * All triangles in model must be contained within the image bounds after being projected using view
+// Post-condition:
+//   Returns the dirty pixel bound based on projected positions
 // worldOrigin is the perceived world's origin in target pixel coordinates
 // modelToWorldSpace is used to place the model freely in the world
-static void renderModel(Model model, OrthoView view, ImageF32 depthBuffer, ImageRgbaU8 diffuseTarget, ImageRgbaU8 normalTarget, FVector2D worldOrigin, Transform3D modelToWorldSpace) {
+static IRect renderModel(Model model, OrthoView view, ImageF32 depthBuffer, ImageRgbaU8 diffuseTarget, ImageRgbaU8 normalTarget, FVector2D worldOrigin, Transform3D modelToWorldSpace) {
 	int pointCount = model_getNumberOfPoints(model);
 	IRect clipBound = image_getBound(depthBuffer);
 
 	Array<FVector3D> projectedPoints(pointCount, FVector3D()); // pixel X, pixel Y, mini-tile height
 
 	// Combine transforms
-	FMatrix3x3 normalToWorldSpace = modelToWorldSpace.transform * view.normalToWorldSpace;
 	// TODO: Combine objectToWorldSpace with worldOrigin to get screen space directly
 	Transform3D objectToWorldSpace = modelToWorldSpace * Transform3D(FVector3D(), view.worldSpaceToScreenDepth);
 
-	// Transform positions
+	// Transform positions and return the dirty box
+	IRect dirtyBox = IRect(clipBound.width(), clipBound.height(), -clipBound.width(), -clipBound.height());
 	for (int point = 0; point < pointCount; point++) {
 		FVector3D projected = objectToWorldSpace.transformPoint(model_getPoint(model, point));
-		projectedPoints[point] = FVector3D(projected.x + worldOrigin.x, projected.y + worldOrigin.y, projected.z);
+		FVector3D screenProjection = FVector3D(projected.x + worldOrigin.x, projected.y + worldOrigin.y, projected.z);
+		projectedPoints[point] = screenProjection;
+		// Expand the dirty bound
+		dirtyBox = IRect::merge(dirtyBox, IRect((int)(screenProjection.x), (int)(screenProjection.y), 1, 1));
+	}
+
+	// Skip early if the culling test fails
+	if (!(IRect::cut(clipBound, dirtyBox).hasArea())) {
+		// Nothing drawn, no dirty rectangle
+		return IRect();
 	}
+	
+	// Combine normal transforms
+	FMatrix3x3 normalToWorldSpace = view.normalToWorldSpace;
 
 	// Render polygons as triangle fans
 	for (int part = 0; part < model_getNumberOfParts(model); part++) {
@@ -851,7 +839,10 @@ static void renderModel(Model model, OrthoView view, ImageF32 depthBuffer, Image
 			int vertA = 0;
 			FVector4D vertexColorA = model_getVertexColor(model, part, poly, vertA) * 255.0f;
 			int indexA = model_getVertexPointIndex(model, part, poly, vertA);
-			FVector3D normalA = normalToWorldSpace.transformTransposed(unpackNormals(model_getTexCoord(model, part, poly, vertA)));
+			
+			// TODO: Merge transforms into normalToWorldSpace in advance
+			FVector3D normalA = normalToWorldSpace.transformTransposed(modelToWorldSpace.transform.transform(unpackNormals(model_getTexCoord(model, part, poly, vertA))));
+			
 			FVector3D pointA = projectedPoints[indexA];
 			LVector2D subPixelA = LVector2D(safeRoundInt64(pointA.x * constants::unitsPerPixel), safeRoundInt64(pointA.y * constants::unitsPerPixel));
 			for (int vertB = 1; vertB < vertexCount - 1; vertB++) {
@@ -860,8 +851,11 @@ static void renderModel(Model model, OrthoView view, ImageF32 depthBuffer, Image
 				int indexC = model_getVertexPointIndex(model, part, poly, vertC);
 				FVector4D vertexColorB = model_getVertexColor(model, part, poly, vertB) * 255.0f;
 				FVector4D vertexColorC = model_getVertexColor(model, part, poly, vertC) * 255.0f;
-				FVector3D normalB = normalToWorldSpace.transformTransposed(unpackNormals(model_getTexCoord(model, part, poly, vertB)));
-				FVector3D normalC = normalToWorldSpace.transformTransposed(unpackNormals(model_getTexCoord(model, part, poly, vertC)));
+				
+				// TODO: Merge transforms into normalToWorldSpace in advance
+				FVector3D normalB = normalToWorldSpace.transformTransposed(modelToWorldSpace.transform.transform(unpackNormals(model_getTexCoord(model, part, poly, vertB))));
+				FVector3D normalC = normalToWorldSpace.transformTransposed(modelToWorldSpace.transform.transform(unpackNormals(model_getTexCoord(model, part, poly, vertC))));
+				
 				FVector3D pointB = projectedPoints[indexB];
 				FVector3D pointC = projectedPoints[indexC];
 				LVector2D subPixelB = LVector2D(safeRoundInt64(pointB.x * constants::unitsPerPixel), safeRoundInt64(pointB.y * constants::unitsPerPixel));
@@ -891,6 +885,7 @@ static void renderModel(Model model, OrthoView view, ImageF32 depthBuffer, Image
 			}
 		}
 	}
+	return dirtyBox;
 }
 
 void sprite_generateFromModel(ImageRgbaU8& targetAtlas, String& targetConfigText, const Model& visibleModel, const Model& shadowModel, const OrthoSystem& ortho, const String& targetPath, int cameraAngles) {
@@ -946,7 +941,7 @@ void sprite_generateFromModel(ImageRgbaU8& targetAtlas, String& targetConfigText
 		for (int a = 0; a < cameraAngles; a++) {
 			image_fill(depthBuffer, -1000000000.0f);
 			image_fill(colorImage[a], ColorRgbaI32(0, 0, 0, 0));
-			computeObjectSpaceNormals(visibleModel);
+			importer_generateNormalsIntoTextureCoordinates(visibleModel);
 			FVector2D origin = FVector2D((float)width * 0.5f, (float)height * 0.5f);
 			renderModel(visibleModel, ortho.view[a], depthBuffer, colorImage[a], normalImage[a], origin, Transform3D());
 			// Convert height into an 8 bit channel for saving

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

@@ -34,6 +34,21 @@ public:
 	: typeIndex(typeIndex), direction(direction), location(location), shadowCasting(shadowCasting) {}
 };
 
+// A 3D model that can be rotated freely
+//   To be rendered during game-play to allow free rotation
+struct ModelInstance {
+public:
+	Model visibleModel;
+	Model shadowModel;
+	Transform3D location; // 3D tile coordinates with translation and 3-axis rotation allowed
+public:
+	// The shadowCasting property is replaced by multiple constructors
+	ModelInstance(const Model& visibleModel, const Model& shadowModel, const Transform3D& location)
+	: visibleModel(visibleModel), shadowModel(shadowModel), location(location) {}
+	ModelInstance(const Model& visibleModel, const Transform3D& location)
+	: visibleModel(visibleModel), shadowModel(Model()), location(location) {}
+};
+
 class SpriteWorldImpl;
 using SpriteWorld = std::shared_ptr<SpriteWorldImpl>;
 
@@ -44,6 +59,7 @@ int sprite_getTypeCount();
 SpriteWorld spriteWorld_create(OrthoSystem ortho, int shadowResolution);
 void spriteWorld_addBackgroundSprite(SpriteWorld& world, const Sprite& sprite);
 void spriteWorld_addTemporarySprite(SpriteWorld& world, const Sprite& sprite);
+void spriteWorld_addTemporaryModel(SpriteWorld& world, const ModelInstance& instance);
 
 // Create a point light that only exists until the next call to spriteWorld_clearTemporary.
 //   position is in tile unit world-space.

+ 4 - 285
Source/SDK/sandbox/tool.cpp

@@ -10,6 +10,7 @@
 #include <functional>
 #include "../../DFPSR/includeFramework.h"
 #include "sprite/spriteAPI.h"
+#include "sprite/importer.h"
 
 using namespace dsr;
 
@@ -327,290 +328,6 @@ static void generateField(ParserState& state, Shape shape, const ImageU8& height
 	}
 }
 
-struct PlyProperty {
-	String name;
-	bool list;
-	int scale = 1; // 1 for normalized input, 255 for uchar
-	// Single property
-	PlyProperty(String name, ReadableString typeName) : name(name), list(false) {
-		if (string_caseInsensitiveMatch(typeName, U"UCHAR")) {
-			this->scale = 255;
-		} else {
-			this->scale = 1;
-		}
-	}
-	// List of properties
-	PlyProperty(String name, ReadableString typeName, ReadableString lengthTypeName) : name(name), list(true) {
-		if (string_caseInsensitiveMatch(typeName, U"UCHAR")) {
-			this->scale = 255;
-		} else {
-			this->scale = 1;
-		}
-		if (string_caseInsensitiveMatch(lengthTypeName, U"FLOAT")) {
-			printText("loadPlyModel: Using floating-point numbers to describe the length of a list is nonsense!\n");
-		}
-	}
-};
-struct PlyElement {
-	String name; // Name of the collection
-	int count; // Size of the collection
-	List<PlyProperty> properties; // Properties on each line (list properties consume additional tokens)
-	PlyElement(const String &name, int count) : name(name), count(count) {}
-};
-enum class PlyDataInput {
-	Ignore, Vertex, Face
-};
-static PlyDataInput PlyDataInputFromName(const ReadableString& name) {
-	if (string_caseInsensitiveMatch(name, U"VERTEX")) {
-		return PlyDataInput::Vertex;
-	} else if (string_caseInsensitiveMatch(name, U"FACE")) {
-		return PlyDataInput::Face;
-	} else {
-		return PlyDataInput::Ignore;
-	}
-}
-struct PlyVertex {
-	FVector3D position = FVector3D(0.0f, 0.0f, 0.0f);
-	FVector4D color = FVector4D(1.0f, 1.0f, 1.0f, 1.0f);
-};
-// When exporting PLY to this tool:
-//   +X is right
-//   +Y is up
-//   +Z is forward
-//   This coordinate system is left handed, which makes more sense when working with depth buffers.
-// If exporting from a right-handed editor, setting Y as up and Z as forward might flip the X axis to the left side.
-//   In that case, flip the X axis when calling this function.
-static void loadPlyModel(ParserState& state, const ReadableString& content, bool shadow, bool flipX) {
-	//printText("loadPlyModel:\n", content, "\n");
-	// Find the target model
-	Model targetModel = shadow ? state.shadow : state.model;
-	int startPointIndex = model_getNumberOfPoints(targetModel);
-	int targetPart = shadow ? 0 : state.part;
-	// Split lines
-	List<String> lines = string_split(content, U'\n', true);
-	List<PlyElement> elements;
-	bool readingContent = false; // True after passing end_header
-	int elementIndex = -1; // current member of elements
-	int memberIndex = 0; // current data line within the content of the current element
-	PlyDataInput inputMode = PlyDataInput::Ignore;
-	// Temporary geometry
-	List<PlyVertex> vertices;
-	if (lines.length() < 2) {
-		printText("loadPlyModel: Failed to identify line-breaks in the PLY file!\n");
-		return;
-	} else if (!string_caseInsensitiveMatch(string_removeOuterWhiteSpace(lines[0]), U"PLY")) {
-		printText("loadPlyModel: Failed to identify the file as PLY!\n");
-		return;
-	} else if (!string_caseInsensitiveMatch(string_removeOuterWhiteSpace(lines[1]), U"FORMAT ASCII 1.0")) {
-		printText("loadPlyModel: Only supporting the ascii 1.0 format!\n");
-		return;
-	}
-	for (int l = 0; l < lines.length(); l++) {
-		// Tokenize the current line
-		List<String> tokens = string_split(lines[l], U' ');
-		if (tokens.length() > 0 && !string_caseInsensitiveMatch(tokens[0], U"COMMENT")) {
-			if (readingContent) {
-				// Parse geometry
-				if (inputMode == PlyDataInput::Vertex || inputMode == PlyDataInput::Face) {
-					// Create new vertex with default properties
-					if (inputMode == PlyDataInput::Vertex) {
-						vertices.push(PlyVertex());
-					}
-					PlyElement *currentElement = &(elements[elementIndex]);
-					int tokenIndex = 0;
-					for (int propertyIndex = 0; propertyIndex < currentElement->properties.length(); propertyIndex++) {
-						if (tokenIndex >= tokens.length()) {
-							printText("loadPlyModel: Undeclared properties given to ", currentElement->name, " in the data!\n");
-							break;
-						}
-						PlyProperty *currentProperty = &(currentElement->properties[propertyIndex]);
-						if (currentProperty->list) {
-							int listLength = string_toInteger(tokens[tokenIndex]);
-							tokenIndex++;
-							// Detect polygons
-							if (inputMode == PlyDataInput::Face && string_caseInsensitiveMatch(currentProperty->name, U"VERTEX_INDICES")) {
-								if (vertices.length() == 0) {
-									printText("loadPlyModel: This ply importer does not support feeding polygons before vertices! Using vertices before defining them would require an additional intermediate representation.\n");
-								}
-								bool flipSides = flipX;
-								if (listLength == 4) {
-									// Use a quad to save memory
-									int indexA = string_toInteger(tokens[tokenIndex]);
-									int indexB = string_toInteger(tokens[tokenIndex + 1]);
-									int indexC = string_toInteger(tokens[tokenIndex + 2]);
-									int indexD = string_toInteger(tokens[tokenIndex + 3]);
-									FVector4D colorA = vertices[indexA].color;
-									FVector4D colorB = vertices[indexB].color;
-									FVector4D colorC = vertices[indexC].color;
-									FVector4D colorD = vertices[indexD].color;
-									if (flipSides) {
-										int polygon = model_addQuad(targetModel, targetPart,
-										  startPointIndex + indexD,
-										  startPointIndex + indexC,
-										  startPointIndex + indexB,
-										  startPointIndex + indexA
-										);
-										model_setVertexColor(targetModel, targetPart, polygon, 0, colorD);
-										model_setVertexColor(targetModel, targetPart, polygon, 1, colorC);
-										model_setVertexColor(targetModel, targetPart, polygon, 2, colorB);
-										model_setVertexColor(targetModel, targetPart, polygon, 3, colorA);
-									} else {
-										int polygon = model_addQuad(targetModel, targetPart,
-										  startPointIndex + indexA,
-										  startPointIndex + indexB,
-										  startPointIndex + indexC,
-										  startPointIndex + indexD
-										);
-										model_setVertexColor(targetModel, targetPart, polygon, 0, colorA);
-										model_setVertexColor(targetModel, targetPart, polygon, 1, colorB);
-										model_setVertexColor(targetModel, targetPart, polygon, 2, colorC);
-										model_setVertexColor(targetModel, targetPart, polygon, 3, colorD);
-									}
-								} else {
-									// Polygon generating a triangle fan
-									int indexA = string_toInteger(tokens[tokenIndex]);
-									int indexB = string_toInteger(tokens[tokenIndex + 1]);
-									FVector4D colorA = vertices[indexA].color;
-									FVector4D colorB = vertices[indexB].color;
-									for (int i = 2; i < listLength; i++) {
-										int indexC = string_toInteger(tokens[tokenIndex + i]);
-										FVector4D colorC = vertices[indexC].color;
-										// Create a triangle
-										if (flipSides) {
-											int polygon = model_addTriangle(targetModel, targetPart,
-											  startPointIndex + indexC,
-											  startPointIndex + indexB,
-											  startPointIndex + indexA
-											);
-											model_setVertexColor(targetModel, targetPart, polygon, 0, colorC);
-											model_setVertexColor(targetModel, targetPart, polygon, 1, colorB);
-											model_setVertexColor(targetModel, targetPart, polygon, 2, colorA);
-										} else {
-											int polygon = model_addTriangle(targetModel, targetPart,
-											  startPointIndex + indexA,
-											  startPointIndex + indexB,
-											  startPointIndex + indexC
-											);
-											model_setVertexColor(targetModel, targetPart, polygon, 0, colorA);
-											model_setVertexColor(targetModel, targetPart, polygon, 1, colorB);
-											model_setVertexColor(targetModel, targetPart, polygon, 2, colorC);
-										}
-										// Iterate the triangle fan
-										indexB = indexC;
-										colorB = colorC;
-									}
-								}
-							}
-							tokenIndex += listLength;
-						} else {
-							// Detect vertex data
-							if (inputMode == PlyDataInput::Vertex) {
-								float value = string_toDouble(tokens[tokenIndex]) / (double)currentProperty->scale;
-								// Swap X, Y and Z to convert from PLY coordinates
-								if (string_caseInsensitiveMatch(currentProperty->name, U"X")) {
-									if (flipX) {
-										value = -value; // Right-handed to left-handed conversion
-									}
-									vertices[vertices.length() - 1].position.x = value;
-								} else if (string_caseInsensitiveMatch(currentProperty->name, U"Y")) {
-									vertices[vertices.length() - 1].position.y = value;
-								} else if (string_caseInsensitiveMatch(currentProperty->name, U"Z")) {
-									vertices[vertices.length() - 1].position.z = value;
-								} else if (string_caseInsensitiveMatch(currentProperty->name, U"RED")) {
-									vertices[vertices.length() - 1].color.x = value;
-								} else if (string_caseInsensitiveMatch(currentProperty->name, U"GREEN")) {
-									vertices[vertices.length() - 1].color.y = value;
-								} else if (string_caseInsensitiveMatch(currentProperty->name, U"BLUE")) {
-									vertices[vertices.length() - 1].color.z = value;
-								} else if (string_caseInsensitiveMatch(currentProperty->name, U"ALPHA")) {
-									vertices[vertices.length() - 1].color.w = value;
-								}
-							}
-						}
-						// Count one for a list size or single property
-						tokenIndex++;
-					}
-					// Complete the vertex
-					if (inputMode == PlyDataInput::Vertex) {
-						FVector3D localPosition = vertices[vertices.length() - 1].position;
-						model_addPoint(targetModel, state.partSettings.location.transformPoint(localPosition));
-					}
-				}
-				memberIndex++;
-				if (memberIndex >= elements[elementIndex].count) {
-					// Done with the element
-					elementIndex++;
-					memberIndex = 0;
-					if (elementIndex >= elements.length()) {
-						// Done with the file
-						if (l < lines.length() - 1) {
-							// Remaining lines will be ignored with a warning
-							printText("loadPlyModel: Ignored ", (lines.length() - 1) - l, " undeclared lines at file end!\n");
-						}
-						return;
-					} else {
-						// Identify the next element by name
-						inputMode = PlyDataInputFromName(elements[elementIndex].name);
-					}
-				}
-			} else {
-				if (tokens.length() == 1) {
-					if (string_caseInsensitiveMatch(tokens[0], U"END_HEADER")) {
-						readingContent = true;
-						elementIndex = 0;
-						memberIndex = 0;
-						if (elements.length() < 2) {
-							printText("loadPlyModel: Need at least two elements to defined faces and vertices in the model!\n");
-							return;
-						}
-						// Identify the first element by name
-						inputMode = PlyDataInputFromName(elements[elementIndex].name);
-					}
-				} else if (tokens.length() >= 3) {
-					if (string_caseInsensitiveMatch(tokens[0], U"ELEMENT")) {
-						elements.push(PlyElement(tokens[1], string_toInteger(tokens[2])));
-						elementIndex = elements.length() - 1;
-					} else if (string_caseInsensitiveMatch(tokens[0], U"PROPERTY")) {
-						if (elementIndex < 0) {
-							printText("loadPlyModel: Cannot declare a property without an element!\n");
-						} else if (readingContent) {
-							printText("loadPlyModel: Cannot declare a property outside of the header!\n");
-						} else {
-							if (tokens.length() == 3) {
-								// Single property
-								elements[elementIndex].properties.push(PlyProperty(tokens[2], tokens[1]));
-							} else if (tokens.length() == 5 && string_caseInsensitiveMatch(tokens[1], U"LIST")) {
-								// Integer followed by that number of properties as a list
-								elements[elementIndex].properties.push(PlyProperty(tokens[4], tokens[3], tokens[2]));
-							} else {
-								printText("loadPlyModel: Unable to parse property!\n");
-								return;
-							}
-						}
-					}
-				}
-			}
-		}
-	}
-}
-
-static void loadModel(ParserState& state, const ReadableString& filename, bool shadow, bool flipX) {
-	int lastDotIndex = string_findLast(filename, U'.');
-	if (lastDotIndex == -1) {
-		printText("The model's filename ", filename, " does not have an extension!\n");
-	} else {
-		ReadableString extension = string_after(filename, lastDotIndex);
-		if (string_caseInsensitiveMatch(extension, U"PLY")) {
-			// Store the whole model file in a string for fast reading
-			String content = string_load(state.sourcePath + filename);
-			// Parse the file from the string
-			loadPlyModel(state, content, shadow, flipX);
-		} else {
-			printText("The extension ", extension, " in ", filename, " is not yet supported! You can implement an importer and call it from the loadModel function in tool.cpp.\n");
-		}
-	}
-}
-
 static void generateBasicShape(ParserState& state, Shape shape, const ReadableString& arg1, const ReadableString& arg2, const ReadableString& arg3, bool shadow) {
 	Transform3D system = state.partSettings.location;
 	Model model = shadow ? state.shadow : state.model;
@@ -701,7 +418,9 @@ static void parse_shape(ParserState& state, List<String>& args, bool shadow) {
 			printText("    Loading a model requires a filename.\n");
 		} else {
 			bool flipX = (shape == Shape::RightHandedModel);
-			loadModel(state, args[1], shadow, flipX);
+			Model targetModel = shadow ? state.shadow : state.model;
+			int targetPart = shadow ? 0 : state.part;
+			importer_loadModel(targetModel, targetPart, string_combine(state.sourcePath, args[1]), flipX, state.partSettings.location);
 		}
 	} else if (args.length() == 2) {
 		// Shape, HeightMap