You're breathtaking!

This commit is contained in:
Wojtek Figat
2020-12-07 23:40:54 +01:00
commit 6fb9eee74c
5143 changed files with 1153594 additions and 0 deletions

217
Source/Engine/CSG/Brush.h Normal file
View File

@@ -0,0 +1,217 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Types.h"
#include "Engine/Core/Types/Guid.h"
#include "Engine/Core/Math/Plane.h"
#include "Engine/Core/Math/AABB.h"
#include "Engine/Core/Math/Vector2.h"
class Scene;
namespace CSG
{
/// <summary>
/// CSG plane surface
/// </summary>
struct Surface : Plane
{
Guid Material;
Vector2 TexCoordScale;
Vector2 TexCoordOffset;
float TexCoordRotation;
float ScaleInLightmap;
public:
/// <summary>
/// Default constructor
/// </summary>
Surface()
: Plane()
, TexCoordScale(1)
, TexCoordOffset(0)
, TexCoordRotation(0)
, ScaleInLightmap(1)
{
}
Surface(const Plane& plane)
: Plane(plane)
, Material(Guid::Empty)
, TexCoordScale(1)
, TexCoordOffset(0)
, TexCoordRotation(0)
, ScaleInLightmap(1)
{
}
Surface(const Surface& plane)
: Plane(plane)
, Material(plane.Material)
, TexCoordScale(1)
, TexCoordOffset(0)
, TexCoordRotation(0)
, ScaleInLightmap(1)
{
}
Surface(const Vector3& normal, float d)
: Plane(normal, d)
, Material(Guid::Empty)
, TexCoordScale(1)
, TexCoordOffset(0)
, TexCoordRotation(0)
, ScaleInLightmap(1)
{
}
Surface(const Vector3& point1, const Vector3& point2, const Vector3& point3)
: Plane(point1, point2, point3)
, Material(Guid::Empty)
, TexCoordScale(1)
, TexCoordOffset(0)
, TexCoordRotation(0)
, ScaleInLightmap(1)
{
}
public:
static Vector3 Intersection(const Vector3& start, const Vector3& end, float sdist, float edist)
{
Vector3 vector = end - start;
float length = edist - sdist;
float delta = edist / length;
return end - (delta * vector);
}
Vector3 Intersection(const Vector3& start, const Vector3& end) const
{
return Intersection(start, end, Distance(start), Distance(end));
}
float Distance(float x, float y, float z) const
{
return
(
(Normal.X * x) +
(Normal.Y * y) +
(Normal.Z * z) -
(D)
);
}
float Distance(const Vector3& vertex) const
{
return
(
(Normal.X * vertex.X) +
(Normal.Y * vertex.Y) +
(Normal.Z * vertex.Z) -
(D)
);
}
static PlaneIntersectionType OnSide(float distance)
{
if (distance > DistanceEpsilon)
return PlaneIntersectionType::Front;
if (distance < -DistanceEpsilon)
return PlaneIntersectionType::Back;
return PlaneIntersectionType::Intersecting;
}
PlaneIntersectionType OnSide(float x, float y, float z) const
{
return OnSide(Distance(x, y, z));
}
PlaneIntersectionType OnSide(const Vector3& vertex) const
{
return OnSide(Distance(vertex));
}
PlaneIntersectionType OnSide(const AABB& box) const
{
Vector3 min;
Vector3 max;
max.X = static_cast<float>((Normal.X >= 0.0f) ? box.MinX : box.MaxX);
max.Y = static_cast<float>((Normal.Y >= 0.0f) ? box.MinY : box.MaxY);
max.Z = static_cast<float>((Normal.Z >= 0.0f) ? box.MinZ : box.MaxZ);
min.X = static_cast<float>((Normal.X >= 0.0f) ? box.MaxX : box.MinX);
min.Y = static_cast<float>((Normal.Y >= 0.0f) ? box.MaxY : box.MinY);
min.Z = static_cast<float>((Normal.Z >= 0.0f) ? box.MaxZ : box.MinZ);
float distance = Vector3::Dot(Normal, max) - D;
if (distance > DistanceEpsilon)
return PlaneIntersectionType::Front;
distance = Vector3::Dot(Normal, min) - D;
if (distance < -DistanceEpsilon)
return PlaneIntersectionType::Back;
return PlaneIntersectionType::Intersecting;
}
};
/// <summary>
/// CSG brush object
/// </summary>
class FLAXENGINE_API Brush
{
public:
/// <summary>
/// Gets the scene.
/// </summary>
/// <returns>Scene</returns>
virtual Scene* GetBrushScene() const = 0;
/// <summary>
/// Returns true if brush affects world
/// </summary>
/// <returns>True if use CSG brush during level geometry building</returns>
virtual bool CanUseCSG() const
{
return true;
}
/// <summary>
/// Gets the CSG brush object ID.
/// </summary>
/// <returns>The unique ID.</returns>
virtual Guid GetBrushID() const = 0;
/// <summary>
/// Gets CSG brush mode
/// </summary>
/// <returns>Mode</returns>
virtual Mode GetBrushMode() const = 0;
/// <summary>
/// Gets brush surfaces
/// </summary>
/// <param name="surfaces">Result list of surfaces</param>
virtual void GetSurfaces(Array<Surface, HeapAllocation>& surfaces) = 0;
/// <summary>
/// Gets brush surfaces amount
/// </summary>
/// <returns>The mount of the brush surfaces.</returns>
virtual int32 GetSurfacesCount() = 0;
public:
/// <summary>
/// Called when brush data gets modified (will request rebuilding in Editor).
/// </summary>
virtual void OnBrushModified();
};
};

View File

@@ -0,0 +1,29 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using System.IO;
using Flax.Build;
using Flax.Build.NativeCpp;
/// <summary>
/// CSG tool module.
/// </summary>
public class CSG : EngineModule
{
/// <inheritdoc />
public override void Setup(BuildOptions options)
{
base.Setup(options);
options.PrivateDependencies.Add("ContentImporters");
options.PublicDefinitions.Add("COMPILE_WITH_CSG_BUILDER");
}
/// <inheritdoc />
public override void GetFilesToDeploy(List<string> files)
{
files.Add(Path.Combine(FolderPath, "Brush.h"));
files.Add(Path.Combine(FolderPath, "Types.h"));
}
}

View File

