Defold의 완전한 3D 지원: 기술 심층 분석 및 아키텍처 튜토리얼
핵심 요약
경량 2D 엔진으로 유명했던 Defold가 glTF 지원과 커스텀 render_script 아키텍처를 통해 고성능 3D 게임 엔진으로 진화한 과정을 심층적으로 분석합니다. 1인칭 카메라 구현을 위한 Quaternion 연산부터 효율적인 3D Multiplayer 데이터 동기화 및 모바일 웹 최적화를 위한 쉐이더 작성법까지 실무적인 가이드를 제공합니다. 특히 초경량 클라이언트의 이점을 극대화하면서 horizOn과 같은 Backend 서비스를 활용해 복잡한 서버 인프라 문제를 해결하는 전략을 제시합니다.
모든 인디 개발자는 게임 엔진이 자신을 배신하는 순간을 알고 있습니다. 가벼운 2D 컨셉으로 시작했다가 기획이 확장되면서 3D 요소가 도입되면, 갑자기 엔진의 빌드 사이즈가 폭발하고, 로딩 시간은 느려지며, WebGL 빌드는 메모리 부족으로 크래시가 발생하곤 합니다. Unity나 Unreal 같은 거대 엔진은 AAA급 품질을 구현하는 데 경이적이지만, frictionless한 크로스 플랫폼 배포—특히 WebGL과 모바일—를 목표로 하는 1인 개발자에게는 마치 식료품점에 가기 위해 탱크를 운전하는 것처럼 느껴질 때가 많습니다.
Enter Defold. 역사적으로 HTML5부터 Nintendo Switch까지 단일 코드베이스로 모든 플랫폼을 지원하는 초고속 제로 블로트(zero-bloat) 2D 엔진으로 정평이 나 있던 Defold가 공식적으로 성장했습니다. 이제 Defold는 완전한 기능을 갖춘 3D 게임 엔진입니다. 이전에도 기술적으로는 내부적으로 3D 컨텍스트에서 렌더링(orthographic camera를 통해 평면을 투영)해 왔지만, 최근 업데이트를 통해 전용 3D 툴링, 완전한 glTF mesh 지원, 그리고 진정한 3D 개발을 위한 간소화된 워크플로우를 도입했습니다.
이는 생태계에 있어 엄청난 변화입니다. 만약 당신이 몇 초 만에 컴파일되고, 한 자릿수 메가바이트 단위의 바이너리를 생성하면서도 역동적인 3D 월드를 렌더링할 수 있는 엔진을 찾고 있다면, Defold는 이제 최상위권의 선택지입니다. 이번 Defold 3D 게임 엔진 튜토리얼 및 아키텍처 분석에서는 Defold의 3D Rendering Pipeline 작동 방식, 커스텀 3D 카메라를 처음부터 스크립팅하는 법, 그리고 경량 3D 게임의 네트워킹을 위한 Backend 측면의 고려 사항을 깊이 있게 다룰 것입니다.
내부 구조: Defold 3D 파이프라인의 실체
3D 환경에서 Defold를 마스터하려면 먼저 그 렌더링 철학을 이해해야 합니다. Defold는 Unreal Engine처럼 미리 구성된 PBR(Physically Based Rendering) 파이프라인을 기본으로 제공하지 않습니다. 대신, Lua로 작성된 고도로 최적화되고 데이터 중심적인 render script를 제공합니다.
Defold의 화면에 그려지는 모든 것은 render_script에 의해 처리됩니다. 기본적으로 이 스크립트는 2D에 최적화되어 있습니다. orthographic projection matrix를 설정하고, 스프라이트를 Z-value(depth)에 따라 정렬하며, 뒤에서 앞으로(back-to-front) 그립니다. Defold의 3D 기능을 활성화하려면, 이 스크립트를 재작성하여 perspective projection matrix를 활용하고, 하드웨어 depth testing을 활성화하며, 3D 모델을 위한 커스텀 render predicates를 정의해야 합니다.
이러한 로우 레벨 접근 방식은 양날의 검과 같습니다. 한편으로는 행렬 수학을 직접 작성해야 하지만, 다른 한편으로는 draw calls에 대한 절대적인 제어권을 가집니다. 이를 통해 모놀리식 엔진으로는 구현하기 매우 어려운 방식으로 초저사양 하드웨어에서도 렌더링을 최적화할 수 있습니다.
커스텀 3D Render Script 설계하기
그리기 순서에 따라 3D 모델이 잘못 겹치지 않고 올바르게 렌더링되도록 하려면 depth buffer(Z-buffer)를 활성화해야 합니다. 다음은 Defold의 기본 파이프라인을 대체하는 기초적인 3D render script입니다.
-- main/3d_pipeline.render_script
function init(self)
-- 배경 클리어 컬러 정의 (RGBA)
self.clear_color = vmath.vector4(0.1, 0.1, 0.12, 1.0)
self.clear_buffers = {
[render.BUFFER_COLOR_BIT] = self.clear_color,
[render.BUFFER_DEPTH_BIT] = 1.0,
[render.BUFFER_STENCIL_BIT] = 0
}
-- render predicates 생성. 'model'은 3D 메시의 기본 태그입니다.
self.predicates = {
model = render.predicate({"model"}),
gui = render.predicate({"gui"}),
text = render.predicate({"text"})
}
end
function update(self)
-- 1. 프레임을 위한 렌더링 상태 설정
render.set_depth_mask(true)
render.set_stencil_mask(0xff)
render.clear(self.clear_buffers)
-- 2. 3D Camera Projection 구성
local window_width = render.get_window_width()
local window_height = render.get_window_height()
if window_width == 0 or window_height == 0 then return end
local aspect_ratio = window_width / window_height
local fov = math.rad(60) -- 60도 Field of View
local near_z = 0.1
local far_z = 1000.0
local proj_matrix = vmath.matrix4_perspective(fov, aspect_ratio, near_z, far_z)
render.set_projection(proj_matrix)
-- 3. Depth Testing이 활성화된 상태로 3D 모델 그리기
render.set_depth_test(render.COMPARE_LEQUAL)
render.set_cull_face(render.FACE_BACK)
render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA)
-- view matrix는 카메라 스크립트로부터 메시지를 통해 전달됩니다.
if self.view_matrix then
render.set_view(self.view_matrix)
render.draw(self.predicates.model)
end
-- 4. 3D 씬 위에 GUI 그리기 (Orthographic)
render.set_depth_mask(false)
render.set_depth_test(render.COMPARE_ALWAYS)
local gui_proj = vmath.matrix4_orthographic(0, window_width, 0, window_height, -1, 1)
render.set_projection(gui_proj)
render.set_view(vmath.matrix4())
render.draw(self.predicates.gui)
render.draw(self.predicates.text)
end
function on_message(self, message_id, message)
if message_id == hash("set_view_matrix") then
self.view_matrix = message.matrix
end
end
여기서 우리가 상태를 얼마나 명시적으로 관리하고 있는지 주목하십시오. depth buffer를 지우고, 현재 윈도우 크기에 기반해 perspective matrix를 계산하며, 래스터화 사이클을 절약하기 위해 back-face culling을 강제한 후, 마지막으로 GUI를 렌더링하기 전에 projection matrix를 다시 orthographic으로 전환합니다. 이를 통해 견고한 분할 파이프라인 렌더링 아키텍처를 구축할 수 있습니다.
1인칭 3D 카메라 컨트롤러 구축하기
render script만으로는 공간을 이동하는 카메라 없이는 아무것도 보여줄 수 없습니다. Defold는 Message Passing 아키텍처를 기반으로 작동합니다. 카메라가 직접 transform.Translate()를 호출하는 객체 지향 엔진과 달리, Defold에서는 카메라 스크립트가 view matrix를 계산하고 이를 방금 작성한 render script로 전달합니다.
마우스 룩(pitch 및 yaw)과 키보드 이동(WASD)을 처리하는 표준 1인칭 카메라를 만들어 보겠습니다.
-- scripts/camera_controller.script
go.property("mouse_sensitivity", 0.2)
go.property("move_speed", 10.0)
function init(self)
msg.post(".", "acquire_input_focus")
self.pitch = 0
self.yaw = 0
-- 마우스 커서 숨기기 및 고정
window.set_mouse_lock(true)
self.forward = vmath.vector3(0, 0, -1)
self.right = vmath.vector3(1, 0, 0)
self.up = vmath.vector3(0, 1, 0)
self.velocity = vmath.vector3(0)
end
function update(self, dt)
local pos = go.get_position()
-- 이동 속도 적용
if vmath.length_sqr(self.velocity) > 0 then
local move_dir = vmath.normalize(self.velocity)
pos = pos + move_dir * self.move_speed * dt
go.set_position(pos)
end
-- view matrix 계산
local rotation = go.get_rotation()
self.forward = vmath.rotate(rotation, vmath.vector3(0, 0, -1))
local target = pos + self.forward
local view_matrix = vmath.matrix4_look_at(pos, target, self.up)
-- 계산된 view matrix를 render 파이프라인으로 전송
msg.post("@render:", "set_view_matrix", { matrix = view_matrix })
-- 다음 프레임을 위해 속도 초기화
self.velocity = vmath.vector3(0)
end
function on_input(self, action_id, action)
if action_id == hash("mouse_moved") then
self.yaw = self.yaw - action.dx * self.mouse_sensitivity
self.pitch = self.pitch + action.dy * self.mouse_sensitivity
-- 카메라가 뒤집히지 않도록 pitch 제한
self.pitch = math.max(-89, math.min(89, self.pitch))
-- Euler angles를 Quaternion으로 변환
local rot_y = vmath.quat_rotation_y(math.rad(self.yaw))
local rot_x = vmath.quat_rotation_x(math.rad(self.pitch))
go.set_rotation(rot_y * rot_x)
elseif action_id == hash("move_forward") then
self.velocity = self.velocity + self.forward
elseif action_id == hash("move_backward") then
self.velocity = self.velocity - self.forward
elseif action_id == hash("move_left") then
self.right = vmath.cross(self.forward, self.up)
self.velocity = self.velocity - self.right
elseif action_id == hash("move_right") then
self.right = vmath.cross(self.forward, self.up)
self.velocity = self.velocity + self.right
end
end
이 스크립트는 마우스 델타 이동량을 캡처하여 pitch와 yaw를 조정하고, 이 Euler angles를 견고한 Quaternion 회전값으로 변환합니다. 그런 다음 해당 회전값에서 직접 forward 및 right 벡터를 도출하여, "W" 키를 눌렀을 때 항상 현재 바라보는 방향으로 이동하도록 보장합니다.
3D Asset 통합 및 커스텀 Shader
최근 Defold 업데이트를 통해 3D Asset을 프로젝트로 가져오는 작업이 매우 간편해졌습니다. 엔진은 웹 및 경량 게임 개발의 업계 표준이 된 .gltf 및 .glb 포맷을 네이티브로 지원합니다.
하지만 메시를 렌더링하려면 material이 필요하고, material에는 Shader가 필요합니다. 기본적으로 Defold는 기초적인 재질을 포함하고 있지만, 직접 GLSL Shader를 작성하면 시각적인 차별성을 확보할 수 있습니다. 모바일이나 HTML5 타겟에 완벽하게 최적화된 빠른 unlit textured shader를 작성해 보겠습니다.
Vertex Shader (model.vp)
// model.vp
uniform highp mat4 view_proj;
uniform highp mat4 world;
attribute highp vec4 position;
attribute mediump vec2 texcoord0;
varying mediump vec2 var_texcoord0;
void main()
{
// 버텍스의 최종 화면 공간 위치 계산
vec4 p = view_proj * world * vec4(position.xyz, 1.0);
var_texcoord0 = texcoord0;
gl_Position = p;
}
Fragment Shader (model.fp)
// model.fp
varying mediump vec2 var_texcoord0;
uniform lowp sampler2D texture_sampler;
uniform lowp vec4 tint;
void main()
{
// 텍스처를 샘플링하고 컬러 틴트를 곱함
lowp vec4 tex_color = texture2D(texture_sampler, var_texcoord0.xy);
gl_FragColor = tex_color * tint;
}
Defold material 파일에서 view_proj 유니폼을 엔진 내장 view-projection matrix에 매핑하고, texture_sampler를 메시의 diffuse texture에 매핑합니다. 이 쉐이더들은 동적 조명이나 shadow maps를 계산하지 않기 때문에 매우 빠르게 실행되며, 저사양 하드웨어에서도 60 FPS를 쉽게 유지할 수 있게 해줍니다.
3D Multiplayer 및 State Synchronization 처리
2D 그리드 기반 게임에서 완전한 3D 환경으로 전환할 때, 네트워킹 아키텍처의 복잡성은 기하급수적으로 증가합니다. 2D에서 3D로 이동한다는 것은 이제 state synchronization이 Z축 깊이, 물리 엔진 간의 부동 소수점 오차, 그리고 전체 quaternion 회전까지 고려해야 함을 의미합니다. 이를 잘못 처리하면 심각한 시각적 끊김(stuttering) 현상이 발생하는데, 이는 How To Fix Player Location Desync In Uefn And Unreal Engine Multiplayer 분석에서 다룬 흔한 문제입니다.
Defold는 경량 엔진이기 때문에 Unreal Engine의 RPC와 같은 무거운 내장 복제 시스템을 제공하지 않습니다. 개발자는 상태 데이터를 효율적으로 패킹할 책임이 있습니다.
3D 위치 동기화를 위해 REST API에 의존하면 게임 루프에 즉시 병목 현상이 발생합니다. 대신 지속적이고 양방향적인 연결이 필요합니다. 이전 가이드에서 Ditch Http Polling An Unreal Engine Websockets Tutorial For Real Time Backends를 다루었지만, 동일한 아키텍처 원칙이 Defold의 Lua 기반 WebSocket 확장 기능에도 직접 적용됩니다.
다음은 WebSocket을 통해 페이로드 크기를 최소화하기 위해 3D transform 데이터를 직렬화하는 방법의 예입니다.
-- scripts/network_sync.lua
local json = require "builtins.scripts.json"
function serialize_transform(go_id)
local pos = go.get_position(go_id)
local rot = go.get_rotation(go_id)
-- 페이로드를 필요한 최소 데이터로 압축
-- 대역폭 절약을 위해 위치값을 소수점 둘째 자리까지 반올림
local payload = {
id = hash_to_hex(go_id),
x = math.floor(pos.x * 100) / 100,
y = math.floor(pos.y * 100) / 100,
z = math.floor(pos.z * 100) / 100,
-- 정확한 복원을 위해 Quaternion의 4개 성분이 모두 필요함
qx = rot.x,
qy = rot.y,
qz = rot.z,
qw = rot.w
}
return json.encode(payload)
end
부동 소수점을 반올림하고 필수 데이터만 패킹함으로써, 빠른 이동 업데이트 중에도 네트워크 버퍼가 넘치는 것을 방지할 수 있습니다. 이 흐름을 관리하는 것이 응답성 좋은 크로스 플랫폼 플레이의 핵심입니다.
크로스 플랫폼 3D의 인프라 부담
Defold에서 고도로 최적화된 3D 게임 클라이언트를 작성하는 것은 매우 만족스러운 작업입니다. 엔진은 방해되지 않고 수 밀리초 만에 컴파일되며, 개발자가 오직 로직에만 집중할 수 있게 해줍니다.
하지만 그 3D 게임에 Multiplayer 기능을 넣거나, 클라우드 저장 기능을 추가하거나, 크로스 플랫폼 리더보드를 구현하기로 결정하는 순간, 관심사는 게임 개발에서 서버 오케스트레이션으로 급격히 이동합니다. 갑자기 Dockerfiles를 작성하고, Kubernetes 클러스터를 구성하고, 세션 상태를 위해 Redis 인스턴스와 씨름하며, DDoS 공격으로부터 WebSocket 게이트웨이를 보호하느라 고군분투하게 됩니다.
이를 직접 구축하려면 로드 밸런서, 데이터베이스 샤딩, SSL 인증서 관리를 설정해야 하며, 신뢰할 수 있는 프로토타입 하나를 실행하는 데만 4~6주의 작업이 소요될 수 있습니다.
horizOn을 사용하면 이러한 Backend 서비스들이 미리 구성된 상태로 제공되므로, 인프라가 아닌 게임 자체를 출시하는 데 집중할 수 있습니다. horizOn은 사용자 인증, 실시간 데이터베이스 동기화, 서버 권한 로직을 위한 네이티브 통합을 제공하며, 독점적인 백엔드 생태계 없이 출시되는 Defold와 같은 경량 엔진의 간극을 완벽하게 메워줍니다. 개발자는 작은 클라이언트 엔진의 속도를 유지하면서도 서버 아키텍처의 무거운 작업은 외부로 위임할 수 있습니다.
Defold 3D 개발을 위한 모범 사례
Defold에서 상업적으로 실현 가능한 3D 프로젝트를 계획하고 있다면 다음 아키텍처 가이드라인을 엄격히 준수하십시오.
- 지오메트리를 고도로 최적화 유지: Defold는 속도를 위해 설계되었습니다. 경량화의 장점을 유지하려면, 특히 HTML5/WebGL을 타겟팅하는 경우 씬당 총 폴리곤 수를 100,000개 미만으로 유지하십시오. 디테일을 표현할 때 고밀도 메시 대신 baked normal maps를 사용하십시오.
- Frustum Culling을 위한 Render Predicates 활용: Defold는 기본적으로 3D 공간에서 카메라 시야 밖에 있는 객체를 자동으로 컬링하지 않습니다. Lua에서 직접 커스텀 frustum culling 로직을 작성하여 시야 밖 객체의 모델 컴포넌트를 동적으로 비활성화함으로써 래스터화 시간을 절약해야 합니다.
- Atlasing을 통한 Draw Calls 통합: 고유한 material과 텍스처마다 GPU로 별도의 draw call을 보내야 합니다. 텍스처를 커다란 texture atlases로 결합하십시오. 10개의 서로 다른 3D 모델이 동일한 재질과 아틀라스를 공유한다면, Defold는 내부적으로 훨씬 더 효율적으로 배칭할 수 있습니다.
- 복잡한 수학 연산 미리 계산: 행렬 곱셈과 quaternion 변환은 Lua에서 매우 비용이 많이 드는 연산입니다. forward 및 right 벡터를 캐싱하고, 매 프레임 무조건 계산하는 대신 플레이어의 회전이 실제로 변경될 때만 다시 계산하십시오.
- 렌더링 주기로부터 로직 분리: 게임 로직(
update)은 60 FPS로 실행될 수 있지만, 커스텀 물리나 네트워킹 단계는 30 FPS로 작동할 수 있습니다. 다양한 모니터 주사율에서도 부드러운 렌더링을 보장하기 위해, 3D 시각적 위치를 최신 상태로 바로 스냅하기보다 속도 기반으로 보간(Interpolate)하십시오. - Lua Garbage Collection 관리: 동적인 3D 환경에서는 벡터 객체와 행렬을 빈번하게 생성하고 소멸시킵니다. Lua의 Garbage Collector를 관리하지 않으면 눈에 띄는 프레임 스파이크가 발생할 수 있습니다.
update루프 내부에서 로컬 변수를 새로 생성하는 대신, 기존vmath.vector3및vmath.matrix4인스턴스의 내부 값을 직접 업데이트하여 재사용하십시오. 총알이나 엔티티를 위해 메모리 풀을 미리 할당하십시오. - 라이팅 외부 베이킹: 커스텀 GLSL Shader에서의 동적 조명은 모바일 기기 성능 예산을 빠르게 소모하므로, glTF 모델을 내보내기 전에 Blender나 Maya를 사용하여 글로벌 일루미네이션과 앰비언트 오클루전을 텍스처에 직접 베이킹하십시오. 아름답게 조명이 베이킹된 단순한 unlit shader는 모바일 웹 브라우저에서 복잡한 동적 쉐이더보다 항상 더 나은 성능을 보여줍니다.
결론
Defold가 견고한 3D 게임 엔진으로 진화한 것은 독립 개발자들에게 엄청난 승리입니다. 특유의 번개처럼 빠른 컴파일 시간과 믿을 수 없을 만큼 작은 바이너리 크기를 유지하면서도, 방대하고 몰입감 있는 3D 월드를 구축하는 데 필요한 원시 수학적 토대와 툴링을 제공합니다. 커스텀 render scripts를 마스터하고, 행렬 연산을 이해하며, 네트워크 데이터를 효율적으로 직렬화함으로써, 훨씬 크고 무거운 엔진들과 기술적으로 경쟁할 수 있는 크로스 플랫폼 타이틀을 제작할 수 있습니다.
고도로 최적화된 3D 클라이언트를 온라인으로 가져가고, 인프라 관리의 번거로움 없이 Multiplayer 백엔드를 확장할 준비가 되었다면, horizOn을 무료로 사용해 보거나 API docs를 확인하여 다음 Defold 프로젝트에 실시간 서비스를 얼마나 빠르게 통합할 수 있는지 확인해 보십시오.