Introducción: Laboratorio 0 OpenGL (+3.2) OpenTK Shaders Buffers CG 2015 Introducción ● En la materia, trabajaremos con la librería gráfica OpenGL ○ En particular, con una versión “moderna”. ● Herramientas que utilizaremos: ○ ○ ○ ○ ○ ○ OpenGL +3.2. OpenTK Visual Studio para C# Herramientas de Modelado: Blender, sketchup, etc. Herramientas para probar shaders: glMan, etc. Herramientas específicas para algún tema. OpenGL: Definición e Historia Ver transparencias SIGGRAPH 2013. Intro: OpenTK ● Open toolkit (OpenTK) es una librería de bajo nivel que encapsula (es un wrapper) las librerías OpenGL, OpenCL y OpenAL. ● Ventajas: ○ ○ ○ ○ ○ ○ ○ ○ Multiplataforma (Windows / Linux) Multilenguaje (Mono/.Net) Código abierto. Fuertemente tipado (reemplaza constantes por enumerados). Uso de genericidad. Código completamente manejado (.NET) (No hay que manejar punteros) Documentación en línea. Provee APIs para manejar Vectores, Matrices, Quaterniones, Bezier, Audio, Input. ● Link: www.opentk.com Intro: OpenTK ● Se descarga e instala desde el sitio oficial. ● Archivos que vamos a utilizar están en [InstallFolder] /Binaries/OpenTK/Release: ○ ○ ○ ○ OpenTK.dll: dll de .Net que debemos incluir. OpenTK.xml: Documentación de las operaciones. OpenTK.GLControl.dll: Componente gráfico para visual studio. OpenTK.GLControl.xml: documentación del GLControl. ● Además del Código fuente, incluye un navegador de ejemplos. (Aunque los ejemplos estan desactualizados.) ○ [InstallFolder]/Binaries/OpenTK/Release/Examples.exe Intro: OpenTK ● Los espacios de nombres (namespaces) que utilizaremos son: ○ ○ OpenTK: Se encuentran las clases y structs auxiliares matemáticas, como Vector, Matrix, etc. OpenTK.Graphics.OpenGL: Se encuentran todas las clases, enums y structs que reemplazan a OpenGL. ● La clase estática principal es: OpenTK.Graphics.OpenGL.GL En OpenGL: En OpenTK: glClear(...); GL.Clear(...); glViewport(...); GL.Viewport(...); ● El componente GLControl nos permitirá crear interfaces gráficas de manera fácil. Lo podemos manejar como cualquier otro componente (botón, slider, label, etc) Introducción ● Durante el transcurso de la materia utilizaremos la API que nos brinda OpenGL, en su versión 3.2 o superior. ● Para trabajar con este OpenGL moderno, es necesario comprender dos conceptos fundamentales que utilizaremos a lo largo del cuatrimestre: Shaders Buffers Programas en OpenGL ● Los programas OpenGL actuales esencialmente deben realizar los siguientes pasos: 1. Crear y cargar los shaders y programas de shaders. 2. Crear los buffers y cargar los datos en ellos. 3. “Conectar” la ubicación de esos datos con las variables en los shaders. 4. Renderizar (Dibujar). Shaders En OpenGL 3.x estamos obligados a trabajar con el pipeline programable. ● Vamos a tener que programar (sólamente algunas etapas) de este pipe. ● Esto nos brinda mucha más flexibilidad, a costa de un mayor esfuerzo en la programación. Para programarlo, vamos a utilizar Shaders. Shaders ● Los shaders son pequeños programas que se ejecutan dentro de la GPU. ○ Su propósito es ejecutar algunas de las etapas del pipeline de rendering. ● Las GPUs actuales nos permiten especificar varias etapas de su pipeline. Sin embargo, nosotros sólo trabajaremos con: ○ ○ ● Shaders de vértices. Shaders de fragmentos. Cada shader tiene determinadas entradas y determinadas salidas. Esquema simplificado de la GPU App. Framebuffer GPU Data flow vertices vertices Vertex processing fragments Rasterizer Vertex shader pixels Fragment processing Fragment shader ¡Debemos programarlos! Shaders en OpenGL En OpenGL, vamos a utilizar el lenguage GLSL (GL Shading Language), que es muy similar a C. Agrega muchas facilidades, pero también tiene restricciones. ● Por ejemplo, tiene soporte para vectores y matrices, pero no soporta recursión. Más sobre GLSL en el Orange Book: OpenGL Shading Language, third ed. GLSL: Tipos de datos y operaciones ● Scalar types: float, ● Vector types: ○ ○ ○ int, bool vec2, vec3, vec4 ivec2, ivec3, ivec4 bvec2, bvec3, bvec4 ● Matrix types: mat2, mat3, mat4, matNxM ● Texture sampling: sampler1D, sampler2D, sampler3D, samplerCube. ● C++ style constructor: vec3 a = vec3(1.0, 2.0, 3.0); ● Operadores aritméticos y lógicos estandar de C/C++ ● Sobrecarga de operadores para vectores y matrices: mat4 m; b = a * m; vec4 a, b, c; c = m * a; //Multip. de vec y mat. GLSL: Componentes y “swizzling” ● Diversos modos de acceso a las componentes del vector: ○ ○ [] (usando corchetes, al estilo C) xyzw, rgba ó strq (usando nombres de componentes) ● Ejemplo: ○ vec3 v; v[1], v.y, v.g, v.t - se refieren todos al mismo elemento (segundo elemento del vector v) ● Swizzling: ○ ○ vec3 a, b; a.xy = b.yx GLSL: Calificadores y funciones ● in, out: Copia atributos de vértices y otras variables hacia o desde los shaders ○ ○ in vec2 textCoord; out vec4 color; ● uniform: Valor constante durante una operación de dibujado. ○ ○ uniform float time; uniform vec4 rotation; ● Funciones built-in: ○ ○ ○ Aritméticas: sqrt, power, abs Trigonométricas: sin, asin Gráficas: length, reflect ● Funciones definidas por el usuario GLSL: Built-in variables ● gl_Position: ○ (requerida) posición del vertice (en espacio de clipping). Salida del shader de vértices. ● gl_FragCoord: ○ posición del fragmento. Entrada al shader de Fragmentos. ● gl_FragDepth: ○ profundidad del fragmento. Entrada al shader de Fragmentos. Obligatorio: ● ● que el shader de vertices retorne la posición en gl_Position. que el shader de fragmentos retorne un color. Ejemplo (código fuente shader vért.) // VERTEX SHADER. Simple. Transforma la posicion. #version 150 Directivas al compilador in vec3 vPos; uniform mat4 projMat; Parámetros de entrada, salida y uniformes uniform mat4 mvMat; void main(){ gl_Position = projMat * mvMat * vec4(vPos, 1.0); } Ejemplo (código fuente shader frag.) // FRAGMENT SHADER. #version 150 uniform vec4 figureColor; out vec4 fColor; void main(){ fColor = figureColor; } Terminología En GLSL, se utiliza una terminología distinta a la de otros lenguajes de shaders. ● Un shader es el código compilado para controlar una determinada etapa (programable) del pipeline. ● Un programa es un conjunto de shaders que controlan las distintas etapas del pipe. Nosotros vamos a construir programas, utilizando shaders, para ser ejecutados en la GPU. Modelo de Compilación GLSL utiliza un modelo de compilación parecido al de C. ● ● Se compilan por separado los códigos fuente de cada shader, para generar códigos objeto. Se linkean varios códigos objetos, para generar el programa ejecutable. El programa ejecutable es el que se envía a la GPU para que controle las etapas del rendering. Estas operaciones de compilación/linkeo se realizan en tiempo de ejecución de nuestra aplicación! ● Esto quiere decir que cada vez que ejecutamos nuestra aplicación, se compila(n) y linkea(n) nuestros shaders/programas. En OpenGL ● En OpenGL, los shaders y programas se manejan (al igual que otros recursos) con objetos. ○ NO los objetos de POO!!. ● Cada objeto tiene asignado un identificador (handle) con el cual podemos manipularlo. ● OpenGL crea, asigna y destruye los objetos (e identificadores) a pedido nuestro. En OpenGL int glCreateShader(tipo); ● Crea un objeto shader vacío y devuelve un identificador para poder referirnos a éste. ● El tipo especifica que shader vamos a crear: de vértices, de fragmentos, geométrico, etc. void glShaderSource(int shader, string source); ● Asigna el código fuente de un determinado shader. En OpenGL void glCompileShader(int shader); ○ Compila el código fuente de un shader. El estado de la compilación e información sobre la misma, puede obtenerse mediante: glGetShader(..) y glGetShaderInfoLog(...). Consulte la documentación de estas funciones. En OpenGL int glCreateProgram(); ○ Crea un objeto programa vacío, y retorna un identificador. void glAttachShader(prog, shader); ○ Adosa un objeto shader a un objeto programa. void glGetDettachShader(prog, shader); ○ Quita el shader del programa. Consulte la documentación de estas funciones. En OpenGL void glLinkProgram(program); ○ Linkea un programa con todos los shaders que tenga adosados. El estado del proceso puede consultarse mediante: glGetProgram(...); glGetProgramInfoLog(...); Consulte la documentación de estas funciones. En OpenGL void glUseProgram(program); ○ Setea un programa linkeado para ser utilizado en el rendering. ○ A partir de ese momento, se usará el programa hasta que no se especifique otro. (Recordar que OpenGL es una máquina de estados) glUseProgram(0); Setea un programa nulo. ○ Estado indeterminado! ○ Para renderizar, hay que volver a setear otro programa. En OpenGL void glDeleteShader(shader); ○ Elimina un objeto Shader. ○ Si el shader está adosado a un programa, no se elimina hasta que se elimine el programa, o bien se desacople del mismo (dettach). void glDeleteProgram(program); ○ Elimina un objeto Programa. ○ Para renderizar, hay que volver a setear otro programa. Secuencia glCreateProgram(...); glCreateShader(...); glShaderSource(...); glCompileShader(...); glAttachShader(...); glLinkProgram(...); glUseProgram(...); Estos pasos se repiten para cada tipo de shader en el programa. Estos pasos se realizan una única vez (Inicialización) Cada vez que se quiera dibujar algo utilizando este programa de shader. Shaders - Parámetros Cada shader tiene entradas y salidas. Estas pueden provenir desde la aplicación o desde otra etapa del pipe. Las entradas del shader de vértices se denominan vertex attributes. Cuando se “linkea” el programa, a cada parámetro de entrada se le asigna una ubicación. ○ Un índice a una tabla interna que nos permitirá especificarle los datos de entrada. Shaders - Parámetros Se dividen en dos: ● Los indices para los atributos de entrada al shader de vertices. ○ Las variables declaradas como in. ● Los indices para los atributos uniformes. ○ Las variables declaradas como uniform. Para obtener los primeros: glGetProgram(...); (Con el flag ActiveAttributes) glGetActiveAttrib(...); Para obtener los segundos: glGetProgram(...); (Con el flag ActiveUniforms) glGetActiveUniformName(...); Modelando Shaders con Clases Como vamos a utilizar frecuentemente los shaders, ya sea para: ○ Crearlos, leerlos de un archivo de texto. ○ Compilarlos y detectar errores. ○ Crear programas para agrupar los shaders. ○ Linkear el programa y detectar errores. ○ Establecer los datos de entrada Podemos tratar de modelarlos con clases, para favorecer su reutilización. Un posible modelo puede ser: Modelando Shaders con Clases ShaderProgram - ID : int - shaders : List<Shader> - uniformLocations : Dictionary<String, int> - attribLocations : Dictionary<String, int> - ShaderProgram() : void - AddShader(shader) : void - Build() : void - Activate() : void - Deactivate() : void - GetAttribLocation(attribName) : int - GetUniformLocation(uniformName) : int - SetUniformValue(uniformName, value) : void Shader 2..* - ID : int - type : ShaderType - fileName : String - source :String - Shader(fileName, type) : void - Compile() : void - Delete() : void - GetID() : int ProgramLinkageException ShaderCompilationException Estudiar el ejemplo adjunto para ver cómo se utilizan estas clases!!! ProgramShaderException Buffers - Introducción ● Veremos el concepto y la forma de trabajar de OpenGL a la hora de almacenar la geometría y atributos de los objetos que deseamos mostrar en pantalla. ● Esto ha ido variando en las distintas versiones de OpenGL, principalmente por cuestiones de performance. ○ ○ Modo inmediato [OBSOLETO]: Cada vez que se dibujaba la escena, se enviaba toda la geometría a la GPU. Buffers: La mayor parte de la información reside en la GPU, se envía una sola vez. Esquema ilustrativo ● Los objetos que dibujemos, son enviados a la GPU, donde son procesados y terminan componiendo la imagen vista en la pantalla. CPU GPU ● Para renderizar un modelo, la GPU espera un flujo de información sobre los vértices, ó vertex stream. Vértices ● Los objetos que queremos dibujar se representan mediante vértices. ● Un vértice tiene una colección de atributos: ○ Posición (el más importante) ○ Color ○ Normal ○ Coordenadas de textura ○ Cualquier otro dato asociado a ese punto en el espacio. ● La posición se procesa en coordenadas homogéneas (4D) Buffers ● Los Buffers son objetos de OpenGL. ○ Son administrados por el contexto de OpenGL. ● Son bloques de memoria asignados por el contexto de OpenGL (GPU) que almacenan información sin formato. ○ Similar al espacio que se reserva al utilizar malloc() en el lenguaje C. ● Se pueden utilizar para almacenar distinto tipo de información: ○ Atributos de vértices, información de pixels, texturas, etc. VBOs, EBOs y VAOs ● Vertex Buffer Objects (VBOs): Son buffers diseñados para almacenar información (atributos) sobre los vértices: ○ ○ ○ ○ ○ Posiciones Colores Normales Coordenadas de Texturas, etc. Indices. A veces llamados Element Buffer Object (EBO). ● Vertex Array Objects (VAOs): Son objetos de OpenGL que contienen uno o más VBOs y (opcional) un EBO, junto con su configuración. ○ Es decir, contiene toda la información para que un objeto pueda ser renderizado Pasos para crear un VBO ● Generar un nombre (handler) para el buffer. ○ glGenBuffers(cant, &ids) ● Seleccionarlo para configurarlo/utilizarlo. ○ ○ ○ glBindBuffer(GL_ARRAY_BUFFER, id) GL_ARRAY_BUFFER: Buffer de datos. GL_ELEMENT_ARRAY_BUFFER: Buffer de índices. ● Inicializarlo con datos. ○ glBufferData(...) / glBufferSubData(...) ● Cuando ya no se necesita más. glDeleteBuffers(cant, ids) glBufferData glBufferData(target, size, data, hint) Reserva espacio para el buffer y llena el mismo con datos. ● target: GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER, etc. ● size: Tamaño (en BYTES!) del buffer. ● data: La información a almacenar. ● hint: Flag para que la implementación de OpenGL decida dónde almacenar el contenido (entre otras cosas) con relación a: ○ Frecuencia de acceso al buffer: STATIC, DYNAMIC, STREAM. ○ Modo de acceso: DRAW, READ, COPY. Por ahora utilizaremos GL_STATIC_DRAW Pasos para crear un VAO ● Generar un nombre (handler) para el objeto. ○ glGenVertexArrays(cant, &ids) ● Seleccionarlo para configurarlo/utilizarlo. ○ glBindVertexArray(id) ● Configuramos cada VBO. ○ ○ ○ glEnableVertexAttribArray(attribLocation) glBindBuffer(bufferType, bufferId) glVertexAttribPointer(...) ● Seleccionamos el EBO a utilizar. ○ glBindBuffer(bufferType, bufferId) ● Cuando ya no se necesita más el VAO. glDeleteVertexArrays(cant, ids) glVertexAttribPointer glVertexAttribPointer(index, size, type, normalized, stride, offset) Especifica la ubicación y formato de un arreglo de atributos de vértices. ● ● ● ● index: Número de atributo (ubicación en el programa de shaders) size: Cantidad de componentes de cada dato (1, 2, 3 ó 4) ○ size = 2: (u, v); (s, t) coordenadas de textura. ○ size = 3: (x, y, z); (r, g ,b) posiciones, colores. ○ size = 4: (x, y, z, w); (r, g, b, a) posiciones coord homogeneas, colores con opacidad/transparencias. type: Tipo de cada componente (BYTE, SHORT, INT, HALF_FLOAT, FLOAT, DOUBLE… ) Los tipos enteros pueden ser con o sin signo.(UNSIGNED) normalized: Si type es entero, se pasan a punto flotante. Este flag indica si se pasa directamente ó si lo tiene que normalizar primero. ○ Rango [-1.0, 1.0] para enteros con signo ○ Rango [0.0, 1.0] para enteros sin signo Stride y Offset En los VBOs almacenamos los atributos de los vértices: ● Cada atributo en un VBO distinto (VBO de posiciones, VBO de colores, VBO de normales, etc.) ● Varios atributos en un mismo VBO. ○ ○ Agrupados: Primero las posiciones, luego colores… Intercalados: posicion, color, normal, posicion,.... ● Esto último lo configuramos con los parámetros stride y offset: ○ ○ Offset: desplazamiento (en bytes) de la primer componente del atributo que estamos configurando. Stride: cantidad de bytes que hay que “saltar” para encontrar la próxima componente del atributo que estamos configurando. Si los datos estan uno a continuación del otro, podemos usar Stride = 0. Dibujar el contenido del VAO En el VAO tenemos toda la configuración para dibujar un objeto ● Seleccionamos el VAO a utilizar: ○ glBindVertexArray(VAO_id) ● Si utilizamos índices, dibujamos con: ○ glDrawElements(primitive, count, idxType, offset) ● Si no usamos índices, dibujamos con: ○ glDrawArrays(primitive, start, count) ● Finalmente, desactivamos el VAO: ○ glBindVertexArray(0) Primitivas Gráficas ● Para formar los objetos geométricos en 3D, se descomponen en primitivas geométricas que OpenGL puede dibujar: ○ ○ Puntos, Líneas, Triángulos Puede utilizar colecciones del mismo tipo de primitiva para optimizar el rendering. Recordemos: Programas en OpenGL ● Los programas OpenGL actuales esencialmente deben realizar los siguientes pasos: 1. Crear y cargar los shaders y programas de shaders. 2. Crear los buffers y cargar los datos en ellos. 3. “Conectar” la ubicación de esos datos con las variables en los shaders. 4. Renderizar (Dibujar). Vinculación buffers - shaders Como vimos, al configurar el VAO, especificamos la ubicación del atributo. Esta ubicación es la que se le asignó a cada variable de tipo IN que hay en el shader de vertices. La ubicación de los atributos puede obtenerse de 3 formas: ● ● ● Dejar que OpenGL asigne una ubicación, y utilizar glGetAttribLocation (...), DESPUES del linkeo. Especificar la ubicación nosotros con glBindAttribLocation(...), ANTES del linkeo. Especificar la ubicación en el código fuente con una directiva layout (location = x) (sólo para #version 330 o superior) Ejemplo - Laboratorio 0 ● Una vez que tenemos claros estos dos conceptos principales (Shaders y Buffers), examinemos el proyecto de Visual Studio del Laboratorio 0.