Architecture & Engineering

절차적 3D 복셀 시스템: 밀도 필드, 마칭 큐브 등가면 생성 및 청크 스트리밍

By DopaBrain 절차적 월드 생성 기술팀 • Published 2026-10-01

로블록스의 기본 스무스 테레인(Smooth Terrain)은 훌륭한 비주얼을 제공하지만, 샌드박스, 우주 탐사, 대규모 채굴 서바이벌 게임에서는 근본적인 구조적 한계를 지닙니다. 4x4x4 스터드로 고정된 복셀 해상도, 구형 행성이나 복잡한 오버행 동굴 제작 시 발생하는 정점 왜곡, 그리고 수 킬로미터 규모의 맵 로딩 시 서버와 클라이언트 메모리가 급증하는 문제입니다.

완벽한 절차적 제어 권한을 확보하기 위해 최고 수준의 테크니컬 개발자들은 마칭 큐브(Marching Cubes) 알고리즘과 로블록스 EditableMesh API를 결합한 커스텀 복셀 엔진을 제작합니다. 본 실전 가이드에서는 Luau 기반의 상용급 복셀 엔진을 구축합니다. 3D 연속 스칼라 필드 구성, 256개 모서리 비트마스크 삼각화 룩업, 중심 차분법 기반 정밀 법선 계산, 그리고 프레임 드랍 없는 청크 LOD 스트리밍 파이프라인을 다룹니다.

1. 네이티브 지형의 한계: 왜 스무스 테레인은 샌드박스에 부적합한가

로블록스의 기본 지형 엔진은 고정 해상도 복셀 구조로 인해 다음과 같은 명확한 한계를 지닙니다:

2. 수학적 기반: 3D 스칼라 필드와 마칭 큐브 알고리즘

마칭 큐브는 3차원 이산 공간의 밀도 필드로부터 2차원 연속 표면(등가면)을 추출합니다:

3. Luau 마칭 큐브 EditableMesh 엔진 구현체

Part 인스턴스 남발 없이 EditableMesh 정점 버퍼에 직접 폴리곤을 생성하는 고성능 Luau 스크립트입니다:

VoxelChunkGenerator.luau (마칭 큐브 코어 엔진)
--!strict
local Workspace = game:GetService("Workspace")
local AssetService = game:GetService("AssetService")

local TRIANGULATION_TABLE = require(script.MarchingCubesTables).Triangles
local EDGE_TABLE = require(script.MarchingCubesTables).Edges

local VoxelChunk = {}
VoxelChunk.__index = VoxelChunk

export type ChunkData = {
    ChunkCoord: Vector3,
    Size: number,
    Resolution: number,
    DensityField: {number},
    MeshPart: MeshPart?,
    EditableMesh: any?,
}

function VoxelChunk.new(coord: Vector3, size: number, resolution: number): ChunkData
    local self = setmetatable({}, VoxelChunk)
    self.ChunkCoord = coord
    self.Size = size
    self.Resolution = resolution
    self.DensityField = table.create((resolution + 1) ^ 3, 0)
    return self
end

function VoxelChunk:GetIndex(x: number, y: number, z: number): number
    local res = self.Resolution + 1
    return x + y * res + z * res * res + 1
end

function VoxelChunk:EvaluateDensity(worldX: number, worldY: number, worldZ: number): number
    -- 3D 밀도 함수: 양수 = 암석, 음수 = 공기
    local baseHeight = 50 - worldY
    local noise3D = math.noise(worldX * 0.02, worldY * 0.02, worldZ * 0.02) * 25
    local caveNoise = math.abs(math.noise(worldX * 0.04, worldY * 0.04, worldZ * 0.04)) * 30
    return baseHeight + noise3D - caveNoise
end

function VoxelChunk:PopulateDensity()
    local res = self.Resolution
    local step = self.Size / res
    local origin = self.ChunkCoord * self.Size

    for z = 0, res do
        for y = 0, res do
            for x = 0, res do
                local worldPos = origin + Vector3.new(x * step, y * step, z * step)
                local idx = self:GetIndex(x, y, z)
                self.DensityField[idx] = self:EvaluateDensity(worldPos.X, worldPos.Y, worldPos.Z)
            end
        end
    end
end

