Per-Component Operações Matemáticas

Com o HLSL, você pode programar sombreadores em um nível de algoritmo. Para entender a linguagem, você precisará saber como declarar variáveis e funções, usar funções intrínsecas, definir tipos de dados personalizados e usar semântica para conectar argumentos de sombreador a outros sombreadores e ao pipeline.

Depois de aprender a criar sombreadores em HLSL, você precisará aprender sobre chamadas de API para que possa: compilar um sombreador para hardware específico, inicializar constantes de sombreador e inicializar outro estado de pipeline, se necessário.

O tipo de vetor

Um vetor é uma estrutura de dados que contém entre um e quatro componentes.

bool    bVector;   // scalar containing 1 Boolean
bool1   bVector;   // vector containing 1 Boolean
int1    iVector;   // vector containing 1 int
float3  fVector;   // vector containing 3 floats
double4 dVector;   // vector containing 4 doubles

O inteiro imediatamente após o tipo de dados é o número de componentes no vetor.

Os inicializadores também podem ser incluídos nas declarações.

bool    bVector = false;
int1    iVector = 1;
float3  fVector = { 0.2f, 0.3f, 0.4f };
double4 dVector = { 0.2, 0.3, 0.4, 0.5 };

Como alternativa, o tipo de vetor pode ser usado para fazer as mesmas declarações:

vector <bool,   1> bVector = false;
vector <int,    1> iVector = 1;
vector <float,  3> fVector = { 0.2f, 0.3f, 0.4f };
vector <double, 4> dVector = { 0.2, 0.3, 0.4, 0.5 };

O tipo de vetor usa colchetes angulares para especificar o tipo e o número de componentes.

Os vetores contêm até quatro componentes, cada um dos quais pode ser acessado usando um dos dois conjuntos de nomenclatura:

  • O conjunto de posições: x,y,z,w
  • O conjunto de cores: r,g,b,a

Essas instruções retornam o valor no terceiro componente.

// Given
float4 pos = float4(0,0,2,1);

pos.z    // value is 2
pos.b    // value is 2

Os conjuntos de nomenclatura podem usar um ou mais componentes, mas não podem ser misturados.

// Given
float4 pos = float4(0,0,2,1);
float2 temp;

temp = pos.xy  // valid
temp = pos.rg  // valid

temp = pos.xg  // NOT VALID because the position and color sets were used.

A especificação de um ou mais componentes vetoriais ao ler componentes é chamada de swizzling. Por exemplo:

float4 pos = float4(0,0,2,1);
float2 f_2D;
f_2D = pos.xy;   // read two components 
f_2D = pos.xz;   // read components in any order       
f_2D = pos.zx;

f_2D = pos.xx;   // components can be read more than once
f_2D = pos.yy;

O mascaramento controla quantos componentes são gravados.

float4 pos = float4(0,0,2,1);
float4 f_4D;
f_4D    = pos;     // write four components          

f_4D.xz = pos.xz;  // write two components        
f_4D.zx = pos.xz;  // change the write order

f_4D.xzyw = pos.w; // write one component to more than one component
f_4D.wzyx = pos;

As atribuições não podem ser gravadas no mesmo componente mais de uma vez. Portanto, o lado esquerdo desta afirmação é inválido:

f_4D.xx = pos.xy;   // cannot write to the same destination components 

Além disso, os espaços de nome do componente não podem ser misturados. Esta é uma gravação de componente inválida:

f_4D.xg = pos.rgrg;    // invalid write: cannot mix component name spaces 

Acessar um vetor como escalar acessará o primeiro componente do vetor. As duas afirmações a seguir são equivalentes.

f_4D.a = pos * 5.0f;
f_4D.a = pos.r * 5.0f;

O tipo de matriz

Uma matriz é uma estrutura de dados que contém linhas e colunas de dados. Os dados podem ser qualquer um dos tipos de dados escalares, no entanto, cada elemento de uma matriz é o mesmo tipo de dados. O número de linhas e colunas é especificado com a cadeia de caracteres linha por coluna que é acrescentada ao tipo de dados.

int1x1    iMatrix;   // integer matrix with 1 row,  1 column
int2x1    iMatrix;   // integer matrix with 2 rows, 1 column
...
int4x1    iMatrix;   // integer matrix with 4 rows, 1 column
...
int1x4    iMatrix;   // integer matrix with 1 row, 4 columns
double1x1 dMatrix;   // double matrix with 1 row,  1 column
double2x2 dMatrix;   // double matrix with 2 rows, 2 columns
double3x3 dMatrix;   // double matrix with 3 rows, 3 columns
double4x4 dMatrix;   // double matrix with 4 rows, 4 columns

O número máximo de linhas ou colunas é 4; o número mínimo é 1.

Uma matriz pode ser inicializada quando é declarada:

float2x2 fMatrix = { 0.0f, 0.1, // row 1
                     2.1f, 2.2f // row 2
                   };   

Ou, o tipo de matriz pode ser usado para fazer as mesmas declarações:

