Компиляция шейдеров
Ранее, мы подключили OpenGL ES контекст к окну, чтобы обрабатывать события от пользователя и выводить графику.
В этой главе, мы будем работать над рендерингом классического треугольника OpenGL. Классический, потому что каждый учебник OpenGL делает это.
Но сначала мы узнаем, как создавать безопасные абстракции в Rust, и создадим инструменты для компиляции шейдера и компоновки программы.
"Современный" OpenGL
Мы будем использовать то, что называется "современным OpenGL". Оказывается, давным-давно это было не так современно. Мы не будем обсуждать здесь графический конвейер с самого начала: вместо этого я предлагаю вам воспользоваться другим "современным учебником OpenGL". Я буду использовать это замечательное руководство в качестве основы. Эта глава будет посвящена реализации на Rust урока "Hello Triangle".
Дополнительная настройка контекста OpenGL
В пердыдущей главе мы говорили о создании окна. В основном мы так и поступали, за исключением того, что я забыл кое-что.
Во-первых, нам нужно указать минимальную версию OpenGL для использования. В нашем случае это можно сделать с помощью атрибутов EGL контекста:
#![allow(unused)] fn main() { let ctx_attribs = [egl::NONE]; let ctx = egl .create_context(display, config, None, &ctx_attribs) .expect("unable to create EGL context"); }
Как вы можете видеть, мы задали пустой список аттрибутов EGL контекста. Если не настраивать аттрибуты контекста, то графическая подсистема возьмет минимальную версию OpenGL ES. В моем случае это OpenGL ES 1.1.
Чтобы увидеть как это работает, добавьте несколько строк после вызова команды egl.make_current
:
#![allow(unused)] fn main() { println!( "GL_RENDERER = {}", gl::get_string(gl::GL_RENDERER).unwrap_or("Unknown".into()) ); println!( "GL_VERSION = {}", gl::get_string(gl::GL_VERSION).unwrap_or("Unknown".into()) ); println!( "GL_VENDOR = {}", gl::get_string(gl::GL_VENDOR).unwrap_or("Unknown".into()) ); println!( "GL_EXTENSIONS = {}", gl::get_string(gl::GL_EXTENSIONS).unwrap_or("Unknown".into()) ); }
В моем случае это выглядит вот так:
> cargo run
Compiling lesson-02-opengl-context v0.1.0 (/home/opengles-tutorial)
Finished dev [unoptimized + debuginfo] target(s) in 7.27s
Running `/home/opengles-tutorial/target/debug/lesson-02-opengl-context`
GL_RENDERER = GeForce GTX 1050/PCIe/SSE2
GL_VERSION = OpenGL ES 1.1 NVIDIA 460.67
GL_VENDOR = NVIDIA Corporation
GL_EXTENSIONS = GL_EXT_debug_label GL_EXT_map_buffer_range GL_EXT_robustness
GL_EXT_texture_compression_dxt1 GL_EXT_texture_compression_s3tc
GL_EXT_texture_format_BGRA8888 GL_KHR_debug GL_EXT_memory_object
GL_EXT_memory_object_fd GL_NV_memory_object_sparse GL_EXT_semaphore
GL_EXT_semaphore_fd GL_NV_timeline_semaphore GL_NV_memory_attachment
GL_NV_texture_compression_s3tc GL_OES_compressed_ETC1_RGB8_texture
GL_EXT_compressed_ETC1_RGB8_sub_texture GL_OES_compressed_paletted_texture
GL_OES_draw_texture GL_OES_EGL_image GL_OES_EGL_image_external GL_OES_EGL_sync
GL_OES_element_index_uint GL_OES_extended_matrix_palette GL_OES_fbo_render_mipmap
GL_OES_framebuffer_object GL_OES_matrix_get GL_OES_matrix_palette
GL_OES_packed_depth_stencil GL_OES_point_size_array GL_OES_point_sprite
GL_OES_rgb8_rgba8 GL_OES_read_format GL_OES_stencil8 GL_OES_texture_cube_map
GL_OES_texture_npot GL_OES_vertex_half_float
Эта книга нацелена на изучение OpenGL ES версии 2.0 и выше. Поэтому настроим аттрибуты контекста, изменив всего одну строчку:
#![allow(unused)] fn main() { let ctx_attribs = [egl::CONTEXT_CLIENT_VERSION, 2, egl::NONE]; }
Теперь вывод будет совершенно иным:
В моем случае это выглядит вот так:
```txt
> cargo run
Compiling lesson-02-opengl-context v0.1.0 (/home/opengles-tutorial)
Finished dev [unoptimized + debuginfo] target(s) in 7.27s
Running `/home/opengles-tutorial/target/debug/lesson-02-opengl-context`
GL_RENDERER = GeForce GTX 1050/PCIe/SSE2
GL_VERSION = OpenGL ES 3.2 NVIDIA 460.67
...
Помимо того, что теперь мы можем работать с OpenGL ES 3.2, также изменились поддерживаемые OpenGL расширения и их количество. Пока что примите OpenGL расширения как данность, мы обсудим их в следующих главах.
Во-вторых, нам нужно настроить область просмотра. Мы можем сделать это один раз, непосредственно перед первым вызовом gl::clear_color
:
#![allow(unused)] fn main() { gl::viewport(0, 0, 900, 700); }
Шейдеры
Мы создадим вспомогательную функцию для компиляции шейдера из строки, а затем другую функцию для связывания скомпилированных шейдеров с программой.
Сначала попробуйте добавить этот код в перед функцийе main
в файл main.rs
:
#![allow(unused)] fn main() { fn shader_from_source(source: &str) -> u32 { // continue here } }
Учитывая этот код, функция shader_from_source
должна возвращать идентификатор шейдера.
Однако создание шейдера может завершиться неудачно, и мы можем захотеть получить сообщение об ошибке.
Для этого мы изменим тип возвращаемого значения на Result<u32, String>
. Если компиляция шедера завершится удачно, то мы получим идентификатор шейдера, иначе мы сможем извлечь сообщение об ошибке.
Начнем с получения id объекта шейдера:
#![allow(unused)] fn main() { fn shader_from_source(source: &str) -> Result<u32, String> { let id = gl::create_shader(gl::GL_VERTEX_SHADER); // continue here } }
Ха! Нам нужно указать тип шейдера. Давайте улучшим сигнатуру функции и добавим к ней тип шейдера. type
- зарезервированное ключевое слово в Rust, но мы можем назвать его kind
:
#![allow(unused)] fn main() { fn shader_from_source( source: &str, kind: kind: gl::GLenum ) -> Result<Result<u32, String>> { let id = gl::create_shader(kind); // continue here } }
Затем нам нужно установить источник для объекта шейдера, используя обертку к glShaderSource функции. Мы можем установить источник шейдера и скомпилировать его:
#![allow(unused)] fn main() { gl::shader_source(id, source.as_bytes()); gl::compile_shader(id); // continue here }
Мы могли бы вернуть Ok(id)
сейчас и двигаться дальше, однако нам реально нужно видеть правильное сообщение об ошибке, если шейдер не компилируется.
Таким образом, получаем статус компиляции шейдера:
#![allow(unused)] fn main() { let success = gl::get_shaderiv(id, gl::GL_COMPILE_STATUS); }
И если это 0
, мы вернем строку с ошибкой, иначе Ok(id)
:
#![allow(unused)] fn main() { if success == 0 { // continue here } Ok(id) }
Нам нужно будет записать возвращенную ошибку в буфер, поэтому нам нужно знать требуемую длину этого буфера.
Мы получим len
запросив GL_INFO_LOG_LENGTH
для объекта шейдера:
#![allow(unused)] fn main() { let len = gl::get_shaderiv(id, gl::GL_INFO_LOG_LENGTH); // continue here }
При этом мы можем попросить OpenGL записать журнал информации о шейдере в значение нашей ошибки и наконец, мы можем вернуть ошибку :
#![allow(unused)] fn main() { return match gl::get_shader_info_log(id, len) { Some(message) => Err(message), None => Ok(id) }; // continue here }
Финальный код функции shader_from_source
:
#![allow(unused)] fn main() { fn shader_from_source(source: &str, kind: gl::GLenum) -> Result<u32, String> { let id = gl::create_shader(kind); gl::shader_source(id, source.as_bytes()); gl::compile_shader(id); let success = gl::get_shaderiv(id, gl::GL_COMPILE_STATUS); if success == 0 { let len = gl::get_shaderiv(id, gl::GL_INFO_LOG_LENGTH); return match gl::get_shader_info_log(id, len) { Some(message) => Err(message), None => Ok(id) }; } Ok(id) } }
Программа
Программа связывания требует следующих вызовов OpenGL:
#![allow(unused)] fn main() { let program_id = gl::create_program(); gl::attach_shader(program_id, vert_shader); gl::attach_shader(program_id, frag_shader); gl::link_program(program_id); }
По аналогии с компиляцмей шейдеров напишем функцию для программы связывания.
Вместо get_shaderiv
, мы испольуем get_programiv
, instead of get_shader_info_log
we use get_program_info_log
.
#![allow(unused)] fn main() { fn program_from_shaders(vert_shader: u32, frag_shader: u32) -> Result<u32, String> { let program_id = gl::create_program(); gl::attach_shader(program_id, vert_shader); gl::attach_shader(program_id, frag_shader); gl::link_program(program_id); // error handling here let success = gl::get_programiv(program_id, gl::GL_LINK_STATUS); if success == 0 { let len = gl::get_programiv(program_id, gl::GL_INFO_LOG_LENGTH); return match gl::get_program_info_log(program_id, len) { Some(message) => Err(message), None => Ok(program_id) }; } gl::detach_shader(program_id, vert_shader); gl::detach_shader(program_id, frag_shader); Ok(program_id) } }
Одно маленькое уточнение: gl::delete_shader
не удалит шейдер, если он все еще прикреплен к программе. Поэтому мы отключаем шейдер после его связывания.
Использование наших шейдеров и программ связывания
Для начала добавим код шейдеров в файл main.rs
. Я добавил его перед функцией shader_from_source
:
#![allow(unused)] fn main() { pub const VERT_SHADER: &str = r" attribute vec3 Position; void main() { gl_Position = vec4(Position, 1.0); } "; pub const FRAG_SHADER: &str = r" precision mediump float; void main() { gl_FragColor = vec4(1.0, 0.5, 0.2, 1.0); } "; }
Прямо перед основным циклом, мы можем скомпилировать наши вершинные и шейдерные программы:
(main.rs, before loop)
#![allow(unused)] fn main() { let vertex_shader = match shader_from_source(VERT_SHADER, gl::GL_VERTEX_SHADER) { Ok(id) => { println!("Vertex Shader Compiled"); id }, Err(err) => panic!("{}", err) }; let fragment_shader = match shader_from_source(FRAG_SHADER, gl::GL_FRAGMENT_SHADER) { Ok(id) => { println!("Fragment Shader Compiled"); id }, Err(err) => panic!("Error: {}", err) }; // continue here }
Затем мы связываем наши шейдеры:
#![allow(unused)] fn main() { match program_from_shaders(vertex_shader, fragment_shader) { Ok(_) => println!("Program linked"), Err(err) => panic!("Error: {}", err) } // continue here }
Запустим программу на выполнение:
> cargo run
Compiling lesson-02-opengl-context v0.1.0 (/home/opengles-tutorial)
Finished dev [unoptimized + debuginfo] target(s) in 7.27s
Running `/home/opengles-tutorial/target/debug/lesson-02-opengl-context`
GL_RENDERER = GeForce GTX 1050/PCIe/SSE2
GL_VERSION = OpenGL ES 1.1 NVIDIA 460.67
GL_VENDOR = NVIDIA Corporation
GL_EXTENSIONS = GL_EXT_debug_label GL_EXT_map_buffer_range GL_EXT_robustness ...
Vertex Shader Compiled
Fragment Shader Compiled
Program linked
Однако мы пока ничего не увидим на экране, потому что мы еще не отправляем никаких команд рисования в OpenGL. Однако если вывод в терминале должен показать нам что шейдеры скомпилировались и программа слинкована.
Мы исправим это в следующий раз.
Как всегда, код этой части главы на github.