@@ -0,0 +1,423 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "CSGBuilder.h"
#include "CSGMesh.h"
#include "CSGData.h"
#include "Engine/Level/Level.h"
#include "Engine/Level/SceneQuery.h"
#include "Engine/Level/Actor.h"
#include "Engine/Core/Log.h"
#include "Engine/Graphics/Models/ModelData.h"
#include "Engine/Content/Content.h"
#include "Engine/Content/Assets/Model.h"
#include "Engine/Physics/CollisionData.h"
#include "Engine/ContentImporters/AssetsImportingManager.h"
#include "Engine/ContentImporters/CreateCollisionData.h"
#include "Engine/Content/Assets/RawDataAsset.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Serialization/MemoryWriteStream.h"
#if USE_EDITOR
#include "Editor/Editor.h"
#endif
#if COMPILE_WITH_CSG_BUILDER
using namespace CSG;
// Enable/disable locking scene during building CSG brushes nodes
#define CSG_USE_SCENE_LOCKS 0
namespace CSGBuilderImpl
{
Array<Scene*> ScenesToRebuild;
void onSceneUnloading(Scene* scene, const Guid& sceneId);
void build(Scene* scene);
bool generateRawDataAsset(Scene* scene, RawData& meshData, Guid& assetId, const String& assetPath);
}
using namespace CSGBuilderImpl;
class CSGBuilderService : public EngineService
{
public:
CSGBuilderService()
: EngineService(TEXT("CSG Builder"), 90)
{
}
bool Init() override;
void Update() override;
};
CSGBuilderService CSGBuilderServiceInstance;
Delegate<Brush*> Builder::OnBrushModified;
void CSGBuilderImpl::onSceneUnloading(Scene* scene, const Guid& sceneId)
{
// Ensure to remove scene (prevent crashes)
ScenesToRebuild.Remove(scene);
}
bool CSGBuilderService::Init()
{
Level::SceneUnloading.Bind(onSceneUnloading);
return false;
}
void CSGBuilderService::Update()
{
// Check if build is pending
if (ScenesToRebuild.HasItems() && Engine::IsReady())
{
auto now = DateTime::NowUTC();
for (int32 i = 0; ScenesToRebuild.HasItems() && i < ScenesToRebuild.Count(); i++)
{
auto scene = ScenesToRebuild[i];
if (now - scene->CSGData.BuildTime >= 0)
{
scene->CSGData.BuildTime.Ticks = 0;
ScenesToRebuild.RemoveAt(i--);
build(scene);
}
}
}
}
bool Builder::IsActive()
{
return ScenesToRebuild.HasItems();
}
void Builder::Build(Scene* scene, float timeoutMs)
{
if (scene == nullptr)
return;
#if USE_EDITOR
// Disable building during play mode
if (Editor::IsPlayMode)
return;
#endif
// Register building
if (!ScenesToRebuild.Contains(scene))
{
ScenesToRebuild.Add(scene);
}
scene->CSGData.BuildTime = DateTime::NowUTC() + TimeSpan::FromMilliseconds(timeoutMs);
}
namespace CSG
{
typedef Dictionary<Actor*, Mesh*> MeshesLookup;
bool walkTree(Actor* actor, MeshesArray& meshes, MeshesLookup& cache)
{
// Check if actor is a brush
auto brush = dynamic_cast<Brush*>(actor);
if (brush)
{
// Check if can build it
if (brush->CanUseCSG())
{
// Skip subtract/common meshes from the beginning (they have no effect)
if (meshes.Count() > 0 || brush->GetBrushMode() == Mode::Additive)
{
// Create new mesh and build for given brush
auto mesh = New<CSG::Mesh>();
mesh->Build(brush);
// Save results
meshes.Add(mesh);
cache.Add(actor, mesh);
}
else
{
// Info
LOG(Info, "Skipping CSG brush '{0}'", actor->ToString());
}
}
}
return true;
}
Mesh* Combine(Actor* actor, MeshesLookup& cache, Mesh* combineParent)
{
ASSERT(actor);
Mesh* result = nullptr;
Mesh* myBrush = nullptr;
cache.TryGet(actor, myBrush);
// Get first child mesh with valid data (has additive brush)
int32 childIndex = 0;
while (childIndex < actor->Children.Count())
{
auto child = Combine(actor->Children[childIndex], cache, combineParent);
childIndex++;
if (child)
{
// If brush was based on additive brush or current actor is a brush we can stop searching
if (child->HasMode(Mode::Additive) || myBrush)
{
// End searching
result = child;
break;
}
if (combineParent)
{
// Combine
combineParent->PerformOperation(child);
}
}
}
// Check if has any child with CSG brush
if (result)
{
// Check if has own brush
if (myBrush)
{
// Combine with first child
myBrush->PerformOperation(result);
// Set this actor brush as a result
result = myBrush;
}
// Merge with the other children
while (childIndex < actor->Children.Count())
{
auto child = Combine(actor->Children[childIndex], cache, result);
if (child)
{
// Combine
result->PerformOperation(child);
}
childIndex++;
}
}
else
{
// Use this actor brush (may be empty)
result = myBrush;
}
return result;
}
Mesh* Combine(Scene* scene, MeshesLookup& cache)
{
#if CSG_USE_SCENE_LOCKS
auto Level = Level::Instance();
Level->Lock();
#endif
Mesh* result = Combine(scene, cache, nullptr);
#if CSG_USE_SCENE_LOCKS
Level->Unlock();
#endif
return result;
}
}
void CSGBuilderImpl::build(Scene* scene)
{
// Start
auto startTime = DateTime::Now();
LOG(Info, "Start building CSG...");
// Temporary data
int32 lastBuildMeshesCount = 0; // TODO: cache this
MeshesArray meshes(Math::Min(lastBuildMeshesCount, 32));
MeshesLookup cache(Math::Min(lastBuildMeshesCount * 4, 32));
Guid outputModelAssetId = Guid::Empty;
Guid outputRawDataAssetId = Guid::Empty;
Guid outputCollisionDataAssetId = Guid::Empty;
// Setup CSG meshes list and build them
{
Function<bool(Actor*, MeshesArray&, MeshesLookup&)> treeWalkFunction(walkTree);
SceneQuery::TreeExecute<Array<CSG::Mesh*>&, MeshesLookup&>(treeWalkFunction, meshes, cache);
}
lastBuildMeshesCount = meshes.Count();
if (meshes.IsEmpty())
goto BUILDING_END;
// Process all meshes (performs actual CSG opterations on geometry in tree structure)
CSG::Mesh* combinedMesh = Combine(scene, cache);
if (combinedMesh == nullptr)
goto BUILDING_END;
// TODO: split too big meshes (too many verts, to far parts, etc.)
// Triangulate meshes
{
// TODO: setup valid loop for splited meshes
// Convert CSG meshes into raw triangles data
RawData meshData;
Array<RawModelVertex> vertexBuffer;
combinedMesh->Triangulate(meshData, vertexBuffer);
meshData.RemoveEmptySlots();
if (meshData.Slots.HasItems())
{
const auto sceneDataFolderPath = scene->GetDataFolderPath();
// Convert CSG mesh data to common storage type
ModelData modelData;
meshData.ToModelData(modelData);
// Convert CSG mesh to the local transformation of the scene
if (!scene->GetTransform().IsIdentity())
{
Matrix t;
scene->GetWorldToLocalMatrix(t);
modelData.TransformBuffer(t);
}
// Import model data to the asset
{
Guid modelDataAssetId = scene->CSGData.Model.GetID();
if (!modelDataAssetId.IsValid())
modelDataAssetId = Guid::New();
const String modelDataAssetPath = sceneDataFolderPath / TEXT("CSG_Mesh") + ASSET_FILES_EXTENSION_WITH_DOT;
if (AssetsImportingManager::Create(AssetsImportingManager::CreateModelTag, modelDataAssetPath, modelDataAssetId, &modelData))
{
LOG(Warning, "Failed to import CSG mesh data");
goto BUILDING_END;
}
outputModelAssetId = modelDataAssetId;
}
// Generate asset with CSG mesh metadata (for collisions and brush queries)
{
Guid rawDataAssetId = scene->CSGData.Data.GetID();
if (!rawDataAssetId.IsValid())
rawDataAssetId = Guid::New();
const String rawDataAssetPath = sceneDataFolderPath / TEXT("CSG_Data") + ASSET_FILES_EXTENSION_WITH_DOT;
if (generateRawDataAsset(scene, meshData, rawDataAssetId, rawDataAssetPath))
{
LOG(Warning, "Failed to create raw CSG data");
goto BUILDING_END;
}
outputRawDataAssetId = rawDataAssetId;
}
// Generate CSG mesh collision asset
{
// Convert CSG mesh to scene local space (fix issues when scene has transformation applied)
if (!scene->GetTransform().IsIdentity())
{
Matrix m1, m2;
scene->GetTransform().GetWorld(m1);
Matrix::Invert(m1, m2);
for (int32 lodIndex = 0; lodIndex < modelData.LODs.Count(); lodIndex++)
{
auto lod = &modelData.LODs[lodIndex];
for (int32 meshIndex = 0; meshIndex < lod->Meshes.Count(); meshIndex++)
{
const auto v = &lod->Meshes[meshIndex]->Positions;
Vector3::Transform(v->Get(), m2, v->Get(), v->Count());
}
}
}
#if COMPILE_WITH_PHYSICS_COOKING
CollisionCooking::Argument arg;
arg.Type = CollisionDataType::TriangleMesh;
arg.OverrideModelData = &modelData;
Guid collisionDataAssetId = scene->CSGData.CollisionData.GetID();
if (!collisionDataAssetId.IsValid())
collisionDataAssetId = Guid::New();
const String collisionDataAssetPath = sceneDataFolderPath / TEXT("CSG_Collision") + ASSET_FILES_EXTENSION_WITH_DOT;
if (AssetsImportingManager::Create(AssetsImportingManager::CreateCollisionDataTag, collisionDataAssetPath, collisionDataAssetId, &arg))
{
LOG(Warning, "Failed to cook CSG mesh collision data");
goto BUILDING_END;
}
outputCollisionDataAssetId = collisionDataAssetId;
#else
outputCollisionDataAssetId = Guid::Empty;
#endif
}
}
}
BUILDING_END:
// Link new (or empty) CSG mesh
scene->CSGData.Data = Content::LoadAsync<RawDataAsset>(outputRawDataAssetId);
scene->CSGData.Model = Content::LoadAsync<Model>(outputModelAssetId);
scene->CSGData.CollisionData = Content::LoadAsync<CollisionData>(outputCollisionDataAssetId);
// TODO: also set CSGData.InstanceBuffer - lightmap scales for the entries so csg mesh gets better quality in lightmaps
scene->CSGData.PostCSGBuild();
// End
meshes.ClearDelete();
auto endTime = DateTime::Now();
LOG(Info, "CSG build in {0} ms! {1} brush(es)", (endTime - startTime).GetTotalMilliseconds(), lastBuildMeshesCount);
}
bool CSGBuilderImpl::generateRawDataAsset(Scene* scene, RawData& meshData, Guid& assetId, const String& assetPath)
{
// Prepare data
MemoryWriteStream stream(4096);
{
// Header (with version number)
stream.WriteInt32(1);
const int32 brushesCount = meshData.Brushes.Count();
stream.WriteInt32(brushesCount);
// Surfaces data locations
int32 surfacesDataOffset = sizeof(int32) * 2 + (sizeof(Guid) + sizeof(int32)) * brushesCount;
for (auto brush = meshData.Brushes.Begin(); brush.IsNotEnd(); ++brush)
{
auto& surfaces = brush->Value.Surfaces;
stream.Write(&brush->Key);
stream.WriteInt32(surfacesDataOffset);
// Calculate offset in data storage to the next brush data
int32 brushDataSize = 0;
for (int32 i = 0; i < surfaces.Count(); i++)
{
brushDataSize += sizeof(int32) + sizeof(Triangle) * surfaces[i].Triangles.Count();
}
surfacesDataOffset += brushDataSize;
}
// Surfaces data
for (auto brush = meshData.Brushes.Begin(); brush.IsNotEnd(); ++brush)
{
auto& surfaces = brush->Value.Surfaces;
for (int32 i = 0; i < surfaces.Count(); i++)
{
auto& triangles = surfaces[i].Triangles;
stream.WriteInt32(triangles.Count());
stream.Write(triangles.Get(), triangles.Count());
}
}
}
// Serialize
BytesContainer bytesContainer;
bytesContainer.Link(stream.GetHandle(), stream.GetPosition());
return AssetsImportingManager::Create(AssetsImportingManager::CreateRawDataTag, assetPath, assetId, (void*)&bytesContainer);
}
#endif

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Delegate.h"
#include "Brush.h"
class Scene;
namespace CSG
{
class RawData;
#if COMPILE_WITH_CSG_BUILDER
/// <summary>
/// CSG geometry builder
/// </summary>
class Builder
{
public:
/// <summary>
/// Action fired when any CSG brush on scene gets edited (different dimensions or transformation etc.)
/// </summary>
static Delegate<Brush*> OnBrushModified;
public:
static bool IsActive();
/// <summary>
/// Build CSG geometry for the given scene.
/// </summary>
/// <param name="scene">The scene.</param>
/// <param name="timeoutMs">The timeout to wait before building CSG (in milliseconds).</param>
static void Build(Scene* scene, float timeoutMs = 50);
};
#endif
};

