Splines em gráficos 3D, a opção mais automatizada

Há cerca de um mês, comecei a aprender Python com o livro Dawson e acordei profundamente no processo de escrever meu jogo no pygame. TK era tal que parecia mais promissor fazer um jogo com gráficos pseudo-tridimensionais, colocando as superfícies salvas dos splines 3D em sprites. Vou escrever sobre o último.

Portanto, existem polígonos (é mais fácil trabalhar com quadrângulos) nos quais queremos esticar superfícies cúbicas para que elas se encaixem perfeitamente - essas superfícies são estrias.



É mais conveniente apresentar um spline para um polígono como uma função

g(u,v)=v cdotg1(u)+(1v) cdotg2(u)+u cdotg3(v)+(1u) cdotg4(v)(1)


Aqui

g1,g2,g3,g4


- polinômios cúbicos que satisfazem algumas condições de contorno (na figura abaixo - curvas verde claro e vermelho e condições derivadas-iniciais - vetores lilás e azul); uev variam de 0 a 1.



Nesta interpretação, alguns graus são perdidos (produtos de 2 e 3 graus de parâmetros), mas os coeficientes polinomiais podem ser encontrados especificando apenas 12 vetores de condições iniciais (4 pontos poligonais e vetores derivativos em relação a u e v para cada ponto). Nas junções, splines coincidem se condições iniciais semelhantes forem definidas para polígonos vizinhos (os vetores derivativos devem ser colineares, a superfície passa pelos mesmos pontos do polígono).

Um problema - a derivada com essa afirmação do problema em toda a borda pode não coincidir - haverá pequenos artefatos nas junções. Você pode pensar em mais 4 condições para corrigir isso e adicionar cuidadosamente outro termo à fórmula

u cdotv cdot(u1) cdot(v1) cdotg5(u,v)


que não estraga as fronteiras, mas esse é um tópico para um artigo separado.

Uma alternativa é a superfície de Bezier , mas propõe definir 16 parâmetros de significado matemático incompreensível (para mim), ou seja, supõe-se que o artista trabalhe com suas próprias mãos. Isso não é muito adequado para mim, então uma bicicleta com muletas segue.

Os coeficientes (1) são mais facilmente calculados encontrando a matriz inversa à matriz de valores e derivadas nos cantos e multiplicando pelas condições de entrada (três vezes, para cada coordenada). A matriz pode ser esta:

Texto oculto
[[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], #g (0,0)
[1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0], #g (1,0)
[1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], #g (0,1)
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # g (1,1)
[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], # dg / du (0,0)
[0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 3, 0], # dg / du (1,0)
[0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], # dg / du (0,1)
[0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3], # dg / du (1,1)
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], # dg / dv (0,0)
[0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1], # dg / dv (1,0)
[0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0], # dg / dv (0,1)
[0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 0, 1]] # dg / dv (1,1)

O NumPy faz um excelente trabalho nisso.

Uma questão permanece - onde obter os vetores dos derivativos. Supõe-se que eles devam ser selecionados de alguma forma com base na posição dos pontos vizinhos (para o polígono) e por motivos de suavidade.
Para mim, pessoalmente, ainda era completamente contra-intuitivo como lidar com o comprimento do vetor derivado. A direção é compreensível, mas o comprimento?

Como resultado, nasceu o seguinte algoritmo:

1. Na primeira etapa, ocorre uma classificação de pontos. No gráfico (que pontos dos polígonos e suas conexões especificam), os ciclos de comprimento 4 são pesquisados ​​e armazenados e, além disso, são registrados os vizinhos mais adequados para a função de estender as arestas do polígono (é determinado antecipadamente quais arestas correspondem a uma alteração no parâmetro u e qual corresponde a v). Aqui está um pedaço de código que pesquisa, classifica e lembra os vizinhos pelo 0º ponto de um ciclo:

Texto oculto
"""   : cykle[5] cykle[7] cykle[4]--cykle[0] --u-- cykle[1]-cykle[6] |v |v cykle[10]-cykle[3] --u-- cykle[2]-cykle[8] cykle[11] cykle[9] """ sosed = [] for i in range(0, N): if self.connects[i][ind] == 1 and i != cykle[0] and i != cykle[1] and i != cykle[3]: sosed += [i] #    ... if(len(sosed) < 2): continue #    else: #      cykle[0] vs0c3 = MinusVecs(self.dots[sosed[0]], self.dots[cykle[3]]) vs1c1 = MinusVecs(self.dots[sosed[1]], self.dots[cykle[1]]) #        vs0c1 = MinusVecs(self.dots[sosed[0]], self.dots[cykle[1]]) vs1c3 = MinusVecs(self.dots[sosed[1]], self.dots[cykle[3]]) Rvsc = [ScalMult(vs0c3,vs0c3),ScalMult(vs1c1,vs1c1),ScalMult(vs0c1,vs0c1),ScalMult(vs1c3,vs1c3) ] n1 = Rvsc.index(max(Rvsc)) if n1 < 2: cykle += [sosed[1],sosed[0]] #  u   v else: cykle += [sosed[0],sosed[1]] 


