Model Credit: "Lowpoly fox" by Vlad Zaichyk is licensed under Creative Commons Attribution.
An EXILED plugin for SCP: Secret Laboratory that renders 3D meshes from OBJ files in the game world using Unity primitive toys (Quads, Spheres, Cylinders, Cubes).
- Plugin:
TriangleScpSlv6.0.0 - Author: Foibos
- Framework: net48 / EXILED 9.14.2
- License: CC-BY-SA 3.0 (required by EXILED)
- Build the project or grab the latest release.
- Copy
TriangleScpSl.dllintoEXILED/Plugins/. - Place your
.obj(and optional.mtl) files inEXILED/Plugins/BlenderModels/. - In Remote Admin, run the command to display or export your model.
This command applies the N-gon pipeline with V3 hierarchical parenting for the lowest possible primitive count. Falls back to V2 stretch clustering when no suitable visible parent exists. Adjust optimization parameters via
NGonConfig.TriangulateNGonV3 <model.obj> [planar threshold] [accuracy]For export, use
ExportSchematicNGonV3with the same pipeline.Alternative: Use
TriangulateNGonV2/ExportSchematicNGonV2for V2 stretch clustering and faster loading process (slightly more primitives but shallower hierarchy).
The plugin converts OBJ mesh faces into Unity primitives that can be spawned in SCP:SL.
Any convex polygon can be decomposed into parallelograms. A parallelogram can be rendered using Unity Quads by exploiting the SetParent deformation trick: a child primitive inherits the non-uniform scale of its invisible parent, producing a sheared shape.
The plugin applies several tricks to minimize the number of primitives needed:
| Optimization | What it does | Savings |
|---|---|---|
| N-gon processing | Works with original polygon faces instead of triangulating first | Use of N parallelograms for one N-gon |
| Coplanar face merging | Merges adjacent same-color faces on the same plane into larger polygons | Fewer, bigger faces to decompose |
| Rectangle detection | When a parallelogram has equal-length diagonals, uses 1 Quad instead of 2 | Up to 50% on box-like geometry |
| Primitive shape detection | Detects spheres, cylinders, cubes in the mesh and replaces them with 1 native primitive | Use of 1 primitive instead of parallelogram decomposition |
| Hidden tail optimization | Hides parallelogram tails inside solid material, avoiding extra primitives | ~10-30% on complex models |
| Stretch clustering (V2) | Groups similarly-oriented parallelograms under shared stretch primitives | Significant on dense meshes, but works as well on smaller ones |
| Hierarchical parenting (V3) | Parents visible quads onto other visible quads, eliminating invisible stretch primitives | Further reduction on top of V2, depends on geometry |
V1 (Exact): Each parallelogram = 2 Quads (base + visible child). Pixel-perfect.
V2 (Approximate): Groups parallelograms by angular similarity under shared "stretch" primitives. Fewer total primitives at the cost of tiny vertex error (configurable accuracy parameter).
V3 (Hierarchical): Extends V2 by parenting visible parallelograms onto other visible parallelograms, creating deeper transform hierarchies. Eliminates invisible stretch primitives when an existing visible quad provides a suitable parent transform. Lowest primitive count of all pipelines.
Every command can be run again while building to cancel, or run again after the model is visible to destroy it.
| Command | Pipeline | Description |
|---|---|---|
TriangulateNGonV3 <file> [planar] [accuracy] |
N-gon + V3 | Lowest primitive count. N-gon + hierarchical parenting. |
TriangulateNGonV2 <file> [planar] [accuracy] |
N-gon + V2 | N-gon decomposition with stretch clustering. |
TriangulateNGon <file> [planar] |
N-gon + V1 | N-gon decomposition with exact parallelograms. |
TriangulateV3 <file> [accuracy] |
Triangle + V3 | Triangle-based with hierarchical parenting. |
TriangulateV2 <file> [accuracy] |
Triangle + V2 | Triangle-based with stretch clustering. Simpler, higher primitive count. |
Triangulate <file> |
Triangle + V1 | Simplest pipeline. Triangle-based, exact parallelograms. |
| Command | Pipeline |
|---|---|
ExportSchematicNGonV3 <file> <output> [planar] [accuracy] |
N-gon + V3 |
ExportSchematicNGonV2 <file> <output> [planar] [accuracy] |
N-gon + V2 |
ExportSchematicNGon <file> <output> [planar] |
N-gon + V1 |
ExportSchematicV3 <file> <output> [accuracy] |
Triangle + V3 |
ExportSchematicV2 <file> <output> [accuracy] |
Triangle + V2 |
ExportSchematic <file> <output> |
Triangle + V1 |
| Command | Description |
|---|---|
NGonConfig [property] [value] |
View or change session config for the N-gon pipeline (see below). |
TriangleExample |
Spawns a random triangle with colored vertex markers. |
TestParallelograms [count] |
Visualizes V2 stretch clustering with random parallelograms. |
TestNGons <file> [stage] |
Visualizes N-gon pipeline stages (0=raw, 1=merged, 2=convex, 3=parallelograms). |
The NGon pipeline is configured through NGonModelConfig, a session-scoped config that persists until the server restarts. Use the NGonConfig RA command to view and change values at runtime. All subsequent NGon commands (TriangulateNGon, TriangulateNGonV2, and their export variants) will use
the updated defaults.
NGonConfig -- list all current values
NGonConfig PlanarThreshold -- get a single value
NGonConfig PlanarThreshold 0.01 -- set a value for this session
| Property | Type | Default | Effect on Quality and Primitive Count |
|---|---|---|---|
UseHiddenTailOptimization |
bool | True |
Hides parallelogram tail triangles inside solid geometry. Saves ~10-30% primitives on complex models. |
DetectPrimitives |
bool | True |
Detects spheres, cylinders, cubes and replaces them with 1 native primitive each. |
Accuracy |
float | 0.001 |
Max vertex error (world units) for V2 stretch clustering. Lower = more precise but more primitives. Only affects V2 commands. |
SmoothMaxAngle |
float | 0.32 |
Max dihedral angle (radians, ~18 deg) between adjacent faces to consider a surface smooth. Controls how aggressively primitive shapes are detected. Higher = more shape detection. |
SmoothMinFraction |
float | 0.7 |
Minimum fraction of smooth edges required for a surface cluster to qualify for primitive detection. Lower = more lenient shape detection. |
UseEdgeWalkSampling |
bool | False |
Enables edge-walk sampling during hidden-tail verification. Catches pits/gaps between discrete sample points, so pits and holes in model will not beaccidentaly covered by HidingParallelogramTail. More accurate but MUCH slower. |
HiddenTailPullIn |
float | 0.03 |
Pull-in distance along normal for hidden-tail solid checks. Larger values produce lower primitives at the cost of model look getting "spiky". |
AllowNonPlanarNGons |
bool | False |
When enabled, non-planar n-gons are decomposed into parallelograms as-is instead of being fan-triangulated into planar triangles first. Reduces primitive count on models with many non-planar faces at the cost of geometric inaccuracy since the resulting parallelograms won't lie perfectly on the original surface. |
PlanarThreshold |
float | 0 |
Maximum vertex displacement when merging coplanar faces. 0 = exact only. Higher values (e.g. 0.0001 or 0.001) merge more aggressively, producing fewer larger polygons and fewer primitives, but with increasing this parameter too high, it can actually increase the amount of primitives, as it deforms the original model too much |
DeduplicateVertexThreshold |
float | 1E-04 |
Vertex deduplication distance. Vertices closer than this are merged. Rarely needs changing. |
DeduplicatePlaneDistThreshold |
float | 1E-04 |
Plane distance deduplication threshold for coplanar detection. Rarely needs changing. |
MaxMsPerFrame |
float | 8 |
Maximum milliseconds per frame before yielding in coroutine mode. Higher = faster loading but more lag. Lower = smoother but slower loading. |
To reduce primitive count (at the cost of accuracy):
- Enable
UseHiddenTailOptimizationandDetectPrimitivesif disabled - Increase
Accuracy(e.g.0.01) to allow more stretch reuse - Increase
HiddenTailPullIn(e.g.0.1) to get less primitives at the cost of more visible spikes on the model - Increase
PlanarThreshold(e.g.0.0001-0.001) to merge more coplanar faces (not that much of optimization) - Increase
SmoothMaxAngle(e.g.0.5) for more aggressive shape detection - Enable
AllowNonPlanarNGonsto keep large n-gons intact instead of splitting them (may cause heavy geometric inaccuracy)
To increase visual accuracy (at the cost of more primitives):
- Set
PlanarThresholdto0 - Enable
UseEdgeWalkSamplingfor hidden tail optimization to catch small pits and holes in the model (warning: very slow) - Decrease
Accuracy(e.g.0.0001- should be enough for any model) - Decrease
SmoothMaxAngleto avoid replacing low-poly geometry with smooth shapes
To speed up loading (at the cost of gameplay smoothness):
- Increase
MaxMsPerFrame(e.g.16or32)
Each subfolder with significant logic has its own README.md with detailed algorithm descriptions.
TriangleScpSl/
Plugin.cs, Config.cs - EXILED plugin entry point
Commands/ - Remote Admin commands
Core/
FileToTriangles/ - Simple OBJ parser (triangle-based, legacy)
ModelFactory/ - Factory for loading OBJ and creating models
Models/
ModelBase.cs - Abstract base with shared fields and methods
ApproximateModel/ - V2 stretch-clustering renderer
HierarchicalModel/ - V3 hierarchical parenting renderer
ExactModel/ - V1 exact parallelogram renderer
NGons/ - N-gon decomposition pipeline
Detectors/ - Primitive shape detection
Triangulation/
Triangle/ - Triangle data and decomposition into parallelograms
Parallelogram/ - Parallelogram data and the 2-Quad shearing primitive
ProjectMerExport/ - ProjectMER schematic JSON export
Paths/ - File path utilities
Runtime/ - Coroutine host
For programmatic use, the key entry points are:
// Load an OBJ with the full N-gon pipeline (recommended, coroutine)
NGonModelConfig config = NGonModelConfig.CreateFromSession();
yield return NGonModelBuilder.LoadCoroutine("model.obj", Color.white, result =>
{
// result.Parallelograms, result.DetectedPrimitives, result.NormalizedFileName
}, config);
NGonModelResult result = NGonModelBuilder.Load("model.obj", Color.white, config);
// Create models (V1 exact, V2 approximate, V3 hierarchical)
var exact = ExactModel.Create(result.Parallelograms, result.DetectedPrimitives, position);
var approx = ApproximateModel.Create(result.Parallelograms, result.DetectedPrimitives, position);
var hierarchical = HierarchicalModel.Create(result.Parallelograms, result.DetectedPrimitives, position);
// Or deferred (build in coroutine to avoid lag)
var model = HierarchicalModel.CreateDeferred(result.Parallelograms, result.DetectedPrimitives, position);
yield return model.BuildTrianglesCoroutine(PrimitiveFlags.Visible, batchSize: 64);
// Control
model.Position = newPos;
model.Rotation = newRot;
model.Scale = Vector3.one * 2f;
model.Color = Color.red;
model.Destroy();See Core/Models/HierarchicalModel/README.md, Core/Models/ApproximateModel/README.md, and Core/Models/ExactModel/README.md for full API details.
- EXILED — Creative Commons Attribution-ShareAlike 3.0 Unported
- Mirror Networking — MIT
- Unity Engine — Unity Companion License
If you find this plugin helpful and enjoy the work, please consider giving it a star on GitHub! ⭐ Stars are free, and they really motivate me to keep developing, optimizing, and improving the plugin. Your support means a lot!