View File

@@ -0,0 +1,197 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "CSGData.h"
#include "Brush.h"
#include "Engine/Graphics/Models/ModelData.h"
#include "Engine/Utilities/RectPack.h"
using namespace CSG;
namespace CSG
{
class LightmapUVsPacker
{
typedef RawData::Surface* ChartType;
public:
struct Node : RectPack<Node, float>
{
Node(float xy, float wh)
: RectPack<Node, float>(xy, xy, wh, wh)
{
}
Node(float x, float y, float width, float height)
: RectPack<Node, float>(x, y, width, height)
{
}
void OnInsert(ChartType chart, float atlasSize)
{
const float invSize = 1.0f / atlasSize;
chart->UVsArea = Rectangle(X * invSize, Y * invSize, chart->Size.X * invSize, chart->Size.Y * invSize);
}
};
private:
Node _root;
const float _atlasSize;
const float _chartsPadding;
public:
LightmapUVsPacker(float atlasSize, float chartsPadding)
: _root(chartsPadding, atlasSize - chartsPadding)
, _atlasSize(atlasSize)
, _chartsPadding(chartsPadding)
{
}
~LightmapUVsPacker()
{
}
Node* Insert(ChartType chart)
{
return _root.Insert(chart->Size.X, chart->Size.Y, _chartsPadding, chart, _atlasSize);
}
};
}
void RawData::Slot::AddSurface(float scaleInLightmap, const Rectangle& lightmapUVsBox, const RawModelVertex* firstVertex, int32 vertexCount)
{
auto& surface = Surfaces.AddOne();
surface.ScaleInLightmap = scaleInLightmap;
surface.LightmapUVsBox = lightmapUVsBox;
surface.Size = lightmapUVsBox.Size * scaleInLightmap;
surface.UVsArea = Rectangle::Empty;
surface.Vertices.Add(firstVertex, vertexCount);
}
void RawData::AddSurface(Brush* brush, int32 brushSurfaceIndex, const Guid& surfaceMaterial, float scaleInLightmap, const Rectangle& lightmapUVsBox, const RawModelVertex* firstVertex, int32 vertexCount)
{
// Add surface to slot
auto slot = GetOrAddSlot(surfaceMaterial);
slot->AddSurface(scaleInLightmap, lightmapUVsBox, firstVertex, vertexCount);
// Add surface to brush
auto& brushData = Brushes[brush->GetBrushID()];
if (brushData.Surfaces.Count() != brush->GetSurfacesCount())
brushData.Surfaces.Resize(brush->GetSurfacesCount());
auto& surfaceData = brushData.Surfaces[brushSurfaceIndex];
auto& triangles = surfaceData.Triangles;
triangles.Resize(vertexCount / 3);
// Copy triangles
for (int32 i = 0; i < vertexCount;)
{
auto& triangle = triangles[i / 3];
triangle.V0 = firstVertex[i++].Position;
triangle.V1 = firstVertex[i++].Position;
triangle.V2 = firstVertex[i++].Position;
}
}
void RawData::ToModelData(ModelData& modelData) const
{
// Generate lightmap UVs (single chart for the whole mesh)
{
// Every surface has custom lightmap scale so we have to try to pack all triangles into
// single rectangle using fast rectangle pack algorithm. Rectangle size can be calculated from
// all triangles surfaces area with margins.
// Sum the area for the atlas surfaces
float areaSum = 0;
for (int32 slotIndex = 0; slotIndex < Slots.Count(); slotIndex++)
{
auto& slot = Slots[slotIndex];
for (int32 i = 0; i < slot->Surfaces.Count(); i++)
{
auto& surface = slot->Surfaces[i];
areaSum += surface.Size.MulValues();
}
}
// Insert only non-empty surfaces
if (areaSum > ZeroTolerance)
{
// Pack all surfaces into atlas
float atlasSize = Math::Sqrt(areaSum) * 1.02f;
int32 triesLeft = 10;
while (triesLeft--)
{
bool failed = false;
const float chartsPadding = (8.0f / 1024.0f) * atlasSize;
LightmapUVsPacker packer(atlasSize, chartsPadding);
for (int32 slotIndex = 0; slotIndex < Slots.Count(); slotIndex++)
{
auto& slot = Slots[slotIndex];
for (int32 i = 0; i < slot->Surfaces.Count(); i++)
{
auto& surface = slot->Surfaces[i];
if (packer.Insert(&surface) == nullptr)
{
// Failed to insert surface, increase atlas size and try again
atlasSize *= 1.5f;
failed = true;
break;
}
}
}
if (!failed)
break;
}
}
}
// Transfer data (use 1-1 mesh-material slot linkage)
modelData.MinScreenSize = 0.0f;
modelData.LODs.Resize(1);
modelData.LODs[0].Meshes.Resize(Slots.Count());
modelData.Materials.Resize(Slots.Count());
for (int32 slotIndex = 0; slotIndex < Slots.Count(); slotIndex++)
{
auto& slot = Slots[slotIndex];
auto& mesh = modelData.LODs[0].Meshes[slotIndex];
auto& materialSlot = modelData.Materials[slotIndex];
ASSERT(slot->Surfaces.HasItems());
// Setup mesh and material slot
mesh = New<MeshData>();
mesh->Name = materialSlot.Name = String(TEXT("Mesh ")) + StringUtils::ToString(slotIndex);
materialSlot.AssetID = slot->Material;
mesh->MaterialSlotIndex = slotIndex;
// Generate vertex and index buffers from the surfaces (don't use vertex colors)
int32 vertexCount = 0;
for (int32 i = 0; i < slot->Surfaces.Count(); i++)
{
auto& surface = slot->Surfaces[i];
vertexCount += surface.Vertices.Count();
}
mesh->EnsureCapacity(vertexCount, vertexCount, false, false);
// Write surfaces into vertex and index buffers
int32 index = 0;
for (int32 i = 0; i < slot->Surfaces.Count(); i++)
{
auto& surface = slot->Surfaces[i];
for (int32 vIndex = 0; vIndex < surface.Vertices.Count(); vIndex++)
{
auto& v = surface.Vertices[vIndex];
mesh->Positions.Add(v.Position);
mesh->UVs.Add(v.TexCoord);
mesh->Normals.Add(v.Normal);
mesh->Tangents.Add(v.Tangent);
mesh->LightmapUVs.Add(v.LightmapUVs * surface.UVsArea.Size + surface.UVsArea.Location);
mesh->Indices.Add(index++);
}
}
}
}

148
Source/Engine/CSG/CSGData.h Normal file
View File

@@ -0,0 +1,148 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Types.h"
#include "Engine/Graphics/Models/Types.h"
#include "Engine/Core/Types/Guid.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Core/Math/Rectangle.h"
#include "Engine/Core/Math/Triangle.h"
class ModelData;
namespace CSG
{
class Brush;
/// <summary>
/// Represents raw CSG mesh data after triangulation. Can be used to export it to model vertex/index buffers. Separates triangles by materials.
/// </summary>
class RawData
{
public:
struct Surface
{
float ScaleInLightmap;
Rectangle LightmapUVsBox;
Vector2 Size;
Rectangle UVsArea;
Array<RawModelVertex> Vertices;
};
struct SurfaceData
{
Array<Triangle> Triangles;
};
struct BrushData
{
Array<SurfaceData> Surfaces;
};
class Slot
{
public:
Guid Material;
Array<Surface> Surfaces;
public:
/// <summary>
/// Initializes a new instance of the <see cref="Slot"/> class.
/// </summary>
/// <param name="material">The material.</param>
Slot(const Guid& material)
: Material(material)
{
}
public:
bool IsEmpty() const
{
return Surfaces.IsEmpty();
}
void AddSurface(float scaleInLightmap, const Rectangle& lightmapUVsBox, const RawModelVertex* firstVertex, int32 vertexCount);
};
public:
/// <summary>
/// The slots.
/// </summary>
Array<Slot*> Slots;
/// <summary>
/// The brushes.
/// </summary>
Dictionary<Guid, BrushData> Brushes;
public:
/// <summary>
/// Initializes a new instance of the <see cref="RawData"/> class.
/// </summary>
RawData()
{
}
/// <summary>
/// Finalizes an instance of the <see cref="RawData"/> class.
/// </summary>
~RawData()
{
Slots.ClearDelete();
}
public:
/// <summary>
/// Gets or adds the slot for the given material.
/// </summary>
/// <param name="material">The material.</param>
/// <returns>The geometry slot.</returns>
Slot* GetOrAddSlot(const Guid& material)
{
for (int32 i = 0; i < Slots.Count(); i++)
{
if (Slots[i]->Material == material)
return Slots[i];
}
auto slot = New<Slot>(material);
Slots.Add(slot);
return slot;
}
public:
void AddSurface(Brush* brush, int32 brushSurfaceIndex, const Guid& surfaceMaterial, float scaleInLightmap, const Rectangle& lightmapUVsBox, const RawModelVertex* firstVertex, int32 vertexCount);
/// <summary>
/// Removes the empty slots.
/// </summary>
void RemoveEmptySlots()
{
for (int32 i = 0; i < Slots.Count() && Slots.HasItems(); i++)
{
if (Slots[i]->IsEmpty())
{
Delete(Slots[i]);
Slots.RemoveAt(i);
i--;
}
}
}
/// <summary>
/// Outputs mesh data to the ModelData storage container.
/// </summary>
/// <param name="modelData">The result model data.</param>
void ToModelData(ModelData& modelData) const;
};
}