matrix <float, 2, 2> fMatrix = { 0.0f, 0.1, // row 1
                                 2.1f, 2.2f // row 2
                               };

O tipo de matriz usa os colchetes angulares para especificar o tipo, o número de linhas e o número de colunas. Este exemplo cria uma matriz de ponto flutuante, com duas linhas e duas colunas. Qualquer um dos tipos de dados escalares pode ser usado.

Esta declaração define uma matriz de valores flutuantes (números de vírgula flutuante de 32 bits) com duas linhas e três colunas:

matrix <float, 2, 3> fFloatMatrix;

Uma matriz contém valores organizados em linhas e colunas, que podem ser acessados usando o operador de estrutura "." seguido por um dos dois conjuntos de nomenclatura:

  • A posição da coluna de linha baseada em zero:
    • _m00, _m01, _m02 _m03
    • _m10, _m11, _m12 _m13
    • _m20, _m21, _m22 _m23
    • _m30, _m31, _m32 _m33
  • A posição de coluna de linha com base em um:
    • _11, _12, _13, _14
    • _21, _22, _23, _24
    • _31, _32, _33, _34
    • _41, _42, _43, _44

Cada conjunto de nomenclatura começa com um sublinhado seguido do número da linha e do número da coluna. A convenção baseada em zero também inclui a letra "m" antes do número da linha e da coluna. Aqui está um exemplo que usa os dois conjuntos de nomenclatura para acessar uma matriz:

// given
float2x2 fMatrix = { 1.0f, 1.1f, // row 1
                     2.0f, 2.1f  // row 2
                   }; 

float f_1D;
f_1D = matrix._m00; // read the value in row 1, column 1: 1.0
f_1D = matrix._m11; // read the value in row 2, column 2: 2.1

f_1D = matrix._11;  // read the value in row 1, column 1: 1.0
f_1D = matrix._22;  // read the value in row 2, column 2: 2.1

Assim como os vetores, os conjuntos de nomenclatura podem usar um ou mais componentes de qualquer conjunto de nomes.

// Given
float2x2 fMatrix = { 1.0f, 1.1f, // row 1
                     2.0f, 2.1f  // row 2
                   };
float2 temp;

temp = fMatrix._m00_m11 // valid
temp = fMatrix._m11_m00 // valid
temp = fMatrix._11_22   // valid
temp = fMatrix._22_11   // valid

Uma matriz também pode ser acessada usando a notação de acesso à matriz, que é um conjunto de índices baseado em zero. Cada índice está entre parênteses retos. Uma matriz 4x4 é acessada com os seguintes índices:

  • [0][0], [0][1], [0][2], [0][3]
  • [1][0], [1][1], [1][2], [1][3]
  • [2][0], [2][1], [2][2], [2][3]
  • [3][0], [3][1], [3][2], [3][3]

Aqui está um exemplo de acesso a uma matriz:

float2x2 fMatrix = { 1.0f, 1.1f, // row 1
                     2.0f, 2.1f  // row 2
                   };
float temp;

temp = fMatrix[0][0] // single component read
temp = fMatrix[0][1] // single component read

Observe que o operador de estrutura "." não é usado para acessar uma matriz. A notação de acesso à matriz não pode usar swizzling para ler mais de um componente.

float2 temp;
temp = fMatrix[0][0]_[0][1] // invalid, cannot read two components

No entanto, o acesso à matriz pode ler um vetor de vários componentes.

float2 temp;
float2x2 fMatrix;
temp = fMatrix[0] // read the first row

Tal como acontece com os vetores, a leitura de mais de um componente da matriz é chamada de swizzling. Mais de um componente pode ser atribuído, supondo que apenas um espaço de nome seja usado. Estas são todas as atribuições válidas:

// Given these variables
float4x4 worldMatrix = float4( {0,0,0,0}, {1,1,1,1}, {2,2,2,2}, {3,3,3,3} );
float4x4 tempMatrix;

tempMatrix._m00_m11 = worldMatrix._m00_m11; // multiple components
tempMatrix._m00_m11 = worldMatrix.m13_m23;

tempMatrix._11_22_33 = worldMatrix._11_22_33; // any order on swizzles
tempMatrix._11_22_33 = worldMatrix._24_23_22;

O mascaramento controla quantos componentes são gravados.

// Given
float4x4 worldMatrix = float4( {0,0,0,0}, {1,1,1,1}, {2,2,2,2}, {3,3,3,3} );
float4x4 tempMatrix;

tempMatrix._m00_m11 = worldMatrix._m00_m11; // write two components
tempMatrix._m23_m00 = worldMatrix._m00_m11;

As atribuições não podem ser gravadas no mesmo componente mais de uma vez. Portanto, o lado esquerdo desta afirmação é inválido:

// cannot write to the same component more than once
tempMatrix._m00_m00 = worldMatrix._m00_m11;

Além disso, os espaços de nome do componente não podem ser misturados. Esta é uma gravação de componente inválida:

// Invalid use of same component on left side
tempMatrix._11_m23 = worldMatrix._11_22; 

