Editor/Builder/DisplacementHelper.cs
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Text;
namespace BspImport.Builder;
internal class DisplacementHelper
{
private static int FindClosestCorner( Vector3[] corners, Vector3 startPosition )
{
int minIndex = -1;
float minDistance = float.MaxValue;
for ( int i = 0; i < 4; i++ )
{
Vector3 segment = startPosition - corners[i];
float distanceSq = segment.LengthSquared;
if ( distanceSq < minDistance )
{
minDistance = distanceSq;
minIndex = i;
}
}
return minIndex;
}
private static Vector3[] RotateCornerArray( Vector3[] corners, int pointStartIndex )
{
var rotatedCorners = new Vector3[4];
for ( int i = 0; i < 4; i++ )
{
rotatedCorners[i] = corners[(i + pointStartIndex) % 4];
}
return rotatedCorners;
}
public static Vector3? GetDisplacementOrigin( ImportContext context, ushort faceIndex )
{
if ( !context.HasCompleteGeometry( out var geo ) )
return null;
if ( faceIndex < 0 || faceIndex >= geo.FacesCount )
return null;
if ( !geo.TryGetFace( faceIndex, out var face ) )
return null;
if ( face.OriginalFaceIndex < 0 || face.OriginalFaceIndex > geo.OriginalFaceCount || !geo.TryGetOriginalFace( face.OriginalFaceIndex, out var oFace ) )
return null;
int surfEdgeIdx = oFace.FirstEdge;
if ( !geo.TryGetSurfaceEdge( surfEdgeIdx, out var edge ) )
return null;
int edgeIndex = edge >= 0 ? edge : -edge;
if ( !geo.TryGetEdgeIndices( edgeIndex, out var edgeIndices ) )
return null;
var indices = edgeIndices.Indices;
if ( indices is null || indices.Length < 2 )
return null;
int vertIndex = edge >= 0 ? indices[0] : indices[1];
if ( !geo.TryGetVertex( vertIndex, out var vertex ) )
return null;
return vertex;
}
/// <summary>
/// Creates a displacement Mesh for a face index. Will return null for invalid state. Will Fallback to the base quad if displacement reconstruction fails.
/// </summary>
/// <param name="context"></param>
/// <param name="faceIndex"></param>
/// <returns>Polygonmesh if any geometry was reconstructed, null for invalid state.</returns>
public static PolygonMesh? CreateDisplacementMesh( ImportContext context, ushort faceIndex )
{
if ( !context.HasCompleteGeometry( out var geo ) )
return null;
if ( faceIndex < 0 || faceIndex >= geo.FacesCount )
return null;
if ( !geo.TryGetFace( faceIndex, out var face ) )
return null;
// passing a non-displacement faceIndex shouldn't fall back, it's wrong usage
if ( face.DisplacementInfo < 0 )
return null;
// we fall back to the base face (quad, I hope) if reconstruction fails along the way
var mesh = new PolygonMesh();
// fetch displacement info
if ( !geo.TryGetDisplacementInfo( face.DisplacementInfo, out var dInfo ) )
{
mesh.AddMeshFace( context, faceIndex );
return mesh;
}
// original face index should point to a base quad
if ( face.OriginalFaceIndex < 0 || !geo.TryGetOriginalFace( face.OriginalFaceIndex, out var oFace ) )
{
mesh.AddMeshFace( context, faceIndex );
return mesh;
}
// gather corner verts from original face
var corners = new List<Vector3>();
for ( int i = 0; i < oFace.EdgeCount; i++ )
{
int surfEdgeIdx = oFace.FirstEdge + i;
if ( !geo.TryGetSurfaceEdge( surfEdgeIdx, out var edge ) )
{
mesh.AddMeshFace( context, faceIndex );
return mesh;
}
int edgeIndex = edge >= 0 ? edge : -edge;
if ( !geo.TryGetEdgeIndices( edgeIndex, out var edgeIndices ) )
{
mesh.AddMeshFace( context, faceIndex );
return mesh;
}
var indices = edgeIndices.Indices;
if ( indices is null || indices.Length < 2 )
{
mesh.AddMeshFace( context, faceIndex );
return mesh;
}
int vertIndex = edge >= 0 ? indices[0] : indices[1];
if ( !geo.TryGetVertex( vertIndex, out var vertex ) )
{
mesh.AddMeshFace( context, faceIndex );
return mesh;
}
corners.Add( vertex );
}
// we expect a quad base
if ( corners.Count != 4 )
{
mesh.AddMeshFace( context, faceIndex );
return mesh;
}
// resolve material for displacement face
string? materialName = null;
Vector3 reflectivity = Vector3.One;
if ( context.TexInfo is not null && face.TexInfo >= 0 && face.TexInfo < context.TexInfo.Length )
{
materialName = face.GetMaterialName( context );
reflectivity = face.GetReflectivity( context );
}
// load material for displacement triangles if we have a name
Material? dispMaterial = null;
if ( !string.IsNullOrEmpty( materialName ) && context.BuildSettings.LoadMaterials )
{
dispMaterial = Material.Load( $"materials/{materialName}.vmat" );
}
// start gathering everything required, orient corners
int pointStartIndex = FindClosestCorner( corners.ToArray(), dInfo.StartPosition );
var rotatedCorners = RotateCornerArray( corners.ToArray(), pointStartIndex );
int power = dInfo.Power;
int side = dInfo.Side;
int count = dInfo.VertCount;
var positions = new Vector3[count];
var uvs = new Vector2[count];
// Read displacement vertices in storage order (X-major: x*side + y)
var storedVerts = new DisplacementVertex[count];
for ( int sx = 0; sx < side; sx++ )
{
for ( int sy = 0; sy < side; sy++ )
{
int dvIndex = dInfo.FirstVertex + sx * side + sy;
if ( !geo.TryGetDisplacementVertex( dvIndex, out var dVert ) )
{
mesh.AddMeshFace( context, faceIndex );
return mesh;
}
storedVerts[sx * side + sy] = dVert;
}
}
// Build base grid positions (without displacement) for orientation matching
var baseGrid = new Vector3[count];
for ( int bx = 0; bx < side; bx++ )
{
for ( int by = 0; by < side; by++ )
{
float s = (float)bx / (side - 1);
float t = (float)by / (side - 1);
var bottom = Vector3.Lerp( rotatedCorners[0], rotatedCorners[1], s );
var top = Vector3.Lerp( rotatedCorners[3], rotatedCorners[2], s );
baseGrid[bx * side + by] = Vector3.Lerp( bottom, top, t );
}
}
// Populate positions/uvs
for ( int sx = 0; sx < side; sx++ )
{
for ( int sy = 0; sy < side; sy++ )
{
var dVert = storedVerts[sx * side + sy];
float s = side <= 1 ? 0f : (float)sx / (side - 1);
float t = side <= 1 ? 0f : (float)sy / (side - 1);
var bottom = Vector3.Lerp( rotatedCorners[0], rotatedCorners[1], s );
var top = Vector3.Lerp( rotatedCorners[3], rotatedCorners[2], s );
var basePos = Vector3.Lerp( bottom, top, t );
var finalPos = basePos + dVert.Displacement * dVert.Distance;
int idx = sy * side + sx; // base grid is row-major (y * side + x)
positions[idx] = finalPos;
uvs[idx] = MapBuilder.GetTexCoords( context, face.TexInfo, finalPos );
}
}
if ( positions.Length > dInfo.VertCount )
{
return mesh;
}
var hVerts = mesh.AddVertices( positions.ToArray() );
// triangulate quads into triangles and add as faces
for ( int y = 0; y < side - 1; y++ )
{
for ( int x = 0; x < side - 1; x++ )
{
int a = y * side + x;
int b = a + 1;
int c = a + side;
int d = c + 1;
// quad vertices
var v_a = positions[a];
var v_b = positions[b];
var v_c = positions[c];
var v_d = positions[d];
// quad uvs
var uv_a = uvs[a];
var uv_b = uvs[b];
var uv_c = uvs[c];
var uv_d = uvs[d];
//var faceVerts = new[] { hVerts[a], hVerts[c], hVerts[d], hVerts[b] };
//var faceUvs = new[] { uvs[a], uvs[c], uvs[d], uvs[b] };
//var hFace = mesh.AddFace( faceVerts );
//mesh.SetFaceTextureCoords( hFace, faceUvs );
//continue;
// just use Triangle for area check
var _tri1 = new Triangle( v_a, v_c, v_b );
var _tri2 = new Triangle( v_c, v_d, v_b );
const float epsilon = 1e-6f;
// skip degenerate faces
if ( _tri1.Area < epsilon || _tri2.Area < epsilon )
{
continue;
}
// per-tri vertices - winding
var v_t1 = new[] { hVerts[a], hVerts[c], hVerts[b] };
var v_t2 = new[] { hVerts[c], hVerts[d], hVerts[b] };
// per-tri uvs
var uv_t1 = new[] { uv_a, uv_c, uv_b };
var uv_t2 = new[] { uv_c, uv_d, uv_b };
var materialFallback = $"bsp_vertex_color";
var t1 = mesh.AddFace( v_t1 );
//mesh.SetEdgeSmoothing( t1.Edge, PolygonMesh.EdgeSmoothMode.Soft );
mesh.SetFaceTextureCoords( t1, uv_t1 );
if ( dispMaterial is not null )
{
mesh.SetFaceMaterial( t1, dispMaterial );
}
else
{
var material = Material.Load( $"materials/{materialFallback}.vmat" );
mesh.SetFaceMaterial( t1, material );
foreach ( var edge in mesh.HalfEdgeHandles )
{
Color col = new Color( reflectivity.x, reflectivity.y, reflectivity.z );
mesh.SetVertexColor( edge, col.ToColor32( true ) );
}
}
var t2 = mesh.AddFace( v_t2 );
//mesh.SetEdgeSmoothing( t2.Edge, PolygonMesh.EdgeSmoothMode.Soft );
mesh.SetFaceTextureCoords( t2, uv_t2 );
if ( dispMaterial is not null )
{
mesh.SetFaceMaterial( t2, dispMaterial );
}
else
{
var material = Material.Load( $"materials/{materialFallback}.vmat" );
mesh.SetFaceMaterial( t2, material );
foreach ( var edge in mesh.HalfEdgeHandles )
{
Color col = new Color( reflectivity.x, reflectivity.y, reflectivity.z );
mesh.SetVertexColor( edge, col.ToColor32( true ) );
}
}
}
}
return mesh;
}
}