View File

@@ -0,0 +1,318 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "CSGMesh.h"
#if COMPILE_WITH_CSG_BUILDER
#include "Engine/Core/Log.h"
void CSG::Mesh::Build(Brush* parentBrush)
{
struct EdgeIntersection
{
int32 EdgeIndex;
int32 PlaneIndices[2];
EdgeIntersection()
{
EdgeIndex = INVALID_INDEX;
}
EdgeIntersection(int32 edgeIndex, int32 planeIndexA, int32 planeIndexB)
{
EdgeIndex = edgeIndex;
PlaneIndices[0] = planeIndexA;
PlaneIndices[1] = planeIndexB;
}
};
struct PointIntersection
{
// TODO: optimize this shit?
int32 VertexIndex;
Array<EdgeIntersection> Edges;
Array<int32> PlaneIndices;
PointIntersection()
{
VertexIndex = INVALID_INDEX;
}
PointIntersection(int32 vertexIndex, const Array<int32>& planes)
{
VertexIndex = vertexIndex;
PlaneIndices = planes;
}
};
// Clear
_bounds.Clear();
_surfaces.Clear();
_polygons.Clear();
_edges.Clear();
_vertices.Clear();
_brushesMeta.Clear();
// Get brush planes
if (parentBrush == nullptr)
return;
auto mode = parentBrush->GetBrushMode();
parentBrush->GetSurfaces(_surfaces);
int32 surfacesCount = _surfaces.Count();
if (surfacesCount > 250)
{
LOG(Error, "CSG brush has too many planes: {0}. Cannot process it!", surfacesCount);
return;
}
// Fix duplicated planes (coplanar ones)
for (int32 i = 0; i < surfacesCount; i++)
{
for (int32 j = i + 1; j < surfacesCount; j++)
{
if (Surface::NearEqual(_surfaces[i], _surfaces[j]))
{
// Change to blank plane
_surfaces[i].Normal = Vector3::Up;
_surfaces[i].D = MAX_float;
continue;
}
}
}
Array<PointIntersection> pointIntersections(surfacesCount * surfacesCount);
Array<int32> intersectingPlanes;
// Find all point intersections where 3 (or more planes) intersect
for (int32 planeIndex1 = 0; planeIndex1 < surfacesCount - 2; planeIndex1++)
{
Surface plane1 = _surfaces[planeIndex1];
for (int32 planeIndex2 = planeIndex1 + 1; planeIndex2 < surfacesCount - 1; planeIndex2++)
{
Surface plane2 = _surfaces[planeIndex2];
for (int32 planeIndex3 = planeIndex2 + 1; planeIndex3 < surfacesCount; planeIndex3++)
{
Surface plane3 = _surfaces[planeIndex3];
// Calculate the intersection point
Vector3 vertex = Plane::Intersection(plane1, plane2, plane3);
// Check if the intersection is valid
if (vertex.IsNaN() || vertex.IsInfinity())
continue;
intersectingPlanes.Clear();
intersectingPlanes.Add(planeIndex1);
intersectingPlanes.Add(planeIndex2);
intersectingPlanes.Add(planeIndex3);
for (int32 planeIndex4 = 0; planeIndex4 < surfacesCount; planeIndex4++)
{
if (planeIndex4 == planeIndex1 || planeIndex4 == planeIndex2 || planeIndex4 == planeIndex3)
continue;
Surface& plane4 = _surfaces[planeIndex4];
PlaneIntersectionType side = plane4.OnSide(vertex);
if (side == PlaneIntersectionType::Intersecting)
{
if (planeIndex4 < planeIndex3)
// Already found this vertex
goto SkipIntersection;
// We've found another plane which goes trough our found intersection point
intersectingPlanes.Add(planeIndex4);
}
else if (side == PlaneIntersectionType::Front)
// Intersection is outside of brush
goto SkipIntersection;
}
int32 vertexIndex = _vertices.Count();
_vertices.Add(vertex);
// Add intersection point to our list
pointIntersections.Add(PointIntersection(vertexIndex, intersectingPlanes));
SkipIntersection:
;
}
}
}
int32 foundPlanes[2];
// Find all our intersection edges which are formed by a pair of planes
// (this could probably be done inside the previous loop)
for (int32 i = 0; i < pointIntersections.Count(); i++)
{
PointIntersection& pointIntersectionA = pointIntersections[i];
for (int32 j = i + 1; j < pointIntersections.Count(); j++)
{
auto& pointIntersectionB = pointIntersections[j];
auto& planesIndicesA = pointIntersectionA.PlaneIndices;
auto& planesIndicesB = pointIntersectionB.PlaneIndices;
int32 foundPlaneIndex = 0;
for (int32 k = 0; k < planesIndicesA.Count(); k++)
{
int32 currentPlaneIndex = planesIndicesA[k];
if (!planesIndicesB.Contains(currentPlaneIndex))
continue;
foundPlanes[foundPlaneIndex] = currentPlaneIndex;
foundPlaneIndex++;
if (foundPlaneIndex == 2)
break;
}
// If foundPlaneIndex is 0 or 1 then either this combination does not exist, or only goes trough one point
if (foundPlaneIndex < 2)
continue;
// Create our found intersection edge
HalfEdge halfEdgeA;
int32 halfEdgeAIndex = _edges.Count();
HalfEdge halfEdgeB;
int32 halfEdgeBIndex = halfEdgeAIndex + 1;
halfEdgeA.TwinIndex = halfEdgeBIndex;
halfEdgeB.TwinIndex = halfEdgeAIndex;
halfEdgeA.VertexIndex = pointIntersectionA.VertexIndex;
halfEdgeB.VertexIndex = pointIntersectionB.VertexIndex;
// Add it to our points
pointIntersectionA.Edges.Add(EdgeIntersection(
halfEdgeAIndex,
foundPlanes[0],
foundPlanes[1]));
pointIntersectionB.Edges.Add(EdgeIntersection(
halfEdgeBIndex,
foundPlanes[0],
foundPlanes[1]));
_edges.Add(halfEdgeA);
_edges.Add(halfEdgeB);
}
}
// TODO: snap vertices to remove gaps (snapping facctor can be defined by the parent brush)
for (int32 i = 0; i < surfacesCount; i++)
{
Polygon polygon;
polygon.SurfaceIndex = i;
polygon.Visible = true;
_polygons.Add(polygon);
}
for (int32 i = pointIntersections.Count() - 1; i >= 0; i--)
{
auto& pointIntersection = pointIntersections[i];
auto& pointEdges = pointIntersection.Edges;
// Make sure that we have at least 2 edges ...
// This may happen when a plane only intersects at a single edge.
if (pointEdges.Count() <= 2)
{
pointIntersections.RemoveAt(i);
continue;
}
auto vertexIndex = pointIntersection.VertexIndex;
auto& vertex = _vertices[vertexIndex];
for (int32 j = 0; j < pointEdges.Count() - 1; j++)
{
auto& edge1 = pointEdges[j];
for (int32 k = j + 1; k < pointEdges.Count(); k++)
{
auto& edge2 = pointEdges[k];
int32 planeIndex1 = INVALID_INDEX;
int32 planeIndex2 = INVALID_INDEX;
// Determine if and which of our 2 planes are identical
if (edge1.PlaneIndices[0] == edge2.PlaneIndices[0])
{
planeIndex1 = 0;
planeIndex2 = 0;
}
else if (edge1.PlaneIndices[0] == edge2.PlaneIndices[1])
{
planeIndex1 = 0;
planeIndex2 = 1;
}
else if (edge1.PlaneIndices[1] == edge2.PlaneIndices[0])
{
planeIndex1 = 1;
planeIndex2 = 0;
}
else if (edge1.PlaneIndices[1] == edge2.PlaneIndices[1])
{
planeIndex1 = 1;
planeIndex2 = 1;
}
else
{
continue;
}
HalfEdge* ingoing;
HalfEdge* outgoing;
int32 outgoingIndex;
Surface& shared_plane = _surfaces[edge1.PlaneIndices[planeIndex1]];
Surface& edge1_plane = _surfaces[edge1.PlaneIndices[1 - planeIndex1]];
Surface& edge2_plane = _surfaces[edge2.PlaneIndices[1 - planeIndex2]];
Vector3 direction = Vector3::Cross(shared_plane.Normal, edge1_plane.Normal);
// Determine the orientation of our two edges to determine which edge is in-going, and which one is out-going
if (Vector3::Dot(direction, edge2_plane.Normal) < 0)
{
ingoing = &_edges[edge2.EdgeIndex];
outgoingIndex = _edges[edge1.EdgeIndex].TwinIndex;
outgoing = &_edges[outgoingIndex];
}
else
{
ingoing = &_edges[edge1.EdgeIndex];
outgoingIndex = _edges[edge2.EdgeIndex].TwinIndex;
outgoing = &_edges[outgoingIndex];
}
// Link the out-going half-edge to the in-going half-edge
ingoing->NextIndex = outgoingIndex;
// Add reference to polygon to half-edge, and make sure our polygon has a reference to a half-edge
// Since a half-edge, in this case, serves as a circular linked list this just works.
auto polygonIndex = edge1.PlaneIndices[planeIndex1];
ingoing->PolygonIndex = polygonIndex;
outgoing->PolygonIndex = polygonIndex;
auto& polygon = _polygons[polygonIndex];
polygon.FirstEdgeIndex = outgoingIndex;
polygon.Bounds.Add(vertex);
}
}
// Add the intersection point to the area of our bounding box
_bounds.Add(vertex);
}
// Setup base brush meta
BrushMeta meta;
meta.Mode = mode;
meta.StartSurfaceIndex = 0;
meta.SurfacesCount = surfacesCount;
meta.Bounds = _bounds;
meta.Parent = parentBrush;
_brushesMeta.Add(meta);
}
#endif

