https://github.com/bdero/flutter-gpu-examples
https://medium.com/flutter/getting-started-with-flutter-gpu-f33d497b7c11
flutter에서 gpu 코드 지원이 가능해졌다고 한다. 관련 내용에 대한 공식사이트 블로그를 보고 예제가 작동이 안되어서 작성자의 깃허브에 들어가 예제코드를 보고 분석하기로 하였다.
간단한 잡지식들
GPU 사용법인 초보인 나에게 대충 돌아가는 원리를 알면 참 도움된다
- gpu예제로 삼각형을 매번 그리는건 gpu가 삼각형 단위로 면을 그리기 때문
- 텍스처라는 개념은 그냥 도화지라고 생각하면 편함
- 점은 버텍스, 컬러는 frag
- 점계산은 버텍스셰이더, 컬러계산은 프래그먼트 셰이더
- 셰이더를 작성하는 코드는 glsl이라고 불림
- 버텍스 셰이더의 최종목적은 gl_Position을 계산하는것, 프래그먼트 셰이더는 frag_color를 계산하는게 목적
- 버텍스셰이더와 프래그먼트 셰이더를 연결해주면 프로그램, flutter gpu에서는 파이프라인이라 한다
- 만약 버텍스마다 컬러가 다르다면 그레디언트 형식으로 그려지게 된다.
final gpu.Texture? texture =
gpu.gpuContext.createTexture(gpu.StorageMode.devicePrivate, 300, 300);
final vertex = shaderLibrary['ColorsVertex']!;
final fragment = shaderLibrary['ColorsFragment']!;
final pipeline = gpu.gpuContext.createRenderPipeline(vertex, fragment);
final gpu.DeviceBuffer? vertexBuffer = gpu.gpuContext
.createDeviceBuffer(gpu.StorageMode.hostVisible, 4 * 6 * 3);
vertexBuffer!.overwrite(Float32List.fromList(<double>[
-0.5, -0.5, 1.0*red, 0.0, 0.0, 1.0, //
0, 0.5, 0.0, 1.0*green, 0.0, 1.0, //
0.5, -0.5, 0.0, 0.0, 1.0*blue, 1.0, //
]).buffer.asByteData());
final commandBuffer = gpu.gpuContext.createCommandBuffer();
final renderTarget = gpu.RenderTarget.singleColor(
gpu.ColorAttachment(texture: texture!),
);
final pass = commandBuffer.createRenderPass(renderTarget);
pass.bindPipeline(pipeline);
pass.bindVertexBuffer(
gpu.BufferView(vertexBuffer,
offsetInBytes: 0, lengthInBytes: vertexBuffer.sizeInBytes), 3);
pass.draw();
commandBuffer.submit();
/// Wrap the Flutter GPU texture as a ui.Image and draw it like normal!
final image = texture.asImage();
canvas.drawImage(image, Offset(-texture.width / 2, 0), Paint());
하나씩 살펴보자
1. 텍스쳐 생성
final gpu.Texture? texture =
gpu.gpuContext.createTexture(gpu.StorageMode.devicePrivate, 300, 300);
렌더링 결과물을 담기위한 텍스쳐를 사용한다. StorageMode.devicePrivate은 이 메모리가 gpu에 저장된다는 의미
2. 셰이더 로드 및 파이프라인 생성
final vertex = shaderLibrary['ColorsVertex']!;
final fragment = shaderLibrary['ColorsFragment']!;
final pipeline = gpu.gpuContext.createRenderPipeline(vertex, fragment);
final gpu.DeviceBuffer? vertexBuffer = gpu.gpuContext
.createDeviceBuffer(gpu.StorageMode.hostVisible, 4 * 6 * 3);
vertexBuffer!.overwrite(Float32List.fromList(<double>[
-0.5, -0.5, 1.0*red, 0.0, 0.0, 1.0, //
0, 0.5, 0.0, 1.0*green, 0.0, 1.0, //
0.5, -0.5, 0.0, 0.0, 1.0*blue, 1.0, //
]).buffer.asByteData());
빌드된 셰이더를 로드한다. 그리고 이를 파이프라인으로 이어준다. 이후 도형을 그릴 버텍스들을 준비한다.
3. 커맨드 버퍼 생성
final commandBuffer = gpu.gpuContext.createCommandBuffer();
gpu 관련된 코드들을 모았다가 한번에 실행시켜주는 코드로 보인다
4. 렌더타겟 설정
final renderTarget = gpu.RenderTarget.singleColor(
gpu.ColorAttachment(texture: texture!),
);
렌더 타겟이란 gpu가 어디다가 그릴지를 정하는 것이다. 아까 만들어준 텍스쳐를 지정해준다.
5. 렌더패스 설정
final pass = commandBuffer.createRenderPass(renderTarget);
렌더패스란 렌더타겟에 그리는 작업에 관한내용을 상술하는 것으로 보인다.
6. 패스 내용 작성
pass.bindPipeline(pipeline);
pass.bindVertexBuffer(
gpu.BufferView(vertexBuffer, offsetInBytes: 0, lengthInBytes: vertexBuffer.sizeInBytes),
3,
);
pass.draw();
렌더패스에다가 파이프라인을 연결하고 버텍스버퍼를 연결해준다. 그리고 draw를 해준다.
여기서 BufferView는 인공지능의 Tensor 같은거라고 생각하면 될듯 하다. 결국에는 array인데 gpu용 전용 데이터포맷 느낌으로 변형해준다
7. 커맨드 실행 및 출력
commandBuffer.submit();
final image = texture.asImage();
canvas.drawImage(image, Offset(-texture.width / 2, 0), Paint());
앞서 한 내용을 이제 gpu에 날려주자. 그리고 결과물을 이미지로 받아서 출력한다.
대충 전체적인 그림을 보면
사전준비작업
- 텍스쳐를 생성한다.
- 셰이더를 작성한다. 이후 이를통해 파이프라인을 만들어준다.
- 버텍스를 생성한다.
그림그리는 과정
- gpu에 렌더링할 타겟 텍스쳐를 정해준다. 렌더타겟이 나옴
- 렌더타겟을 통해 렌더패스를 받아낸다
- 렌더패스에 그리는 과정을 기록한다.
- 커맨드 버퍼로 submit한다
- texture의 결과물을 이미지로 출력한다.
사각형은 어떻게 그리나?
opengl은 삼각형 단위로 면을 그린다. 사각형은 삼각형이 2개이니 고로 사각형은 버텍스가 6개 필요하다.
vertexBuffer!.overwrite(Float32List.fromList(<double>[
// 첫 번째 삼각형
-0.5, -0.5, 1.0*red, 0.0, 0.0, 1.0, // 좌하단
-0.5, 0.5, 0.0, 1.0*green, 0.0, 1.0, // 좌상단
0.5, -0.5, 0.0, 0.0, 1.0*blue, 1.0, // 우하단
// 두 번째 삼각형
0.5, 0.5, 0.0, 0.0, 1.0*blue, 1.0, // 우상단
0.5, -0.5, 0.0, 0.0, 1.0*blue, 1.0, // 우하단
-0.5, 0.5, 0.0, 1.0*green, 0.0, 1.0, // 좌상단
]).buffer.asByteData());
헌데 이렇게하면 중복되는 버텍스가 있다. 이를 해결하기위해 점들을 미리 정의하고, 인덱스를 통해 점들만 쏙 빼가는 형식으로 버텍스버퍼를 만들면 중복되는 문제를 해결할 수 있다.
// 버텍스 데이터 (변경하지 않음)
vertexBuffer!.overwrite(Float32List.fromList(<double>[
-0.5, -0.5, 1.0*red, 0.0, 0.0, 1.0, // 좌하단
-0.5, 0.5, 0.0, 1.0*green, 0.0, 1.0, // 좌상단
0.5, -0.5, 0.0, 0.0, 1.0*blue, 1.0, // 우하단
0.5, 0.5, 0.0, 0.0, 1.0*blue, 1.0, // 우상단
]).buffer.asByteData());
// 인덱스 버퍼
indexBuffer!.overwrite(Uint16List.fromList(<int>[
0, 1, 2, // 첫 번째 삼각형
1, 3, 2, // 두 번째 삼각형
]).buffer.asByteData());
OpenGL과의 비교
아래는 opengl을 통한 예제이다.
VBO : 실제 버텍스 데이터를 전송
VAO : 버텍스를 어떻게 읽을지에 대한 데이터 정의. 데이터는 바이트단위로 읽어지다보니 이를 어떻게 읽어야할지 내용이 필요한듯?
#include <glad/glad.h> // GLAD는 OpenGL 함수 로딩
#include <GLFW/glfw3.h> // GLFW는 창 관리
// 윈도우 크기가 변경될 때 호출될 콜백 함수
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
glViewport(0, 0, width, height); // 뷰포트 크기 설정
}
// 입력을 처리하는 함수 (예: ESC 키 입력 시 종료)
void processInput(GLFWwindow* window) {
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// 간단한 버텍스 셰이더 소스 코드
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos; // 정점 좌표
void main() {
gl_Position = vec4(aPos, 1.0); // 정점의 위치 지정
}
)";
// 간단한 프래그먼트 셰이더 소스 코드
const char* fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
void main() {
FragColor = vec4(1.0, 0.5, 0.2, 1.0); // 픽셀의 색상 지정 (오렌지색)
}
)";
int main() {
// GLFW 초기화 및 버전 설정
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// GLFW 윈도우 생성
GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGL Triangle", NULL, NULL);
if (window == NULL) {
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
// GLAD 초기화 (OpenGL 함수 로드)
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
return -1;
}
// 뷰포트 설정
glViewport(0, 0, 800, 600);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// 버텍스 셰이더 컴파일
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// 프래그먼트 셰이더 컴파일
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
// 셰이더 프로그램 생성 및 링크
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 컴파일된 셰이더 삭제 (프로그램에 링크 후 필요 없음)
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// 삼각형의 정점 데이터 정의
float vertices[] = {
-0.5f, -0.5f, 0.0f, // 좌하단
0.5f, -0.5f, 0.0f, // 우하단
0.0f, 0.5f, 0.0f // 상단
};
// 버텍스 배열 객체 및 버텍스 버퍼 객체 생성
unsigned int VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// VAO 바인딩
glBindVertexArray(VAO);
// VBO 바인딩 및 버텍스 데이터 전송
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 정점 속성 지정 (위치 속성)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// VBO, VAO 바인딩 해제 (선택적)
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
// 렌더링 루프
while (!glfwWindowShouldClose(window)) {
// 입력 처리
processInput(window);
// 화면을 검은색으로 지우기
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 셰이더 프로그램 사용
glUseProgram(shaderProgram);
// VAO 바인딩 및 삼각형 그리기
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
// GLFW 버퍼 스왑 및 이벤트 처리
glfwSwapBuffers(window);
glfwPollEvents();
}
// 리소스 해제
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
// GLFW 종료
glfwTerminate();
return 0;
}
'Flutter' 카테고리의 다른 글
연속적인 애니메이션을 만드는 방법 (0) | 2024.08.22 |
---|---|
Bloc에서 event를 await 하는 방법 (0) | 2024.08.14 |
Child가 Rebuild 되지 않도록 하는 원리 (0) | 2024.07.23 |
위젯트리에서 중간에있는 위젯만 리빌드 하고 싶을 때 (1) | 2024.07.22 |
애니메이션을 주고싶은데 자식위젯이 너무 많을 때 (0) | 2024.07.11 |
댓글