portaldacalheta.pt
  • Principal
  • Vida Designer
  • Design De Marca
  • Ciclo De Vida Do Produto
  • Ferramentas E Tutoriais
Tecnologia

Gráficos 3D: Um Tutorial WebGL



O mundo dos gráficos 3D pode ser muito intimidante no início. Se você deseja apenas criar um logotipo 3D interativo ou projetar um jogo completo, se você não conhece os princípios da renderização 3D, pode ficar preso em uma biblioteca que abstrai muitas coisas.

Usar uma biblioteca pode ser a ferramenta certa e JavaScript tem um código aberto incrível na forma de three.js . Existem algumas desvantagens em usar soluções pré-fabricadas:



  • Eles podem ter muitos recursos que você não planeja usar. O tamanho dos recursos de base minimizados tres.js é de cerca de 500kB, e quaisquer recursos adicionais (fazer upload de arquivos de modelo real é um deles) tornam a carga útil ainda maior. Transferir tantos dados apenas para exibir um logotipo giratório em seu site seria um desperdício.
  • Uma camada extra de abstração pode fazer modificações que normalmente são fáceis, difíceis de fazer. Sua maneira criativa de sombrear um objeto na tela pode ser simples de implementar ou exigir dezenas de horas de trabalho para incorporar às abstrações de sua biblioteca.
  • Embora a biblioteca seja muito bem otimizada na maioria dos cenários, adornos extras podem ser removidos para seu caso de uso específico. O renderizador pode fazer com que certos procedimentos sejam executados milhões de vezes na placa gráfica. Cada instrução removida de tal procedimento significa que uma placa gráfica mais fraca pode lidar com seu conteúdo sem problemas.

Mesmo que você decida usar uma biblioteca gráfica de alto nível, ter um conhecimento básico das coisas ocultas permite que você a use com mais eficácia. As bibliotecas também podem ter funções avançadas, como ShaderMaterial em three.js. Conhecer os princípios da representação gráfica permite que você use esses recursos.



Ilustração de um logotipo 3D do ApeeScape em uma tela WebGL



Nosso objetivo é fornecer uma breve introdução a todos os conceitos-chave por trás da renderização de gráficos 3D e do uso de WebGL para seu aplicativo. Você verá que a coisa mais comum que é feita é mostrar e mover objetos 3D em um espaço vazio.

o código final está disponível para você experimentar e brincar.



é reagir a um framework ou biblioteca

Representando Modelos 3D

A primeira coisa que você precisa entender é como os modelos 3D são renderizados. Um modelo é feito de uma malha de triângulos. Cada triângulo é representado por três vértices para cada um dos vértices do triângulo. Existem três propriedades anexadas aos vértices que são as mais comuns.

Posição do vértice

A posição é a propriedade mais intuitiva de um vértice. É a posição no espaço 3D, representada por um vetor 3D de coordenadas. Se você souber as coordenadas exatas de três pontos no espaço, terá todas as informações de que precisa para desenhar um triângulo simples entre eles. Para que os modelos tenham uma aparência realmente boa quando renderizados, há mais algumas coisas que precisam ser fornecidas ao renderizador.



Vertex normal

Esferas com o mesmo wireframe, com sombreamento plano e suave aplicado

Considere os dois modelos anteriores. Eles consistem nas mesmas posições de vértice, embora pareçam totalmente diferentes quando representados. Como isso é possível?



Além de dizer ao renderizador onde queremos que um vértice seja encontrado, também podemos dar uma dica de como a superfície está inclinada nessa posição exata. A pista está na forma do vetor normal da superfície naquele ponto específico do modelo, representado por um vetor 3D. A imagem a seguir deve fornecer um aspecto mais descritivo de como isso é tratado.

Comparação entre vetores normais para sombreamento plano e suave



As superfícies esquerda e direita correspondem à bola esquerda e direita na imagem anterior, respectivamente. As setas vermelhas representam os vetores normais que são especificados para um vértice, enquanto as setas azuis representam os cálculos do processador de como um vetor normal deve encontrar todos os pontos entre os vértices. A imagem mostra uma demonstração do espaço 2D, mas o mesmo princípio se aplica ao 3D.

O vetor normal é uma indicação de como as luzes iluminarão a superfície. Quanto mais próxima a direção de um raio de luz estiver do vetor normal, mais brilhante será o ponto. Ter mudanças graduais na direção do vetor normal causa gradientes leves, enquanto mudanças abruptas sem mudanças entre eles resultam em superfícies com iluminação constante entre eles, bem como mudanças repentinas na iluminação.



Coordenadas de textura

A última propriedade significativa são as coordenadas de textura, comumente chamadas de mapeamento UV. Você tem um padrão e uma textura que deseja aplicar a ele. A textura possui várias áreas e estas representam as imagens que queremos aplicar às diferentes partes do modelo. Deve haver uma maneira de marcar qual triângulo deve ser representado por qual parte da textura. É aí que entra o mapeamento de textura.

Para cada vértice, plotamos duas coordenadas, U e V. Essas coordenadas representam uma posição na textura, com U representando o eixo horizontal e V o eixo vertical. Os valores não estão em pixels, mas em uma posição percentual na imagem. O canto esquerdo inferior da imagem é representado por dois zeros, enquanto o canto superior direito é representado por dois uns.

Um triângulo é pintado pegando as coordenadas UV de cada vértice no triângulo e aplicando a imagem que é capturada entre essas coordenadas na textura.

Demonstração de mapeamento UV, com um patch proeminente e costura visível no modelo

Você pode ver uma demonstração de mapeamento UV na imagem acima. O modelo esférico foi tirado e cortado em partes que são pequenas o suficiente para serem achatadas em uma superfície 2D. As costuras onde foram feitos os cortes são marcadas com linhas mais grossas. Um dos patches foi destacado para que você possa ver bem como as coisas combinam. Você também pode ver como uma costura no meio do sorriso posiciona partes da boca em duas manchas diferentes.

Os wireframes não fazem parte da textura, mas são sobrepostos na parte superior da imagem para que você possa ver como as coisas se correlacionam.

Carregando um modelo OBJ

Acredite ou não, isso é tudo que você precisa saber para criar seu próprio carregador de modelo simples. o Formato de arquivo OBJ é bastante simples implementar um analisador em algumas linhas de código.

O arquivo lista as posições dos vértices em um formato v , com um quarto flutuador opcional, que iremos ignorar, para manter as coisas simples. Os vértices são representados de forma semelhante com vn . Finalmente, as coordenadas da textura são representadas por vt , com um terceiro flutuador opcional que continuaremos a ignorar. Em todos os três casos, flutuador representam as respectivas coordenadas. Essas três propriedades se acumulam em três matrizes.