View File

@@ -0,0 +1,377 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "CSGMesh.h"
#if COMPILE_WITH_CSG_BUILDER
using namespace CSG;
void CSG::Mesh::resolvePolygon(Polygon& polygon, PolygonOperation op)
{
switch (op)
{
case Keep:
break;
case Remove:
{
polygon.Visible = false;
break;
}
case Flip:
{
polygon.Inverted = true;
break;
}
default:
break;
}
}
void CSG::Mesh::edgeSplit(int32 edgeIndex, const Vector3& vertex)
{
// Splits a half edge
/*
original:
thisEdge
*<======================
---------------------->*
twin
split:
newEdge thisEdge
*<=========*<===========
--------->*----------->*
thisTwin newTwin
*/
HalfEdge& thisEdge = _edges[edgeIndex];
auto thisTwinIndex = thisEdge.TwinIndex;
auto& thisTwin = _edges[thisTwinIndex];
auto thisEdgeIndex = thisTwin.TwinIndex;
HalfEdge newEdge;
auto newEdgeIndex = _edges.Count();
HalfEdge newTwin;
auto newTwinIndex = newEdgeIndex + 1;
auto vertexIndex = _vertices.Count();
newEdge.PolygonIndex = thisEdge.PolygonIndex;
newTwin.PolygonIndex = thisTwin.PolygonIndex;
newEdge.VertexIndex = thisEdge.VertexIndex;
thisEdge.VertexIndex = vertexIndex;
newTwin.VertexIndex = thisTwin.VertexIndex;
thisTwin.VertexIndex = vertexIndex;
newEdge.NextIndex = thisEdge.NextIndex;
thisEdge.NextIndex = newEdgeIndex;
newTwin.NextIndex = thisTwin.NextIndex;
thisTwin.NextIndex = newTwinIndex;
newEdge.TwinIndex = thisTwinIndex;
thisTwin.TwinIndex = newEdgeIndex;
thisEdge.TwinIndex = newTwinIndex;
newTwin.TwinIndex = thisEdgeIndex;
_edges.Add(newEdge);
_edges.Add(newTwin);
_vertices.Add(vertex);
}
PolygonSplitResult CSG::Mesh::polygonSplit(const Surface& cuttingPlane, int32 inputPolygonIndex, Polygon** outputPolygon)
{
// Splits a polygon into two pieces, or categorizes it as outside, inside or aligned
// Note: This method is not optimized! Code is simplified for clarity!
// for example: Plane.Distance / Plane.OnSide should be inlined manually and shouldn't use enums, but floating point values directly!
Polygon& inputPolygon = _polygons[inputPolygonIndex];
int32 prev = inputPolygon.FirstEdgeIndex;
int32 current = _edges[prev].NextIndex;
int32 next = _edges[current].NextIndex;
int32 last = next;
int32 enterEdge = INVALID_INDEX;
int32 exitEdge = INVALID_INDEX;
auto prevVertex = _vertices[_edges[prev].VertexIndex];
auto prevDistance = cuttingPlane.Distance(prevVertex); // distance to previous vertex
auto prevSide = Surface::OnSide(prevDistance); // side of plane of previous vertex
auto currentVertex = _vertices[_edges[current].VertexIndex];
auto currentDistance = cuttingPlane.Distance(currentVertex); // distance to current vertex
auto currentSide = Surface::OnSide(currentDistance); // side of plane of current vertex
do
{
auto nextVertex = _vertices[_edges[next].VertexIndex];
auto nextDistance = cuttingPlane.Distance(nextVertex); // distance to next vertex
auto nextSide = Surface::OnSide(nextDistance); // side of plane of next vertex
if (prevSide != currentSide) // check if edge crossed the plane ...
{
if (currentSide != PlaneIntersectionType::Intersecting) // prev:inside/outside - current:inside/outside - next:??
{
if (prevSide != PlaneIntersectionType::Intersecting) // prev:inside/outside - current:outside - next:??
{
// Calculate intersection of edge with plane split the edge into two, inserting the new vertex
auto newVertex = Surface::Intersection(prevVertex, currentVertex, prevDistance, currentDistance);
edgeSplit(current, newVertex);
if (prevSide == PlaneIntersectionType::Back) // prev:inside - current:outside - next:??
{
// edge01 exits:
//
// outside
// 1
// *
// ......./........ intersect
// /
// 0
// inside
exitEdge = current;
}
else if (prevSide == PlaneIntersectionType::Front) // prev:outside - current:inside - next:??
{
// edge01 enters:
//
// outside
// 0
// \
// .......\........ intersect
// *
// 1
// inside
enterEdge = current;
}
prev = _edges[prev].NextIndex;
prevSide = PlaneIntersectionType::Intersecting;
if (exitEdge != INVALID_INDEX && enterEdge != INVALID_INDEX)
break;
current = _edges[prev].NextIndex;
currentVertex = _vertices[_edges[current].VertexIndex];
next = _edges[current].NextIndex;
nextVertex = _vertices[_edges[next].VertexIndex];
}
}
else // prev:?? - current:intersects - next:??
{
if (prevSide == PlaneIntersectionType::Intersecting || // prev:intersects - current:intersects - next:??
nextSide == PlaneIntersectionType::Intersecting || // prev:?? - current:intersects - next:intersects
prevSide == nextSide) // prev:inside/outde - current:intersects - next:inside/outde
{
if (prevSide == PlaneIntersectionType::Back || // prev:inside - current:intersects - next:intersects/inside
nextSide == PlaneIntersectionType::Back) // prev:intersects/inside - current:intersects - next:inside
{
// outside
// 0 1
// --------*....... intersect
// \
// 2
// inside
//
// outside
// 1 2
// ........*------- intersect
// /
// 0
// inside
//
// outside
// 1
//........*....... intersect
// / \
// 0 2
// inside
//
prevSide = PlaneIntersectionType::Back;
enterEdge = exitEdge = INVALID_INDEX;
break;
}
else if (prevSide == PlaneIntersectionType::Front || // prev:outside - current:intersects - next:intersects/outside
nextSide == PlaneIntersectionType::Front) // prev:intersects/outside - current:intersects - next:outside
{
// outside
// 2
// /
//..------*....... intersect
// 0 1
// inside
//
// outside
// 0
// \
//........*------- intersect
// 1 2
// inside
//
// outside
// 0 2
// \ /
//........*....... intersect
// 1
// inside
//
prevSide = PlaneIntersectionType::Front;
enterEdge = exitEdge = INVALID_INDEX;
break;
}
}
else // prev:inside/outside - current:intersects - next:inside/outside
{
if (prevSide == PlaneIntersectionType::Back) // prev:inside - current:intersects - next:outside
{
// find exit edge:
//
// outside
// 2
// 1 /
// ........*....... intersect
// /
// 0
// inside
exitEdge = current;
if (enterEdge != INVALID_INDEX)
break;
}
else // prev:outside - current:intersects - next:inside
{
// find enter edge:
//
// outside
// 0
// \ 1
// ........*....... intersect
// \
// 2
// inside
enterEdge = current;
if (exitEdge != INVALID_INDEX)
break;
}
}
}
}
prev = current;
current = next;
next = _edges[next].NextIndex;
prevDistance = currentDistance;
currentDistance = nextDistance;
prevSide = currentSide;
currentSide = nextSide;
prevVertex = currentVertex;
currentVertex = nextVertex;
} while (next != last);
// We should never have only one edge crossing the plane
ASSERT((enterEdge == INVALID_INDEX) == (exitEdge == INVALID_INDEX));
// Check if we have an edge that exits and an edge that enters the plane and split the polygon into two if we do
if (enterEdge != INVALID_INDEX && exitEdge != INVALID_INDEX)
{
// enter .
// .
// =====>*----->
// .
//
// outside. inside
// .
// <-----*<=====
// .
// . exit
Polygon outsidePolygon;
HalfEdge outsideEdge, insideEdge;
auto outsidePolygonIndex = _polygons.Count();
auto outsideEdgeIndex = _edges.Count();
auto insideEdgeIndex = outsideEdgeIndex + 1;
outsideEdge.TwinIndex = insideEdgeIndex;
insideEdge.TwinIndex = outsideEdgeIndex;
insideEdge.PolygonIndex = inputPolygonIndex;
outsideEdge.PolygonIndex = outsidePolygonIndex;
HalfEdge* exitEdgeP = &_edges[exitEdge];
HalfEdge* enterEdgeP = &_edges[enterEdge];
outsideEdge.VertexIndex = exitEdgeP->VertexIndex;
insideEdge.VertexIndex = enterEdgeP->VertexIndex;
outsideEdge.NextIndex = exitEdgeP->NextIndex;
insideEdge.NextIndex = enterEdgeP->NextIndex;
exitEdgeP->NextIndex = insideEdgeIndex;
enterEdgeP->NextIndex = outsideEdgeIndex;
outsidePolygon.FirstEdgeIndex = outsideEdgeIndex;
inputPolygon.FirstEdgeIndex = insideEdgeIndex;
outsidePolygon.Visible = inputPolygon.Visible;
outsidePolygon.Inverted = inputPolygon.Inverted;
outsidePolygon.SurfaceIndex = inputPolygon.SurfaceIndex;
_edges.Add(outsideEdge);
_edges.Add(insideEdge);
// calculate the bounds of the polygons
outsidePolygon.Bounds.Clear();
auto first = _edges[outsidePolygon.FirstEdgeIndex];
auto iterator = first;
do
{
outsidePolygon.Bounds.Add(_vertices[iterator.VertexIndex]);
iterator.PolygonIndex = outsidePolygonIndex;
iterator = _edges[iterator.NextIndex];
} while (iterator != first);
inputPolygon.Bounds.Clear();
first = _edges[inputPolygon.FirstEdgeIndex];
iterator = first;
do
{
inputPolygon.Bounds.Add(_vertices[iterator.VertexIndex]);
iterator = _edges[iterator.NextIndex];
} while (iterator != first);
_polygons.Add(outsidePolygon);
*outputPolygon = &_polygons[outsidePolygonIndex];
return Split;
}
switch (prevSide)
{
case PlaneIntersectionType::Back:
return CompletelyInside;
case PlaneIntersectionType::Front:
return CompletelyOutside;
default:
{
auto polygonPlane = _surfaces[inputPolygon.SurfaceIndex];
auto result = Vector3::Dot(polygonPlane.Normal, cuttingPlane.Normal);
return result > 0 ? PlaneAligned : PlaneOppositeAligned;
}
}
}
#endif

