20250407
gbufferpass와 gui를 구현했습니다.
아래는 동일한 메쉬(Plane)를 기반으로, 서로 다른 9개의 Material을 적용해 렌더링한 장면입니다.
현재는 G-buffer 렌더 패스 중 Albedo Attachment만 시각화하고 있습니다.
이 구조에서 가장 중점을 둔 부분은 인스턴싱(Instancing)입니다.
모든 오브젝트는 동일한 메시(Plane)를 공유하지만, 각각 다른 머티리얼을 갖습니다.
그럼에도 불구하고, 아래와 같이 단 하나의 Draw Call만으로 모든 객체가 렌더링됩니다.
이것이 가능한 이유는 다음과 같은 Bindless + 인스턴스 기반 구조 덕분입니다.
쉐이더에서는 인스턴스 ID를 기반으로 각종 인덱스를 조회하고, 그 인덱스를 통해 필요한 데이터를 가져옵니다.
이 방식은 오브젝트가 수천 개로 늘어나더라도, 드로우콜은 메시 단위로만 최소화할 수 있어 매우 효율적입니다.
struct alignas(8) ObjectInstance {
int modelMatrixIndex;
int materialIndex;
};
struct alignas(16) ModelBuffer {
glm::mat4 model;
};
셰이더로 실시간으로 전달되는 데이터는 이렇게 입니다.
ObjectInstance는 인스턴스당 모델 행렬 인덱스와 머티리얼 인덱스를 저장하고,
ModelBuffer는 실제 모델 매트릭스를 저장하는 구조입니다.
나머지는 bindless구조로 미리 다 저장이 되어있어 인덱스로 접근합니다.
1. 먼저 같은 모델끼리 오브젝트를 묶습니다.
std::unordered_map<int32_t, std::vector<int32_t>> objectMap;
std::vector<ModelBuffer> modelBuffers(m_objects.size());
for (int32_t i = 0; i < m_objects.size(); i++) {
objectMap[m_objects[i].modelIndex].push_back(i);
modelBuffers[i].model = m_objects[i].transform;
}
2. modelMatrix 정보를 저장합니다.
m_modelBuffers[currentFrame]->updateStorageBuffer(&modelBuffers[0], sizeof(ModelBuffer) * m_objects.size());
3. ObjectInstance를 구성하고 메시 단위로 인스턴싱 드로우콜을 호출합니다.
std::vector<ObjectInstance> objectInstances;
objectInstances.reserve(m_objects.size() * 16);
int32_t index = 0;
for (const auto& [key, value] : objectMap) {
for (int32_t i = 0; i < m_modelList[key].mesh.size(); i++) {
int32_t startIndex = index;
for (int32_t j = 0; j < value.size(); j++) {
int32_t materialIndex;
if (m_objects[value[j]].overrideMaterialIndex.size() > i) {
materialIndex = m_objects[value[j]].overrideMaterialIndex[i];
}
else {
materialIndex = m_modelList[key].material[i];
}
objectInstances.push_back({value[j], materialIndex});
index++;
}
m_meshList[m_modelList[key].mesh[i]]->drawInstance(m_commandBuffers->getCommandBuffers()[currentFrame], value.size(), startIndex);
}
}
m_objectInstanceBuffers[currentFrame]->updateStorageBuffer(&objectInstances[0], sizeof(ObjectInstance) * objectInstances.size());
다시 정리하면 이렇습니다.
먼저 모든 오브젝트를 순회하면서 같은 모델을 사용하는 오브젝트끼리 objectMap에 묶어줍니다.
이때 각 오브젝트의 transform은 ModelBuffer에 저장되며, 이 순서가 modelMatrixIndex가 됩니다.
이후 objectMap을 다시 순회하면서 ObjectInstance를 구성합니다.
모델에 여러 메시가 존재할 수 있기 때문에,
각 메시마다 몇 개의 인스턴스를 그릴지 계산하고 drawInstance를 호출합니다.
이 drawInstance는 메시 단위로 단 한 번만 호출되며, 그 아래 오브젝트들은 모두 인스턴싱으로 렌더링됩니다.
Material mat = materials[materialIndex];
vec3 albedo = mat.baseColor.rgb;
if (mat.albedoTexIndex >= 0) {
albedo *= texture(textures[nonuniformEXT(mat.albedoTexIndex)], fragTexCoord).rgb;
}
outPosition = vec4(fragWorldPos, 1.0);
outNormal = vec4(normal, float(materialIndex));
outAlbedo = vec4(albedo, 1.0);
outPBR = vec4(ao, roughness, metallic, 1.0);
셰이더안에서는 이런식으로 texture를 찾아 gbuffer를 만듭니다.
추후
ray tracing적용 할 때
ray에 맞는 mesh의 material도 이런식으로 찾을 수 있게 할 예정입니다.
이제 lightpass와 그림자를 구성하면 기본적인 실험 세팅이 완료됩니다.
수요일 안으로 완성하는게 목표입니다.