As faces são representadas por grupos de vértices. Cada vértice é representado pelo índice de cada uma das propriedades, então os índices começam em 1. Existem várias maneiras de como isso é representado, mas vamos nos ater a 'v1 / vt1 / vn1 v2 / vt2 / vn2 v3 / vt3 / vn3`, exigindo que todas as três propriedades sejam fornecidas e limitando o número de vértices para cada face a três. Todas essas limitações foram feitas para manter o carregador o mais simples possível, já que todas as outras opções requerem algum processamento trivial extra antes de poderem estar em um formato que o WebGL goste.

Colocamos muitos requisitos em nosso carregador de arquivos. Isso pode parecer limitante, mas os aplicativos de modelagem 3D tendem a oferecer a capacidade de definir essas limitações ao exportar um modelo como um arquivo OBJ.

O código a seguir analisa uma string que representa um arquivo OBJ e cria um modelo na forma de uma matriz de faces.

function Geometry (faces) this.faces = faces // Parses an OBJ file, passed as a string Geometry.parseOBJ = function (src) { var POSITION = /^vs+([d.+-eE]+)s+([d.+-eE]+)s+([d.+-eE]+)/ var NORMAL = /^vns+([d.+-eE]+)s+([d.+-eE]+)s+([d.+-eE]+)/ var UV = /^vts+([d.+-eE]+)s+([d.+-eE]+)/ var FACE = /^fs+(-?d+)/(-?d+)/(-?d+)s+(-?d+)/(-?d+)/(-?d+)s+(-?d+)/(-?d+)/(-?d+)(?:s+(-?d+)/(-?d+)/(-?d+))?/ lines = src.split(' ') var positions = [] var uvs = [] var normals = [] var faces = [] lines.forEach(function (line) { // Match each line of the file against various RegEx-es var result if ((result = POSITION.exec(line)) != null) { // Add new vertex position positions.push(new Vector3(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]))) } else if ((result = NORMAL.exec(line)) != null) { // Add new vertex normal normals.push(new Vector3(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]))) } else if ((result = UV.exec(line)) != null) { // Add new texture mapping point uvs.push(new Vector2(parseFloat(result[1]), 1 - parseFloat(result[2]))) } else if ((result = FACE.exec(line)) != null) { // Add new face var vertices = [] // Create three vertices from the passed one-indexed indices for (var i = 1; i <10; i += 3) { var part = result.slice(i, i + 3) var position = positions[parseInt(part[0]) - 1] var uv = uvs[parseInt(part[1]) - 1] var normal = normals[parseInt(part[2]) - 1] vertices.push(new Vertex(position, normal, uv)) } faces.push(new Face(vertices)) } }) return new Geometry(faces) } // Loads an OBJ file from the given URL, and returns it as a promise Geometry.loadOBJ = function (url) { return new Promise(function (resolve) { var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState == XMLHttpRequest.DONE) { resolve(Geometry.parseOBJ(xhr.responseText)) } } xhr.open('GET', url, true) xhr.send(null) }) } function Face (vertices) [] function Vertex (position, normal, uv) function Vector3 (x, y, z) 0 this.y = Number(y) function Vector2 (x, y)

A estrutura Geometry Ele contém os dados exatos necessários para enviar um modelo à placa gráfica para ser processado. Antes de fazer isso, você provavelmente deseja ter a capacidade de mover o modelo na tela.

Executando Transformações Espaciais

Todos os pontos do modelo que carregamos são relativos ao seu sistema de coordenadas. Se quisermos transladar, girar e dimensionar o modelo, o que devemos fazer é realizar esta operação em seu sistema de coordenadas. O sistema de coordenadas A, em relação ao sistema de coordenadas B, é definido pela posição de seu centro como um vetor p_ab e o vetor para cada um de seus eixos x_ab, y_ab e z_ab, representando a direção desse eixo. Portanto, se um ponto for movido em 10 no x do sistema de coordenadas A, então - no sistema de coordenadas B - ele se moverá na direção de x_ab, multiplicado por 10.

Todas essas informações são armazenadas na seguinte forma de matriz:

x_ab.x y_ab.x z_ab.x p_ab.x x_ab.y y_ab.y z_ab.y p_ab.y x_ab.z y_ab.z z_ab.z p_ab.z 0 0 0 1

Se quisermos transformar o vetor 3D q, precisamos apenas multiplicar a matriz de transformação pelo vetor:

javascript obter data hora atual
q.x q.y q.z 1

Isso faz com que o ponto se mova em q.x ao longo do novo eixo x, por q.y ao longo do novo eixo y e por q.z ao longo do novo eixo z. Finalmente, faz com que o ponto se mova mais ao longo do vetor p, razão pela qual usamos um como o elemento final da multiplicação.

A grande vantagem de usar essas matrizes é o fato de que, se temos múltiplas transformações para realizar no vértice, podemos fundi-las em uma transformação, multiplicando suas matrizes antes de transformar o próprio vértice.

Existem várias transformações que podem ser executadas e vamos dar uma olhada nas principais.

Sem transformação

Se nenhuma transformação ocorrer, o vetor p é um vetor zero, o vetor x é [1, 0, 0], y é [0, 1, 0] e z é [0, 0, 1]. De agora em diante, vamos nos referir a esses valores como valores padrão para esses vetores. A aplicação desses valores nos dá uma matriz de identidade:

1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1

Este é um bom ponto de partida para encadear transformações.

Tradução

Transformação de lugar para tradução

No momento da tradução, todos os vetores, exceto o vetor p, têm seus valores padrão. Isso resulta na seguinte matriz:

1 0 0 p.x 0 1 0 p.y 0 0 1 p.z 0 0 0 1

Ajustamento

Transformação de lugar para ajuste

Ajustar um modelo significa reduzir a quantidade que cada coordenada contribui para a posição de um ponto. Não há deslocamento uniforme causado pelo ajuste, então o vetor p mantém seu valor padrão. Os vetores de eixo padrão devem ser multiplicados por seus respectivos fatores de escala, resultando na seguinte matriz:

s_x 0 0 0 0 s_y 0 0 0 0 s_z 0 0 0 0 1

Aqui s_x, s_y e s_z representam o ajuste aplicado a cada eixo.

Rotação

Transformação de quadro para rotação em torno do eixo Z

