렌더링

렌더링

키워드
PBR
머테리얼
프레임 버퍼
디퍼드
 
 
 
 
 

CSEngine 렌더링 특징

 
CSEngine은 OpenGL을 기반으로 다양한 종류의 렌더링을 지원합니다.
 

 
 

다양한 Renderers

 
CSEngine은 Render Group 클래스에 의해 다음과 같은 렌더러를 구성했습니다.
 

렌더링 작동 원리

렌더링을 어떤 방식으로 구현했는지에 대한 설명입니다.
해당 렌더링 관련 클래스 다이어그램은 렌더링 파이프라인에 도달하는 방식을 설명합니다.
해당 렌더링 관련 클래스 다이어그램은 렌더링 파이프라인에 도달하는 방식을 설명합니다.
디퍼드 렌더링과 포워드 렌더링이 적용된 해당 파이프라인은 아래와 같은 순서로 돌아갑니다.
💡
아래의 코드는 로직을 돌 때 중요한 파트들만 적어놨습니다. 클래스의 구조는 소스코드를 참조해주시면 감사하겠습니다.
RenderMgr 단계
렌더링 단계는 렌더 레이어에 의해 순서가 정해지고 카메라, 빛, 렌더링 객체의 정보를 모두 받아 렌더링합니다.
  1. 프레임 버퍼 별로 렌더링을 진행
    1. // 렌더링 순서 : Depth Buffers -> Render Buffers -> Main Render Buffer // 1. 그림자를 위한 깊이 버퍼 렌더링을 먼저 실행합니다. const auto& lightObjects = lightMgr->GetAll(); const auto& shadowObjects = lightMgr->GetShadowObject(); const auto& shadowEnvironment = m_environmentMgr->GetShadowEnvironment(); for (const auto& light : lightObjects) { if(light->m_disableShadow) continue; RenderShadowInstance(*light, *shadowEnvironment, shadowObjects); } lightMgr->RefreshShadowCount(); // 메인 렌더링을 위한 메인 카메라 확보 const auto& cameraObjects = cameraMgr->GetAll(); const auto& mainCamera = cameraMgr->GetCurrentCamera(); // 2. 활성화 된 서브 카메라들의 프레임 버퍼를 렌더링 합니다. for (const auto& camera : cameraObjects) { if(!camera->GetIsEnable() || camera == mainCamera || camera->GetFrameBuffer() == nullptr) continue; ResetBuffer(*camera); RenderGbuffers(*camera); // Deferred Render RenderInstances(*camera); // Forward Render } if(mainCamera == nullptr) return; // 3. 메인 프레임버퍼를 렌더링 합니다. ResetBuffer(*mainCamera); RenderGbuffers(*mainCamera); // Deferred Render RenderInstances(*mainCamera); // Forward Render
  1. 디퍼드 렌더링을 먼저 진행합니다.
    1. /** ====================== * 1. Geometry Pass */ [...] // 생략 // 포워드와 비슷한 로직으로 렌더링을 진행합니다. for (const auto& render : renderLayer) { if (render == nullptr) continue; if (!render->isRenderActive) continue; render->SetMatrix(cameraMatrix); render->Render(); } [...] // 생략 /** ====================== * 2. Light Pass */ if(frameBuffer == nullptr) { glBindFramebuffer(GL_FRAMEBUFFER, 0); glViewport(0, 0, (GLsizei) *m_width, (GLsizei) *m_height); } else { frameBuffer->AttachFrameBuffer(); } // TODO: Background 설정 따로 적용하도록 수정 // TODO: 뒷배경 색상 적용 안됨 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); gbuffer.AttachLightPass(); //라이트 매니저로부터 광원 데이터를 쉐이더에 전달합니다. lightMgr->AttachLightToShader(lightPassHandle); lightMgr->AttachLightMapToShader(lightPassHandle, lightMgr->GetShadowCount()); gbuffer.AttachLightPassTexture(lightMgr->GetShadowCount() + lightMgr->GetLightMapCount()); gbuffer.RenderLightPass(); // 평면에 G버퍼 기반으로 라이팅을 렌더링합니다. /** ====================== * 3. Blit the depth buffer */ gbuffer.AttachGeometryFrameBuffer(GL_READ_FRAMEBUFFER); if(frameBuffer == nullptr) { glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); } else { frameBuffer->AttachFrameBuffer(GL_DRAW_FRAMEBUFFER); } // 현재 프레임버퍼에 디퍼드 렌더링에 쓰인 깊이 버퍼를 병합합니다. glBlitFramebuffer(0, 0, *m_width, *m_height, 0, 0, bufferWidth, bufferHeight, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
  1. 렌더 레이어 및 쉐이더로 포워드 렌더링을 진행합니다.
    1. const auto cameraMatrix = camera.GetCameraMatrixStruct(); const auto& frameBuffer = camera.GetFrameBuffer(); int customHandlerID = custom_handler != nullptr ? custom_handler->Program : -1; // 메인 프레임 버퍼 예외 처리 if(frameBuffer == nullptr) { glBindFramebuffer(GL_FRAMEBUFFER, 0); glViewport(0, 0, (GLsizei) m_width, (GLsizei) m_height); } else { // 깊이 버퍼 예외처리를 진행합니다. if(frameBuffer->GetBufferStatus() == SFrameBuffer::DEPTH_ONLY) { customHandlerID = m_environmentMgr->GetShadowEnvironment()->Program; } // 커스텀 프레임 버퍼를 활성화 시킵니다. frameBuffer->AttachFrameBuffer(); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } OrderRenderLayer orderRenderLayer(m_rendersLayer.begin(), m_rendersLayer.end()); // 렌더 레이어 순서대로 렌더링 for (const auto& orderLayerPair : orderRenderLayer) { const auto& orderLayer = orderLayerPair.second; ProgramRenderLayer programComp(orderLayer.begin(), orderLayer.end()); // 같은 쉐이더 프로그램 끼리 묶어 한번에 렌더링 for (const auto& programPair : programComp) { const auto& handler = *programPair.first; const auto& renderComp = programPair.second; if (programPair.first == nullptr) continue; if (renderComp.empty()) continue; // OpenGL 프로그램 설정 및 렌더링 준비 glUseProgram(handler.Program); // 아래부턴 오브젝트 렌더링 단계입니다.
  1. 라이팅 적용 후 렌더 객체 렌더링
    1. lightMgr->AttachLightToShader(&handler); // Attach Light for (const auto& render : renderComp) { // 렌더 오브젝트 렌더링 if (render == nullptr) continue; if (!render->isRenderActive) continue; // Attribute 적용 glEnableVertexAttribArray(handler.Attributes.Position); glEnableVertexAttribArray(handler.Attributes.Normal); glEnableVertexAttribArray(handler.Attributes.TextureCoord); glEnableVertexAttribArray(handler.Attributes.Weight); glEnableVertexAttribArray(handler.Attributes.JointId); // 카메라 매트릭스 GPU에 전송 render->SetMatrix(camera, cameraPosition, projection); render->Render(); // 렌더링 // Attribute 해제 glDisableVertexAttribArray(handler.Attributes.Position); glDisableVertexAttribArray(handler.Attributes.Normal); glDisableVertexAttribArray(handler.Attributes.TextureCoord); glDisableVertexAttribArray(handler.Attributes.Weight); glDisableVertexAttribArray(handler.Attributes.JointId); } } }