Além disso, ao construir o spline, a derivada ao longo da aresta (por exemplo, com relação ao parâmetro u) no ponto do polígono é selecionada com base na localização de dois pontos da aresta e um ponto adjacente (sejam os pontos vec1, vec2 e vec3; o ponto no qual a derivada é procurada é o segundo).



Inicialmente, tentei usar o vetor normalizado vec3 - vec1 (como apliquei o teorema de Lagrange) para esse papel, mas surgiram problemas precisamente com o comprimento do vetor derivado - tornar uma constante uma má ideia.

Digressão lírica:

Para uma pequena ilustração do que o vetor derivado é na versão paramétrica, passamos a uma analogia bidimensional simples - aqui está uma parte de um arco circular:



g(u)=(Rsin(u pi/2),Rcos(u pi/2))


g(u)=(R pi/2 cdotcos(u pi/2),R pi/2 cdotsin(u pi/2))


I.e. módulo derivativo = R * pi / 2 e, de um modo geral, depende do tamanho da peça do arco, que definimos através do parâmetro

O que fazer com isso agora? Leo Tolstoi nos disse que tudo é redondo = bom; portanto, se definirmos a derivada em um ponto como se desejássemos construir um arco lá, obteremos uma curva suave e bonita.

O fim da digressão.

A segunda etapa da pesquisa derivada:

2. Através de nossos três pontos vec1, vec2, vec3, construímos um plano; nesse plano, procuramos um círculo que passa por todos os três pontos. A derivada desejada será direcionada ao longo da tangente ao círculo no ponto vec2, e o módulo do vetor da derivada deve ser igual ao produto do raio do círculo e do ângulo do setor, que formam dois pontos da face do polígono (semelhante à nossa analogia simples e plana a partir da digressão lírica).

g=R cdot phi


Tudo parece simples, eis o código da função, por exemplo (o NumPy é usado novamente):

não é um código, mas ...
 def create_dd(vec1, vec2, tuda, vec3): """     1-2       3   == 1""" #0.         -         :( vecmult = VecMult(MinusVecs(vec2,vec1),MinusVecs(vec3,vec1)) #  vec2-vec1 x vec3 - vec1 if vecmult[0]*vecmult[0] + vecmult[1]*vecmult[1] + vecmult[2]*vecmult[2] < 0.000001: #  0 if tuda == 1: return NormVec(MinusVecs(vec3,vec2)) else: return NormVec(MinusVecs(vec1,vec2)) #1.   ,     #           2   M = np.zeros((3,3),float) C = np.zeros((3,1),float) M[0][0] = 2*vec2[0] - 2*vec1[0] M[0][1] = 2*vec2[1] - 2*vec1[1] M[0][2] = 2*vec2[2] - 2*vec1[2] C[0] = -vec1[0]*vec1[0] - vec1[1]*vec1[1] - vec1[2]*vec1[2] + vec2[0]*vec2[0] + vec2[1]*vec2[1] + vec2[2]*vec2[2] M[1][0] = 2*vec3[0] - 2*vec2[0] M[1][1] = 2*vec3[1] - 2*vec2[1] M[1][2] = 2*vec3[2] - 2*vec2[2] C[1] = -vec2[0]*vec2[0] - vec2[1]*vec2[1] - vec2[2]*vec2[2] + vec3[0]*vec3[0] + vec3[1]*vec3[1] + vec3[2]*vec3[2] #   ,   4     M[2][0] = vecmult[0] M[2][1] = vecmult[1] M[2][2] = vecmult[2] C[2] = vecmult[0]*vec1[0] + vecmult[1]*vec1[1] + vecmult[2]*vec1[2] # M *    = C Mobr = np.linalg.inv(M) Rvec = np.dot(Mobr,C) res = NormVec(VecMult(MinusVecs(Rvec,vec2), vecmult)) #    ka #R = math.sqrt((Rvec[0]-vec1[0])**2 + (Rvec[1]-vec1[1])**2 + (Rvec[2]-vec1[2])**2) #   = 3.14*2*asin(l/(2*R))/180 * R,      l  ,    l = 1.2*math.sqrt((vec1[0]-vec2[0])**2 + (vec1[1]-vec2[1])**2 + (vec1[2]-vec2[2])**2) if ScalMult(res, MinusVecs(vec2, vec1)) > 0: #     return MultVxC(res, tuda*l) else: return MultVxC(res, -tuda*l) 


Bem, em geral, tudo funciona de alguma forma. Para demonstração, peguei um cubo 5x5x5 e alterei a posição dos pontos com um randomizador:

GIF, 8Mb

Source: https://habr.com/ru/post/pt473790/


All Articles