A imagem acima mostra o que acontece quando giramos o quadro de coordenadas em torno do eixo Z.

A rotação não resulta em deslocamento uniforme, então o vetor p mantém seu valor padrão. Agora as coisas ficam um pouco mais complicadas. As rotações fazem com que o movimento ao longo de um determinado eixo no sistema de coordenadas original se mova em uma direção diferente. Portanto, se girarmos um sistema de coordenadas 45 graus em torno do eixo Z, movendo-se ao longo do eixo x do sistema de coordenadas original, um movimento ocorre em uma direção diagonal entre o eixo xeo eixo y no novo sistema de coordenadas.

Para manter as coisas simples, mostraremos como as matrizes de transformação procuram rotações em torno dos eixos principais.

Around X: 1 0 0 0 0 cos(phi) sin(phi) 0 0 -sin(phi) cos(phi) 0 0 0 0 1 Around Y: cos(phi) 0 sin(phi) 0 0 1 0 0 -sin(phi) 0 cos(phi) 0 0 0 0 1 Around Z: cos(phi) -sin(phi) 0 0 sin(phi) cos(phi) 0 0 0 0 1 0 0 0 0 1

Implementação

Tudo isso pode ser implementado como uma classe que armazena 16 números, armazenando matrizes em um ordem das colunas principais .

function Transformation () { // Create an identity transformation this.fields = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] } // Multiply matrices, to chain transformations Transformation.prototype.mult = function (t) { var output = new Transformation() for (var row = 0; row <4; ++row) { for (var col = 0; col < 4; ++col) { var sum = 0 for (var k = 0; k < 4; ++k) { sum += this.fields[k * 4 + row] * t.fields[col * 4 + k] } output.fields[col * 4 + row] = sum } } return output } // Multiply by translation matrix Transformation.prototype.translate = function (x, y, z) // Multiply by scaling matrix Transformation.prototype.scale = function (x, y, z) // Multiply by rotation matrix around X axis Transformation.prototype.rotateX = function (angle) // Multiply by rotation matrix around Y axis Transformation.prototype.rotateY = function (angle) // Multiply by rotation matrix around Z axis Transformation.prototype.rotateZ = function (angle)

Olhar através de uma câmera

Aí vem a parte fundamental da apresentação de objetos na tela: a câmera. Existem dois componentes principais para uma câmera; Sua posição e como ele projeta os objetos observados na tela.

A posição da câmera é tratada com um truque simples. Não há diferença visual entre mover a câmera um metro para a frente e mover o mundo inteiro um metro para trás. Então, naturalmente, fazemos o último aplicando o inverso da matriz como uma transformação.

O segundo componente principal é a maneira como os objetos observados são projetados na lente. No WebGL, tudo o que é visível na tela está em uma caixa. A caixa se estende entre -1 e 1 em cada eixo. Tudo o que é visível está dentro dessa caixa. Podemos usar a mesma abordagem de matriz de transformação para criar uma matriz de projeção.

Projeção ortográfica

Espaço retangular que é transformado nas dimensões * framebuffer * apropriadas usando projeção ortográfica

A projeção mais simples é a [projeção ortográfica] (https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographical-projection-matrix/orthographic-projection-matrix). Uma caixa é tomada no espaço que indica a largura, altura e profundidade com a suposição de que seu centro está na posição zero. A projeção então redimensiona a caixa para caber na caixa descrita anteriormente dentro da qual o WebGL observa os objetos. Como queremos redimensionar cada dimensão para dois, definimos cada eixo como 2/size, onde size é a dimensão do respectivo eixo. Uma pequena ressalva é o fato de que estamos multiplicando o eixo Z por um negativo. Isso é feito porque queremos inverter a direção dessa dimensão. A matriz final tem esta forma:

2/width 0 0 0 0 2/height 0 0 0 0 -2/depth 0 0 0 0 1

Projeção em perspectiva

Frustum sendo transformado nas dimensões * framebuffer * apropriadas usando projeção em perspectiva

Não vamos entrar em detalhes de como essa projeção foi projetada, mas apenas usar o fórmula final , o que é bastante normal por enquanto. Podemos simplificar colocando a projeção na posição zero nos eixos xey, tornando os limites direito / esquerdo e superior / inferior iguais a width/2 e height/2, respectivamente. Os parâmetros n e f representam os planos de recorte near e far, que são a menor e a maior distância que um ponto pode estar para ser capturado pela câmera. Eles são representados pelos lados paralelos do tronco na imagem acima.

Uma projeção em perspectiva é geralmente representada por um campo de visão (usaremos o vertical), proporção da tela , e as distâncias de planos próximos e distantes. Essa informação pode ser usada para calcular width e height, e então a matriz pode ser criada a partir do seguinte modelo:

2*n/width 0 0 0 0 2*n/height 0 0 0 0 (f+n)/(n-f) 2*f*n/(n-f) 0 0 -1 0

Para calcular a largura e altura, as seguintes fórmulas podem ser usadas:

height = 2 * near * Math.tan(fov * Math.PI / 360) width = aspectRatio * height

FOV (campo de visão) representa o ângulo vertical que a câmera captura com sua lente. A proporção da imagem representa a proporção entre a largura e a altura da imagem e é baseada nas dimensões da tela que estamos exibindo.

Implementação

Agora podemos representar uma câmera como uma classe, que armazena a posição da câmera e a matriz de projeção. Também precisamos saber como calcular as transformações inversas. Resolver inversões matriciais gerais pode ser problemático, mas há um abordagem simplificada para o nosso caso em particular.

function Camera () { this.position = new Transformation() this.projection = new Transformation() } Camera.prototype.setOrthographic = function (width, height, depth) { this.projection = new Transformation() this.projection.fields[0] = 2 / width this.projection.fields[5] = 2 / height this.projection.fields[10] = -2 / depth } Camera.prototype.setPerspective = function (verticalFov, aspectRatio, near, far) { var height_div_2n = Math.tan(verticalFov * Math.PI / 360) var width_div_2n = aspectRatio * height_div_2n this.projection = new Transformation() this.projection.fields[0] = 1 / height_div_2n this.projection.fields[5] = 1 / width_div_2n this.projection.fields[10] = (far + near) / (near - far) this.projection.fields[10] = -1 this.projection.fields[14] = 2 * far * near / (near - far) this.projection.fields[15] = 0 } Camera.prototype.getInversePosition = function () { var orig = this.position.fields var dest = new Transformation() var x = orig[12] var y = orig[13] var z = orig[14] // Transpose the rotation matrix for (var i = 0; i <3; ++i) { for (var j = 0; j < 3; ++j) { dest.fields[i * 4 + j] = orig[i + j * 4] } } // Translation by -p will apply R^T, which is equal to R^-1 return dest.translate(-x, -y, -z) }

Esta é a peça final de que precisamos antes de começar a desenhar na tela.

Desenhe um objeto com o canal WebGL Graphics

A superfície mais simples que você pode desenhar é um triângulo. Na verdade, a maioria das coisas que você desenha no espaço 3D consiste em um grande número de triângulos.

Uma visão básica do que as etapas do canal do gráfico fazem

A primeira coisa a entender é como a tela é renderizada em WebGL. É um espaço 3D, medindo entre -1 e 1 no eixo x , Y Y com . Por padrão, este eixo com não é usado, mas você está interessado em gráficos 3D, portanto, deve ativá-lo imediatamente.

Com isso em mente, a seguir, três etapas necessárias para desenhar um triângulo nesta superfície.

Você pode definir três vértices que representariam o triângulo que deseja desenhar. Os dados são serializados e enviados para a GPU (unidade de processamento gráfico). Com um modelo completo disponível, você pode fazer isso para todos os triângulos no modelo. As posições dos vértices que você fornece estão no espaço de coordenadas local do modelo que você carregou. Simplificando, as posições que você fornece são as exatas do arquivo e não as que você obtém após realizar as transformações de matriz.

Agora que você deu os vértices à GPU, diga à GPU qual lógica usar ao colocar os vértices na tela. Esta etapa será usada para aplicar nossas transformações de matriz. A GPU é muito boa em multiplicar muitas matrizes 4x4, então faremos bom uso desse recurso.

Na última etapa, a GPU rasterizará esse triângulo. Rasterização é o processo de obter gráficos vetoriais e determinar quais pixels na tela precisam ser pintados para que o objeto gráfico vetorial seja exibido. Em nosso caso, a GPU está tentando determinar quais pixels estão dentro de cada triângulo. Para cada pixel, a GPU perguntará com que cor você deseja que seja pintado.

Estes são os quatro elementos necessários para desenhar o que você quiser e são o exemplo mais simples de um canal de gráficos . A seguir, uma olhada em cada um deles e uma implementação simples.

o Suavizador de quadros Predeterminado

O elemento mais importante para um aplicativo WebGL é o contexto WebGL. Você pode acessá-lo com gl = canvas.getContext ('webgl') ou usar ’experimental-webgl' alternativamente, caso o navegador atualmente usado não suporte todas as funções do WebGL. A 'tela' a que nos referimos é o elemento DOM da tela em que queremos desenhar. O contexto contém muitas coisas, entre as quais está o Suavizador de quadros predeterminado.

Poderia descrever um Suavizador de quadros como qualquer buffer (objeto) em que você pode desenhar. Por padrão, o Suavizador de quadros O padrão armazena a cor de cada pixel na tela ao qual o contexto WebGL está vinculado. Conforme descrito na seção anterior, quando desenhamos sobre o Suavizador de quadros , cada pixel está localizado entre -1 e 1 no eixo x Y Y . Algo que também mencionamos é o fato de que, por padrão, WebGL não usa o eixo com . Essa funcionalidade pode ser ativada executando gl.enable (gl.DEPTH_TEST). Ótimo, mas o que é um teste de profundidade?

Habilitar o teste de profundidade permite que um pixel armazene cor e profundidade. A profundidade é a coordenada com desse pixel. Depois de desenhar um pixel em uma certa profundidade com , para atualizar a cor desse pixel, você deve desenhar em uma posição com mais perto da câmera. Caso contrário, a tentativa de desenho será ignorada. Isso permite a ilusão de 3D, pois desenhar objetos que estão atrás de outros objetos farão com que os objetos sejam obstruídos por outros objetos na frente deles.

Todos os desenhos que você fizer ficarão na tela até que você os diga para limpar. Para fazer isso, você deve chamar gl.clear (gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT). Isso limpa a cor e o buffer de profundidade. Para escolher a cor para a qual os pixels apagados são definidos, use gl.clearColor (rojo, verde, azul, alfa).

Vamos criar um renderizador que usa uma tela e a limpa sob demanda:

function Renderer (canvas) Renderer.prototype.setClearColor = function (red, green, blue) { gl.clearColor(red / 255, green / 255, blue / 255, 1) } Renderer.prototype.getContext = function () { return this.gl } Renderer.prototype.render = function () gl.DEPTH_BUFFER_BIT) var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) loop() function loop () { renderer.render() requestAnimationFrame(loop) }