RenderComponent 단계
오브젝트를 렌더링하기 위해 Render Component는 머테리얼과 매쉬를 받아와 GPU에 값을 넘겨줍니다.
  1. Material 요소를 GPU에 바인딩
    1. if (m_mesh == nullptr || m_material_clone == nullptr) return; // 예외 처리 AttachMaterials();
  1. 스키닝 애니메이션에 필요한 뼈 데이터 GPU에 바인딩
    1. m_material_clone->SetSkinningUniform( m_mesh->GetMeshID(), (m_skinningMesh != nullptr) ? m_skinningMesh->GetJointMatrix() : std::vector<mat4>());
  1. 메쉬 데이터를 GPU에 Attribute형식으로 전송
    1. m_material_clone->SetAttribute(m_mesh->GetMeshID());
Mesh 단계
오브젝트의 형태를 나타냅니다. 대부분의 Mesh는 DAE 파일을 가져오면서 프리팹화를 거친 다음 생성됩니다.
💡
상세한 순서는 애니메이션 항목에서 확인하실 수 있습니다!
  1. 에셋 호출을 통한 SPrefab으로 생성
    1. //Create! SPrefab* prefab = SResource::Create<SPrefab>(file_id);
  1. DAELoader 클래스의 Mesh 데이터 불러오기
    1. XNode meshData = root_g.getChild("geometry").getChild("mesh"); // 포지션 값을 임시로 저장합니다. ReadPositions(meshData, m_skeletonData == nullptr ? std::vector<VertexSkinData*>() : m_skinningData->get_verticesSkinData()); std::string normalsId = ""; std::string texCoordsId = ""; // indices 설정 가능 여부를 확인한 뒤 임시 저장 작업을 진행합니다. try { XNode polylist = meshData.getChild("polylist"); normalsId = polylist.getNodeByAttribute("input", "semantic", "NORMAL") .getAttribute("source").value.substr(1); XNode childWithAttribute = polylist.getNodeByAttribute("input", "semantic", "TEXCOORD"); texCoordsId = childWithAttribute.getAttribute("source").value.substr(1); } catch (int error) { try { XNode triangles = meshData.getChild("triangles"); XNode normals = triangles.getNodeByAttribute("input", "semantic", "NORMAL"); normalsId = normals.getAttribute("source").value.substr(1); XNode childWithAttribute = triangles.getNodeByAttribute("input", "semantic", "TEXCOORD"); texCoordsId = childWithAttribute.getAttribute("source").value.substr(1); } catch (int error) {} } // 노멀값과 UV값을 받아옵니다. 받아오는 방식은 이전 로직과 모두 같습니다. ReadNormals(meshData, normalsId); ReadUVs(meshData, texCoordsId); // playlist 또는 triangles 요소를 이용해 indices를 작성합니다. AssembleVertices(meshData); // 이후엔 각 구조체에 맞게 포팅하는 작업이 이루어져있습니다.
  1. MeshSurface로 포팅하여 리소스 관리 매니저에 저장
    1. struct Vertex { // Vertex 값을 MeshSurface에 넣기위한 임시 구조체 선언 vec3 Position; vec3 Normal; vec2 TexCoord; vec3 Weight; vec3 JointId; }; // 뼈대 유무 확인 후 매쉬 성질 결정 if(jointIds != nullptr) m_meshId.m_hasJoint = true; m_Verts.resize(sizeVert * 14); Vertex* vertex_tmp = reinterpret_cast<Vertex*>(&m_Verts[0]); // 포팅 작업 for (int i = 0; i < sizeVert; ++i) { vertex_tmp->Position.x = *(vertices)++; // 포지션 vertex_tmp->Position.y = *(vertices)++; vertex_tmp->Position.z = *(vertices)++; ... // 생략 if (jointIds == nullptr) { vertex_tmp->JointId.Set(0, 0, 0); } else { vertex_tmp->JointId.x = *(jointIds)++; // 뼈대 ID 값 vertex_tmp->JointId.y = *(jointIds)++; vertex_tmp->JointId.z = *(jointIds)++; } ++vertex_tmp; } m_vertexSize = sizeVert;
  1. Mesh Component에서 매쉬 버퍼를 GPU에 바인딩
    1. // 버텍스를 담을 VBO를 생성합니다. std::vector<float> vertices; surface.GenerateVertices(vertices); GLuint vertexBuffer; glGenBuffers(1, &vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(vertices[0]), &vertices[0], GL_STATIC_DRAW); // 인덱스 VBO를 인덱스 값이 유효한지 확인 후 생성합니다. int vertexCount = surface.GetVertexCount(); int indexCount = surface.GetTriangleIndexCount(); GLuint indexBuffer; if (indexCount < 0) { indexBuffer = 0; indexCount = -1; } else { std::vector<GLushort> indices(indexCount); surface.GenerateTriangleIndices(indices); glGenBuffers(1, &indexBuffer); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer); glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexCount * 3 * sizeof(GLushort), &indices[0], GL_STATIC_DRAW); } // 데이터가 모두 바인딩 되면 MeshSurface에 관련 ID값을 넣습니다. surface.m_meshId.m_vertexBuffer = vertexBuffer; surface.m_meshId.m_vertexSize = vertexCount; surface.m_meshId.m_indexBuffer = indexBuffer; surface.m_meshId.m_indexSize = indexCount; m_meshId = surface.m_meshId; // 데이터를 언바인딩합니다. glBindBuffer(GL_ARRAY_BUFFER, 0);
Material 단계
오브젝트의 색상 및 여러 효과들을 지정한 값을 들고오는 과정입니다.
  1. 에셋 호출을 통한 SMaterial로 생성
    1. const XNode* m_root; try { m_root = XFILE(asset->path.c_str()).getRoot(); // XML 파일 불러오기 } catch (int e) { return; } // Material 파일 파싱 작업 XNode cse_mat = m_root->getChild("CSEMAT"); XNode shader_node = cse_mat.getChild("shader"); auto var_nodes = shader_node.children; auto shader_file_id = shader_node.getAttribute("id").value; auto shaderHandle = SResource::Create<GLProgramHandle>(shader_file_id); // 쉐이더 불러오기 if(shaderHandle == nullptr) return; for (const auto& node : var_nodes) { auto element_value = node.value.toStringVector(); auto element_type = node.getAttribute("type").value; SType type = XMLParser::GetType(element_type); auto element_name = node.getAttribute("name").value; auto element_count = node.getAttribute("count").value; Element* element = new Element; // Material의 속성값(Element) 1차 포팅 element->type = type; element->count = std::stoi(element_count); element->value_str = element_value; m_elements.insert(std::pair<std::string, Element*>(element_name, element)); } SAFE_DELETE(m_root); m_handle = shaderHandle; // 쉐이더 핸들 Material 포인터 안에 저장
  1. Render Component가 생성되면서 Material 인스턴스로 복제 & 2차 포팅 작업
    1. // 인스턴스 복제 이후 호출되는 코드 for (const auto& element_pair : m_elements) { const auto& element_name = element_pair.first.c_str(); const auto& element = element_pair.second; // OpenGL 바인딩 함수가 객체로 이미 할당했는지 확인 if (element->attachFunc != nullptr) continue; bool isUniform = true; const auto& handleElement = m_handle->UniformLocation(element_name); if (handleElement == nullptr) continue; element->id = handleElement->id; SetBindFuncByType(element, isUniform); // OpenGL 바인딩 함수 할당 }
  1. 포팅된 데이터를 최종적으로 OpenGL에 바인딩 하는 함수 할당
    1. // OpenGL 바인딩 함수 할당 void SMaterial::SetBindFuncByType(Element* element, bool isUniform) { const GLenum type = element->type; switch (type) { case SType::FLOAT: SetFloatFunc(element, XMLParser::parseFloat(element->value_str[0].c_str())); ... // 생략 case SType::TEXTURE: SetTextureFunc(element, XMLParser::parseTexture(element->value_str[0].c_str())); } return; } // Float형식의 데이터를 OpenGL에 바인딩 void SMaterial::SetFloatFunc(SMaterial::Element* element, float value) { if(element == nullptr) return; element->attachFunc = [element, value]() { // functional 객체로 할당 // 할당 후 렌더링 시 functional 객체만 호출하도록 구현 glUniform1f(element->id, value); }; }
  1. 렌더링 시 바인딩 함수만 호출
    1. for (const auto& element_pair : m_elements) { const auto& element = element_pair.second; if(element->id < 0) continue; element->attachFunc(); // functional 객체로 할당된 바인딩 함수 호출 }