View File

@@ -0,0 +1,263 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "CSGMesh.h"
#if COMPILE_WITH_CSG_BUILDER
#include "Engine/Core/Math/Matrix.h"
#include "Engine/Core/Math/Rectangle.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "CSGData.h"
bool CSG::Mesh::Triangulate(RawData& data, Array<RawModelVertex>& cacheVB) const
{
// Reject empty meshes
int32 verticesCount = _vertices.Count();
if (verticesCount == 0)
return true;
// Mesh triangles data
cacheVB.Clear();
cacheVB.EnsureCapacity(_polygons.Count() * 3);
Array<RawModelVertex> surfaceCacheVB(32);
// Cache submeshes by material to lay them down
// key- brush index, value- direcotry for surafecs (key: surface index, value: list with start vertex per triangle)
Dictionary<int32, Dictionary<int32, Array<int32>>> polygonsPerBrush(64);
// Build index buffer
int32 firstI, secondI, thirdI;
int32 triangleIndices[3];
Vector2 uvs[3];
RawModelVertex rawVertex;
for (int32 i = 0; i < _polygons.Count(); i++)
{
const Polygon& polygon = _polygons[i];
if (!polygon.Visible || polygon.FirstEdgeIndex == INVALID_INDEX)
continue;
HalfEdge iterator = _edges[polygon.FirstEdgeIndex];
firstI = iterator.VertexIndex;
iterator = _edges[iterator.NextIndex];
secondI = iterator.VertexIndex;
int32 lastIndex = -1;
const HalfEdge& polygonFirst = _edges[polygon.FirstEdgeIndex];
while (iterator != polygonFirst)
{
if (lastIndex == iterator.NextIndex)
break;
lastIndex = iterator.NextIndex;
iterator = _edges.At(lastIndex);
thirdI = iterator.VertexIndex;
// Validate triangle
if (firstI != secondI && firstI != thirdI && secondI != thirdI)
{
// Cache triangle parent surface info
const Surface& surface = _surfaces[polygon.SurfaceIndex];
Vector3 uvPos, centerPos;
Matrix trans, transRotation, finalTrans;
// Build triangle indices
if (polygon.Inverted)
{
triangleIndices[0] = thirdI;
triangleIndices[1] = secondI;
triangleIndices[2] = firstI;
}
else
{
triangleIndices[0] = firstI;
triangleIndices[1] = secondI;
triangleIndices[2] = thirdI;
}
Vector3 surfaceUp = surface.Normal;
Vector3 surfaceForward = -Vector3::Cross(surfaceUp, Vector3::Right);
if (surfaceForward.IsZero())
surfaceForward = Vector3::Forward;
Matrix::CreateWorld(Vector3::Zero, surfaceForward, surfaceUp, trans);
Matrix::RotationAxis(surfaceUp, surface.TexCoordRotation * DegreesToRadians, transRotation);
Matrix::Multiply(transRotation, trans, finalTrans);
Vector3::Transform(Vector3::Zero, finalTrans, centerPos);
// Calculate normal vector
Vector3 v0 = _vertices[triangleIndices[0]];
Vector3 v1 = _vertices[triangleIndices[1]];
Vector3 v2 = _vertices[triangleIndices[2]];
Vector3 vd0 = v1 - v0;
Vector3 vd1 = v2 - v0;
Vector3 normal = Vector3::Normalize(vd0 ^ vd1);
// Calculate texture uvs based on vertex position
for (int32 q = 0; q < 3; q++)
{
Vector3 pos = _vertices[triangleIndices[q]];
Vector3::Transform(pos, finalTrans, uvPos);
Vector2 texCoord = Vector2(uvPos.X - centerPos.X, uvPos.Z - centerPos.Z);
// Apply surface uvs transformation
uvs[q] = (texCoord * (surface.TexCoordScale * CSG_MESH_UV_SCALE)) + surface.TexCoordOffset;
}
// Calculate tangent (it needs uvs)
Vector2 uvd0 = uvs[1] - uvs[0];
Vector2 uvd1 = uvs[2] - uvs[0];
float dR = (uvd0.X * uvd1.Y - uvd1.X * uvd0.Y);
if (Math::IsZero(dR))
dR = 1.0f;
float r = 1.0f / dR;
Vector3 tangent = (vd0 * uvd1.Y - vd1 * uvd0.Y) * r;
Vector3 bitangent = (vd1 * uvd0.X - vd0 * uvd1.X) * r;
tangent.Normalize();
// Gram-Schmidt orthogonalize
Vector3 newTangentUnormalized = tangent - normal * Vector3::Dot(normal, tangent);
const float length = newTangentUnormalized.Length();
// Workaround to handle degenerated case
if (Math::IsZero(length))
{
tangent = Vector3::Cross(normal, Vector3::UnitX);
if (Math::IsZero(tangent.Length()))
tangent = Vector3::Cross(normal, Vector3::UnitY);
tangent.Normalize();
bitangent = Vector3::Cross(normal, tangent);
}
else
{
tangent = newTangentUnormalized / length;
bitangent.Normalize();
}
// Build triangle
int32 vertexIndex = cacheVB.Count();
for (int32 q = 0; q < 3; q++)
{
rawVertex.Position = _vertices[triangleIndices[q]];
rawVertex.TexCoord = uvs[q];
rawVertex.Normal = normal;
rawVertex.Tangent = tangent;
rawVertex.Bitangent = bitangent;
rawVertex.LightmapUVs = Vector2::Zero;
rawVertex.Color = Color::Black;
cacheVB.Add(rawVertex);
}
// Find brush that produced that surface
bool isValid = false;
for (int32 brushIndex = 0; brushIndex < _brushesMeta.Count(); brushIndex++)
{
auto& brushMeta = _brushesMeta[brushIndex];
if (Math::IsInRange(polygon.SurfaceIndex, brushMeta.StartSurfaceIndex, brushMeta.StartSurfaceIndex + brushMeta.SurfacesCount - 1))
{
// Cache triangle
polygonsPerBrush[brushIndex][polygon.SurfaceIndex].Add(vertexIndex);
isValid = true;
break;
}
}
ASSERT_LOW_LAYER(isValid);
}
secondI = thirdI;
}
}
// Check if mesh has no triangles
if (cacheVB.IsEmpty())
return true;
// TODO: preallocate data material slots and buffers so data.AddTriangle will be simple copy op
// Setup result mesh data
for (auto iPerBrush = polygonsPerBrush.Begin(); iPerBrush != polygonsPerBrush.End(); ++iPerBrush)
{
auto& brushMeta = _brushesMeta[iPerBrush->Key];
for (auto iPerSurface = iPerBrush->Value.Begin(); iPerSurface != iPerBrush->Value.End(); ++iPerSurface)
{
int32 surfaceIndex = iPerSurface->Key;
int32 brushSurfaceIndex = surfaceIndex - brushMeta.StartSurfaceIndex;
int32 triangleCount = iPerSurface->Value.Count();
int32 vertexCount = triangleCount * 3;
Vector2 lightmapUVsMin = Vector2::Maximum;
Vector2 lightmapUVsMax = Vector2::Minimum;
const Surface& surface = _surfaces[surfaceIndex];
// Generate lightmap uvs per brush surface
{
// We just assume that brush surface is now some kind of rectangle (after triangulation)
// So we can project vertices locations into texture space uvs [0;1] based on minimum and maximum vertex location
Vector3 surfaceNormal = surface.Normal;
// Calculate bounds
Vector2 min = Vector2::Maximum, max = Vector2::Minimum;
Matrix projection, view, vp;
Vector3 up = Vector3::Up;
if (Vector3::NearEqual(surfaceNormal, Vector3::Up))
up = Vector3::Right;
else if (Vector3::NearEqual(surfaceNormal, Vector3::Down))
up = Vector3::Forward;
Matrix::LookAt(Vector3::Zero, surfaceNormal, up, view);
Matrix::Ortho(1000, 1000, 0.00001f, 100000.0f, projection);
Matrix::Multiply(view, projection, vp);
Array<Vector2> pointsCache(vertexCount);
for (int32 triangleIndex = 0; triangleIndex < triangleCount; triangleIndex++)
{
int32 triangleStartVertex = iPerSurface->Value[triangleIndex];
for (int32 k = 0; k < 3; k++)
{
auto& vertex = cacheVB[triangleStartVertex + k];
Vector3 projected = Vector3::Project(vertex.Position, 0, 0, 1000, 1000, 0, 1, vp);
Vector2 projectecXY = Vector2(projected);
min = Vector2::Min(projectecXY, min);
max = Vector2::Max(projectecXY, max);
pointsCache.Add(projectecXY);
}
}
// Normalize projected positions to get lightmap uvs
float projectedSize = (max - min).MaxValue();
for (int32 triangleIndex = 0; triangleIndex < triangleCount; triangleIndex++)
{
int32 triangleStartVertex = iPerSurface->Value[triangleIndex];
for (int32 k = 0; k < 3; k++)
{
auto& vertex = cacheVB[triangleStartVertex + k];
vertex.LightmapUVs = (pointsCache[triangleIndex * 3 + k] - min) / projectedSize;
lightmapUVsMin = Vector2::Min(lightmapUVsMin, vertex.LightmapUVs);
lightmapUVsMax = Vector2::Max(lightmapUVsMax, vertex.LightmapUVs);
}
}
}
// Write triangles
surfaceCacheVB.Clear();
for (int32 triangleIndex = 0; triangleIndex < triangleCount; triangleIndex++)
{
int32 triangleStartVertex = iPerSurface->Value[triangleIndex];
surfaceCacheVB.Add(cacheVB[triangleStartVertex + 0]);
surfaceCacheVB.Add(cacheVB[triangleStartVertex + 1]);
surfaceCacheVB.Add(cacheVB[triangleStartVertex + 2]);
}
Rectangle lightmapUVsBox(lightmapUVsMin, lightmapUVsMax - lightmapUVsMin);
data.AddSurface(brushMeta.Parent, brushSurfaceIndex, surface.Material, surface.ScaleInLightmap, lightmapUVsBox, surfaceCacheVB.Get(), surfaceCacheVB.Count());
}
}
return false;
}
#endif