Anexar este script ao seguinte HTML apresentará um retângulo azul brilhante na tela

requestAnimationFrame

O chamado N faz com que o nó seja chamado novamente assim que a renderização anterior for concluída e todo o tratamento de eventos for concluído.

Objetos Vertex Buffer

A primeira coisa a fazer é definir os vértices que deseja desenhar. Você pode fazer isso descrevendo-os por meio de vetores no espaço 3D. Depois disso, você precisa mover esses dados para a GPU RAM, criando um novo Buffer Vertex Buffer (FEV).

UMA Objeto buffer geralmente é um objeto que armazena uma matriz de blocos de memória na GPU. Começar um Fev , isso denota apenas para que a GPU pode usar a memória. Na maioria das vezes, os objetos de buffer que você criar serão VBOs .

Você pode preencher o Fev pegando todos os vértices 3N que temos e criando uma matriz de flutua com elementos 2N para posição de vértice y VBOs normais de vértice e Geometry para coordenadas de textura Fev . Cada grupo de três flutua , ou dois flutua para coordenadas UV, representa coordenadas individuais de um vértice. Em seguida, passamos esses arrays para a GPU, e nossos vértices estão prontos para o resto do canal.

Como os dados agora estão na RAM da GPU, você pode removê-los da RAM de uso geral. Isto é, a menos que você queira modificá-lo mais tarde e recarregá-lo. Cada modificação deve ser seguida por um upload, pois as modificações em nossos arrays JS não se aplicam a VBOs na RAM real da GPU.

Abaixo está um exemplo de código, que fornece todas as funcionalidades descritas. Uma observação importante é o fato de que as variáveis ​​armazenadas na GPU não são coletadas como lixo. Isso significa que temos que excluí-los manualmente, uma vez que não queremos mais usá-los. Daremos apenas um exemplo de como isso é feito aqui e não nos concentraremos mais nesse conceito. A supressão da variável GPU só é necessária se você planeja parar de usar determinada geometria em todo o programa.