function VoxelChunk:BuildMesh(parent: Instance)
    local editableMesh = AssetService:CreateEditableMesh()
    local res = self.Resolution
    local step = self.Size / res
    local isoLevel = 0.0

    -- 큐브 모서리 상대 오프셋
    local cornerOffsets = {
        Vector3.new(0, 0, 0), Vector3.new(step, 0, 0), Vector3.new(step, step, 0), Vector3.new(0, step, 0),
        Vector3.new(0, 0, step), Vector3.new(step, 0, step), Vector3.new(step, step, step), Vector3.new(0, step, step)
    }

    for z = 0, res - 1 do
        for y = 0, res - 1 do
            for x = 0, res - 1 do
                local cubeIndex = 0
                local cornerDensities = {}
                local cornerPositions = {}

                for i = 1, 8 do
                    local offset = cornerOffsets[i]
                    local gx = x + math.round(offset.X / step)
                    local gy = y + math.round(offset.Y / step)
                    local gz = z + math.round(offset.Z / step)
                    local d = self.DensityField[self:GetIndex(gx, gy, gz)]
                    cornerDensities[i] = d
                    cornerPositions[i] = Vector3.new(x * step, y * step, z * step) + offset

                    if d > isoLevel then
                        cubeIndex = bit32.bor(cubeIndex, bit32.lshift(1, i - 1))
                    end
                end

                if cubeIndex > 0 and cubeIndex < 255 then
                    local triList = TRIANGULATION_TABLE[cubeIndex + 1]
                    for t = 1, #triList, 3 do
                        local e1, e2, e3 = triList[t], triList[t + 1], triList[t + 2]
                        -- 모서리 보간 및 정점 생성
                        local v1 = self:InterpolateEdge(e1, cornerPositions, cornerDensities, isoLevel)
                        local v2 = self:InterpolateEdge(e2, cornerPositions, cornerDensities, isoLevel)
                        local v3 = self:InterpolateEdge(e3, cornerPositions, cornerDensities, isoLevel)

                        local id1 = editableMesh:AddVertex(v1)
                        local id2 = editableMesh:AddVertex(v2)
                        local id3 = editableMesh:AddVertex(v3)
                        editableMesh:AddTriangle(id1, id2, id3)
                    end
                end
            end
        end
    end

    local meshPart = AssetService:CreateMeshPartAsync(Content.fromObject(editableMesh))
    meshPart.Position = self.ChunkCoord * self.Size
    meshPart.Anchored = true
    meshPart.Parent = parent
    self.MeshPart = meshPart
end

return VoxelChunk

4. 청크 스트리밍 및 옥트리 LOD(Level of Detail) 아키텍처

무한한 절차적 세계를 구현하려면 거리 기반의 정밀한 기하 구조 단순화가 필수적입니다:

5. 실시간 복셀 채굴, 지형 파괴 및 변형 테크닉

커스텀 복셀 엔진의 가장 큰 매력은 실시간으로 지형을 파괴하고 건설하는 샌드박스 메커니즘입니다:

Frequently Asked Questions

커스텀 마칭 큐브의 성능은 네이티브 로블록스 스무스 테레인과 비교해 어떤가요?

네이티브 지형은 C++ 엔진 레벨에서 고도로 최적화되어 있지만 구조가 경직되어 있습니다. 반면 EditableMesh와 병렬 Luau를 결합한 마칭 큐브 엔진은 멀티스레드로 구동되어 구형 행성, 실시간 무제한 채굴, 완벽한 샌드박스 자유도를 60 FPS로 실현할 수 있습니다.

EditableMesh로 생성된 복셀 지형에서 플레이어 충돌(Collision)이 정상 작동하나요?

작동합니다. AssetService:CreateMeshPartAsync를 통해 EditableMesh로부터 MeshPart를 인스턴스화하면 로블록스 물리 엔진이 정밀 충돌 형상을 생성합니다. 초고속 로컬 충돌 판정은 수학적 3D 밀도 필드를 레이캐스트하여 즉시 판정할 수도 있습니다.

서로 다른 LOD를 가진 청크 경계면에서 틈새(Seam)가 생기는 것은 어떻게 방지하나요?

가장 보편적인 방법은 청크 경계면에 얇은 수직 폴리곤 스커트(Transition Skirt)를 내려 틈새를 덮는 것입니다. 더 정밀한 구현을 위해서는 LOD 전이 전용 룩업 테이블(Transvoxel)을 적용하여 정점들을 직접 용접(Welding)합니다.

수천 개의 원거리 복셀 청크를 메모리 부족 없이 스트리밍하는 비결은 무엇인가요?

플레이어 주변의 활성 청크만 완전한 EditableMesh 지오메트리를 유지하고, 원거리 청크는 압축된 밀도 시드(Seed)나 저해상도 높이맵만 메모리에 캐싱한 뒤 플레이어가 접근할 때 온디맨드로 메시를 합성합니다.

Explore More Interactive Tests & Guides

Discover personalized cognitive assessments, stress evaluations, and game psychology tools on DopaBrain.

Go to Portal Hub