View File

@@ -0,0 +1,318 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "CSGMesh.h"
#if COMPILE_WITH_CSG_BUILDER
#include "Engine/Core/Collections/Array.h"
using namespace CSG;
bool CSG::Mesh::HasMode(Mode mode) const
{
for (int32 i = 0; i < _brushesMeta.Count(); i++)
{
if (_brushesMeta[i].Mode == mode)
return true;
}
return false;
}
void CSG::Mesh::PerformOperation(Mesh* other)
{
ASSERT(other->_brushesMeta.Count() > 0);
// Check if has more than one submeshes
if (other->_brushesMeta.Count() > 1)
{
// Just add all subbrushes
Add(other);
}
else
{
auto mode = other->_brushesMeta[0].Mode;
// Switch between mesh operation mode
switch (mode)
{
case Mode::Additive:
{
// Check if both meshes do not intesect
if (AABB::IsOutside(_bounds, other->GetBounds())) // TODO: test every sub bounds not whole _bounds
{
// Add vertices to the mesh without any additional calculations
Add(other);
}
else
{
// Remove common part from other mesh and then combine them
//intersect(other, Remove, Keep);
Add(other);
// TODO: ogarnac to bo to jest zle xD
}
}
break;
case Mode::Subtractive:
{
// Check if both meshes do not intesect
if (AABB::IsOutside(_bounds, other->GetBounds())) // TODO: test every sub bounds not whole _bounds
{
// Do nothing
}
else
{
// Perform intersection operations
intersect(other, Remove, Keep);
other->intersect(this, Flip, Remove);
// Merge meshes
Add(other);
}
}
break;
}
}
}
void CSG::Mesh::Add(const Mesh* other)
{
ASSERT(this != other && other);
// Cache data
int32 baseIndexVertices = _vertices.Count();
int32 baseIndexSurfaces = _surfaces.Count();
int32 baseIndexEdges = _edges.Count();
int32 baseIndexPolygons = _polygons.Count();
auto oVertices = other->GetVertices();
auto oSurfaces = other->GetSurfaces();
auto oEdges = other->GetEdges();
auto oPolygons = other->GetPolygons();
auto oMeta = &other->_brushesMeta;
// Clone vertices
for (int32 i = 0; i < oVertices->Count(); i++)
{
_vertices.Add(oVertices->At(i));
}
// Clone surfaces
for (int32 i = 0; i < oSurfaces->Count(); i++)
{
_surfaces.Add(oSurfaces->At(i));
}
// Clone edges
for (int32 i = 0; i < oEdges->Count(); i++)
{
HalfEdge edge = oEdges->At(i);
edge.PolygonIndex += baseIndexPolygons;
edge.TwinIndex += baseIndexEdges;
edge.NextIndex += baseIndexEdges;
edge.VertexIndex += baseIndexVertices;
_edges.Add(edge);
}
// Clone polygons
for (int32 i = 0; i < oPolygons->Count(); i++)
{
Polygon polygon = oPolygons->At(i);
polygon.SurfaceIndex += baseIndexSurfaces;
polygon.FirstEdgeIndex += baseIndexEdges;
_polygons.Add(polygon);
}
// Clone meta
for (int32 i = 0; i < oMeta->Count(); i++)
{
BrushMeta meta = oMeta->At(i);
meta.StartSurfaceIndex += baseIndexSurfaces;
_brushesMeta.Add(meta);
}
// Increase bounds
_bounds.Add(other->GetBounds());
}
void CSG::Mesh::intersect(const Mesh* other, PolygonOperation insideOp, PolygonOperation outsideOp)
{
// insideOp - operation for polygons being inside the other brush
// outsideOp - operation for polygons being outside the other brush
// Prevent from redudant action
if (insideOp == Keep && outsideOp == Keep)
return;
// Check every sub brush from other mesh
int32 count = other->_brushesMeta.Count();
for (int32 subMeshIndex = 0; subMeshIndex < count; subMeshIndex++)
{
auto& brushMeta = other->_brushesMeta[subMeshIndex];
// Check if can intersect with that sub mesh data
if (!AABB::IsOutside(brushMeta.Bounds, _bounds))
{
PolygonOperation iOp = insideOp;
PolygonOperation oOp = outsideOp;
if (brushMeta.Mode == Mode::Subtractive)
{
iOp = Remove;
oOp = Keep;
}
intersectSubMesh(other, subMeshIndex, iOp, oOp);
}
}
// Update bounds
updateBounds();
}
void CSG::Mesh::intersectSubMesh(const Mesh* other, int32 subMeshIndex, PolygonOperation insideOp, PolygonOperation outsideOp)
{
// Cache data
auto oSurfaces = other->GetSurfaces();
auto& brushMeta = other->_brushesMeta[subMeshIndex];
auto oBounds = brushMeta.Bounds;
int32 startBrushSurface = brushMeta.StartSurfaceIndex;
int32 endBrushSurface = startBrushSurface + brushMeta.SurfacesCount;
// Check every polygon (itereate fron end since we can ass new polygons and we don't want to process them)
for (int32 i = _polygons.Count() - 1; i >= 0; i--)
{
auto& polygon = _polygons[i];
if (!polygon.Visible || polygon.FirstEdgeIndex == INVALID_INDEX)
continue;
AABB polygonBounds = polygon.Bounds;
auto finalResult = CompletelyInside;
// A quick check if the polygon lies outside the planes we're cutting our polygons with
if (!AABB::IsOutside(oBounds, polygonBounds))
{
PolygonSplitResult intermediateResult;
Polygon* outsidePolygon;
for (int32 otherIndex = startBrushSurface; otherIndex < endBrushSurface; otherIndex++)
{
auto& cuttingSurface = oSurfaces->At(otherIndex);
auto side = cuttingSurface.OnSide(polygonBounds);
if (side == PlaneIntersectionType::Front)
{
finalResult = CompletelyOutside;
continue;
}
if (side == PlaneIntersectionType::Back)
{
continue;
}
intermediateResult = polygonSplit(cuttingSurface, i, &outsidePolygon);
if (intermediateResult == CompletelyOutside)
{
finalResult = CompletelyOutside;
break;
}
if (intermediateResult == Split)
{
resolvePolygon(*outsidePolygon, outsideOp);
}
else if (intermediateResult != CompletelyInside)
{
finalResult = intermediateResult;
}
}
}
else
{
finalResult = CompletelyOutside;
}
switch (finalResult)
{
case CompletelyInside:
{
resolvePolygon(_polygons[i], insideOp);
break;
}
case CompletelyOutside:
{
resolvePolygon(_polygons[i], outsideOp);
break;
}
// The polygon can only be visible if it's part of the last brush that shares it's surface area,
// otherwise we'd get overlapping polygons if two brushes overlap.
// When the (final) polygon is aligned with one of the cutting planes, we know it lies on the surface of
// the CSG node we're cutting the polygons with. We also know that this node is not the node this polygon belongs to
// because we've done that check earlier on. So we flag this polygon as being invisible.
case PlaneAligned:
// TODO: check this case
//polygon.Visible = false;
//aligned.Add(inputPolygon);
break;
case PlaneOppositeAligned:
// TODO: check this case
//polygon.Visible = false;
//revAligned.Add(inputPolygon);
break;
case Split:
break;
default:
break;
}
}
}
void CSG::Mesh::updateBounds()
{
_bounds.Clear();
int32 triangleIndices[3];
for (int32 i = 0; i < _polygons.Count(); i++)
{
auto& polygon = _polygons[i];
if (!polygon.Visible || polygon.FirstEdgeIndex == INVALID_INDEX)
continue;
HalfEdge iterator = _edges[polygon.FirstEdgeIndex];
triangleIndices[0] = iterator.VertexIndex;
iterator = _edges[iterator.NextIndex];
triangleIndices[1] = iterator.VertexIndex;
const HalfEdge& polygonFirst = _edges[polygon.FirstEdgeIndex];
while (iterator != polygonFirst)
{
iterator = _edges.At(iterator.NextIndex);
triangleIndices[2] = iterator.VertexIndex;
// Validate triangle
if (triangleIndices[0] != triangleIndices[1] && triangleIndices[0] != triangleIndices[2] && triangleIndices[1] != triangleIndices[2])
{
for (int32 q = 0; q < 3; q++)
{
Vector3 pos = _vertices[triangleIndices[q]];
_bounds.Add(pos);
}
}
triangleIndices[1] = triangleIndices[2];
}
}
}
void CSG::Mesh::doPolygonsOperation(bool isInverted, bool visibility)
{
for (int32 i = 0; i < _polygons.Count(); i++)
{
if (_polygons[i].Inverted == isInverted)
_polygons[i].Visible = visibility;
}
}
#endif