Ordenação matricial

A ordem de empacotamento da matriz para parâmetros uniformes é definida como coluna-maior por padrão. Isto significa que cada coluna da matriz é armazenada num único registo constante. Por outro lado, uma matriz de linha maior agrupa cada linha da matriz em um único registro constante. A embalagem matricial pode ser alterada com a diretiva #pragmapack_matrix ou com a palavra-chave row_major ou column_major.

Os dados em uma matriz são carregados em registros constantes de sombreador antes de um sombreador ser executado. Há duas opções para como os dados da matriz são lidos: em ordem de linha maior ou em ordem de coluna maior. Ordem de coluna maior significa que cada coluna da matriz será armazenada em um único registro constante, e ordem de linha maior significa que cada linha da matriz será armazenada em um único registro constante. Esta é uma consideração importante para quantos registros constantes são usados para uma matriz.

Uma matriz de linha maior é apresentada da seguinte forma:

11
21
31
41

12
22
32
42

13
23
33
43

14
24
34
44

 

Uma matriz de coluna maior é apresentada da seguinte forma:

11
12
13
14

21
22
23
24

31
32
33
34

41
42
43
44

 

A ordenação da matriz de linha maior e de coluna maior determina a ordem em que os componentes da matriz são lidos a partir das entradas de sombreador. Uma vez que os dados são gravados em registros constantes, a ordem da matriz não tem efeito sobre como os dados são usados ou acessados a partir do código de sombreador. Além disso, as matrizes declaradas em um corpo de sombreador não são agrupadas em registros constantes. A ordem de empacotamento de linha maior e coluna principal não tem influência sobre a ordem de empacotamento dos construtores (que sempre segue a ordem de linha principal).

A ordem dos dados em uma matriz pode ser declarada em tempo de compilação ou o compilador ordenará os dados em tempo de execução para o uso mais eficiente.

Exemplos

O HLSL usa dois tipos especiais, um tipo vetorial e um tipo de matriz para facilitar a programação de gráficos 2D e 3D. Cada um desses tipos contém mais de um componente; Um vetor contém até quatro componentes e uma matriz contém até 16 componentes. Quando vetores e matrizes são usados em equações HLSL padrão, a matemática realizada é projetada para trabalhar por componente. Por exemplo, o HLSL implementa esta multiplicação:

float4 v = a*b;

como uma multiplicação de quatro componentes. O resultado são quatro escalares:

float4 v = a*b;

v.x = a.x*b.x;
v.y = a.y*b.y;
v.z = a.z*b.z;
v.w = a.w*b.w;

São quatro multiplicações em que cada resultado é armazenado em um componente separado de v. Isso é chamado de multiplicação de quatro componentes. O HLSL usa matemática de componentes, o que torna os sombreadores de escrita muito eficientes.

Isso é muito diferente de uma multiplicação que normalmente é implementada como um produto ponto que gera um único escalar:

v = a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w;

Uma matriz também usa operações por componente em HLSL:

float3x3 mat1,mat2;
...
float3x3 mat3 = mat1*mat2;

O resultado é uma multiplicação por componente das duas matrizes (em oposição a uma multiplicação de matriz padrão 3x3). Uma multiplicação por matriz por componente produz este primeiro termo:

mat3.m00 = mat1.m00 * mat2._m00;

Isto é diferente de uma multiplicação matricial 3x3 que produziria este primeiro termo:

// First component of a four-component matrix multiply
mat.m00 = mat1._m00 * mat2._m00 + 
          mat1._m01 * mat2._m10 + 
          mat1._m02 * mat2._m20 + 
          mat1._m03 * mat2._m30;

Versões sobrecarregadas da função intrínseca multiplicar manipulam casos em que um operando é um vetor e o outro operando é uma matriz. Tais como: vetor * vetor, vetor * matriz, matriz * vetor e matriz * matriz. Por exemplo:

float4x3 World;

float4 main(float4 pos : SV_POSITION) : SV_POSITION
{
    float4 val;
    val.xyz = mul(pos,World);
    val.w = 0;

    return val;
}   

produz o mesmo resultado que:

float4x3 World;

float4 main(float4 pos : SV_POSITION) : SV_POSITION
{
    float4 val;
    val.xyz = (float3) mul((float1x4)pos,World);
    val.w = 0;

    return val;
}   

Este exemplo converte o vetor pos em um vetor coluna usando o cast (float1x4). Alterar um vetor por casting ou trocar a ordem dos argumentos fornecidos para multiplicar é equivalente a transpor a matriz.

A conversão automática de transmissão faz com que as funções intrínsecas de multiplicação e ponto retornem os mesmos resultados usados aqui:

{
  float4 val;
  return mul(val,val);
}

Este resultado da multiplicação é um vetor 1x4 * 4x1 = 1x1. Isto é equivalente a um produto ponto:

{
  float4 val;
  return dot(val,val);
}

que retorna um único valor escalar.

tipos de dados (DirectX HLSL)