Vedo로 Civil3D의 지표면 시각화하기(1/2)


1.시작
Civil3D를 사용해보신 분이라면 아시겠지만, 지표면이 많은 도면을 3D 화면으로 보려고 시점을 조금이라도 변경하려고 하면 너무나 많은 시간이 소요된다. 특히, 여러 개의 지표면이 중첩된 복잡한 도면에서는 회전을 하는 순간 분 단위로 기다려야 하는 경우도 발생한다. 객체 뷰어로 보고 싶은 지표면을 모두 선택한 다음에 보는 것도 한 방법이지만, 작업을 돌리는 중에 뷰어를 껐다 키는 것이 불편하고, 여기에 절성토 구획을 보는 기능도 추가를 하고 싶어서 따로 지표면 뷰어를 제작하게 되었다.
2.Vedo
Python 기반의 여러 3D 시각화 라이브러리를 찾던 중에 찾은 Vedo라는 3D 시각화 라이브러리다.
PyVista 나 VTK, Mayavi, pythreejs 등등이 후보로 올라왔었지만, 별도의 렌더링 파이프라인 없이 몇 줄의 코드로 바로 3D 메시를 시각화 할 수 있고, 내가 원하는 기능(지표면 간 높이 차를 히트맵으로 보여주는 기능)이 그대로 예시로 작성되어 있어서 Vedo를 사용하기로 결정했다.
3. Pipe vs TCP/IP
C# 기반 프로세스에서 Python 기반 뷰어로 데이터를 전달하는 방법은 여러 가지가 있다. 그냥 외부에 .json
파일을 생성한 후 변경된 이벤트만 뷰어 쪽에 던질 수도 있지만, 그럴 경우 입출력 오버헤드가 생기기 때문에, 직접 데이터를 이진 직렬화를 수행한 스트림을 통해 전달하는 방식을 선택했다.
IPC 를 구현하는 데 2가지 방식을 고려했는데, 하나는 Named Pipe, 다른 하나는 TCP/IP 소켓이다.
Named Pipe는 동일 머신 내의 C# ↔ Python 간 통신에서 빠르고 가볍게 동작하며, 설정이 단순하다는 장점이 있다. 반면 TCP/IP는 원격 연결이나 멀티 플랫폼 확장을 고려할 때 더 유연한 구조를 제공한다.
본 프로젝트에서는 Civil3D와 Python vedo
뷰어 간 통신의 실시간성과 확장성을 동시에 고려하여, 두 방식 모두 대응 가능한 구조로 설계하였다. 메시 전송 모듈은 내부적으로 SurfacePacket
구조체를 직렬화한 이진 데이터를 스트림으로 변환하여 전송하며, Python 쪽에서는 이를 역직렬화하여 실시간으로 메시를 재구성하고 시각화하는 방향으로 프로젝트 구성을 마쳤다.
이러한 구조는 지표면이 대량으로 변경되거나 여러 종류의 메시를 병합해서 처리해야 하는 경우에도 안정적으로 작동하며, 향후 단일 Surface 갱신, 변위 비교, 사용자 인터랙션 처리 등으로의 확장도 용이하다.
4. Mesh 전달 방법
지표면의 삼각형 정보를 어떤 식으로 전달해야 Vedo에서 Mesh를 구성할 수 있는지는 Vedo의 ‘hello world mesh’ 예시에 친절하게 적혀있다.
verts = [(50,50,50), (70,40,50), (50,40,80), (80,70,50)]
cells = [(0,1,2), (2,1,3), (1,0,3)]
verts
: 메시의 정점(Point3D) 목록 각 튜플은(x, y, z)
좌표- 총 4개 정점
cells
: 각 셀(=면)은 3개의 정점 인덱스로 구성된 삼각형(face)(0,1,2)
는verts[0]
,verts[1]
,verts[2]
로 이루어진 삼각형을 의미
즉, 정점의 좌표 배열과 해당 정점 배열의 인덱스를 가지고 면 정보를 구성한다.
이런 식으로 Civil3d의 지표면을 이루는 삼각형을 순회하면서 해당 정보를 저장해보자.
C#
public class SurfacePacket
{
public string Name { get; set; } // 지표면 이름
public string Type { get; set; } // EG, FG, NOSANG, NOCHE
public List<Point3d> Vertices { get; set; } // 정점 구성
public List<int[]> Faces { get; set; } // 면 구성
}
//...
//...
foreach (var sid in allSurfaceIds)
{
if (!(tr.GetObject(sid, OpenMode.ForRead) is TinSurface tin)) continue;
var tris = tin.GetTriangles(false);
if (tris.Count == 0) continue;
// 3. 폴더명 가져오기
string folderName = "";
if (tin.FolderId != ObjectId.Null)
{
var folder = tr.GetObject(tin.FolderId, OpenMode.ForRead) as Folder;
folderName = folder?.Name ?? "";
}
// 4. 타입 분류
string type = ResolveSurfaceType(folderName);
// 5. SurfacePacket 생성
var surface = new SurfacePacket
{
Name = tin.Name,
Type = type,
Vertices = new List<Point3d>(),
Faces = new List<int[]>(),
};
foreach (var tri in tris)
{
int startIdx = surface.Vertices.Count;
surface.Vertices.AddRange(new[] {
tri.Vertex1.Location,
tri.Vertex2.Location,
tri.Vertex3.Location
});
surface.Faces.Add(new[] { startIdx, startIdx + 1, startIdx + 2 });
}
surfaces.Add(surface);
//...
5.데이터 전달
1. TCP 클라이언트 연결
using (var client = new TcpClient("127.0.0.1", 9009))
using (var stream = client.GetStream())
TcpClient
로localhost:9009
에 연결을 시도연결 성공 시,
NetworkStream
을 열어 데이터를 보낼 준비
2. 메모리 스트림 및 바이너리 라이터 준비
using (var ms = new MemoryStream())
using (var bw = new BinaryWriter(ms))
데이터를 메모리에 먼저 바이너리로 쓰기 위해
MemoryStream
생성BinaryWriter
를 통해 숫자/문자열/배열을 압축되지 않은 순수 이진 데이터로 씀
3. 헤더와 Surface 수량 전송
bw.Write(Encoding.UTF8.GetBytes(header));
bw.Write(surfaces.Count);
먼저
MODE:INIT_ALL\n
같은 문자열 헤더를 UTF-8 바이트로 작성이어서 surface 개수를
int
로 전송 → Python 쪽에서 루프 파싱 가능
4. 각 SurfacePacket 직렬화
foreach (var s in surfaces)
{
var nameBytes = Encoding.UTF8.GetBytes(s.Name);
var typeBytes = Encoding.UTF8.GetBytes(s.Type ?? "UNKNOWN");
bw.Write(nameBytes.Length); // 이름 길이 (int)
bw.Write(nameBytes); // 이름 (byte[])
bw.Write(typeBytes.Length); // 타입 길이
bw.Write(typeBytes); // 타입
bw.Write(s.Vertices.Count); // 정점 개수
foreach (var pt in s.Vertices)
{
bw.Write(pt.X); bw.Write(pt.Y); bw.Write(pt.Z); // 각 정점 좌표 (double 3개)
}
bw.Write(s.Faces.Count); // 삼각형 면 개수
foreach (var f in s.Faces)
{
bw.Write(f[0]); bw.Write(f[1]); bw.Write(f[2]); // 인덱스 (int 3개)
}
}
파싱 가능한 구조 예시
[헤더][Surface 개수]
[이름 길이][이름][타입 길이][타입]
[정점 수][x,y,z]*n
[면 수][i0,i1,i2]*n
... (반복)
Python 측에서는 이 구조에 맞춰 .recv()
→ .frombuffer()
또는 struct.unpack()
등으로 복원
5. 실제 전송
var fullBytes = ms.ToArray();
stream.Write(fullBytes, 0, fullBytes.Length);
stream.Flush();
메모리 스트림에 쓴 모든 바이트를 TCP 스트림에 한 번에 전송
Flush()
로 버퍼 클리어
다음 편에서는 VSCode 환경에서 Vedo를 설치하고 해당 데이터를 전달받는 과정을 작성해보겟다
Subscribe to my newsletter
Read articles from HyunKun Cho directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