Também adicionamos serialização à nossa classe Geometry.prototype.vertexCount = function () { return this.faces.length * 3 } Geometry.prototype.positions = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.position answer.push(v.x, v.y, v.z) }) }) return answer } Geometry.prototype.normals = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.normal answer.push(v.x, v.y, v.z) }) }) return answer } Geometry.prototype.uvs = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.uv answer.push(v.x, v.y) }) }) return answer } //////////////////////////////// function VBO (gl, data, count) { // Creates buffer object in GPU RAM where we can store anything var bufferObject = gl.createBuffer() // Tell which buffer object we want to operate on as a VBO gl.bindBuffer(gl.ARRAY_BUFFER, bufferObject) // Write the data, and set the flag to optimize // for rare changes to the data we're writing gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW) this.gl = gl this.size = data.length / count this.count = count this.data = bufferObject } VBO.prototype.destroy = function () { // Free memory that is occupied by our buffer object this.gl.deleteBuffer(this.data) } e elementos dentro dele.

VBO

O tipo de dados gl gera o Fev no contexto WebGL passado, com base na matriz passada como o segundo parâmetro.

verdadeiro custo de uma calculadora de funcionário

Você pode ver três chamadas para o contexto createBuffer (). A chamada bindBuffer () criar o amortecedor . A chamada ARRAY_BUFFER instrui a máquina de estado WebGL a usar esta memória específica como a atual Fev (bufferData ()) para todas as operações futuras, até que seja indicado o contrário. Depois disso, definimos o valor do Fev atual para os dados fornecidos, com deleteBuffer() .

Também fornecemos um método de destruição que remove nosso objeto de buffer GPU RAM, usando function Mesh (gl, geometry) { var vertexCount = geometry.vertexCount() this.positions = new VBO(gl, geometry.positions(), vertexCount) this.normals = new VBO(gl, geometry.normals(), vertexCount) this.uvs = new VBO(gl, geometry.uvs(), vertexCount) this.vertexCount = vertexCount this.position = new Transformation() this.gl = gl } Mesh.prototype.destroy = function () { this.positions.destroy() this.normals.destroy() this.uvs.destroy() } .

Você pode usar três VBOs e uma transformação para descrever todas as propriedades de uma malha, junto com sua posição.

Geometry.loadOBJ('/assets/model.obj').then(function (geometry) { var mesh = new Mesh(gl, geometry) console.log(mesh) mesh.destroy() })

Como exemplo, aqui está como podemos carregar um modelo, armazenar suas propriedades na malha ( malha ) e depois destruí-lo:

atributo

Shaders

O que se segue é o processo de duas etapas descrito anteriormente, que envolve mover pontos para as posições desejadas e pintar todos os pixels individuais. Para fazer isso, escrevemos um programa que é executado na placa de vídeo muitas vezes. Este programa normalmente consiste em pelo menos duas partes. A primeira parte é um Vertex Shader , que é executado para cada vértice, e as saídas onde devemos colocar o vértice na tela, entre outras coisas. A segunda parte é o Fragmento de shader , que é executado para cada pixel que um triângulo cobre na tela e produz a cor na qual o pixel deve ser pintado.

Shaders de vértices

Digamos que você queira um modelo que se mova para a esquerda e para a direita na tela. Em uma abordagem ingênua, você pode atualizar a posição de cada vértice e enviá-lo de volta para a GPU. Esse processo é caro e demorado. Alternativamente, você daria um programa para a GPU rodar para cada vértice e fazer todas essas operações em paralelo com um processador que é construído para fazer exatamente esse trabalho. Esse é o papel de um sombreador de vértice .

UMA shader vértices é a parte do pipeline de renderização que processa vértices individuais. Uma chamada para shader de vértices recebe um único vértice e gera um único vértice após aplicar todas as transformações possíveis ao vértice.

Shaders eles são escritos em GLSL. Existem muitos elementos únicos nesta linguagem, mas a maior parte da sintaxe é muito semelhante a C, por isso deve ser compreensível para a maioria das pessoas.

Existem três tipos de variáveis ​​que entram e saem de um shader de vértices, e todos eles têm um uso específico:

ao fazer uma apresentação complexa, qual das seguintes
  • uniforme - São entradas que contêm propriedades específicas de um vértice. Anteriormente, descrevemos a posição de um vértice como um atributo na forma de um vetor de três elementos. Você pode ver os atributos como valores que descrevem um vértice.
  • uniforme - Essas são entradas que são iguais para cada vértice dentro da mesma chamada de renderização. Digamos que queremos mover nosso modelo, definindo uma matriz de transformação. Você pode usar uma variável variaciones para descrever isso. Você também pode usar recursos de GPU, como texturas. Você pode ver esses uniformes como valores que descrevem um modelo ou uma parte de um modelo.
  • attribute vec3 position; attribute vec3 normal; attribute vec2 uv; uniform mat4 model; uniform mat4 view; uniform mat4 projection; varying vec3 vNormal; varying vec2 vUv; void main() { vUv = uv; vNormal = (model * vec4(normal, 0.)).xyz; gl_Position = projection * view * model * vec4(position, 1.); } - Estas são as saídas que passamos para o fragmento de shader . Como existem potencialmente milhares de pixels para um triângulo de vértices, cada pixel receberá um valor interpolado para esta variável, dependendo da posição. Portanto, se um vértice envia 500 como saída e em outro 100, um pixel que está no meio deles receberá 300 como entrada para aquela variável. Você pode ver as variações como valores que descrevem superfícies entre vértices.

Então, digamos que você deseja criar um shader de vértices que recebe uma posição normal e coordenadas uv para cada vértice, e uma posição de visualização (posição reversa da câmera) e uma matriz de projeção para cada objeto renderizado. Digamos que você também queira pintar pixels individuais com base em suas coordenadas UV e normais. Você se perguntará 'Como seria esse código?'

main

A maioria desses elementos deve ser autoexplicativa. A coisa mais importante a notar é que não há valores de retorno na função variantes. Todos os valores que gostaríamos de retornar são atribuídos a variáveis ​​gl_Position ou para variáveis ​​especiais. Aqui atribuímos a vec4, que é um vetor quadridimensional, portanto, a última dimensão deve estar sempre em um. Outra coisa estranha que você pode notar é a maneira como construímos um vec4 fora do vetor de posição. Você pode construir um float usando quatro vec2 s, dois variables s ou qualquer outra combinação que resulte em quatro elementos. Existem muitos tipos de fundição aparentemente estranhos que fazem sentido quando você está familiarizado com as matrizes de transformação.

