Usando transformadores 3D¶
Introdução¶
Se você nunca fez jogos 3D antes, trabalhar com rotações em três dimensões pode ser confuso no início. Vindo de 2D, a coisa natural é ter um raciocínio tipo "Ah, é como girar em 2D, exceto que agora as rotações acontecem em X, Y e Z".
A princípio, isso parece fácil. Para jogos simples, essa forma de pensar pode até ser suficiente. Infelizmente, muitas vezes é incorreto.
Ângulos em três dimensões são mais comumente referidos como "Ângulos de Euler".
Os ângulos de Euler foram introduzidos pelo matemático Leonhard Euler no início do século XVIII.
Essa forma de representar as rotações 3D foi inovadora na época, mas apresenta várias deficiências quando usada no desenvolvimento de jogos (o que é de se esperar de um cara com um chapéu engraçado). A ideia deste documento é explicar o porquê, bem como descrever as melhores práticas para lidar com transformações ao programar jogos 3D.
Problemas dos ângulos de Euler¶
Embora possa parecer intuitivo que cada eixo tenha uma rotação, a verdade é que não é prático.
Ordem dos eixos¶
A principal razão para isso é que não existe uma maneira única de construir uma orientação a partir dos ângulos. Não existe uma função matemática padrão que junte todos os ângulos e produza uma rotação 3D real. A única maneira de produzir uma orientação a partir de ângulos é girar o objeto ângulo por ângulo, em uma ordem arbitrária.
Isso pode ser feito pela primeira vez girando em X, então Y e depois em Z. Alternativamente, você pode primeiro girar em Y, então em Z e finalmente em X. Tudo funciona, mas dependendo da ordem, a orientação final do objeto não será necessariamente a mesma . De fato, isso significa que existem várias maneiras de construir uma orientação de 3 ângulos diferentes, dependendo da *ordem das rotações.
A seguir está uma visualização de eixos de rotação (em X, Y, Z) em um "gimbal" (da Wikipedia). Como você pode ver, a orientação de cada eixo depende da rotação do anterior:
Você pode estar se perguntando como isso te afeta. Vejamos um exemplo prático:
Imagine que você está trabalhando em um controle de primeira pessoa (por exemplo, num jogo FPS). Movendo o mouse para a esquerda e para a direita controla seu ângulo de visão paralelo ao chão, enquanto move-o para cima e para baixo move a vista do jogador para cima e para baixo.
Neste caso, para alcançar o efeito desejado, a rotação deve ser aplicada primeiro no eixo Y ("para cima" neste caso, uma vez que Godot usa uma orientação "Y-Up"), seguida pela rotação no eixo X.
Se tivéssemos que aplicar rotação no eixo X primeiro, e então no Y, o efeito não seria o desejado:
Dependendo do tipo de jogo ou efeito desejado, a ordem em que você deseja que as rotações dos eixos sejam aplicadas pode ser diferente. Portanto, aplicar rotações em X, Y e Z não é suficiente: você também precisa de uma ordem de rotação.
Interpolação¶
Outro problema com o uso de ângulos de Euler é a interpolação. Imagine que você deseja fazer a transição entre duas posições diferentes de câmera ou inimigo (incluindo rotações). Uma maneira lógica de abordar isso é interpolar os ângulos de uma posição para a outra. Seria de esperar que ficasse assim:
Mas isso nem sempre tem o efeito esperado ao usar ângulos:
A câmera realmente girou na direção oposta!
Há algumas razões para que isto tenha acontecido:
As rotações não mapeiam linearmente a orientação, portanto interpolá-las nem sempre resulta no caminho mais curto (ou seja, ir de
270
a0
graus não é o mesmo que ir de270
a360
, mesmo que os ângulos sejam equivalentes).O gimbal lock está em jogo (o primeiro e o último eixo girado se alinham, portanto, um grau de liberdade é perdido). Veja a `página da Wikipedia sobre Gimbal Lock <https://pt.wikipedia.org/wiki/Gimbal_lock> `_ para uma explicação detalhada deste problema.
Diga não aos ângulos de Euler¶
O resultado de tudo isso é que você deve não utilizar o rotation
propriedade de nós Spatial no Godot para jogos. Ele existe para ser utilizado principalmente no editor, para coerência com a engine 2D, e para rotações simples (geralmente apenas um eixo, ou mesmo dois em casos limitados). Por mais que você se sinta tentado, não o utilize.
Em vez disso, há uma maneira melhor de resolver seus problemas de rotação.
Introduzindo transformadores¶
Godot utiliza o tipo de dados classe_Transform para orientações. Cada Spatial node contém uma propriedade transform
que é relativa à transformação do pai, se o pai for derivado do tipo Spatial.
Também é possível acessar as coordenadas de transformação do mundo através da propriedade global_transform
.
Um transform tem um Basis (transform.basis sub-property), que consiste em três vetores Vector3. Estes são acessados através da propriedade transform.basis
e podem ser acessados diretamente por transform.basis.x
, transform.basis.y`, e`transform.basis.z`
. Cada ponto vetorial na direção que seu eixo foi girado, então eles efetivamente descrevem a rotação total do nó. A escala (embora seja uniforme) também pode ser inferida a partir do comprimento dos eixos. Uma basis também pode ser interpretada como uma matriz 3x3 e usada como transform.basis[x][y]`
.
Uma base padrão (não modificada) é semelhante a:
var basis = Basis()
# Contains the following default values:
basis.x = Vector3(1, 0, 0) # Vector pointing along the X axis
basis.y = Vector3(0, 1, 0) # Vector pointing along the Y axis
basis.z = Vector3(0, 0, 1) # Vector pointing along the Z axis
// Due to technical limitations on structs in C# the default
// constructor will contain zero values for all fields.
var defaultBasis = new Basis();
GD.Print(defaultBasis); // prints: ((0, 0, 0), (0, 0, 0), (0, 0, 0))
// Instead we can use the Identity property.
var identityBasis = Basis.Identity;
GD.Print(identityBasis.x); // prints: (1, 0, 0)
GD.Print(identityBasis.y); // prints: (0, 1, 0)
GD.Print(identityBasis.z); // prints: (0, 0, 1)
// The Identity basis is equivalent to:
var basis = new Basis(Vector3.Right, Vector3.Up, Vector3.Back);
GD.Print(basis); // prints: ((1, 0, 0), (0, 1, 0), (0, 0, 1))
Isto também é similar a uma matriz de identidade 3x3.
Seguindo a convenção OpenGL, X
é o eixo Right, Y
é o eixo Up e Z
é o eixo Forward.
Juntamente com a base, uma transformação também tem uma origem. Este é um Vector3 especificando quão longe da origem real (0, 0, 0)
esta transformação é. Combinando a base com a origem, um transform representa eficientemente uma única translação, rotação e escala no espaço.
Uma maneira de visualizar uma transformação é olhar o gizmo 3D de um objeto enquanto está no modo "espaço local".
As setas do gizmo mostram os eixos X
, Y
, e Z
(em vermelho, verde, e azul respectivamente) da base, enquanto o centro do gizmo está na origem do objeto.
Para mais informações sobre a matemática de vetores e transformações, leia os tutoriais Matemática vetorial.
Manipulação de transformadores¶
É claro que as transformações não são tão simples de manipular quanto os ângulos e têm problemas próprios.
É possível girar uma transformação, seja multiplicando sua base por outra (isso é chamado de acumulação), ou usando os métodos de rotação.
var axis = Vector3(1, 0, 0) # Or Vector3.RIGHT
var rotation_amount = 0.1
# Rotate the transform around the X axis by 0.1 radians.
transform.basis = Basis(axis, rotation_amount) * transform.basis
# shortened
transform.basis = transform.basis.rotated(axis, rotation_amount)
Vector3 axis = new Vector3(1, 0, 0); // Or Vector3.Right
float rotationAmount = 0.1f;
// Rotate the transform around the X axis by 0.1 radians.
transform.basis = new Basis(axis, rotationAmount) * transform.basis;
// shortened
transform.basis = transform.basis.Rotated(axis, rotationAmount);
Um método em Spatial simplifica isso:
# Rotate the transform around the X axis by 0.1 radians.
rotate(Vector3(1, 0, 0), 0.1)
# shortened
rotate_x(0.1)
// Rotate the transform around the X axis by 0.1 radians.
Rotate(new Vector3(1, 0, 0), 0.1f);
// shortened
RotateX(0.1f);
Isso gira o nó em relação ao nó pai.
Para girar em relação ao espaço do objeto (a própria transformação do nó), use o seguinte:
# Rotate around the object's local X axis by 0.1 radians.
rotate_object_local(Vector3(1, 0, 0), 0.1)
// Rotate around the object's local X axis by 0.1 radians.
RotateObjectLocal(new Vector3(1, 0, 0), 0.1f);
Erros de precisão¶
Fazer operações sucessivas de transformação resultará em uma perda de precisão devido a erro de ponto flutuante. Isto significa que a escala de cada eixo pode não ser mais exatamente 1.0
, e podem não ser exatamente 90
graus um do outro.
Se uma transformação for girada a cada quadro, ela eventualmente começará a se deformar com o tempo. Isto é inevitável.
Existem duas maneiras diferentes de lidar com isso. A primeira é ortonormalizar a transformação depois de algum tempo (talvez uma vez por quadro, se você modificá-la a cada quadro):
transform = transform.orthonormalized()
transform = transform.Orthonormalized();
Isto fará com que todos os eixos tenham 1.0
de comprimento de novo e estejam 90
graus um do outro. Entretanto, qualquer escala aplicada à transformação será perdida.
Recomenda-se não escalar os nós que vão ser manipulados; em vez disso, escalar os nós de seus filhos (como o MeshInstance). Se for absolutamente necessário escalar o nó, então reaplique-o no final:
transform = transform.orthonormalized()
transform = transform.scaled(scale)
transform = transform.Orthonormalized();
transform = transform.Scaled(scale);
Obtendo informação¶
Você pode estar pensando neste ponto: "Tá bom, mas como obter ângulos de uma transformação?". A resposta novamente é: você não tem. Você deve fazer o seu melhor para parar de pensar em ângulos.
Imagine que você precisa atirar uma bala na direção que seu jogador está virado. Basta utilizar o eixo dianteiro (geralmente Z
ou -Z
).
bullet.transform = transform
bullet.speed = transform.basis.z * BULLET_SPEED
bullet.Transform = transform;
bullet.LinearVelocity = transform.basis.z * BulletSpeed;
O inimigo está olhando para o jogador? Utilize o produto escalar para isso (veja o tutorial Matemática vetorial para uma explicação do produto escalar):
# Get the direction vector from player to enemy
var direction = enemy.transform.origin - player.transform.origin
if direction.dot(enemy.transform.basis.z) > 0:
enemy.im_watching_you(player)
// Get the direction vector from player to enemy
Vector3 direction = enemy.Transform.origin - player.Transform.origin;
if (direction.Dot(enemy.Transform.basis.z) > 0)
{
enemy.ImWatchingYou(player);
}
Desviar à esquerda:
# Remember that +X is right
if Input.is_action_pressed("strafe_left"):
translate_object_local(-transform.basis.x)
// Remember that +X is right
if (Input.IsActionPressed("strafe_left"))
{
TranslateObjectLocal(-Transform.basis.x);
}
Pulo:
# Keep in mind Y is up-axis
if Input.is_action_just_pressed("jump"):
velocity.y = JUMP_SPEED
velocity = move_and_slide(velocity)
// Keep in mind Y is up-axis
if (Input.IsActionJustPressed("jump"))
velocity.y = JumpSpeed;
velocity = MoveAndSlide(velocity);
Todos os comportamentos e lógicas comuns podem ser feitos apenas com vetores.
Definindo informações¶
Há, é claro, casos em que você quer definir informações para uma transformação. Imagine um controle em primeira pessoa ou uma câmera orbitando. Estes são definitivamente feitos usando ângulos, porque você quer que as transformações aconteçam em uma ordem específica.
Para estes casos, mantenha os ângulos e rotações fora do transform e coloque-os em cada quadro. Não tente recuperá-los e reutilizá-los porque o transform não deve ser usada desta maneira.
Exemplo de olhar ao redor, estilo FPS:
# accumulators
var rot_x = 0
var rot_y = 0
func _input(event):
if event is InputEventMouseMotion and event.button_mask & 1:
# modify accumulated mouse rotation
rot_x += event.relative.x * LOOKAROUND_SPEED
rot_y += event.relative.y * LOOKAROUND_SPEED
transform.basis = Basis() # reset rotation
rotate_object_local(Vector3(0, 1, 0), rot_x) # first rotate in Y
rotate_object_local(Vector3(1, 0, 0), rot_y) # then rotate in X
// accumulators
private float _rotationX = 0f;
private float _rotationY = 0f;
public override void _Input(InputEvent @event)
{
if (@event is InputEventMouseMotion mouseMotion)
{
// modify accumulated mouse rotation
_rotationX += mouseMotion.Relative.x * LookAroundSpeed;
_rotationY += mouseMotion.Relative.y * LookAroundSpeed;
// reset rotation
Transform transform = Transform;
transform.basis = Basis.Identity;
Transform = transform;
RotateObjectLocal(Vector3.Up, _rotationX); // first rotate about Y
RotateObjectLocal(Vector3.Right, _rotationY); // then rotate about X
}
}
Como você pode ver, em tais casos é ainda mais simples manter a rotação do lado de fora, então use o transform como a orientação final.
Interpolando com quaternions¶
A interpolação entre duas transformações pode ser feita eficientemente com quaternions. Mais informações sobre como funcionam os quaternions podem ser encontradas em outros lugares ao redor da Internet. Para uso prático, é suficiente entender que praticamente seu principal uso é fazer uma interpolação do caminho mais próximo. Assim como em duas rotações, um quaternion permitirá a interpolação entre eles usando o eixo mais próximo.
Converter uma rotação para quaternion é simples.
# Convert basis to quaternion, keep in mind scale is lost
var a = Quat(transform.basis)
var b = Quat(transform2.basis)
# Interpolate using spherical-linear interpolation (SLERP).
var c = a.slerp(b,0.5) # find halfway point between a and b
# Apply back
transform.basis = Basis(c)
// Convert basis to quaternion, keep in mind scale is lost
var a = transform.basis.Quat();
var b = transform2.basis.Quat();
// Interpolate using spherical-linear interpolation (SLERP).
var c = a.Slerp(b, 0.5f); // find halfway point between a and b
// Apply back
transform.basis = new Basis(c);
A referência do tipo classe_Quat tem mais informações sobre o tipo de dados (também pode fazer acumulação de transformação, pontos de transformação, etc., embora isto seja utilizado com menos freqüência). Se você interpolar ou aplicar operações a quaternions muitas vezes, tenha em mente que elas precisam ser eventualmente normalizadas. Caso contrário, eles também sofrerão de erros de precisão numérica.
Quaternions são úteis ao fazer interpolações de câmera/percurso/etc., pois o resultado será sempre correto e suave.
Transforms são seus amigos¶
Para a maioria dos iniciantes, acostumar-se a trabalhar com transforms pode levar algum tempo. No entanto, uma vez acostumado a eles, você vai apreciar sua simplicidade e poder.
Não hesite em pedir ajuda sobre este tópico em qualquer uma das comunidades online da Godot ` <https://godotengine.org/community>`_ e, assim que você se tornar confiante o suficiente, por favor, ajude outros!