152
Source/Engine/CSG/CSGMesh.h Normal file
View File

@@ -0,0 +1,152 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Math/AABB.h"
#include "Engine/Core/Collections/Array.h"
#include "Brush.h"
#include "HalfEdge.h"
#include "Polygon.h"
#include "Engine/Graphics/Models/Types.h"
#define CSG_MESH_UV_SCALE (1.0f / 100.0f)
#if COMPILE_WITH_CSG_BUILDER
namespace CSG
{
class RawData;
enum PolygonOperation
{
Keep,
Remove,
Flip
};
/// <summary>
/// CSG mesh object
/// </summary>
class Mesh
{
private:
struct BrushMeta
{
Mode Mode;
int32 StartSurfaceIndex;
int32 SurfacesCount;
AABB Bounds;
Brush* Parent;
};
private:
AABB _bounds;
Array<Polygon> _polygons;
Array<HalfEdge> _edges;
Array<Vector3> _vertices;
Array<Surface> _surfaces;
Array<BrushMeta> _brushesMeta;
public:
/// <summary>
/// Mesh index (assigned by the builder)
/// </summary>
int32 Index = 0;
/// <summary>
/// Gets mesh bounds area
/// </summary>
/// <returns>Mesh bounds</returns>
AABB GetBounds() const
{
return _bounds;
}
/// <summary>
/// Check if any of this brush sub-brushes is on given type
/// </summary>
/// <returns>True if brush is using given mode</returns>
bool HasMode(Mode mode) const;
/// <summary>
/// Gets array with polygons
/// </summary>
/// <returns>Polygon</returns>
const Array<Polygon>* GetPolygons() const
{
return &_polygons;
}
/// <summary>
/// Gets array with surfaces
/// </summary>
/// <returns>Surfaces</returns>
const Array<Surface>* GetSurfaces() const
{
return &_surfaces;
}
/// <summary>
/// Gets array with edges
/// </summary>
/// <returns>Half edges</returns>
const Array<HalfEdge>* GetEdges() const
{
return &_edges;
}
/// <summary>
/// Gets array with vertices
/// </summary>
/// <returns>Vertices</returns>
const Array<Vector3>* GetVertices() const
{
return &_vertices;
}
public:
/// <summary>
/// Build mesh from brush
/// </summary>
/// <param name="parentBrush">Parent brush to use</param>
void Build(Brush* parentBrush);
/// <summary>
/// Triangulate mesh
/// </summary>
/// <param name="data">Result data</param>
/// <param name="cacheVB">Cache data</param>
/// <returns>True if cannot generate valid mesh data (due to missing triangles etc.)</returns>
bool Triangulate(RawData& data, Array<RawModelVertex>& cacheVB) const;
/// <summary>
/// Perform CSG operation with another mesh
/// </summary>
/// <param name="other">Other mesh data to process</param>
void PerformOperation(Mesh* other);
/// <summary>
/// Add other mesh data
/// </summary>
/// <param name="other">Other mesh to merge with</param>
void Add(const Mesh* other);
private:
void intersect(const Mesh* other, PolygonOperation insideOp, PolygonOperation outsideOp);
void intersectSubMesh(const Mesh* other, int32 subMeshIndex, PolygonOperation insideOp, PolygonOperation outsideOp);
void updateBounds();
static void resolvePolygon(Polygon& polygon, PolygonOperation op);
void edgeSplit(int32 edgeIndex, const Vector3& vertex);
PolygonSplitResult polygonSplit(const Surface& cuttingPlane, int32 inputPolygonIndex, Polygon** outputPolygon);
void doPolygonsOperation(bool isInverted, bool visibility);
};
typedef Array<Mesh*> MeshesArray;
};
#endif

View File

@@ -0,0 +1,51 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
namespace CSG
{
/// <summary>
/// Half edge structure
/// </summary>
struct HalfEdge
{
// ^
// | polygon
// next |
// half-edge |
// | half-edge
// vertex *<======================
// ---------------------->*
// twin-half-edge
int32 NextIndex;
int32 TwinIndex;
int32 VertexIndex;
int32 PolygonIndex;
// TODO: remove this code for faster performance
HalfEdge()
{
NextIndex = 0;
TwinIndex = 0;
VertexIndex = 0;
PolygonIndex = 0;
}
bool operator==(const HalfEdge& b) const
{
return NextIndex == b.NextIndex
&& TwinIndex == b.TwinIndex
&& VertexIndex == b.VertexIndex
&& PolygonIndex == b.PolygonIndex;
}
bool operator!=(const HalfEdge& b) const
{
return NextIndex != b.NextIndex
|| TwinIndex != b.TwinIndex
|| VertexIndex != b.VertexIndex
|| PolygonIndex != b.PolygonIndex;
}
};
};

View File

@@ -0,0 +1,58 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Math/AABB.h"
namespace CSG
{
enum PolygonSplitResult
{
/// <summary>
/// Polygon is completely inside half-space defined by plane
/// </summary>
CompletelyInside,
/// <summary>
/// Polygon is completely outside half-space defined by plane
/// </summary>
CompletelyOutside,
/// <summary>
/// Polygon has been split into two parts by plane
/// </summary>
Split,
/// <summary>
/// Polygon is aligned with cutting plane and the polygons' normal points in the same direction
/// </summary>
PlaneAligned,
/// <summary>
/// Polygon is aligned with cutting plane and the polygons' normal points in the opposite direction
/// </summary>
PlaneOppositeAligned
};
/// <summary>
/// Polygon structure
/// </summary>
struct Polygon
{
int32 FirstEdgeIndex;
int32 SurfaceIndex;
bool Visible;
bool Inverted;
AABB Bounds;
Polygon()
{
// TODO: remove this constructor to boost performance??
FirstEdgeIndex = INVALID_INDEX;
SurfaceIndex = INVALID_INDEX;
Inverted = false;
Visible = false;
Bounds = AABB();
}
};
};

11
Source/Engine/CSG/Types.h Normal file
View File

@@ -0,0 +1,11 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Config.h"
#include "Engine/Level/Actors/BrushMode.h"
namespace CSG
{
typedef ::BrushMode Mode;
};