Você também pode ver que aqui podemos facilmente realizar transformações de matriz. GLSL é projetado especificamente para este tipo de trabalho. A posição de saída é calculada multiplicando-se a projeção, visualização e matriz do modelo e aplicando-a à posição. A saída normal acaba de ser transformada no espaço do mundo. Mais tarde explicaremos por que paramos aí com as transformações normais.

Por enquanto, vamos mantê-lo simples e passar para a pintura de pixels individuais.

Fragmento de Shaders

UMA fragmento de shader é a etapa após a rasterização no canal gráfico. Gera cor, profundidade e outros dados para cada pixel do objeto que está sendo pintado

Os princípios por trás da implementação de sombreadores de fragmento são muito semelhantes aos shaders de vértices. No entanto, existem três grandes diferenças:

  • Não há mais saídas atributos e as entradas gl_FragColor foram substituídos por entradas 'mistas'. Acabamos de passar nosso canal, e as coisas que são a saída no shader vértice agora são entradas no fragmento de shader .
  • Nossa única saída agora é vec4, que é um #ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec2 clampedUv = clamp(vUv, 0., 1.); gl_FragColor = vec4(clampedUv, 1., 1.); } . Os elementos representam vermelho, verde, azul e alfa (RGBA), respectivamente, com variáveis ​​no intervalo de 0 a 1. Você deve manter alfa em 1, a menos que esteja fazendo transparência. No entanto, transparência é um conceito bastante avançado, por isso nos limitamos a objetos opacos.
  • No início do fragmento de shader , você precisa definir a precisão do float, o que é importante para interpolações. Em quase todos os casos, apenas siga as linhas do seguinte shader .

Com isso em mente, você pode facilmente escrever um shader que pinta o canal vermelho com base na posição U, o canal verde com base na posição V e define o canal azul para o máximo.

clamp

A função function ShaderProgram (gl, vertSrc, fragSrc) { var vert = gl.createShader(gl.VERTEX_SHADER) gl.shaderSource(vert, vertSrc) gl.compileShader(vert) if (!gl.getShaderParameter(vert, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(vert)) throw new Error('Failed to compile shader') } var frag = gl.createShader(gl.FRAGMENT_SHADER) gl.shaderSource(frag, fragSrc) gl.compileShader(frag) if (!gl.getShaderParameter(frag, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(frag)) throw new Error('Failed to compile shader') } var program = gl.createProgram() gl.attachShader(program, vert) gl.attachShader(program, frag) gl.linkProgram(program) if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error(gl.getProgramInfoLog(program)) throw new Error('Failed to link program') } this.gl = gl this.position = gl.getAttribLocation(program, 'position') this.normal = gl.getAttribLocation(program, 'normal') this.uv = gl.getAttribLocation(program, 'uv') this.model = gl.getUniformLocation(program, 'model') this.view = gl.getUniformLocation(program, 'view') this.projection = gl.getUniformLocation(program, 'projection') this.vert = vert this.frag = frag this.program = program } // Loads shader files from the given URLs, and returns a program as a promise ShaderProgram.load = function (gl, vertUrl, fragUrl) { return Promise.all([loadFile(vertUrl), loadFile(fragUrl)]).then(function (files) { return new ShaderProgram(gl, files[0], files[1]) }) function loadFile (url) { return new Promise(function (resolve) { var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState == XMLHttpRequest.DONE) { resolve(xhr.responseText) } } xhr.open('GET', url, true) xhr.send(null) }) } } apenas limite tudo flutua em um objeto para estar dentro dos limites dados. O resto do código deve ser bem direto.

Com tudo isso em mente, o que falta fazer é implementá-lo em WebGL.

Combinando Shaders em um Programa

A próxima etapa é combinar os shaders em um programa:

ShaderProgram.prototype.use = function () { this.gl.useProgram(this.program) }

Não há muito a dizer sobre o que está acontecendo aqui. A cada shader é atribuída uma string como fonte e compilada, após o que verificamos se há erros de compilação. Então, criamos um programa ligando esses dois shaders . Por fim, armazenamos indicadores para todos os atributos relevantes e uniformes para a posteridade.

Na verdade, desenhando o modelo

Por último, mas não menos importante, desenhe o modelo.

Escolha primeiro o programa shader o que você quer usar

Transformation.prototype.sendToGpu = function (gl, uniform, transpose) gl.uniformMatrix4fv(uniform, transpose Camera.prototype.use = function (shaderProgram) { this.projection.sendToGpu(shaderProgram.gl, shaderProgram.projection) this.getInversePosition().sendToGpu(shaderProgram.gl, shaderProgram.view) }

Em seguida, ele envia todos os uniformes relacionados à câmera para a GPU. Esses uniformes mudam apenas uma vez para cada mudança de câmera ou movimento.

VBO.prototype.bindToAttribute = function (attribute) { var gl = this.gl // Tell which buffer object we want to operate on as a VBO gl.bindBuffer(gl.ARRAY_BUFFER, this.data) // Enable this attribute in the shader gl.enableVertexAttribArray(attribute) // Define format of the attribute array. Must match parameters in shader gl.vertexAttribPointer(attribute, this.size, gl.FLOAT, false, 0, 0) }

Finalmente, as transformações são realizadas e VBOs e são atribuídos a uniformes e atributos, respectivamente. Uma vez que isso tem que ser feito a cada Fev , você pode criar sua vinculação de dados como um método.

drawArrays ()

Em seguida, uma matriz de três é atribuída flutua para o uniforme. Cada tipo de uniforme tem uma assinatura diferente, então o documentação e mais documentação eles são seus amigos aqui. Por fim, desenhe a matriz triangular na tela. Diz à chamada de desenho [TRIÁNGULOS] (https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/drawArrays) de qual vértice pode começar e quantos vértices pode desenhar. O primeiro parâmetro passado informa ao WebGL como ele interpretará a matriz de vértices. Usando PUNTOS Pegue três vezes três vértices e desenhe um triângulo para cada trinca. Use Mesh.prototype.draw = function (shaderProgram) { this.positions.bindToAttribute(shaderProgram.position) this.normals.bindToAttribute(shaderProgram.normal) this.uvs.bindToAttribute(shaderProgram.uv) this.position.sendToGpu(this.gl, shaderProgram.model) this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertexCount) } Eu desenharia apenas um ponto para cada vértice passado. Existem muito mais opções, mas não há necessidade de descobrir tudo ao mesmo tempo. Abaixo está o código para desenhar um objeto:

Renderer.prototype.setShader = function (shader) { this.shader = shader } Renderer.prototype.render = function (camera, objects) { this.gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) var shader = this.shader if (!shader) { return } shader.use() camera.use(shader) objects.forEach(function (mesh) { mesh.draw(shader) }) }

O renderizador precisa ser estendido um pouco, para acomodar todos os elementos adicionais que precisam ser manipulados. Deve ser possível anexar um programa de shader e renderizar uma série de objetos com base na posição atual da câmera.

var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) var gl = renderer.getContext() var objects = [] Geometry.loadOBJ('/assets/sphere.obj').then(function (data) { objects.push(new Mesh(gl, data)) }) ShaderProgram.load(gl, '/shaders/basic.vert', '/shaders/basic.frag') .then(function (shader) { renderer.setShader(shader) }) var camera = new Camera() camera.setOrthographic(16, 10, 10) loop() function loop () { renderer.render(camera, objects) requestAnimationFrame(loop) }

Podemos combinar todos os elementos que temos para finalmente desenhar algo na tela:

#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); gl_FragColor = vec4(brown, 1.); }

Objeto desenhado na tela, com cores dependendo das coordenadas UV

Isso parece um pouco aleatório, mas você pode ver os diferentes patches na esfera, dependendo de onde eles estão no mapa UV. Você pode mudar o shader para pintar o objeto de marrom. Basta definir a cor de cada pixel como RGBA para marrom:

#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); vec3 sunlightDirection = vec3(-1., -1., -1.); float lightness = -clamp(dot(normalize(vNormal), normalize(sunlightDirection)), -1., 0.); gl_FragColor = vec4(brown * lightness, 1.); }

Objeto marrom desenhado na tela

Não parece muito convincente. Parece que a cena precisa de alguns efeitos de sombreamento.

Adicionando luz

As luzes e sombras são as ferramentas que nos permitem perceber a forma dos objetos. As luzes vêm em várias formas e tamanhos: lâmpadas que brilham em forma de cone, lâmpadas que espalham luz em todas as direções e, o mais interessante, o sol, que está tão longe que toda a luz que brilha em nós irradia, para todas as tentativas. E na mesma direção.

A luz solar parece ser a mais simples de implementar, pois tudo que você precisa fornecer é a direção de propagação de todos os raios. Para cada pixel que você desenha na tela, verifique o ângulo em que a luz atinge o objeto. É aqui que entram os normais de superfície. Objeto marrom com luz do sol

Você pode ver todos os raios de luz fluindo na mesma direção e atingindo a superfície em ângulos diferentes, que são baseados no ângulo entre o raio de luz e a normal da superfície. Quanto mais eles combinam, mais forte é a luz.

Se você fizer um produto pontual entre os vetores normalizados para o raio de luz e a superfície normal, você obterá -1 se o raio atingir a superfície perfeitamente perpendicular, 0 se o raio for paralelo à superfície e 1 se for iluminado a partir do lado oposto. Portanto, qualquer coisa entre 0 e 1 não deve adicionar luz, enquanto os números entre 0 e -1 devem aumentar gradualmente a quantidade de luz que atinge o objeto. Você pode testar isso adicionando uma luz sólida no código shader .

#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); vec3 sunlightDirection = vec3(-1., -1., -1.); float lightness = -clamp(dot(normalize(vNormal), normalize(sunlightDirection)), -1., 0.); float ambientLight = 0.3; lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(brown * lightness, 1.); }

Objeto marrom à luz do sol e luz ambiente

Colocamos o sol para brilhar na direção frente-esquerda-baixo. Você pode ver como o shader , embora o padrão seja muito irregular. Você também pode notar a escuridão no canto inferior esquerdo. Podemos adicionar um nível de luz ambiente, o que tornará a área na sombra mais brilhante.

#ifdef GL_ES precision highp float; #endif uniform vec3 lightDirection; uniform float ambientLight; varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(brown * lightness, 1.); }

Objeto texturizado com efeitos de luz

Você pode obter esse mesmo efeito inserindo uma classe de luz que armazena a direção da luz e a intensidade da luz ambiente. Você pode então alterar o snippet de shader para acomodar essa adição.

Agora ele shader se converte em:

function Light () { this.lightDirection = new Vector3(-1, -1, -1) this.ambientLight = 0.3 } Light.prototype.use = function (shaderProgram) { var dir = this.lightDirection var gl = shaderProgram.gl gl.uniform3f(shaderProgram.lightDirection, dir.x, dir.y, dir.z) gl.uniform1f(shaderProgram.ambientLight, this.ambientLight) }

Agora você pode definir a luz:

this.ambientLight = gl.getUniformLocation(program, 'ambientLight') this.lightDirection = gl.getUniformLocation(program, 'lightDirection')

Na aula do programa shader , adicione os uniformes necessários:

Renderer.prototype.render = function (camera, light, objects) { this.gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) var shader = this.shader if (!shader) { return } shader.use() light.use(shader) camera.use(shader) objects.forEach(function (mesh) { mesh.draw(shader) }) }

No programa, adicione uma chamada para a nova luz no renderizador:

var light = new Light() loop() function loop () { renderer.render(camera, light, objects) requestAnimationFrame(loop) }

O nó vai mudar um pouco:

sampler2D

Se você fez tudo certo, a imagem renderizada deve ser igual à última imagem.

Um último passo a considerar seria adicionar uma textura real ao nosso modelo. Vamos fazer isso agora.

Adicionando texturas

HTML5 tem um ótimo suporte para carregar imagens, então não há necessidade de fazer análises de imagens muito intensas. As imagens são passadas para GLSL como sampler2D contando o shader qual das texturas vinculadas a amostra. Há um número limitado de texturas que podem ser vinculadas e o limite é baseado no hardware usado. A #ifdef GL_ES precision highp float; #endif uniform vec3 lightDirection; uniform float ambientLight; uniform sampler2D diffuse; varying vec3 vNormal; varying vec2 vUv; void main() { float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(texture2D(diffuse, vUv).rgb * lightness, 1.); } pode ser consultado para cores em certas posições. É aqui que entram as coordenadas UV. Aqui está um exemplo em que substituímos o marrom por cores de amostra.

this.diffuse = gl.getUniformLocation(program, 'diffuse')

O novo uniforme deve ser adicionado à lista no programa do shader :

function Texture (gl, image) { var texture = gl.createTexture() // Set the newly created texture context as active texture gl.bindTexture(gl.TEXTURE_2D, texture) // Set texture parameters, and pass the image that the texture is based on gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image) // Set filtering methods // Very often shaders will query the texture value between pixels, // and this is instructing how that value shall be calculated gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) this.data = texture this.gl = gl } Texture.prototype.use = function (uniform, binding) Texture.load = function (gl, url) { return new Promise(function (resolve) { var image = new Image() image.onload = function () { resolve(new Texture(gl, image)) } image.src = url }) }

Finalmente, implementaremos o carregamento da textura. Conforme declarado acima, o HTML5 fornece recursos para carregar imagens. Tudo o que precisamos fazer é enviar a imagem para a GPU:

sampler2D

O processo não é muito diferente do processo usado para carregar e ligar VBOs . A principal diferença é que não estamos mais vinculados a um atributo, mas sim vinculamos o índice da textura a um uniforme inteiro. O tipo Mesh nada mais é do que um ponteiro movido para uma textura.

Agora tudo o que você precisa fazer é estender a classe function Mesh (gl, geometry, texture) { // added texture var vertexCount = geometry.vertexCount() this.positions = new VBO(gl, geometry.positions(), vertexCount) this.normals = new VBO(gl, geometry.normals(), vertexCount) this.uvs = new VBO(gl, geometry.uvs(), vertexCount) this.texture = texture // new this.vertexCount = vertexCount this.position = new Transformation() this.gl = gl } Mesh.prototype.destroy = function () { this.positions.destroy() this.normals.destroy() this.uvs.destroy() } Mesh.prototype.draw = function (shaderProgram) { this.positions.bindToAttribute(shaderProgram.position) this.normals.bindToAttribute(shaderProgram.normal) this.uvs.bindToAttribute(shaderProgram.uv) this.position.sendToGpu(this.gl, shaderProgram.model) this.texture.use(shaderProgram.diffuse, 0) // new this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertexCount) } Mesh.load = function (gl, modelUrl, textureUrl) { // new var geometry = Geometry.loadOBJ(modelUrl) var texture = Texture.load(gl, textureUrl) return Promise.all([geometry, texture]).then(function (params) { return new Mesh(gl, params[0], params[1]) }) } para lidar com texturas também:

var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) var gl = renderer.getContext() var objects = [] Mesh.load(gl, '/assets/sphere.obj', '/assets/diffuse.png') .then(function (mesh) { objects.push(mesh) }) ShaderProgram.load(gl, '/shaders/basic.vert', '/shaders/basic.frag') .then(function (shader) { renderer.setShader(shader) }) var camera = new Camera() camera.setOrthographic(16, 10, 10) var light = new Light() loop() function loop () { renderer.render(camera, light, objects) requestAnimationFrame(loop) }

E o script final principal ficaria assim:

function loop () { renderer.render(camera, light, objects) camera.position = camera.position.rotateY(Math.PI / 120) requestAnimationFrame(loop) }

Cabeça girada durante a animação da câmera

quanto tempo leva para obter a certificação AWs

Até mesmo torcer pode ser fácil neste momento. Se você quiser que a câmera gire em torno de nosso objeto, basta adicionar uma linha de código:

void main() { float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = lightness > 0.1 ? 1. : 0.; // new lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(texture2D(diffuse, vUv).rgb * lightness, 1.); }

Cabeça com iluminação de desenho animado aplicada

Você está livre para brincar com shaders . Adicionar uma linha de código transformará essa iluminação realista em algo de desenho animado.

|_+_|

É tão simples quanto dizer à iluminação para ir aos seus extremos, dependendo se ela cruzou um limite definido.

Para onde ir a seguir

Existem muitas fontes de informação para aprender todos os truques e complexidades do WebGL. E a melhor parte é que se você não conseguir encontrar uma resposta relacionada ao WebGL, você pode procurá-la no OpenGL, já que o WebGL é baseado em um subconjunto do OpenGL com alguns nomes alterados.

Sem nenhuma ordem específica, aqui estão algumas fontes excelentes para informações mais detalhadas, para WebGL e OpenGL.

  • WebGL Basics
  • Aprendendo WebGL
  • Um tutorial muito detalhado Tutorial OpenGL , irá guiá-lo por todos os princípios fundamentais descritos aqui, de uma forma muito lenta e detalhada.
  • E aqui está Muitos , Muitos outros sites dedicados a ensinar os princípios da computação gráfica.
  • Documentação MDN para WebGL
  • Especificação Khronos WebGL 1.0 se você estiver interessado em entender os detalhes mais técnicos de como a API WebGL deve funcionar em todos os casos.

O valor da pesquisa do usuário

Design Ux

O valor da pesquisa do usuário
A introdução definitiva ao gerenciamento ágil de projetos

A introdução definitiva ao gerenciamento ágil de projetos

Ágil

Publicações Populares
Rede Al-Jazeera America é encerrada hoje
Rede Al-Jazeera America é encerrada hoje
Por que as moedas dos mercados emergentes são voláteis?
Por que as moedas dos mercados emergentes são voláteis?
Um pai compartilha sua jornada pessoal ao lidar com uma filha disléxica
Um pai compartilha sua jornada pessoal ao lidar com uma filha disléxica
Você precisa de um herói: o gerente de projeto
Você precisa de um herói: o gerente de projeto
Folha de dicas de CSS rápida e prática do ApeeScape
Folha de dicas de CSS rápida e prática do ApeeScape
 
Tutorial OpenCV: Detecção de objetos em tempo real usando MSER no iOS
Tutorial OpenCV: Detecção de objetos em tempo real usando MSER no iOS
Discurso de Barack Obama marca contagem regressiva não oficial para americanos negros
Discurso de Barack Obama marca contagem regressiva não oficial para americanos negros
Arquitetura orientada a serviços com AWS Lambda: um tutorial passo a passo
Arquitetura orientada a serviços com AWS Lambda: um tutorial passo a passo
Dez principais regras de design de front-end para desenvolvedores
Dez principais regras de design de front-end para desenvolvedores
UE adia negociações comerciais com a Austrália em meio a negociações de submarinos
UE adia negociações comerciais com a Austrália em meio a negociações de submarinos
Publicações Populares
  • o que são componentes em angular
  • qual é a diferença entre ac e s corp
  • reagir nativo no android studio
  • node.js é usado para criar programas do lado do servidor.
  • realidade mista vs realidade aumentada
Categorias
  • Vida Designer
  • Design De Marca
  • Ciclo De Vida Do Produto
  • Ferramentas E Tutoriais
  • © 2022 | Todos Os Direitos Reservados

    portaldacalheta.pt