Paisaje planetario. Problemas ambientales del paisaje natural.

AlexWIN32 20 de agosto de 2017 a las 13:36

Paisaje planetario

  • API,
  • C++
  • Desarrollo de juegos
  • Tutorial

Es difícil argumentar que el paisaje es una parte integral de la mayoría de los juegos de computadora al aire libre. El método tradicional para implementar cambios en el relieve de la superficie que rodea al jugador es el siguiente: tomamos una malla, que es un plano, y para cada primitiva en esta malla hacemos un desplazamiento a lo largo de la normal a este plano en un valor específico. a este primitivo. En términos simples, tenemos una textura de un solo canal de 256 por 256 píxeles y una malla plana. Para cada primitiva, tomamos el valor de la textura según sus coordenadas en el plano. Ahora simplemente desplazamos las coordenadas de la primitiva a lo largo de la normal al plano según el valor resultante (Fig.1)

Fig.1 mapa de altura + plano = paisaje

1. Sector

Obviamente, no es aconsejable construir un paisaje para toda la esfera a la vez; la mayor parte no será visible. Por lo tanto, necesitamos crear un área mínima de espacio, una cierta primitiva que formará el relieve de la parte visible de la esfera. Lo llamaré sector. ¿Cómo podemos conseguirlo? Así que mire la Figura 2a. La célula verde es nuestro sector. A continuación, construiremos seis cuadrículas, cada una de las cuales es una cara de un cubo (Fig. 2b). Ahora normalicemos las coordenadas de las primitivas que forman las mallas (Fig. 2c).


Fig.2

Como resultado, obtuvimos un cubo proyectado sobre una esfera, donde el sector es el área de una de sus caras. ¿Por qué funciona esto? Considere un punto arbitrario en la cuadrícula como un vector desde el origen. ¿Qué es la normalización vectorial? Esta es la transformación de un vector dado en un vector en la misma dirección, pero con una longitud unitaria. El proceso es el siguiente: primero encontramos la longitud del vector en la métrica euclidiana según el teorema de Pitágoras

Luego divide cada uno de los componentes del vector por este valor.

Ahora preguntémonos ¿qué es una esfera? Una esfera es un conjunto de puntos equidistantes de un punto dado. La ecuación paramétrica de una esfera se ve así

Donde x0, y0, z0 son las coordenadas del centro de la esfera y R es su radio. En nuestro caso, el centro de la esfera es el origen y el radio es igual a uno. sustituyamos valores conocidos y sacar la raíz de dos lados de la ecuación. Resulta lo siguiente

Literalmente la última transformación nos dice lo siguiente: “Para pertenecer a la esfera, la longitud del vector debe ser igual a uno”. Esto es lo que logramos mediante la normalización.

¿Qué pasa si la esfera tiene un centro y un radio arbitrarios? Puedes encontrar el punto que le pertenece usando la siguiente ecuación

Donde pS es un punto de la esfera, C es el centro de la esfera, pNorm es el vector previamente normalizado y R es el radio de la esfera. En términos simples, lo que sucede aquí es "nos movemos desde el centro de la esfera hacia un punto en la cuadrícula a una distancia R". Como cada vector tiene una unidad de longitud, al final todos los puntos equidistan del centro de la esfera por la distancia de su radio, lo que hace que la ecuación de la esfera sea verdadera.

2. Gestión

Necesitamos conseguir un grupo de sectores que sean potencialmente visibles desde el punto de vista. ¿Pero cómo hacer esto? Supongamos que tenemos una esfera con centro en algún punto. También tenemos un sector que se ubica sobre la esfera y un punto P ubicado en el espacio cerca de la esfera. Ahora construyamos dos vectores, uno dirigido desde el centro de la esfera al centro del sector, el otro, desde el centro de la esfera al punto de observación. Mire la Fig. 3: el sector puede ser visible solo si el valor absoluto del ángulo entre estos vectores es inferior a 90 grados.


Fig.3 a - ángulo inferior a 90 - el sector es potencialmente visible. b - ángulo mayor que 90 - sector no visible

¿Cómo conseguir este ángulo? Para hacer esto, necesitas usar el producto escalar de vectores. Para el caso tridimensional se calcula de la siguiente manera:

El producto escalar tiene la propiedad distributiva:

Anteriormente definimos la ecuación para la longitud de un vector; ahora podemos decir que la longitud de un vector es igual a la raíz de producto escalar de este vector sobre sí mismo. O viceversa: el producto escalar de un vector consigo mismo es igual al cuadrado de su longitud.

Ahora veamos la ley de los cosenos. Una de sus dos formulaciones se ve así (Fig.4):


Fig.4 ley de los cosenos

Si tomamos a y b como las longitudes de nuestros vectores, entonces lo que estamos buscando es el ángulo alfa. Pero ¿cómo obtenemos el valor de c? Mira: si restamos a de b, obtenemos un vector dirigido de a a b, y como un vector se caracteriza solo por su dirección y longitud, podemos ubicar gráficamente su comienzo al final del vector a. De esto podemos decir que c es igual a la longitud del vector b - a. Entonces lo hicimos

Expresemos los cuadrados de longitudes como productos escalares.

Abramos los corchetes usando la propiedad distributiva.

acortémoslo un poco

Finalmente, dividiendo ambos lados de la ecuación por menos dos, obtenemos

Ésta es otra propiedad del producto escalar. En nuestro caso, necesitamos normalizar los vectores para que sus longitudes sean iguales a uno. No tenemos que calcular el ángulo: el valor del coseno es suficiente. Si es menor que cero, entonces podemos decir con seguridad que este sector no nos interesa.

3. Cuadrícula

Es hora de pensar en cómo dibujar primitivos. Como dije antes, el sector es el componente principal de nuestro diagrama, por lo que para cada sector potencialmente visible dibujaremos una malla cuyas primitivas formarán el paisaje. Cada una de sus celdas se puede mostrar usando dos triángulos. Debido a que cada celda tiene caras adyacentes, los valores de la mayoría de los vértices de los triángulos se repiten en dos o más celdas. Para evitar duplicar datos en el búfer de vértices, llenemos el búfer de índice. Si se utilizan índices, con su ayuda la canalización de gráficos determina qué primitiva en el búfer de vértices debe procesar. (Fig. 5) La topología que elegí es la lista de triángulos (D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST)


Fig.5 Visualización visual de índices y primitivas.

Crear un buffer de vértices separado para cada sector es demasiado costoso. Es mucho más eficiente utilizar un único búfer con coordenadas en el espacio de la cuadrícula, es decir, x es una columna e y es una fila. Pero, ¿cómo se puede obtener de ellos un punto en la esfera? Un sector es un área cuadrada que comienza en un cierto punto S. Todos los sectores tienen la misma longitud de borde; llamémoslo SLen. La cuadrícula cubre toda el área del sector y además tiene la misma cantidad de filas y columnas, por lo que para encontrar la longitud del borde de la celda podemos construir la siguiente ecuación

Donde СLen es la longitud del borde de la celda, MSize es el número de filas o columnas de la cuadrícula. Divida ambas partes por MSize y obtenga CLen


Fig.6 Visualización visual de la formación de un punto en la cuadrícula.

Para obtener un punto en la esfera, usamos la ecuación derivada anteriormente

4. Altura

Todo lo que hemos conseguido hasta ahora poco se parece al paisaje. Es hora de agregar lo que lo hará así: la diferencia de altura. Imaginemos que tenemos una esfera de radio unitario con centro en el origen, así como un conjunto de puntos (P0, P1, P2... PN) que se encuentran sobre dicha esfera. Cada uno de estos puntos se puede representar como un vector unitario desde el origen. Ahora imagine que tenemos un conjunto de valores, cada uno de los cuales es la longitud de un vector específico (Fig. 7).

Almacenaré estos valores en una textura bidimensional. Necesitamos encontrar la relación entre las coordenadas del píxel de la textura y el vector de puntos en la esfera. Empecemos.

Además del sistema cartesiano, un punto de una esfera también se puede describir mediante un sistema de coordenadas esférico. En este caso, sus coordenadas estarán formadas por tres elementos: el ángulo de acimut, el ángulo polar y el valor de la distancia más corta desde el origen al punto. El ángulo de acimut es el ángulo entre el eje X y la proyección del rayo desde el origen hasta un punto del plano XZ. Puede tomar valores desde cero hasta 360 grados. El ángulo polar es el ángulo entre el eje Y y el rayo desde el origen hasta el punto. También se le puede llamar cenital o normal. Acepta valores de cero a 180 grados. (ver figura 8)


Fig.8 Coordenadas esféricas

Para convertir de cartesiano a esférico utilizo las siguientes ecuaciones (supongo que el eje Y está hacia arriba):

Donde d es la distancia al punto, a es el ángulo polar, b es el ángulo de acimut. El parámetro d también se puede describir como "la longitud del vector desde el origen hasta el punto" (como se puede ver en la ecuación). Si usamos coordenadas normalizadas, podemos evitar la división al encontrar el ángulo polar. En realidad, ¿por qué necesitamos estos ángulos? Al dividir cada uno de ellos por su rango máximo, obtenemos coeficientes de cero a uno y los usamos para tomar muestras de la textura en el sombreador. Al obtener el coeficiente del ángulo polar, es necesario tener en cuenta el cuarto en el que se ubica el ángulo. “Pero el valor de la expresión z/x no está definido cuando x es igual a cero”, dices. Además, diré que cuando z es igual a cero, el ángulo será cero independientemente del valor de x.

Agreguemos algunos casos especiales para estos valores. Tenemos coordenadas normalizadas (normales); agreguemos varias condiciones: si el valor X de la normal es cero y el valor Z es mayor que cero, entonces el coeficiente es 0,25, si X es cero y Z es menor que cero, entonces será 0,75. Si el valor de Z es cero y X es menor que cero, entonces en este caso el coeficiente será igual a 0,5. Todo esto se puede verificar fácilmente en un círculo. Pero, ¿qué hacer si Z es cero y X es mayor que cero; después de todo, en este caso tanto 0 como 1 serán correctos? Imaginemos que hemos elegido 1; bueno, tomemos un sector con un ángulo de acimut mínimo de 0 y un máximo de 90 grados. Ahora veamos los primeros tres vértices en la primera fila de la cuadrícula que muestra este sector. Para el primer vértice, cumplimos la condición y configuramos la coordenada de textura X en 1. Obviamente, para los dos vértices siguientes esta condición no se cumplirá: las esquinas para ellos están en el primer cuarto y como resultado obtenemos algo como esto. conjunto - (1,0, 0,05, 0,1). Pero para un sector con ángulos de 270 a 360 para los últimos tres vértices de la misma fila, todo será correcto: la condición para el último vértice funcionará y obtendremos el conjunto (0,9, 0,95, 1,0). Si elegimos cero como resultado, obtendremos los conjuntos (0,0, 0,05, 0,1) y (0,9, 0,95, 0,0); en cualquier caso, esto provocará distorsiones superficiales bastante notables. Entonces apliquemos lo siguiente. Tomemos el centro del sector, luego normalicemos su centro, moviéndolo así a la esfera. Ahora calculemos el producto escalar del centro normalizado y el vector (0, 0, 1). Hablando formalmente, este vector es normal al plano XY y, al calcular su producto escalar con el vector central del sector normalizado, podemos entender en qué lado del plano está el centro. Si es menor que cero, entonces el sector está detrás del plano y necesitamos el valor 1. Si el producto escalar es mayor que cero, entonces el sector está delante del plano y por lo tanto el valor límite será 0. (ver Figura 9)


Fig.9 El problema de elegir entre 0 y 1 para las coordenadas de textura.

Aquí está el código para obtener coordenadas de textura a partir de esféricas. Tenga en cuenta que debido a errores de cálculo, no podemos verificar la igualdad de los valores normales a cero, sino que debemos comparar sus valores absolutos con algún valor umbral (por ejemplo, 0,001).

//norm - coordenadas normalizadas del punto para el cual obtenemos coordenadas de textura //offset - coordenadas normalizadas del centro del sector al que pertenece la norma //zeroTreshold - valor umbral (0.001) float2 GetTexCoords(norma float3, offset float3) ( flotante tX = 0.0f, tY = 0.0f; bool normaXIsZero = abs(norma.x)< zeroTreshold; bool normZIsZero = abs(norm.z) < zeroTreshold; if(normXIsZero || normZIsZero){ if(normXIsZero && norm.z >0,0f) tX = 0,25f;< 0.0f && normZIsZero) tX = 0.5f; else if(normXIsZero && norm.z < 0.0f) tX = 0.75f; else if(norm.x >de lo contrario si (norma.x< 0.0f) tX = 1.0f; else tX = 0.0f; } }else{ tX = atan(norm.z / norm.x); if(norm.x < 0.0f && norm.z >0.0f && normaZIsZero)( si(punto(float3(0.0f, 0.0f, 1.0f), desplazamiento)< 0.0f && norm.z < 0.0f) tX += 3.141592; else if(norm.x >0,0f) tX += 3,141592;< 0.0f) tX = 3.141592 * 2.0f + tX; tX = tX / (3.141592 * 2.0f); } tY = acos(norm.y) / 3.141592; return float2(tX, tY); }
de lo contrario si (norma.x

0.0f && norma.z

Daré una versión intermedia del sombreador de vértices.

//startPos - comienzo de la cara del cubo //vec1, vec2 - vectores de dirección de la cara del cubo //gridStep - tamaño de celda //sideSize - longitud del borde del sector //GetTexCoords() - convierte coordenadas esféricas en coordenadas de textura VOut ProcessVertex(VIn entrada) ( float3 planePos = startPos + vec1 * input.netPos.x * gridStep.x + vec2 * input.netPos.y * gridStep.y; float3 sphPos = normalize(planePos); normalize(startPos + (vec1 + vec2) * sideSize * 0.5f); float2 tc = GetTexCoords(sphPos, normOffset); altura flotante = mainHeightTex.SampleLevel(mainHeightTexSampler, 0).x; posL = sphPos * (sphereRadius + altura (posL, 1.0f), worldViewProj); salida.texCoords = tc; devolver salida;

5. Iluminación

Lo mismo ocurre con el valor del coseno -1, sólo que en este caso los vectores apuntan en direcciones opuestas. Resulta que cuanto más cerca estén el vector normal y el vector de la fuente de luz del estado de colinealidad, mayor será el coeficiente de iluminación de la superficie a la que pertenece la normal. También supone que una superficie no se puede iluminar si su normal apunta en la dirección opuesta a la fuente, razón por la cual solo uso valores de coseno positivos.

Estoy usando una fuente paralela, por lo que se puede ignorar su posición. Lo único que debemos tener en cuenta es que estamos utilizando un vector para la fuente de luz. Es decir, si la dirección de los rayos es (1,0, -1,0, 0), necesitamos utilizar el vector (-1,0, 1,0, 0). Lo único que nos resulta difícil es el vector normal. Calcular la normal al avión es simple: necesitamos producir producto vectorial dos vectores que lo describen. Es importante recordar que el producto vectorial es anticonmutativo; es necesario tener en cuenta el orden de los factores. En nuestro caso, podemos obtener la normal al triángulo, conociendo las coordenadas de sus vértices en el espacio de malla, de la siguiente manera (tenga en cuenta que no tomo en cuenta los casos límite para p.x y p.y)

Flotador3 p1 = GetPosOnSphere(p); float3 p2 = GetPosOnSphere(float2(p.x + 1, p.y)); float3 p3 = GetPosOnSphere(float2(p.x, p.y + 1)); flotador3 v1 = p2 - p1; flotador3 v2 = p3 - p1; float3 n = normalzie(cross(v1, v2));
Pero eso no es todo. La mayoría de los vértices de la malla pertenecen a cuatro planos a la vez. Para obtener un resultado aceptable, debe calcular el promedio normal de la siguiente manera:

Na = normalizar(n0 + n1 + n2 + n3)
Implementar este método en una GPU es bastante costoso: necesitamos dos etapas para calcular las normales y promediarlas. Además, la eficiencia deja mucho que desear. En base a esto, elegí otro método: utilizar un mapa normal (Fig. 10).


Fig.10 Mapa normal

El principio de trabajar con él es el mismo que con un mapa de altura: transformamos las coordenadas esféricas del vértice de la malla en textura y hacemos una selección. Pero no podremos utilizar estos datos directamente; después de todo, estamos trabajando con una esfera y el vértice tiene su propia normalidad que debe tenerse en cuenta. Por lo tanto, utilizaremos los datos del mapa normal como coordenadas de la base TBN. ¿Qué es una base? Aquí tienes un ejemplo. Imagina que eres un astronauta y estás sentado en una baliza en algún lugar del espacio. Recibe un mensaje del MCC: "Debe moverse desde la baliza 1 metro hacia la izquierda, 2 metros hacia arriba y 3 metros hacia adelante". ¿Cómo se puede expresar esto matemáticamente? (1, 0, 0) * 1 + (0, 1, 0) * 2 + (0, 0, 1) * 3 = (1,2,3). EN forma matricial Esta ecuación se puede expresar de la siguiente manera:

Ahora imagina que también estás sentado en una baliza, solo que ahora te escriben desde el centro de control: “te enviamos vectores de dirección allí; debes moverte 1 metro por el primer vector, 2 metros por el segundo y 3 metros por el tercero." La ecuación para las nuevas coordenadas será:

La notación ampliada se ve así:

O en forma matricial:

Entonces, una matriz con los vectores V1, V2 y V3 es una base, y el vector (1,2,3) son las coordenadas en el espacio de esta base.

Imaginemos ahora que tienes un conjunto de vectores (base M) y sabes dónde estás en relación con la baliza (punto P). Necesita averiguar sus coordenadas en el espacio de esta base: qué tan lejos debe moverse a lo largo de estos vectores para terminar en el mismo lugar. Imaginemos las coordenadas requeridas (X)

Si P, M y X fueran números, simplemente dividiríamos ambos lados de la ecuación por M, pero, por desgracia... Vayamos por el otro lado, según la propiedad de la matriz inversa.

Donde I es la matriz identidad. En nuestro caso se ve así

¿Qué nos aporta esto? Intente multiplicar esta matriz por X y obtendrá

También es necesario aclarar que la multiplicación de matrices tiene la propiedad de asociatividad

Podemos considerar legítimamente un vector como una matriz de 3 por 1.

Considerando todo lo anterior, podemos concluir que para colocar X en el lado derecho de la ecuación, necesitamos en el orden correcto multiplica ambos lados por la matriz M inversa

Necesitaremos este resultado más adelante.

Ahora volvamos a nuestro problema. Usaré una base ortonormal: esto significa que V1, V2 y V3 son ortogonales entre sí (forman un ángulo de 90 grados) y tienen una longitud unitaria. V1 será el vector tangente, V2 será el vector bitangente y V3 será el normal. En la forma tradicional transpuesta de DirectX, la matriz tiene este aspecto:

Donde T es el vector tangente, B es el vector bitangente y N es la normal. Encontrémoslos. Es más fácil con normal basicamente todo estas son las coordenadas normalizadas del punto. El vector bitangente es igual al producto cruzado del vector normal y tangente. Lo más difícil será con el vector tangente. Es igual a la dirección de la tangente a la circunferencia en un punto. Miremos este momento. Primero, encontremos las coordenadas de un punto en el círculo unitario en el plano XZ para algún ángulo a

La dirección de la tangente a la circunferencia en este punto se puede encontrar de dos maneras. El vector a un punto del círculo y el vector tangente son ortogonales; por lo tanto, dado que las funciones sin y cos son periódicas, simplemente podemos sumar pi/2 al ángulo a y obtener la dirección deseada. Según la propiedad de desplazamiento pi/2:

Obtenemos el siguiente vector:

También podemos usar la diferenciación; consulte el Apéndice 3 para obtener más detalles. Entonces, en la Figura 11 puede ver una esfera para la cual se ha construido una base para cada vértice. Los vectores azules denotan normales, rojos - vectores tangentes, verdes - vectores bitangentes.


Fig.11 Esfera con bases TBN en cada vértice. Rojo - vectores tangentes, verde - vectores bitangentes, vectores azules - normales

Hemos resuelto la base; ahora obtengamos un mapa normal. Para ello utilizaremos el filtro de Sobel. El filtro Sobel calcula el gradiente de brillo de la imagen en cada punto (en términos generales, el vector de cambio de brillo). El principio del filtro es que es necesario aplicar una determinada matriz de valores, que se denomina "núcleo", a cada píxel y sus vecinos dentro de la dimensión de esta matriz. Supongamos que procesamos el píxel P con el núcleo K. Si no está en el borde de la imagen, entonces tiene ocho vecinos: arriba a la izquierda, arriba, arriba a la derecha, etc. Llamémoslos tl, t, tb, l, r, bl, b, br. Entonces, aplicar el kernel K a este píxel es el siguiente:

Pn = tl * K(0, 0) + t * K(0,1) + tb * K(0,2) +
           l * K(1, 0) + P * K(1,1) + r * K(1,2) +
          bl * K(2, 0) + b * K(2,1) + br * K(2,2)

Este proceso se llama "convolución". El filtro Sobel utiliza dos núcleos para calcular el gradiente vertical y horizontal. Denotémoslos como Kx y Ku:

La base está ahí: puedes empezar a implementarla. Primero necesitamos calcular el brillo del píxel. Utilizo la conversión del modelo de color RGB al modelo YUV para el sistema PAL:

Pero como nuestra imagen está inicialmente en escala de grises, este paso se puede omitir. Ahora necesitamos "contraer" la imagen original con los núcleos Kx y Ky. Esto nos dará los componentes X e Y del gradiente. El valor normal de este vector también puede ser muy útil: no lo usaremos, pero las imágenes que contienen valores normales de gradiente normalizados tienen algunos usos útiles. Por normalización me refiero a la siguiente ecuación

Donde V es el valor que normalizamos, Vmin y Vmax son el rango de estos valores. En nuestro caso, se realiza un seguimiento de los valores mínimo y máximo durante el proceso de generación. A continuación se muestra un ejemplo de implementación de un filtro Sobel:

Float SobelFilter::GetGrayscaleData(const Punto2 &Coords) ( Punto2 coords; coords.x = Math::Saturate(Coords.x, RangeI(0, image.size.width - 1)); coords.y = Math::Saturate( Coords.y, RangeI(0, image.size.height - 1)); int32_t offset = (coords.y * image.size.width + coords.x) * image.pixelSize; const uint8_t *pixel = &image.pixels; return (image.pixelFormat == PXL_FMT_R8) ? píxel: (0.30f * píxel + //R 0.59f * píxel + //G 0.11f * píxel //B ) void SobelFilter::Process() ( RangeF dirXVr, dirYVr, magNormVr; para(int32_t y = 0; y< image.size.height; y++) for(int32_t x = 0; x < image.size.width; x++){ float tl = GetGrayscaleData({x - 1, y - 1}); float t = GetGrayscaleData({x , y - 1}); float tr = GetGrayscaleData({x + 1, y - 1}); float l = GetGrayscaleData({x - 1, y }); float r = GetGrayscaleData({x + 1, y }); float bl = GetGrayscaleData({x - 1, y + 1}); float b = GetGrayscaleData({x , y + 1}); float br = GetGrayscaleData({x + 1, y + 1}); float dirX = -1.0f * tl + 0.0f + 1.0f * tr + -2.0f * l + 0.0f + 2.0f * r + -1.0f * bl + 0.0f + 1.0f * br; float dirY = -1.0f * tl + -2.0f * t + -1.0f * tr + 0.0f + 0.0f + 0.0f + 1.0f * bl + 2.0f * b + 1.0f * br; float magNorm = sqrtf(dirX * dirX + dirY * dirY); int32_t ind = y * image.size.width + x; dirXData = dirX; dirYData = dirY; magNData = magNorm; dirXVr.Update(dirX); dirYVr.Update(dirY); magNormVr.Update(magNorm); } if(normaliseDirections){ for(float &dirX: dirXData) dirX = (dirX - dirXVr.minVal) / (dirXVr.maxVal - dirXVr.minVal); for(float &dirY: dirYData) dirY = (dirY - dirYVr.minVal) / (dirYVr.maxVal - dirYVr.minVal); } for(float &magNorm: magNData) magNorm = (magNorm - magNormVr.minVal) / (magNormVr.maxVal - magNormVr.minVal); }
Hay que decir que el filtro de Sobel tiene la propiedad de separabilidad lineal, por lo que este método se puede optimizar.

La parte difícil ha terminado: todo lo que queda es escribir las coordenadas X e Y de la dirección del gradiente en los canales R y G de los píxeles normales del mapa. Para la coordenada Z, uso uno. También utilizo un vector 3D de coeficientes para ajustar estos valores. El siguiente es un ejemplo de cómo generar un mapa normal con comentarios:

//ImageProcessing::ImageData Imagen - imagen original. El constructor contiene el formato de píxel y los datos de la imagen ImageProcessing::SobelFilter sobelFilter; sobelFilter.Init(Imagen); sobelFilter.NormaliseDirections() = falso; sobelFilter.Proceso(); const auto &resX =sobelFilter.GetFilteredData(ImageProcessing::SobelFilter::SOBEL_DIR_X); const auto &resY =sobelFilter.GetFilteredData(ImageProcessing::SobelFilter::SOBEL_DIR_Y); ImageProcessing::ImageData destImage = (DXGI_FORMAT_R8G8B8A8_UNORM, Image.size); size_t dstImageSize = Imagen.tamaño.ancho * Imagen.tamaño.alto * destImage.pixelSize; estándar::vector dstImgPixels(dstImageSize); para(int32_t d = 0 ; d< resX.size(); d++){ //используем вектор настроечных коэффициентов. У меня он равен (0.03, 0.03, 1.0) Vector3 norm = Vector3::Normalize({resX[d] * NormalScalling.x, resY[d] * NormalScalling.y, 1.0f * NormalScalling.z}); Point2 coords(d % Image.size.width, d / Image.size.width); int32_t offset = (coords.y * Image.size.width + coords.x) * destImage.pixelSize; uint8_t *pixel = &dstImgPixels; //переводим значения из области [-1.0, 1.0] в а затем в область pixel = (0.5f + norm.x * 0.5f) * 255.999f; pixel = (0.5f + norm.y * 0.5f) * 255.999f; pixel = (0.5f + norm.z * 0.5f) * 255.999f; } destImage.pixels = &dstImgPixels; SaveImage(destImage, OutFilePath);
Ahora daré un ejemplo del uso de un mapa normal en un sombreador:

//texCoords - coordenadas de textura que obtuvimos usando el método descrito en el párrafo 4 //normalL - vértice normal //lightDir - vector de la fuente de luz //Ld - color de la fuente de luz //Kd - color del material de la superficie iluminada float4 normColor = mainNormalTex SampleLevel(mainNormalTexSampler, texCoords, 0); //transfiere el valor del área a [-1.0, 1.0] y normaliza el resultado float3 normalT = normalize(2.0f * mainNormColor.rgb - 1.0f); //traduce la coordenada de textura X del área a flotante ang = texCoords.x * 3.141592f * 2.0f; tangente float3; tangente.x = -sin(ang); tangente.y = 0.0f; tangente.z = cos(ang); float3 bitangente = normalizar(cruz(normalL, tangente)); float3x3 tbn = float3x3(tangente, bitangente, normalL); float3 resNormal = mul(normalT, tbn); diferencia flotante = saturar(punto(resNormal, lightDir.xyz)); float4 resColor = Ld * Kd * diff;

6.Nivel de detalle

Bueno, ¡ahora nuestro paisaje está iluminado! Puedes volar a la Luna - agregar un mapa de altura, establecer el color del material, cargar los sectores, establecer el tamaño de la cuadrícula en (16, 16) y... Sí, algo es demasiado pequeño - pondré (256 , 256) - oh, algo está ralentizando todo, y ¿por qué tanto detalle en sectores distantes? Además, cuanto más cerca esté el observador del planeta, menos sectores podrá ver. Sí... ¡aún nos queda mucho trabajo por hacer! Primero, descubramos cómo cortar sectores innecesarios. El valor determinante aquí será la altura del observador desde la superficie del planeta: cuanto más alta sea, más sectores podrá ver (Fig. 12)


Fig. 12 Dependencia de la altura del observador del número de sectores procesados

Encontramos la altura de la siguiente manera: construimos un vector desde la posición del observador hasta el centro de la esfera, calculamos su longitud y le restamos el valor del radio de la esfera. Anteriormente dije que si el producto escalar del vector por el observador y el vector por el centro del sector es menor que cero, entonces este sector no nos interesa; ahora, en lugar de cero, usaremos un valor que depende linealmente de la altura. Primero, definamos las variables, de modo que tengamos un valor mínimo y máximo para el producto escalar y un valor mínimo y máximo para la altura. Construyamos el siguiente sistema de ecuaciones.

Ahora expresemos A en la segunda ecuación.

Sustituyamos A de la segunda ecuación en la primera.

Expresemos B a partir de la primera ecuación.

Sustituye B de la primera ecuación en la segunda.

Ahora sustituyamos las variables en la función.

y recibiremos

Donde Hmin y Hmax son los valores mínimo y máximo de altura, Dmin y Dmax son los valores mínimo y máximo del producto escalar. Este problema se puede resolver de otra manera; consulte el Apéndice 4.

Ahora necesitamos comprender los niveles de detalle. Cada uno de ellos definirá el rango del producto escalar. En pseudocódigo, el proceso de determinar si un sector pertenece a un determinado nivel se ve así:

Recorra todos los sectores, calcule el producto escalar del vector por el observador y el vector por el centro del sector, si el producto escalar es menor que el umbral mínimo calculado anteriormente, pase al siguiente sector, recorra los niveles de detalle, si el producto escalar está dentro de los límites definidos para este nivel, agregar el sector a este nivel, fin de ciclo por niveles de detalle fin de ciclo para todos los sectores
Necesitamos calcular el rango de valores para cada nivel. Primero, construyamos un sistema de dos ecuaciones.

Resuelto, obtenemos

Usando estos coeficientes, definimos la función

Donde Rmax es el rango del producto escalar (D(H) - Dmin), Rmin es el rango mínimo determinado por el nivel. Estoy usando un valor de 0,01. Ahora necesitamos restar el resultado de Dmax.

Usando esta función obtendremos áreas para todos los niveles. He aquí un ejemplo:

Const float dotArea = dotRange.maxVal - dotRange.minVal; constante flotante Rmax = dotArea, Rmin = 0.01f; float lodsCnt = lods.size(); flotador A = Rmáx; flotador B = powf(Rmin / Rmax, 1.0f / (lodsCnt - 1.0f)); para(tamaño_t g = 0; g< lods.size(); g++){ lods[g].dotRange.minVal = dotRange.maxVal - A * powf(B, g); lods[g].dotRange.maxVal = dotRange.maxVal - A * powf(B, g + 1); } lods.dotRange.maxVal = 1.0f;
Ahora podemos determinar a qué nivel de detalle pertenece el sector (Fig. 13).


Fig. 13 Diferenciación de colores de sectores según niveles de detalle

A continuación debes calcular el tamaño de las cuadrículas. Será muy costoso almacenar su propia malla para cada nivel; es mucho más eficiente cambiar el detalle de una malla sobre la marcha mediante teselación. Para hacer esto, necesitamos, además de los habituales sombreadores de vértices y píxeles, implementar también sombreadores de casco y dominio. En el sombreador de Hull, la tarea principal es preparar los puntos de control. Consta de dos partes: la función principal y la función que calcula los parámetros del punto de control. Debe especificar valores para los siguientes atributos:

dominio
particionar
topología de salida
puntos de control de salida
función constante de parche
A continuación se muestra un ejemplo de un sombreador de Hull para mosaico de triángulos:

Struct PatchData (bordes flotantes: SV_TessFactor; flotante dentro: SV_InsideTessFactor;); PatchData GetPatchData(InputPatch Parche, uint PatchId: SV_PrimitiveID) ( salida PatchData; flotar tessFactor = 2.0f; salida.edges = tessFactor; salida.edges = tessFactor; salida.edges = tessFactor; salida.inside = tessFactor; salida de retorno; ) VIn ProcessHull(InputPatch Parche, uint PointId: SV_OutputControlPointID, uint PatchId: SV_PrimitiveID) (devuelve parche;)
Verá, el trabajo principal se realiza en GetPatchData(). Su tarea es establecer el factor de teselación. Hablaremos de ello más adelante, ahora pasemos al sombreador de dominio. Recibe puntos de control del sombreador de Hull y coordenadas del teselador. El nuevo valor de las coordenadas de posición o textura en el caso de división por triángulos se debe calcular mediante la siguiente fórmula

N = C1 * F.x + C2 * F.y + C3 * F.z

Donde C1, C2 y C3 son los valores de los puntos de control, F son las coordenadas del teselador. También en el sombreador de dominio debe establecer el atributo de dominio, cuyo valor corresponde al especificado en el sombreador de casco. A continuación se muestra un ejemplo de un sombreador de dominio:

Cbuffer buff0: registro (b0) (matriz worldViewProj;) estructura PatchData (bordes flotantes: SV_TessFactor; flotador interior: SV_InsideTessFactor;); PIn ProcessDomain(PatchData Patch, float3 Coord: SV_DomainLocation, const OutputPatch
Tri) ( float3 posL = Tri.posL * Coord.x + Tri.posL * Coord.y + Tri.posL * Coord.z; float2 texCoords = Tri.texCoords * Coord.x + Tri.texCoords * Coord.y + Tri .texCoords * PIn salida.posH = mul(float4(posL, 1.0f), salida.normalW = Tri.normalW; salida de retorno;

El papel del sombreador de vértices en este caso se reduce al mínimo; para mí, simplemente "pasa" los datos a la siguiente etapa.

Ahora necesitamos implementar algo similar. Nuestra tarea principal es calcular el factor de teselación, o más precisamente, trazar su dependencia de la altura del observador. Construyamos un sistema de ecuaciones nuevamente.

Resolviendo de la misma manera que antes, obtenemos
Donde Tmin y Tmax son los coeficientes de teselación mínimo y máximo, Hmin y Hmax son los valores mínimo y máximo de la altura del observador. Mi coeficiente mínimo de teselación es uno. el máximo se establece por separado para cada nivel

(por ejemplo 1, 2, 4, 16).

Tomemos el logaritmo decimal de los dos lados de la ecuación.

Según la propiedad de los logaritmos, podemos reescribir la ecuación de la siguiente manera

Ahora todo lo que tenemos que hacer es dividir ambos lados por log(2)

Pero eso no es todo. X es aproximadamente 2,58. A continuación, debe restablecer la parte fraccionaria y elevar dos a la potencia del número resultante. Aquí está el código para calcular factores de teselación para niveles de detalle.

Flotador h = cámara->GetHeight(); const RangeF &hR = rangoaltura; for(LodsStorage::Lod &lod: lods)( //derivado del sistema //A + B * Hmax = Lmin //A + B * Hmin = Lmax //y obteniendo A y luego sustituyendo B en la segunda igualdad float mTf = (float )lod.GetMaxTessFactor(); float tessFactor = 1.0f + (mTf - 1.0f) * ((h - hR.maxVal) / (hR.minVal - hR.maxVal) (1.0f, mTf)); = pow(2.0f, piso(log(tessFactor) / log(2)));

7. Ruido

Veamos cómo podemos aumentar el detalle del paisaje sin cambiar el tamaño del mapa de altura. Lo que me viene a la mente es cambiar el valor de altura al valor obtenido de la textura de ruido degradado. Las coordenadas en las que tomaremos muestras serán N veces mayores que las principales. Al realizar el muestreo, se utilizará el tipo de direccionamiento espejo (D3D11_TEXTURE_ADDRESS_MIRROR) (consulte la Fig. 14).


Fig. 14 Esfera con mapa de altura + esfera con mapa de ruido = esfera con altura final

En este caso, la altura se calculará de la siguiente manera:

//float2 tc1 - coordenadas de textura obtenidas desde un punto normalizado, //como se describió anteriormente //texCoordsScale - multiplicador de coordenadas de textura. En mi caso, es igual al valor 300 //mainHeightTex, mainHeightTexSampler - textura del mapa de altura //distHeightTex, distHeightTexSampler - textura de ruido degradado //maxTerrainHeight - altura máxima del paisaje. En mi caso 0.03 float2 tc2 = tc1 * texCoordsScale; float4 mainHeightTexColor = mainHeightTex.SampleLevel(mainHeightTexSampler, tc1, 0); float4 distHeightTexColor = distHeightTex.SampleLevel(distHeightTexSampler, tc2, 0); altura flotante = (mainHeighTexColor.x + distHeighTexColor.x) * maxTerrainHeight;
Hasta ahora, la naturaleza periódica se expresa significativamente, pero con la adición de iluminación y textura, la situación mejorará. ¿Qué es una textura de ruido degradado? En términos generales, es una cuadrícula de valores aleatorios. Averigüemos cómo hacer coincidir los tamaños de la red con el tamaño de la textura. Digamos que queremos crear una textura de ruido de un tamaño de 256 por 256 píxeles. Todo es sencillo, si las dimensiones de la celosía coinciden con las dimensiones de la textura, obtendremos algo parecido al ruido blanco en un televisor. Pero ¿qué pasa si nuestra red tiene dimensiones, digamos, de 2 por 2? La respuesta es simple: utilice la interpolación. Una formulación de interpolación lineal es la siguiente:

Esta es la opción más rápida, pero al mismo tiempo la menos adecuada para nosotros. Es mejor utilizar la interpolación basada en coseno:

Pero no podemos simplemente interpolar entre los valores en los bordes de la diagonal (esquina inferior izquierda y superior derecha de la celda). En nuestro caso, será necesario aplicar la interpolación dos veces. Imaginemos una de las celdas de la red. Tiene cuatro esquinas: llamémoslas V1, V2, V3, V4. Además, dentro de esta celda habrá su propio sistema de coordenadas bidimensional, donde el punto (0, 0) corresponde a V1 y el punto (1, 1) a V3 (ver Fig. 15a). Para obtener un valor con coordenadas (0.5, 0.5), primero necesitamos obtener dos valores interpolados en X: entre V1 y V4 y entre V2 y V3, y finalmente interpolar en Y entre estos valores (Fig. 15b).

He aquí un ejemplo:

Coordenadas float2 (0.5f, 0.5f) float4 P1 = lerp(V1, V4, coords.x); float4 P2 = lerp(V2, V3, coordenadas.x); float4 P = lerp(P1, P2, coordenadas.y)


Fig. 15 a - Imagen de una celda de celosía con coordenadas V1, V2, V3 y V4. b - Secuencia de dos interpolaciones usando una celda como ejemplo

Ahora hagamos lo siguiente: para cada píxel de la textura de ruido, tome el valor interpolado para la cuadrícula de 2x2, luego agreguele el valor interpolado para la cuadrícula de 4x4, multiplicado por 0,5, luego para la cuadrícula de 8x8, multiplicado por 0,25, etc. . hasta un cierto límite; esto se llama suma de octavas (Fig. 16). La fórmula se ve así:


Fig. 16 Ejemplo de suma de octavas

A continuación se muestra un ejemplo de implementación:

Para(int32_t x = 0; x< size.width; x++) for(int32_t y = 0; y < size.height; y++){ float val = 0.0f; Vector2 normPos = {(float)x / (float)(sideSize - 1), (float)y / (float)(sideSize - 1)}; for(int32_t o = 0; o < octavesCnt; o++){ float frequency = powf(2.0f, (float)(startFrequency + o)); float intencity = powf(intencityFactor, (float)o); Vector2 freqPos = normPos * frequency; Point2 topLeftFreqPos = Cast(Posfrecuencia);
También para V1, V2, V3 y V4 puedes obtener la suma del valor mismo y sus vecinos de esta manera:

Float GetSmoothValue(const Point2 &Coords) ( esquinas flotantes = (GetValue((Coords.x - 1, Coords.y - 1)) + GetValue((Coords.x + 1, Coords.y - 1)) + GetValue((Coords .x - 1, Coords.y + 1)) + GetValue((Coords.x + 1, Coords.y + 1))) / 16.0f; lados flotantes = (GetValue((Coords.x - 1, Coords.y) )) + GetValue((Coords.x + 1, Coords.y)) + GetValue((Coords.x, Coords.y - 1)) + GetValue((Coords.x, Coords.y + 1))) / 8.0 f; centro flotante = GetValue(Coords) / 4.0f centro de retorno + lados + esquinas)
y utilice estos valores para la interpolación. Aquí está el resto del código:

Float GetInterpolatedValue(const Point2 &TopLeftCoord, const Point2 &BottomRightCoord, float XFactor, float YFactor) ( Point2 tlCoords(TopLeftCoord.x, TopLeftCoord.y); Point2 trCoords(BottomRightCoord.x, TopLeftCoord.y); Point2 brCoords(Inferior DerechaCoord.x, BottomRightCoord .y); Point2 blCoords(TopLeftCoord.x, BottomRightCoord.y); float tl = (useSmoothValues) ? ? OTTOMVAL = MATH :: Cosinterpolación (BL, BR, XFACTOR); topVal = Math::CosInterpolation(tl, tr, XFactor return Math::CosInterpolation(topVal, bottomVal, YFactor);
Para concluir la subsección, quiero decir que todo lo que he descrito hasta este punto es una implementación ligeramente diferente del ruido Perlin de la canónica.
Hemos resuelto las alturas; ahora veamos qué hacer con las normales. Al igual que con el mapa de altura principal, necesitamos generar un mapa normal a partir de la textura de ruido. Luego, en el sombreador, simplemente agregamos la normal del mapa principal con la normal de la textura de ruido. Debo decir que esto no es del todo correcto, pero da un resultado aceptable. He aquí un ejemplo:
//float2 texCoords1 - coordenadas de textura obtenidas del punto normalizado, como se describió anteriormente //mainNormalTex, mainNormalTexSampler - mapa normal principal //distNormalTex, distNormalTexSampler - mapa normal de ruido de gradiente float2 texCoords2 = texCoords1 * texCoordsScale; float4 mainNormColor = mainNormalTex.SampleLevel(mainNormalTexSampler, TexCoords1, 0); float4 distNormColor = distNormalTex.SampleLevel(distNormalTexSampler, TexCoords2, 0); float3 principalNormal = 2.0f * principalNormColor.rgb - 1.0f; float3 distNormal = 2.0f * distNormColor.rgb - 1.0f; float3 normal = normalizar (mainNormal + distNormal);

8. Instancia de hardware

Hagamos optimización. Ahora el ciclo de dibujar sectores en pseudocódigo se ve así

Recorra todos los sectores, calcule el producto escalar de un vector por un punto y un vector por el centro del sector, si es mayor que cero, establezca el valor de S en los datos del sombreador, establezca el valor de V1 en el sombreador datos, establecer el valor de V2 en los datos del sombreador, dibujar la malla, fin de la condición, fin del bucle sobre todos los sectores
El rendimiento de este enfoque es extremadamente bajo. Hay varias opciones de optimización: puede construir un árbol cuádruple para cada plano del cubo, para no calcular el producto escalar de cada sector. También puedes actualizar los valores de V1 y V2 no para cada sector, sino para los seis planos del cubo al que pertenecen. Elegí la tercera opción: Instancia. Brevemente sobre lo que es. Digamos que quieres dibujar un bosque. Tiene un modelo de árbol y también tiene un conjunto de matrices de transformación: posiciones del árbol, posible escalado o rotación. Puede crear un búfer que contendrá los vértices de todos los árboles convertidos al espacio mundial; esta es una buena opción, el bosque no recorre el mapa. Pero, ¿qué pasa si es necesario implementar transformaciones, por ejemplo, árboles meciéndose con el viento? Puede hacer esto: copiar los datos de los vértices del modelo N veces en un búfer, agregando el índice del árbol (de 0 a N) a los datos del vértice. A continuación, actualizamos la matriz de matrices de transformación y la pasamos como variable al sombreador. En el sombreador seleccionamos la matriz deseada por el índice del árbol. ¿Cómo se puede evitar la duplicación de datos? En primer lugar, me gustaría llamar su atención sobre el hecho de que estos vértices se pueden recopilar desde varios buffers. Para describir un vértice, debe especificar el índice de origen en el campo InputSlot de la estructura D3D11_INPUT_ELEMENT_DESC. Esto se puede utilizar al implementar la transformación de la animación facial; digamos que tiene dos buffers de vértices que contienen dos estados de cara y desea interpolar linealmente estos valores. A continuación se explica cómo describir un vértice:

D3D11_INPUT_ELEMENT_DESC desc = ( /*part1*/ ("POSICIÓN", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, ("NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, _INPUT_PER_VERTEX_DATA, 0), ("TEXCOORD", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0), /*part2*/ ("POSICIÓN", 1, DXGI_FORMAT_R32G32B32_FLOAT, 1, 0, D3D11_INPUT_PER_VERTEX_DATA, 0), ("NORM" AL", DXGI_FORMAT_R32G32B32_FLOAT, 1, 12 , D3D11_INPUT_PER_VERTEX_DATA , 0), ("TEXCOORD", 1, DXGI_FORMAT_R32G32B32_FLOAT, 1, 24, D3D11_INPUT_PER_VERTEX_DATA, 0) )
en el sombreador el vértice debe describirse de la siguiente manera:

Estructura VIn ( float3 posición1: POSICIÓN0; float3 normal1: NORMAL0; float2 tex1: TEXCOORD0; float3 posición2: POSICIÓN1; float3 normal2: NORMAL1; float2 tex2: TEXCOORD1; )
entonces simplemente interpolas los valores

Float3 res = lerp(entrada.posición1, entrada.posición2, factor);
¿Por qué digo esto? Volvamos al ejemplo de los árboles. Recopilaremos el vértice de dos fuentes: la primera contendrá la posición en el espacio local, las coordenadas de textura y la normal, la segunda contendrá la matriz de transformación en forma de cuatro vectores de cuatro dimensiones. La descripción del vértice se ve así:

D3D11_INPUT_ELEMENT_DESC desc = ( /*part1*/ ("POSICIÓN", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, ("NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, _INPUT_PER_VERTEX_DATA, 0), ("TEXCOORD", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0), /*part2*/ ("MUNDO", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D11_INPUT_PER_INSTANCE_DATA, 1), ( "MUNDO", 1, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 16, D3D11_INPUT_PER_INSTANCE_DATA , 1), ("MUNDO", 2, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 32, D3D11_INPUT_PER_INSTANCE_DATA, 1), ("MUNDO", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 48, ANCE_DATA, 1), )
Tenga en cuenta que en la segunda parte, el campo InputSlotClass es igual a D3D11_INPUT_PER_INSTANCE_DATA y el campo InstanceDataStepRate es igual a uno (para obtener una breve descripción del campo InstanceDataStepRate, consulte el Apéndice 1). En este caso, el recopilador utilizará los datos de todo el búfer del origen con tipo D3D11_INPUT_PER_VERTEX_DATA para cada elemento del origen con tipo D3D11_INPUT_PER_INSTANCE_DATA. En este caso, en el sombreador estos vértices se pueden describir de la siguiente manera:

Estructura VIn ( float3 posL: POSICIÓN; float3 normalL: NORMAL; float2 tex: TEXCOORD; fila_major float4x4 mundo: MUNDO; );
Al crear un segundo búfer con los atributos D3D11_USAGE_DYNAMIC y D3D11_CPU_ACCESS_WRITE, podemos actualizarlo desde el lado de la CPU. Necesita dibujar este tipo de geometría usando llamadas a DrawInstanced() o DrawIndexedInstanced(). También hay llamadas a DrawInstancedIndirect() y DrawIndexedInstancedIndirect(); sobre ellas, consulte el Apéndice 2.

A continuación se muestra un ejemplo de configuración de buffers y uso de la función DrawIndexedInstanced():

//vb - búfer de vértices //tb - búfer de "instancia" //ib - búfer de índice //vertexSize - tamaño del elemento en el búfer de vértices //instanceSize - tamaño del elemento en el búfer de "instancia" //indicesCnt - número de índices //instancesCnt - número de "instancias" std::vector búferes = (vb, tb); estándar::vector zancadas = (tamaño de vértice, tamaño de instancia); estándar::vector compensaciones = (0, 0); deviceContext->IASetVertexBuffers(0,buffers.size(),&buffers,&strides,&offsets); deviceContext->IASetIndexBuffer(ib, DXGI_FORMAT_R32_UINT, 0); deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); deviceContext->DrawIndexedInstanced(indicesCnt, instanciasCnt, 0, 0, 0);
Ahora finalmente volvamos a nuestro tema. El sector puede describirse mediante un punto del plano al que pertenece y dos vectores que describen este plano. Por tanto, el pico estará formado por dos fuentes. El primero son las coordenadas del espacio de la cuadrícula, el segundo son los datos del sector. La descripción del vértice se ve así:

Estándar::vector meta = ( //coordenadas en el espacio de la cuadrícula ("POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0) //primer vector de cara ("TEXCOORD", 0, DXGI_FORMAT_R32G32B32_FLOAT, 1, 0, UT_PER_INSTANCE_DATA, 1), / /segundo vector de cara ("TEXCOORD", 1, DXGI_FORMAT_R32G32B32_FLOAT, 1, 12, D3D11_INPUT_PER_INSTANCE_DATA, 1), //inicio de cara ("TEXCOORD", 2, DXGI_FORMAT_R32G32B32_FLOAT, 1, 24, D3D11_INPUT_PER_ INSTANCE_DATA, 1) )
Tenga en cuenta que estoy usando un vector 3D para almacenar las coordenadas del espacio de la cuadrícula (no se usa ninguna coordenada z)

9. Eliminación de frutos

Otro componente de optimización importante es el recorte de la pirámide de visibilidad (culling de Frustum). La pirámide de visibilidad es el área de la escena que la cámara “ve”. ¿Cómo construirlo? Primero, recuerde que un punto puede estar en cuatro sistemas de coordenadas: sistema de coordenadas local, mundial, de vista y de proyección. La transición entre ellos se realiza a través de matrices - mundo, especie y matriz de proyección, y las transformaciones deben ocurrir secuencialmente - de local a mundo, de mundo a especie y finalmente de especie a espacio de proyección. Todas estas transformaciones se pueden combinar en una multiplicando estas matrices.

Usamos una proyección en perspectiva, que implica la llamada "división uniforme": después de multiplicar el vector (Px, Py, Pz, 1) por la matriz de proyección, sus componentes deben dividirse por el componente W de este vector. Después de la transición al espacio de proyección y la división uniforme, el punto termina en el espacio NDC. El espacio NDC es un conjunto de tres coordenadas x, y, z, donde x e y pertenecen a [-1, 1], y z - (hay que decir que en OpenGL los parámetros son algo diferentes).

Ahora vayamos a resolver nuestro problema. En mi caso, la pirámide está ubicada en el espacio de visualización. Necesitamos seis planos que lo describan (Fig. 17a). Un plano se puede describir utilizando una normal y un punto que pertenece a este plano. Primero, obtengamos los puntos; para ello, tomamos el siguiente conjunto de coordenadas en el espacio NDC:

Estándar::vector PuntosN = ((-1.0f, -1.0f, 0.0f, 1.0f), (-1.0f, 1.0f, 0.0f, 1.0f), (1.0f, 1.0f, 0.0f, 1.0f), (1.0 f, -1,0f, 0,0f, 1,0f), (-1,0f, -1,0f, 1,0f, 1,0f), (-1,0f, 1,0f, 1,0f, 1,0f), (1,0f, 1,0f , 1,0f, 1,0f), (1,0f, -1,0f, 1,0f, 1,0f));
Mire, en los primeros cuatro puntos el valor z es 0 - esto significa que pertenecen al plano de recorte cercano, en los últimos cuatro puntos z es igual a 1 - pertenecen al plano de recorte lejano. Ahora es necesario convertir estos puntos para ver el espacio. ¿Pero cómo?

Recuerde el ejemplo del astronauta, aquí ocurre lo mismo. Necesitamos multiplicar los puntos por la matriz de proyección inversa. Es cierto que después de esto todavía necesitamos dividir cada uno de ellos por su coordenada W. Como resultado, obtenemos las coordenadas necesarias (Fig. 17b). Veamos ahora las normales: deben estar dirigidas dentro de la pirámide, por lo que debemos elegir el orden necesario para calcular el producto vectorial.

Matrix4x4 invProj = Matrix4x4::Inverse(camera->GetProjMatrix()); estándar::vector PuntosV; for(const Punto4F &pN: puntosN)( Punto4F pV = invProj.Transform(pN); pV /= pV.w; puntosV.push_back(Cast (pV)); ) planos = (puntosV, puntosV, puntosV); //planos cercanos = (puntosV, puntosV, puntosV); //planos del plano lejano = (puntosV, puntosV, puntosV); //planos del plano izquierdo = (puntosV, puntosV, puntosV); //planos del plano recto = (puntosV, puntosV, puntosV); //planos del plano superior = (puntosV, puntosV, puntosV); //plano inferior planos.normal *= -1.0f; planos.normal *= -1.0f;


Fig. 17 Pirámide de visibilidad

La pirámide está construida: es hora de usarla. No dibujamos aquellos sectores que no caen dentro de la pirámide. Para determinar si un sector está dentro de la pirámide de visibilidad, verificaremos la esfera delimitadora ubicada en el centro de este sector. Esto no da resultados exactos, pero en este caso No veo nada malo en que se dibujen varios sectores adicionales. El radio de la esfera se calcula de la siguiente manera:

Donde TR es la esquina superior derecha del sector, BL es la esquina inferior izquierda. Como todos los sectores tienen la misma área, basta con calcular el radio una vez.

¿Cómo podemos determinar si la esfera que describe el sector está dentro de la pirámide de visibilidad? Primero debemos determinar si la esfera interseca el plano y, en caso contrario, en qué lado del mismo se encuentra. Llevemos el vector al centro de la esfera.

Donde P es un punto del plano y S es el centro de la esfera. Ahora calculemos el producto escalar de este vector y la normal del plano. La orientación se puede determinar utilizando el signo del producto escalar; como se mencionó anteriormente, si es positivo, entonces la esfera está delante del plano, si es negativo, entonces la esfera está detrás. Queda por determinar si la esfera cruza el plano. Tomemos dos vectores: N (vector normal) y V. Ahora construyamos un vector de N a V, llamémoslo K. Entonces, necesitamos encontrar una longitud N tal que forme un ángulo de 90 grados con K (formalmente hablando, de modo que N y K fueran ortogonales). Oki doki, mira la figura 18a: por las propiedades de un triángulo rectángulo sabemos que

Necesitamos encontrar el coseno. Usando la propiedad del producto escalar mencionada anteriormente

Divide ambos lados por |V|*|N| y obtenemos

Usemos este resultado:

Desde |V| es solo un número, podemos reducirlo por |V|, y luego obtenemos

Dado que el vector N está normalizado, el último paso es simplemente multiplicarlo por el valor resultante; de ​​lo contrario, el vector debe normalizarse; en este caso, la ecuación final se ve así:

Donde D es nuestro nuevo vector. Este proceso se llama "Proyección vectorial" (Fig. 18b). Pero ¿por qué necesitamos esto? Sabemos que un vector está determinado por su longitud y dirección y no cambia de ninguna manera dependiendo de su posición; esto significa que si colocamos D de manera que apunte a S, entonces su longitud será igual a la distancia mínima desde S. al avión (Fig.18c)


Fig. 18 a Proyección de N sobre V, b Visualización de la longitud de N proyectado en relación con un punto, c Visualización
longitud del N proyectado aplicado a una esfera centrada en S

Como no necesitamos un vector proyectado, solo necesitamos calcular su longitud. Dado que N es un vector unitario, sólo necesitamos calcular el producto escalar de V y N. Juntando todo, finalmente podemos concluir que la esfera corta al plano si el valor del producto escalar del vector con el centro de la esfera y la normal al plano es mayor que cero y menor que el valor del radio de esta esfera.

Para afirmar que la esfera está dentro de la pirámide de visibilidad, debemos asegurarnos de que interseca uno de los planos o está frente a cada uno de ellos. La pregunta se puede plantear de otra manera: si la esfera no se cruza y está ubicada detrás de al menos uno de los planos, definitivamente está fuera de la pirámide de visibilidad. Eso es lo que haremos. Tenga en cuenta que transfiero el centro de la esfera al mismo espacio en el que se encuentra la pirámide: al espacio de visualización.

Bool Frustum::TestSphere(const Point3F &Pos, float Radius, const Matrix4x4 &WorldViewMatrix) const ( Point3F posV = WorldViewMatrix.Transform(Pos); for(const Plane &pl: planos)( Vector3 toSphPos = posV - pl.pos; if(Vector3 ::Punto(aSphPos, pl.normal)< -Radius) return false; } return true; }

10. Grietas

Otro problema que tenemos que resolver son las grietas en los límites de los niveles de detalle (Fig. 19).


Fig. 19 demostración de grietas en el paisaje.

En primer lugar, debemos identificar aquellos sectores que se encuentran en el límite de los niveles de detalle. A primera vista, parece que se trata de una tarea que requiere muchos recursos; después de todo, el número de sectores en cada nivel cambia constantemente. Pero si utiliza datos de adyacencia, la solución se vuelve mucho más sencilla. ¿Qué son los datos de adyacencia? Mire, cada sector tiene cuatro vecinos. El conjunto de referencias a ellos, ya sean punteros o índices, son los datos de adyacencia. Con su ayuda podemos determinar fácilmente qué sector se encuentra en la frontera; basta con comprobar a qué nivel pertenecen sus vecinos.

Bueno, busquemos a los vecinos de cada sector. Y nuevamente, no necesitamos recorrer todos los sectores. Imaginemos que estamos trabajando con un sector con coordenadas X e Y en el espacio de la cuadrícula.

Si no toca el borde del cubo, entonces las coordenadas de sus vecinos serán las siguientes:

Vecino superior - (X, Y - 1)
Vecino inferior - (X, Y + 1)
Vecino izquierdo - (X - 1, Y)
Vecino derecho - (X + 1, Y)

Si el sector toca un borde, lo colocamos en un recipiente especial. Después de procesar las seis caras, contendrá todos los sectores límite del cubo. Es en este contenedor donde tenemos que realizar la búsqueda. Calculemos de antemano los bordes de cada sector:

Estructura SectorEdges ( CubeSectors::Sector *propietario; typedef std::par Borde; bordes de borde; ); estándar::vector sectoresBordes; //borderSectors - un contenedor con sectores de borde for(CubeSectors::Sector &sec: borderSectors)( // cada sector contiene dos vectores que describen la cara del cubo // al que pertenece Vector3 v1 = sec.vec1 * sec.sideSize ; Vector3 v2 = sec.vec2 * sec.sideSize; //sec.startPos - el inicio del sector en el espacio local SectorEdges; secEdges.owner = secEdges.edges = (sec.startPos, sec.startPos + v1); , sec.startPos + v2); sec.startPos + v2, sec.startPos + v2 + v1); secEdges.edges = (sec.startPos + v1, sec.startPos + v2 + v1);
Luego viene la búsqueda en sí.

For(SectorEdges &edgs: sectoresEdges) for(size_t e = 0; e< 4; e++) if(edgs.owner->adyacencia[e] == nullptr) FindSectorEdgeAdjacency(edgs, (AdjacencySide)e, sectoresEdges);
La función FindSectorEdgeAdjacency() se ve así

Void CubeSectors::FindSectorEdgeAdjacency(SectorEdges &Sector, CubeSectors::AdjacencySide Side, std::vector &Neibs) ( SectorEdges::Edge &e = Sector.edges; for(SectorEdges &edgs2: Neibs)( if(edgs2.owner == Sector.owner) continuar; for(size_t e = 0; e< 4; e++){ SectorEdges::Edge &e2 = edgs2.edges[e]; if((Math::Equals(e.first, e2.first) && Math::Equals(e.second, e2.second)) || (Math::Equals(e.second, e2.first) && Math::Equals(e.first, e2.second))) { Sector.owner->adyacencia = edgs2.propietario;
edgs2.propietario->adyacencia[e] = Sector.propietario;

devolver; ) ) ) )
Tenga en cuenta que actualizamos los datos de adyacencia para dos sectores: el (Sector) buscado y el vecino encontrado.

Estándar::vector Ahora, usando los datos de adyacencia que recibimos, tenemos que encontrar aquellos bordes de los sectores que pertenecen al límite de los niveles de detalle. El plan es este: antes de dibujar, encontraremos los sectores límite. Luego, para cada sector en el búfer de instancia, además del sector principal
Después de haber distribuido los sectores por niveles de detalle, determinamos los coeficientes de teselación vecina para cada sector:

For(LodsStorage::Lod &lod: lods)( const std::vector §ores = lod.GetSectors();
bool lastLod = lod.GetInd() == lods.GetCount() - 1;

for(Sector *s: sectores)( int32_t tessFacor = s->GetTessFactor(); s->GetBorderTessFactor() = ( GetNeibTessFactor(s, Sector::ADJ_BOTTOM, tessFacor, lastLod), GetNeibTessFactor(s, Sector::ADJ_LEFT, tessFacor, lastLod), GetNeibTessFactor(s, Sector::ADJ_TOP, tessFacor, lastLod), GetNeibTessFactor(s, Sector::ADJ_RIGHT, tessFacor, lastLod) ) )< TessFactor) ? (float)neibTessFactor: 0.0f; }
Una función que busca un factor de teselación vecino:

Terreno flotante::GetNeibTessFactor(Sector *Sec, Sector::AdjacencySide Side, int32_t TessFactor, bool IsLastLod) ( Sector *neib = Sec->GetAdjacency(); int32_t neibTessFactor = neib->GetTessFactor(); return (neibTessFactor

Si devolvemos cero, entonces el vecino del lado Side no nos interesa. Permítanme adelantarme y decir que debemos eliminar las grietas del lado del nivel con un alto coeficiente de teselación.
Ahora pasemos al sombreador. Permítanme recordarles que primero necesitamos obtener las coordenadas de la cuadrícula usando las coordenadas del teselador. Luego estas coordenadas se convierten en un punto en la cara del cubo, este punto se normaliza y ahora tenemos un punto en la esfera:

Float3 p = Tri.netPos * Coord.x + Tri.netPos * Coord.y + Tri.netPos * Coord.z; float3 planePos = Tri.startPos + Tri.vec1 * p.x * gridStep.x + Tri.vec2 * p.y * gridStep.y; float3 sphPos = normalizar(planePos);
Primero debemos averiguar si el vértice pertenece a la primera o a la última fila de la cuadrícula, o a la primera o a la última columna; en este caso, el vértice pertenece a un borde del sector. Pero esto no es suficiente: necesitamos determinar si el vértice pertenece al límite LOD. Para ello utilizamos información sobre sectores vecinos, o más bien sus niveles de teselación: Float4 bTf = Tri.borderTessFactor; bool isEdge = (bTf.x != 0.0f && p.y == 0.0f) || //abajo (bTf.y != 0.0f && p.x == 0.0f) || //izquierda (bTf.z != 0.0f && p.y == gridSize.y) || //arriba (bTf.w != 0.0f && p.x == gridSize.x) //derecha- eliminando realmente las grietas. Mire la figura 20. La línea roja es la cara de un vértice perteneciente al segundo nivel de detalle. Las dos líneas azules son los bordes del tercer nivel de detalle. Necesitamos que V3 pertenezca a la línea roja, es decir, que se encuentre en el borde del segundo nivel. Como las alturas V1 y V2 son iguales para ambos niveles, V3 se puede encontrar mediante interpolación lineal entre ellos.


Fig. 20 Demostración de las caras que forman una grieta en forma de líneas.

Hasta ahora no tenemos ni V1 ni V2, ni el coeficiente F. Primero necesitamos encontrar el índice del punto V3. Es decir, si la malla tiene un tamaño de 32 por 32 y el coeficiente de teselación es cuatro, entonces este índice será de cero a 128 (32 * 4). Ya tenemos coordenadas en el espacio de la cuadrícula p - dentro este ejemplo pueden ser por ejemplo (15.5, 16). Para obtener el índice, es necesario multiplicar una de las coordenadas p por el coeficiente de teselación. También necesitamos el comienzo de la cara y la dirección hasta su final, una de las esquinas del sector.

Borde flotanteVertInd = 0.0f; float3 bordeVec = float3(0.0f, 0.0f, 0.0f); float3 startPos = float3(0.0f, 0.0f, 0.0f); uint neibTessFactor = 0; if(bTf.x != 0.0f && p.y == 0.0f)( // borde inferiorVertInd = p.x * Tri.tessFactor; edgeVec = Tri.vec1; startPos = Tri.startPos; neibTessFactor = (uint)Tri.borderTessFactor.x ; )else if(bTf.y != 0.0f && p.x == 0.0f)( // borde izquierdoVertInd = p.y * Tri.tessFactor; edgeVec = Tri.vec2; startPos = Tri.startPos; neibTessFactor = (uint)Tri. borderTessFactor.y )else if(bTf.z != 0.0f && p.y == gridSize.y)( // borde superiorVertInd = p.x * Tri.tessFactor; edgeVec = Tri.vec1; startPos = Tri.startPos + Tri.vec2 * (gridStep.x * gridSize.x); neibTessFactor = (uint)Tri.borderTessFactor.z; )else if(bTf.w != 0.0f && p.x == gridSize.x)( // borde derechoVertInd = p.y * Tri .tessFactor; edgeVec = Tri.vec2; startPos = Tri.startPos + Tri.vec1 * (gridStep.x * gridSize.x);
A continuación necesitamos encontrar los índices para V1 y V2. Imagina que tienes el número 3. Necesitas encontrar los dos números más cercanos que sean múltiplos de dos. Para hacer esto, calcula el resto al dividir tres por dos: es igual a uno. Luego restas o sumas este resto a tres y obtienes el resultado deseado. Lo mismo ocurre con los índices, pero en lugar de dos tendremos una relación de coeficientes de teselación de niveles de detalle. Es decir, si el tercer nivel tiene un coeficiente de 16 y el segundo tiene 2, entonces la proporción será igual a 8. Ahora, para obtener las alturas, primero debes obtener los puntos correspondientes en la esfera normalizando los puntos de la cara. Ya hemos preparado el comienzo y la dirección del borde; solo queda calcular la longitud del vector de V1 a V2. Dado que la longitud del borde de la celda de la cuadrícula original es gridStep.x, la longitud que necesitamos es gridStep.x / Tri.tessFactor. Luego, a partir de los puntos de la esfera, obtendremos la altura, como se describió anteriormente.

Float GetNeibHeight(float3 EdgeStartPos, float3 EdgeVec, float VecLen, float3 NormOffset) ( float3 neibPos = EdgeStartPos + EdgeVec * VecLen; neibPos = normalize(neibPos); return GetHeight(neibPos, NormOffset); ) float vertOffset = gridStep.x / Tri. tesFactor; uint tessRatio = (uint)tessFactor / (uint)neibTessFactor; uint ind = (uint)edgeVertInd % tessRatio; uint leftNeibInd = (uint)edgeVertInd - ind; float leftNeibHeight = GetNeibHeight(startPos, edgeVec, vertOffset * leftNeibInd, normOffset); uint rightNeibInd = (uint)edgeVertInd + ind; float rightNeibHeight = GetNeibHeight(startPos, edgeVec, vertOffset * rightNeibInd, normOffset);
Bueno, el último componente es el factor F. Lo obtenemos dividiendo el resto de la división entre la razón de los coeficientes (ind) por la razón de los coeficientes (tessRatio)

Factor de flotación = (flotante)ind / (flotante)tessRatio;
La etapa final es la interpolación lineal de alturas y la obtención de un nuevo vértice.

Float altura promedio = lerp(alturaNeibizquierda, alturaNeibderecha, factor); posL = sphPos * (radioesfera + altura promedio);
También puede aparecer una grieta donde se unen sectores con coordenadas de textura de borde iguales a 1 o 0. En este caso, tomo el valor promedio entre las alturas de las dos coordenadas:

Float GetHeight(float2 TexCoords) ( float2 texCoords2 = TexCoords * texCoordsScale; float mHeight = mainHeightTex.SampleLevel(mainHeightTexSampler, TexCoords, 0).x; float dHeight = distHeightTex.SampleLevel(distHeightTexSampler, texCoords 2, 0).x; return (mHeight + dHeight) * maxTerrainHeight; ) float GetHeight(float3 SphPos, float3 NormOffset) ( float2 texCoords1 = GetTexCoords(SphPos, NormOffset); float height = GetHeight(texCoords1); if(texCoords1.x == 1.0f)( float height2 = GetHeight ( float2(0.0f, texCoords1.y)); return lerp(altura, altura2, 0.5f); si no (texCoords1.x == 0.0f)( float height2 = GetHeight(float2(1.0f, texCoords1.y)) ) ; devolver lerp(altura, altura2, 0.5f);

11. Procesamiento de GPU

Traslademos el procesamiento del sector a la GPU. Tendremos dos sombreadores Compute: el primero realizará un recorte a lo largo de la pirámide de visibilidad y determinará el nivel de detalle, el segundo recibirá los coeficientes de teselación de límites para eliminar las grietas. La división en dos etapas es necesaria porque, como en el caso de la CPU, no podemos determinar correctamente los vecinos de los sectores hasta que realicemos la poda. Dado que ambos sombreadores utilizarán datos LOD y trabajarán con sectores, presenté dos estructura general: Sector y Lod

Sector de estructura ( float3 vec1, vec2; float3 startPos; float3 normCenter; int adyacencia; float borderTessFactor; int lod; ); estructura Lod ( RangeF dotRange; float tessFactor; relleno flotante; color float4; );
Usaremos tres buffers principales: de entrada (contiene información inicial sobre los sectores), intermedio (contiene datos de los sectores obtenidos como resultado de la primera etapa) y final (se transmitirá para el sorteo). Los datos del búfer de entrada no cambiarán, por lo que es razonable usar el valor D3D11_USAGE_IMMUTABLE en el campo Uso de la estructura D3D11_BUFFER_DESC. Simplemente escribiremos los datos de todos los sectores en él con la única diferencia de que para los datos de adyacencia usaremos índices de sector. , y no indicadores de ellos. Para el índice de nivel de detalle y los coeficientes de teselación de límites, establezca los valores en cero:

Estático const size_t sectorSize = sizeof(Vector3) + //vec1 sizeof(Vector3) + //vec2 sizeof(Point3F) + //normCenter sizeof(Point3F) + //startPos sizeof(Point4) + //adyacencia sizeof(Vector4) + //borderTessFactor sizeof(int32_t);//lod size_t sectoresDataSize = sectores.GetSectors().size() * sectorSize; estándar::vector sectoresData(sectoresDataSize); char* ptr = const Sector* primerPtr = §ors.GetSectors(); for(const Sector &sec: sectores)( Utils::AddToStream (ptr, sec.GetVec1()); Utilidades::AddToStream (ptr, sec.GetVec2()); Utilidades::AddToStream (ptr, sec.GetStartPos()); (ptr, sec.GetStartPos()); (ptr, sec.GetStartPos()); Utilidades::AddToStream (ptr, sec.GetNormCenter());
Utilidades::AddToStream

(ptr, sec.GetAdjacency() - firstPtr);
Utilidades::AddToStream

(ptr, Vector4()); Utilidades::AddToStream (ptr, 0); ) inputData = Utils::DirectX::CreateBuffer(§orsData,//Datos sin procesar sectoresDataSize,//Tamaño del búfer D3D11_BIND_SHADER_RESOURCE,//indicadores de enlace D3D11_USAGE_IMMUTABLE,//uso 0,//indicadores de acceso a CPU D3D11_RESOURCE_MISC_BUFFER_STRUCTURED,//indicadores varios sectorSize) ; // paso de byte de estructura< dotRange.minVal || dotVal >Ahora unas palabras sobre el búfer intermedio. Desempeñará dos funciones: salida para el primer sombreador y entrada para el segundo, por lo que especificaremos el valor D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE. También crearemos dos vistas para ello: UnorderedAccessView, que permitirá al sombreador escribir el resultado de su trabajo en él, y ShaderResourceView, con el que usaremos el búfer como entrada. Su tamaño será el mismo que el del búfer de entrada creado anteriormente.< 4; l++){ Lod lod = lods[l]; if(dotVal >UINT miscFlags = D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE; middleData = Utils::DirectX::CreateBuffer(sectors.GetSectors().size() * sectorSize,//Tamaño del búfer miscFlags, D3D11_USAGE_DEFAULT,//uso 0,//indicadores de acceso a la CPU D3D11_RESOURCE_MISC_BUFFER_STRUCTURED,//indicadores misceláneos sectorSize);/ /estructura byte paso intermedioUAW = Utils::DirectX::CreateUnorderedAccessView(intermediateData, D3D11_BUFFER_UAV(0, sectores.GetSectors().size(), 0)); middleSRV = Utils::DirectX::CreateShaderResourceView(intermediateData, D3D11_BUFFEREX_SRV(0, sectores.GetSectors().size(), 0));<= lod.dotRange.maxVal) sector.lod = l + 1; } outputData = sector; }
Después de calcular el producto escalar, comprobamos si el sector se encuentra en la región potencialmente visible. A continuación, aclaramos el hecho de su visibilidad usando la llamada IsVisible(), que es idéntica a la llamada Frustum::TestSphere() mostrada anteriormente. El funcionamiento de la función depende de las variables worldView, esferaRadius, frustumPlanesPosV y frustumPlanesNormalsV, cuyos valores deben pasarse al sombreador de antemano. A continuación definimos el nivel de detalle. Tenga en cuenta que indicamos el índice de nivel a partir de uno; esto es necesario para descartar en la segunda etapa aquellos sectores cuyo nivel de detalle es igual a cero.

Ahora necesitamos preparar los buffers para la segunda etapa. Queremos usar el búfer como salida para el sombreador Compute y como entrada para el teselador; para esto necesitamos especificar el valor D3D11_BIND_UNORDERED_ACCESS en el campo BindFlags. D3D11_BIND_VERTEX_BUFFER. Tendremos que trabajar con los datos del buffer directamente, por lo que indicaremos el valor D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS en el campo MiscFlags. Para mostrar dicho buffer usaremos el valor DXGI_FORMAT_R32_TYPELESS en el campo Flags, y en el campo NumElements indicaremos el total. buffer dividido por cuatro

Size_t instanciasByteSize = instanciaByteSize * sectores.GetSectors().size(); datos de salida = Utils::DirectX::CreateBuffer(instancesByteSize, D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_VERTEX_BUFFER, D3D11_USAGE_DEFAULT, 0, D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS, 0); D3D11_BUFFER_UAV uavParams = (0, instanciasByteSize / 4, D3D11_BUFFER_UAV_FLAG_RAW); salidaUAW = Utils::DirectX::CreateUnorderedAccessView(outputData, uavParams, DXGI_FORMAT_R32_TYPELESS);
También necesitamos un mostrador. Con su ayuda, abordamos la memoria en el sombreador y usamos su valor final en el argumento instanciaCount de la llamada DrawIndexedInstanced(). Implementé el contador como un búfer de 16 bytes. Además, al crear un mapeo en el campo Banderas del campo D3D11_BUFFER_UAV, utilicé el valor D3D11_BUFFER_UAV_FLAG_COUNTER

Contador = Utils::DirectX::CreateBuffer(sizeof(UINT), D3D11_BIND_UNORDERED_ACCESS, D3D11_USAGE_DEFAULT, 0, D3D11_RESOURCE_MISC_BUFFER_STRUCTURED, 4); D3D11_BUFFER_UAV uavParams = (0, 1, D3D11_BUFFER_UAV_FLAG_COUNTER); contadorUAW = Utils::DirectX::CreateUnorderedAccessView(contador, uavParams);
Es hora de dar el código para el segundo sombreador.

(ptr, Vector4()); datos de entrada: registro (t0); RWByteAddressBuffer datos de salida: registro (u0); RWStructuredBuffer contador: registro (u1);
void Process(int3 TId: SV_DispatchThreadID) ( int ind = TId.x; Sector sector = inputData; if(sector.lod != 0)( sector.borderTessFactor = GetNeibTessFactor(sector, 0); //Inferior sector.borderTessFactor = GetNeibTessFactor (sector, 1); //Sector izquierdo.borderTessFactor = GetNeibTessFactor(sector, 2); //Sector superior.borderTessFactor = GetNeibTessFactor(sector, 3); //Derecho int c = contador.IncrementCounter(); (c * tamaño de datos + 0, asuint(sector.startPos.x)); salidaData.Store(c * tamaño de datos + 4, asuint(sector.startPos.y)); .startPos.z)); salidaData.Store(c * tamañodatos + 12, asuint(sector.vec1.x)); salidaData.Store(c * tamañodatos + 16, asuint(sector.vec1.y)); (c * tamaño de datos + 20, asuint(sector.vec1.z)); datos de salida.Store(c * tamaño de datos + 24, asuint(sector.vec2.x)); .vec2.y)); salidaData.Store(c * tamañodatos + 32, asuint(sector.vec2.z)); salidaData.Store(c * tamañodatos + 36, asuint(sector.borderTessFactor));

salidaData.Store(c * dataSize + 40, asuint(sector.borderTessFactor));
salidaData.Store(c * dataSize + 44, asuint(sector.borderTessFactor)); salidaData.Store(c * dataSize + 48, asuint(sector.borderTessFactor)); Para conocer los métodos DrawIndexedInstancedIndirect() y CopyStructureCount(), consulte el Apéndice 2.

12. cámara

Seguramente sabes cómo construir un modelo de una cámara FPS (First Person Shooter) sencilla. Sigo este escenario:
  • 1. Desde dos ángulos obtengo el vector de dirección.
  • 2. usando el vector dirección y el vector (0, 1, 0) obtengo la base
  • 3. Según el vector de dirección y vacctor a la derecha, obtenidos en el paso 2, cambio la posición de la cámara

En nuestro caso, la situación es algo más complicada: en primer lugar, debemos movernos en relación con el centro del planeta y, en segundo lugar, al construir una base, en lugar del vector (0, 1, 0), debemos usar la normal de la esfera en el punto donde nos encontramos ahora. para alcanzar resultados deseados, Usaré dos bases. Según el primero, la posición cambiará, el segundo describirá la orientación de la cámara. Las bases son interdependientes, pero primero calculo la base de posición, así que comenzaré con eso. Supongamos que tenemos una base de posición inicial (pDir, pUp, pRight) y un vector de dirección vDir a lo largo del cual queremos movernos una cierta distancia. En primer lugar, necesitamos calcular las proyecciones de vDir sobre pDir y pRight. Sumándolos, obtenemos un vector de dirección actualizado (Fig. 21).


Fig. 21 Proceso visual de obtención de projDir

Donde P es la posición de la cámara, mF y mS son coeficientes, es decir, cuánto debemos movernos hacia adelante o hacia los lados.

No podemos usar PN como nueva posición de la cámara porque PN no pertenece a la esfera. En cambio, encontramos la normal de la esfera en el punto PN, y esta normal será el nuevo valor del vector ascendente. Ahora podemos formar una base actualizada.

Vector3 nUp = Vector3::Normalize(PN - esferaPos); Vector3 nDir = projDir Vector3 nDerecha = Vector3::Normalize(Vector3::Cross(pUp, pDir))
donde esferaPos es el centro de la esfera.

Necesitamos asegurarnos de que cada uno de sus vectores sea ortogonal a los otros dos. Según la propiedad del producto cruzado, nRight satisface esta condición. Queda por lograr lo mismo para nUp y nDir. Para hacer esto, proyecte nDir sobre nUp y reste el vector resultante de nDir (Fig. 22)


Fig.22 Ortogonalización de nDir con respecto a nUp

Podríamos hacer lo mismo con nUp, pero luego cambiaría de dirección, lo que en nuestro caso es inaceptable. Ahora normalizamos nDir y obtenemos una base de dirección ortonormal actualizada.

La segunda etapa clave es la construcción de una base de orientación. La principal dificultad es obtener el vector dirección. la solución más adecuada es convertir un punto con un ángulo polar a, un ángulo de acimut by una distancia del origen igual a uno de coordenadas esféricas a cartesianas. Sólo si realizamos dicha transición para un punto con un ángulo polar igual a cero, obtendremos un vector mirando hacia arriba. Esto no es del todo adecuado para nosotros, ya que incrementaremos los ángulos y asumiremos que dicho vector mirará hacia adelante. Simplemente cambiando el ángulo 90 grados se resolverá el problema, pero es más elegante usar la regla de cambio de ángulo, que establece que

Hagámoslo. Como resultado, obtenemos lo siguiente

Donde a es el ángulo polar, b es el ángulo de acimut.

Este resultado no es del todo adecuado para nosotros: necesitamos construir un vector de dirección con respecto a la base de posición. Reescribamos la ecuación para vDir:

Todo es como los astronautas: tanto en esta dirección, tanto en aquella dirección. Ahora debería ser obvio que si reemplazamos los vectores base estándar con pDir, pUp y pRight, obtendremos la dirección que necesitamos. Como esto

Puedes representar lo mismo en forma de multiplicación de matrices.

El vector vUp será inicialmente igual a pUp. Al calcular el producto cruzado de vUp y vDir, obtenemos vRight

Ahora nos aseguraremos de que vUp sea ortogonal al resto de vectores base. El principio es el mismo que cuando se trabaja con nDir.

Hemos resuelto lo básico; solo queda calcular la posición de la cámara. se hace asi

Donde esferaPos es el centro de la esfera, esferaRadius es el radio de la esfera y altura es la altura sobre la superficie de la esfera. Aquí está el código de funcionamiento de la cámara descrita:

Factor de movimiento flotante = 0,0 f, Factor lateral = 0,0 f, Factor de altura = 0,0 f; DirectInput::GetInsance()->ProcessKeyboardDown(( (DIK_W, [&]())(moveFactor = 1.0f;)), (DIK_S, [&]())(moveFactor = -1.0f;)), (DIK_D , [ &]())(sideFactor = 1.0f;)), (DIK_A, [&]())(sideFactor = -1.0f;)), (DIK_Q, [&]())(heightFactor = 1.0f; )), (DIK_E, [&]())(heightFactor = -1.0f;)) )); if(moveFactor != 0.0f || sideFactor != 0.0f)( Vector3 newDir = Vector3::Normalize(pDir * Vector3::Dot(pDir, vDir) + pRight * Vector3::Dot(pRight, vDir)); Point3F nuevaPos = pos + (nuevaDir * moveFactor + pDerecha * sideFactor) * Tf * velocidad; pArriba = Vector3::Normalize(nuevaPos - pDir = Vector3::Normalize(pDir - pArriba * Vector3::Punto(pArriba); pDir)); pos = esferaPos + pUp * (esferaRadio + altura); if(heightFactor != 0.0 f)( altura = Math::Saturate(altura + heightFactor * Tf * velocidad, heightRange); pos = esferaPos + pUp * ( esferaRadio + altura ) DirectInput::MouseState mState = DirectInput::GetInsance()->GetMouseDelta( ); if(mState.x != 0 || mState.y != 0 || moveFactor != 0.0f || sideFactor != 0.0f)( if(mState.x != 0) ángulos.x = ángulos.x + mState .x / 80.0f; if(mState.y != 0) ángulos.y = Math::Saturate(angles.y + mState.y / 80.0f, RangeF(-Pi * 0.499f, Pi * 0.499f)); vDir = Vector3::Normalize(pRight * sinf(angles.x) * cosf(angles.y) + pUp * -sinf(angles.y) + pDir * cosf(angles.x) * cosf(angles.y)); vUp = pUp; vRight = Vector3::Normalize(Vector3::Cross(vUp, vDir)); vUp = Vector3::Normalize(vUp - vDir * Vector3::Dot(vDir, vUp)); (((vDerecha, 0.0f), (vArriba, 0.0f), (vDir, 0.0f), (pos, 1.0f)));
Tenga en cuenta que restablecemos angles.x después de actualizar la base de posición. Esto es fundamental. Imaginemos que simultáneamente cambiamos el ángulo de visión y nos movemos alrededor de la esfera. Primero proyectaremos el vector de dirección en pDir y pRight, obtendremos el desplazamiento (newPos) y actualizaremos la posición en función de él. La segunda condición también funcionará y comenzaremos a actualizar la base de orientación. Pero como pDir y pRight ya han sido cambiados dependiendo de vDir, sin restablecer el ángulo de azimut (angles.x) la rotación será más “empinada”

Conclusión

Agradezco al lector su interés en el artículo. Espero que la información contenida en él le haya resultado accesible, interesante y útil. Pueden enviarme sugerencias y comentarios por correo. [correo electrónico protegido] o dejarlo como comentario.

¡Te deseo éxito!

Apéndice 1

el campo InstanceDataStepRate contiene información sobre cuántas veces dibujar los datos D3D11_INPUT_PER_VERTEX_DATA para un elemento D3D11_INPUT_PER_INSTANCE_DATA. En nuestro ejemplo, todo es simple: uno a uno. “¿Pero por qué necesitamos dibujar lo mismo varias veces?” - preguntas. Una pregunta razonable. La radio armenia responde: supongamos que tenemos 99 bolas de tres colores diferentes. Podemos describir el vértice de esta manera:

Colores UINTTasa = 99/3; estándar::vector meta = (("POSICIÓN", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0), ("NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0), ( "TEXCOORD ", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0), ("MUNDO", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D11_INPUT_PER_INSTANCE_DATA, 1), ("MUNDO", 1, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, D3D11_INPUT_PER_INSTANCE_DATA, 1), ("MUNDO", 2, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 32, D3D11_INPUT_PER_INSTANCE_DATA, 1), ("MUNDO", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 48, D3D11_INPUT_PER_INSTANCE_DATA, 1), ("COLOR", 0, FORMAT_R32G32B32A32_FLOAT, 2, 0, D3D11_INPUT_PER_INSTANCE_DATA, coloresRate),);
Tenga en cuenta que el vértice se recopila de tres fuentes y los datos de esta última se actualizan una vez cada 33 "instancias". Como resultado, obtendremos 33 instancias del primer color, otras 33 del segundo, etc. Ahora creemos los buffers. Además, dado que los colores no cambiarán, podemos crear un búfer con colores con el indicador D3D11_USAGE_IMMUTABLE. Esto significa que después de inicializar el búfer, solo la GPU tendrá acceso de solo lectura a sus datos. Aquí está el código para crear los buffers:

MatricesTb = Utils::DirectX::CreateBuffer(sizeof(Matrix4x4) * 99, D3D11_BIND_VERTEX_BUFFER, D3D11_USAGE_DYNAMIC, D3D11_CPU_ACCESS_WRITE); coloresTb = Utils::DirectX::CreateBuffer(colores, D3D11_BIND_VERTEX_BUFFER, D3D11_USAGE_IMMUTABLE, 0);
luego, si es necesario, actualizamos el búfer con matrices (yo uso las funciones de mi biblioteca, espero que todo quede claro)

Utilidades::DirectX::Mapa (matricesTb, [&](Matrix4x4 *Data) ( //primero escribimos datos para las bolas del primer color en el buffer //luego para el segundo, etc. Tenga en cuenta que la correspondencia necesaria de //datos con los colores debe ser asegurado en la etapa de generación de los datos del buffer ));
El acceso a datos en el sombreador se puede implementar de la misma manera que describí anteriormente.


Apéndice 2

A diferencia de DrawIndexedInstanced(), llamar a DrawIndexedInstancedIndirect() toma como argumento un búfer que contiene toda la información que utiliza para llamar a DrawIndexedInstanced(). Además, este búfer debe crearse con el indicador D3D11_RESOURCE_MISC_DRAWINDIRECT_ARGS. A continuación se muestra un ejemplo de cómo crear un búfer:

//indicesCnt - el número de índices que queremos mostrar //instancesCnt - el número de "instancias" que queremos mostrar std::vector args = ( indicesCnt, //IndexCountPerInstancestancesCnt, //InstanceCount 0, //StartIndexLocation 0,//BaseVertexLocation 0//StartInstanceLocation); D3D11_BUFFER_DESC bd = (); bd.Uso = D3D11_USAGE_DEFAULT; bd.ByteWidth = tamaño de (UINT) * args.size(); bd.BindFlags = 0; bd.CPUAccessFlags = 0; bd.MiscFlags = D3D11_RESOURCE_MISC_DRAWINDIRECT_ARGS; bd.StructureByteStride = 0; Búfer ID3D11Buffer*; D3D11_SUBRESOURCE_DATA initData = (); initData.pSysMem = HR(DeviceKeeper::GetDevice()->CreateBuffer(&bd, &initData, &buffer)); ejemplo de llamada a DrawIndexedInstancedIndirect(): DeviceKeeper::GetDeviceContext()->DrawIndexedInstancedIndirect(indirectArgs, 0);
como segundo argumento pasamos el desplazamiento en bytes desde el comienzo del búfer desde el cual necesitamos comenzar a leer datos. ¿Cómo se puede utilizar esto? Por ejemplo, al implementar un recorte de geometría invisible en la GPU. En general, la cronología es la siguiente: primero, en el sombreador Compute, llenamos el AppendStructuredBuffer, que contiene datos de geometría visibles. Luego, usando CopyStructureCount() establecemos la cantidad de instancias que queremos mostrar en la cantidad de entradas en este búfer y llamamos a DrawIndexedInstancedIndirect()


Apéndice 3

Supongamos que el valor de la coordenada x es igual al resultado de la función X con el argumento a, y el valor de la coordenada z es el resultado de la función Z con el mismo argumento:

Ahora necesitamos calcular la derivada de cada función. Esencialmente, la derivada de la función en punto dado igual a la tasa de cambio de los valores de la función exactamente en este punto. Según las reglas para derivar funciones trigonométricas:

Lo que finalmente nos da el mismo resultado:

¿Por qué podemos utilizar valores de velocidad como componentes de un vector de dirección? Así lo entiendo yo. Imaginemos que tenemos una función vectorial (para t >= 0):

Calculemos la derivada de la coordenada X.

Ahora para Y

Hemos encontrado que el vector velocidad es igual a (2, 3), ahora encontremos el punto de partida

Como resultado, podemos expresar la función P(t) de la siguiente manera:

Lo cual en palabras simples se puede describir como “el punto se mueve desde el origen con coordenadas (3, 2) hasta t en la dirección (2, 3)”. Ahora tomemos otro ejemplo:

Calculemos nuevamente la derivada de la coordenada X.

Y para la coordenada Y

Ahora el vector de velocidad cambia según el argumento. En este caso, en palabras simples, la situación se puede describir de la siguiente manera: "El punto se mueve desde el origen con coordenadas (3, 2) y la dirección de su movimiento cambia constantemente".


Apéndice 4

Definamos una función F(H) que tomará la altura en el área y devolverá un valor entre 0 y 1, donde F(Hmin) = 0 y F(Hmax) = 1. Resolver el sistema de ecuaciones

Recibí

Como resultado, la función F toma la forma

Ahora necesitamos una función que tome un factor de altura entre 0 y 1 y devuelva el valor mínimo para el producto escalar. Hay que tener en cuenta que cuanto más cerca esté el observador de la superficie, más grande será. Esto da como resultado la siguiente ecuación:

Abramos los corchetes

Simplifiquemos y obtengamos

Ahora expresamos D(F(H)) y obtenemos

Etiquetas:

  • directox11
  • renderizado del terreno
Agregar etiquetas

Durante la evolución de la Tierra, el cambio en la apariencia de los paisajes terrestres fue una reacción a la transformación de las condiciones naturales. Toda la diversidad de la envoltura geográfica, conocida como geosistemas, paisajes o complejos naturales, refleja los resultados de diversas manifestaciones de temperatura y humedad, que a su vez están sujetas al equilibrio de radiación.

Estos sistemas dinámicos de diferentes rangos, caracterizados por la integridad, la interacción especial de sus elementos constitutivos y su funcionamiento, productividad y apariencia, forman colectivamente la envoltura geográfica y se relacionan con ella como partes de un todo. Tienen su propio potencial natural (recursos naturales), cuyas mediciones permiten clasificar los geosistemas y estudiar sus cambios. El principio unificador de estas estructuras es el intercambio de flujos de materia y energía, su acumulación y consumo parcial. Así, el intercambio de energía y masa dentro de la envoltura geográfica sirve de base para su diferenciación, y sus cambios se reflejan en la apariencia de la superficie terrestre. Este proceso asegura la zonificación y zonalidad geográfica moderna de la Tierra y la diversidad de paisajes específicos de diversos grados de organización.

Sin embargo, durante la evolución de la envoltura geográfica, los cambios en sus sistemas terrestres también estuvieron asociados con procesos y fenómenos profundos, expresados ​​​​en parte en la superficie (zonas de vulcanismo, sismicidad, formación de montañas, etc.). Al mismo tiempo, junto con los cambios directos en la base litogénica de los paisajes y la envoltura geográfica en su conjunto, este último recibió materia y energía adicionales, lo que se reflejó en el funcionamiento de sus componentes individuales y del sistema en su conjunto. Esta “complementariedad” (a veces, probablemente significativa) se manifestó no sólo cuantitativamente, en la circulación global de materia y energía, sino también en cambios cualitativos en los componentes individuales. El papel de los procesos de desgasificación de la Tierra y su intercambio de energía y masa con la atmósfera y la hidrosfera aún no se ha estudiado suficientemente. Sólo desde mediados del siglo XX. Apareció información sobre la composición material de la materia del manto y sus características cuantitativas.

La investigación de V.I. Bgatov ha establecido que el oxígeno atmosférico no es tanto de origen fotosintético como profundo. El esquema generalmente aceptado del ciclo del carbono en la naturaleza debe corregirse mediante el suministro de sus compuestos desde las entrañas de la tierra, en particular durante las erupciones volcánicas. Al parecer, cantidades no menores de la sustancia entran en la capa de agua durante las erupciones submarinas, especialmente en zonas de expansión, arcos de islas volcánicas y en puntos calientes individuales. La cantidad total anual de compuestos de carbono provenientes del subsuelo hacia el océano y la atmósfera es comparable a la masa de formación anual de carbonatos en los cuerpos de agua y, aparentemente, excede el volumen de acumulación de carbono orgánico por las plantas terrestres.

El calentamiento climático natural y su intensificación antropogénica deberían provocar un cambio en los límites de las zonas y zonas geográficas y contribuir a la modificación de los paisajes individuales.

Sin embargo, el desarrollo de la sociedad humana y la expansión de sus necesidades y capacidades conducen a la reestructuración artificial de complejos naturales de diferentes escalas y a la formación de paisajes culturales que afectan el funcionamiento de la envoltura geográfica, alterando el curso natural. Entre estos impactos, los más obvios son los siguientes:

Tenga en cuenta que la suma de las emisiones anuales de contaminantes no está totalmente justificada teórica y prácticamente, ya que al ingresar al entorno geográfico se asimilan, se transforman bajo la influencia de otros y funcionan de manera diferente. Es importante analizar cada liberación antropogénica importante, teniendo en cuenta sus reacciones con los compuestos existentes.

Un cambio en la energía de la capa geográfica o sus partes provoca una reestructuración de la estructura interna y los procesos de funcionamiento del geosistema y fenómenos relacionados. Este proceso es complejo y está regulado por múltiples conexiones directas y de retroalimentación (figura 9.4). Los impactos antropogénicos en el entorno geográfico provocan cambios en la composición y el estado del medio ambiente, alteran la composición cuantitativa y cualitativa de la materia viva (hasta mutaciones) y modifican los sistemas existentes de intercambio de energía, masa y humedad. Sin embargo, la evidencia actualmente disponible sugiere que los cambios antropogénicos no afectan fundamentalmente la envoltura geográfica. El equilibrio relativo de su existencia y la sostenibilidad del desarrollo están garantizados principalmente por causas naturales, cuya escala excede la influencia humana. De esto no se sigue que la propia envoltura geográfica siempre supere la creciente presión antropogénica. Las intervenciones en la naturaleza deben regularse desde el punto de vista de la conveniencia de sus manifestaciones, en beneficio de la humanidad y sin daños significativos al medio ambiente natural. Los conceptos que se desarrollan en esta dirección se denominan desarrollo sostenible (equilibrado). Deben basarse en patrones geológicos generales y en las características del estado actual y el desarrollo de la envoltura geográfica.

Para concluir, abordemos la afirmación emergente de que la envoltura geográfica moderna se está volviendo cada vez más antroposfera, o parte de emergentes noosfera. Tenga en cuenta que el concepto de "noosfera" es en gran medida de naturaleza filosófica. El impacto humano sobre el medio ambiente y la implicación de productos de desecho en él es un fenómeno innegable. Es importante comprender que la mayoría de las veces una persona cambia su hábitat no conscientemente, sino a través de consecuencias imprevistas. Además, estas implementaciones no están dirigidas a todos los componentes de la envoltura geográfica, sino sólo a los componentes necesarios para las personas (bosques, suelo, materias primas, etc.). Así, sólo hay focos de cambio, aunque a veces muy significativos y graves, y aunque la actividad humana aumenta, la naturaleza todavía se desarrolla principalmente bajo la influencia de procesos naturales. Por lo tanto, en la actualidad deberíamos hablar de determinadas zonas de la envoltura geográfica, donde el entorno natural cambia significativamente y se desarrolla bajo la influencia de procesos regulados por el hombre.

Arroz. 9.4. Algunas retroalimentaciones que rigen el clima global

Durante la evolución de la Tierra, el cambio en la apariencia de los paisajes terrestres fue una reacción a la transformación de las condiciones naturales. Toda la diversidad de la envoltura geográfica, conocida como geosistemas, paisajes o complejos naturales, refleja los resultados de diversas manifestaciones de temperatura y humedad, que a su vez están sujetas al equilibrio de radiación.

Estos sistemas dinámicos de diferentes rangos, caracterizados por la integridad, la interacción especial de sus elementos constitutivos y su funcionamiento, productividad y apariencia, forman colectivamente la envoltura geográfica y se relacionan con ella como partes de un todo. Tienen su propio potencial natural (recursos naturales), cuyas mediciones permiten clasificar los geosistemas y estudiar sus cambios. El principio unificador de estas estructuras es el intercambio de flujos de materia y energía, su acumulación y consumo parcial. Así, el intercambio de energía y masa dentro de la envoltura geográfica sirve de base para su diferenciación, y sus cambios se reflejan en la apariencia de la superficie terrestre. Este proceso asegura la zonificación y zonalidad geográfica moderna de la Tierra y la diversidad de paisajes específicos de diversos grados de organización.

Sin embargo, durante la evolución de la envoltura geográfica, los cambios en sus sistemas terrestres también estuvieron asociados con procesos y fenómenos profundos, expresados ​​​​en parte en la superficie (zonas de vulcanismo, sismicidad, formación de montañas, etc.). Al mismo tiempo, junto con los cambios directos en la base litogénica de los paisajes y la envoltura geográfica en su conjunto, este último recibió materia y energía adicionales, lo que se reflejó en el funcionamiento de sus componentes individuales y del sistema en su conjunto. Esta “complementariedad” (a veces, probablemente significativa) se manifestó no sólo cuantitativamente, en la circulación global de materia y energía, sino también en cambios cualitativos en los componentes individuales. El papel de los procesos de desgasificación de la Tierra y su intercambio de energía y masa con la atmósfera y la hidrosfera aún no se ha estudiado suficientemente. Sólo desde mediados del siglo XX. Apareció información sobre la composición material de la materia del manto y sus características cuantitativas.

La investigación de V.I. Bgatov ha establecido que el oxígeno atmosférico no es tanto de origen fotosintético como profundo. El esquema generalmente aceptado del ciclo del carbono en la naturaleza debe corregirse mediante el suministro de sus compuestos desde las entrañas de la tierra, en particular durante las erupciones volcánicas. Al parecer, cantidades no menores de la sustancia entran en la capa de agua durante las erupciones submarinas, especialmente en zonas de expansión, arcos de islas volcánicas y en puntos calientes individuales. La cantidad total anual de compuestos de carbono provenientes del subsuelo hacia el océano y la atmósfera es comparable a la masa de formación anual de carbonatos en los cuerpos de agua y, aparentemente, excede el volumen de acumulación de carbono orgánico por las plantas terrestres.

El calentamiento climático natural y su intensificación antropogénica deberían provocar un cambio en los límites de las zonas y zonas geográficas y contribuir a la modificación de los paisajes individuales.

Sin embargo, el desarrollo de la sociedad humana y la expansión de sus necesidades y capacidades conducen a la reestructuración artificial de complejos naturales de diferentes escalas y a la formación de paisajes culturales que afectan el funcionamiento de la envoltura geográfica, alterando el curso natural. Entre estos impactos, los más obvios son los siguientes:

1) La creación de embalses y sistemas de riego cambia el albedo de la superficie, el régimen de intercambio de calor y humedad, lo que, a su vez, afecta la temperatura del aire y la nubosidad.

2) La conversión de tierras en tierras agrícolas o la destrucción de la vegetación (deforestación masiva) cambia el albedo y las condiciones térmicas, altera el ciclo de sustancias debido a una reducción de las superficies activas para la fotosíntesis. El impacto más significativo en términos de escala fue el desarrollo masivo de tierras vírgenes y en barbecho, cuando muchos millones de hectáreas de pastos verdes y tierras en barbecho fueron aradas y sembradas. El aumento de la capacidad de absorción de la superficie terrestre, la alteración de su rugosidad y la continuidad del suelo y la cubierta vegetal cambiaron el balance de radiación, provocaron una transformación en la circulación de las masas de aire y un aumento de los vientos, lo que provocó tormentas de polvo y una disminución. en la transparencia de la atmósfera. El resultado de las transformaciones fue el traslado de paisajes productivos estables a otros inestables con la intensificación de los procesos de desertificación y el riesgo en el uso del suelo.

3) La redistribución de la escorrentía superficial (regulación del flujo, creación de presas y embalses) conduce con mayor frecuencia al inundamiento de las áreas circundantes. Al mismo tiempo, cambia el albedo de la superficie subyacente, aumenta la humedad, la frecuencia de las nieblas, la nubosidad y la permeabilidad del aire, lo que altera la transferencia natural de calor y masa entre la superficie terrestre y la atmósfera. La represa del flujo de agua y la formación de espacios pantanosos cambian la naturaleza de la descomposición de los desechos vegetales, lo que provoca la entrada en la atmósfera de cantidades adicionales de gases de efecto invernadero (dióxido de carbono, metano, etc.), cambiando su composición y transparencia.

4) La creación de estructuras hidroeléctricas en los ríos, la construcción de represas con la formación de cascadas de agua que caen durante todo el año, cambian el régimen anual de los ríos, alteran la situación del hielo, la distribución de los sedimentos transportables y transforman el sistema río-atmósfera. Los embalses no helados con niebla constante y evaporación de la superficie del agua (incluso en invierno) afectan el curso de las temperaturas, la circulación de las masas de agua, empeoran las condiciones climáticas y cambian el hábitat de los organismos vivos. Impacto de la central hidroeléctrica en grandes ríos(Yenisei, Angara, Kolyma, Volga, etc.) se sienten a decenas de kilómetros río abajo y en todas las partes represadas de los embalses, y los cambios climáticos generales cubren cientos de kilómetros cuadrados. El lento suministro de sedimentos fluviales y su redistribución provocan la alteración de los procesos geomorfológicos y la destrucción de las desembocaduras y riberas de los ríos. piscinas de agua(por ejemplo, la destrucción del delta del Nilo y de la parte sureste de la costa mediterránea tras la construcción de la presa de Asuán y su interceptación de una parte importante de los sedimentos sólidos transportados por el río).

5) Los trabajos de recuperación, acompañados del drenaje de grandes espacios, alteran el régimen existente de calor, humedad e intercambio y contribuyen al desarrollo de retroalimentación negativa durante la transformación de los paisajes. Así, el secado excesivo de los sistemas pantanosos en varias regiones (Polesie, Región de Novgorod, Región de Irtysh) provocó la muerte de la cubierta vegetal natural y la aparición de procesos de deflación, que incluso en zonas con suficiente humedad formaron arenas movedizas. Como resultado, aumentó el polvo de la atmósfera, aumentó la rugosidad de la superficie y cambió el régimen del viento.

6) Un aumento de la rugosidad de la superficie terrestre durante la construcción de diversas estructuras (edificios, minas y vertederos, almacenes industriales, etc.) provoca cambios en las condiciones del viento, los niveles de polvo y las características meteorológicas y climáticas.

7) Diversos contaminantes que ingresan en grandes cantidades a todos los entornos naturales cambian, en primer lugar, la composición material y la capacidad energética del aire, el agua, las formaciones superficiales, etc. Este cambio en los agentes naturales determina la transformación de los procesos naturales que llevan a cabo. así como diversas interacciones con el medio ambiente y otros factores naturales.

Tenga en cuenta que la suma de las emisiones anuales de contaminantes no está totalmente justificada teórica y prácticamente, ya que al ingresar al entorno geográfico se asimilan, se transforman bajo la influencia de otros y funcionan de manera diferente. Es importante analizar cada liberación antropogénica importante, teniendo en cuenta sus reacciones con los compuestos existentes.

Un cambio en la energía de la capa geográfica o sus partes provoca una reestructuración de la estructura interna y los procesos de funcionamiento del geosistema y fenómenos relacionados. Este proceso es complejo y está regulado por múltiples conexiones directas y de retroalimentación (figura 9.4). Los impactos antropogénicos en el entorno geográfico provocan cambios en la composición y el estado del medio ambiente, alteran la composición cuantitativa y cualitativa de la materia viva (hasta mutaciones) y modifican los sistemas existentes de intercambio de energía, masa y humedad. Sin embargo, la evidencia actualmente disponible sugiere que los cambios antropogénicos no afectan fundamentalmente la envoltura geográfica. El equilibrio relativo de su existencia y la sostenibilidad del desarrollo están garantizados principalmente por causas naturales, cuya escala excede la influencia humana. De esto no se sigue que la propia envoltura geográfica siempre supere la creciente presión antropogénica. Las intervenciones en la naturaleza deben regularse desde el punto de vista de la conveniencia de sus manifestaciones, en beneficio de la humanidad y sin daños significativos al medio ambiente natural. Los conceptos que se desarrollan en esta dirección se denominan desarrollo sostenible (equilibrado). Deben basarse en patrones geológicos generales y en las características del estado actual y el desarrollo de la envoltura geográfica.

Para concluir, abordemos la afirmación emergente de que la envoltura geográfica moderna se está volviendo cada vez más antroposfera, o parte de emergentes noosfera. Tenga en cuenta que el concepto de "noosfera" es en gran medida de naturaleza filosófica. El impacto humano sobre el medio ambiente y la implicación de productos de desecho en él es un fenómeno innegable. Es importante comprender que la mayoría de las veces una persona cambia su hábitat no conscientemente, sino a través de consecuencias imprevistas. Además, estas implementaciones no están dirigidas a todos los componentes de la envoltura geográfica, sino sólo a los componentes necesarios para las personas (bosques, suelo, materias primas, etc.). Así, sólo hay focos de cambio, aunque a veces muy significativos y graves, y aunque la actividad humana aumenta, la naturaleza todavía se desarrolla principalmente bajo la influencia de procesos naturales. Por lo tanto, en la actualidad deberíamos hablar de determinadas zonas de la envoltura geográfica, donde el entorno natural cambia significativamente y se desarrolla bajo la influencia de procesos regulados por el hombre.

Arroz. 9.4. Algunas retroalimentaciones que rigen el clima global

Preguntas de seguridad

¿Qué fenómenos se clasifican como cambios globales en la envoltura geográfica?

¿Cuáles son las características específicas de los cambios globales de finales del siglo XX y principios del XXI?

¿Qué es el efecto invernadero y cuáles son sus consecuencias?

Qué es problema común¿Antropogenización de la envoltura geográfica?

¿Cuál es el problema con el calentamiento climático?

¿Cuáles son los peligros de la contaminación por petróleo?

¿Qué es la crisis ambiental global, cómo y dónde se manifiesta?

¿Cuál es el significado de visiones optimistas y pesimistas sobre el desarrollo del planeta Tierra?

Que impacto hielo polar¿Tiene un impacto en la envoltura geográfica?

¿Qué son los cambios del paisaje terrestre?

LITERATURA

Alpatiev A. M. Desarrollo, transformación y protección del medio natural. - L., 1983.

Balandin R.K., Bondarev L.G. Naturaleza y civilización. - M., 1988.

Indicación biológica en antropoecología. - L., 1984.

Bitkaeva L.Kh., Nikolaev V.A. Paisajes y desertificación antropogénica de Terek Sands. - M., 2001.

Bokov V.A., Lushchik A.V. Fundamentos de la seguridad ambiental. - Simferópol, 1998.

Vernadsky V.I. Biosfera y noosfera. - M., 1989.

Problemas geográficos de finales del siglo XX / Rep. ed. Yu.P.Seliverstov. - San Petersburgo, 1998.

Geografía y medio ambiente / Responsable. ed. N. S. Kasimov, S. M. Malkhazova. - M., 2000.

Cambios globales en el medio natural (clima y régimen hídrico) / Rep. ed. N.S. - M., 2000.

Los cambios climáticos globales y regionales y sus consecuencias naturales y socioeconómicas / Responsable. ed. V.M.Kotliakov. - M., 2000.

Global problemas ambientales en el umbral del siglo XXI / Rep. ed. FT Yanshina. - M., 1998.

Govorushko S. M. La influencia de los procesos naturales en la actividad humana. - Vladivostok, 1999.

Golubev G.N. Geoecología. - M., 1999.

Gorshkov V. G. Fundamentos físicos y biológicos de la sostenibilidad de la vida. - M., 1995.

Gorshkov SP. Fundamentos conceptuales de la geoecología. - Smolensk, 1998.

Grigóriev A. A. Lecciones ecológicas del pasado y del presente. - L., 1991.

Grigoriev A. A., Kondratiev K. Ya. Ecodinámica y geopolítica. - T. 11. Desastres ambientales. - San Petersburgo, 2001.

Gumilyov L. N. Etnogénesis y biosfera de la Tierra. - L., 1990.

Danilov A.D., Korol I.L. Ozono atmosférico: sensaciones y realidad. - L., 1991.

Dotto l. El planeta Tierra está en peligro. - M., 1988.

Zaletaev V.S. Entorno ecológicamente desestabilizado. Ecosistemas de zonas áridas en un régimen hidrológico cambiante. - M., 1989.

Tierra y humanidad. Problemas globales/ Países y pueblos. - M., 1985.

Zubakov V.A. Ecogea - Casa Tierra. Brevemente sobre el futuro. Contornos del concepto ecogay de una salida a la crisis ambiental global. - San Petersburgo, 1999.

Zubakov V.A. Casa Tierra. Contornos de la cosmovisión ecogeosófica. (Desarrollo científico de la estrategia de mantenimiento). - San Petersburgo, 2000.

Isáchenko A.G. Optimización del entorno natural. - M., 1980.

Isáchenko A.G. Geografía ecológica de Rusia. - San Petersburgo, 2001.

Kondratyev K. Ya. Clima global. - M., 1992.

Kotliakov V. M. Ciencia. Sociedad. Ambiente. - M., 1997.

Kotlyakov V.M., Grosvald M.G., Lorius K. Climas pasados ​​desde las profundidades de las capas de hielo. - M., 1991.

Lavrov S.B., Sdasyuk G.V. Este mundo contrastante. - M., 1985.

Medio Ambiente / Ed. A. M. Ryabchikova. - M., 1983.

Fundamentos de Geoecología / Ed. V. G. Morachevsky. - San Petersburgo, 1994.

Petrov K. M. Procesos naturales de restauración de tierras devastadas. - San Petersburgo, 1996.

Problemas de la ecología en Rusia / Responsable. ed. V. I. Danilov-Danilyan, V. M. Kotlyakov. - M., 1993.

Rusia en el mundo que nos rodea: 1998. Colección analítica / Ed. ed. N.N.Moiseeva, S.A. Stepanova. - M., 1998.

Rown S. Crisis del ozono. La evolución en quince años de una amenaza global inesperada. - M., 1993.

Sociedad Geográfica Rusa: nuevas ideas y caminos / Responsable. ed. A.O.Brinken, S.B.Lavrov, Yu.P. - San Petersburgo, 1995.

Seliverstov Yu. El problema del riesgo ambiental global // Noticias de la Sociedad Geográfica Rusa. - 1994. - Edición. 2.

Seliverstov Yu. Antropogenización de la naturaleza y el problema de la crisis ambiental // Vestnik San Petersburgo. Universidad. - 1995. - Serie. 7.- Asunto. 2.

Seliverstov Yu. Crisis ambiental planetaria: causas y realidades // Vestnik San Petersburgo. Universidad. - 1995. - Serie. 7.- Asunto. 4.

Fortescue J. Geoquímica del medio ambiente. - M., 1985.

Alternativa ecológica / Ed. ed. M.Ya. - M., 1990.

Imperativos ambientales del desarrollo sostenible de Rusia / Ed. V.T.Pulyaeva.-L., 1996.

Problemas ambientales: ¿qué está pasando, quién tiene la culpa y qué hacer? / Ed. V.I.Danilov-Danilyan. - M., 1997.

Yanshin A.L., Melua A.I. Lecciones de las crisis ambientales. - M., 1991.

La superficie de la Tierra no permanece sin cambios. Durante los millones de años que ha existido nuestro planeta, su apariencia ha estado constantemente influenciada por diversas fuerzas naturales. Los cambios que se producen en la superficie terrestre son provocados tanto por fuerzas internas como por lo que sucede en la atmósfera.

Así, las montañas se formaron como resultado del movimiento de la corteza terrestre. Las masas rocosas fueron empujadas hacia la superficie, aplastadas y rotas, dando como resultado la formación de varios tipos de montañas. Con el paso del tiempo, la lluvia y las heladas aplastaron las montañas, creando acantilados y valles separados.

Algunas montañas se formaron como resultado de erupciones volcánicas. La roca fundida burbujeó hacia la superficie de la Tierra a través de agujeros en la corteza, capa por capa, hasta que finalmente emergió una montaña. El Vesubio en Italia es una montaña de origen volcánico.

También se pueden formar montañas volcánicas bajo el agua. Por ejemplo, las islas hawaianas son los picos de montañas volcánicas.

El sol, el viento y el agua provocan una constante destrucción de las rocas. Este proceso se llama erosión. Pero no sólo puede afectar a las rocas. Así, la erosión causada por el hielo, el viento y el agua arrastra el suelo de la Tierra.

En los lugares donde se deslizan hacia el mar, los glaciares cortan las llanuras, formando valles y fiordos, bahías marinas estrechas y sinuosas.

Los fiordos se formaron durante edad de hielo cuando los continentes estaban cubiertos por una gruesa capa de hielo y nieve.

Este hielo, a su vez, provocó la formación de glaciares, que son ríos de hielo de lento movimiento.

Deslizándose desde las montañas hacia los valles, se abrieron paso los glaciares, cuyo espesor de hielo a veces alcanzaba varias decenas de metros. La fuerza de su movimiento fue muy grande.

Al principio, a lo largo del camino de los glaciares se formaron estrechas gargantas, luego la monstruosa fuerza del glaciar las amplió, abriendo el camino hacia abajo. Poco a poco este espacio se hizo más profundo y más amplio.

Después del final de la Edad del Hielo, el hielo y la nieve comenzaron a derretirse. A medida que el hielo se derritió, aumentó el ancho de los ríos. Al mismo tiempo, el nivel del mar estaba subiendo. Así, en lugar de ríos, se formaron fiordos.

Las orillas de los fiordos suelen ser laderas rocosas, que a veces alcanzan alturas de 1.000 metros (3.000 pies).

Algunos fiordos son tan profundos que los barcos pueden atravesarlos.

Una gran cantidad de fiordos se encuentran en las costas de Finlandia y Groenlandia. Pero los fiordos más bellos se encuentran en Noruega. El fiordo más largo también se encuentra en Noruega. Se llama Sognefjord. Su longitud es de 180 kilómetros (113 millas).

Cuando el hielo se derrite, las morrenas (acumulaciones de fragmentos de roca) quedan atrás y forman picos montañosos en zigzag. Los ríos excavan barrancos en rocas sueltas y, en algunos lugares, enormes cañones (valles fluviales profundos con pendientes pronunciadas), como el Gran Cañón en Arizona (EE. UU.). Tiene una longitud de 349 kilómetros.

Las lluvias y los vientos son verdaderos escultores y esculpen verdaderos grupos escultóricos y figuras diversas. En Australia existen las llamadas Rocas del Viento y no muy lejos de Krasnoyarsk hay pilares de piedra. Ambos se formaron como resultado de la erosión eólica.

La erosión de la superficie terrestre está lejos de ser un proceso inofensivo. Cada año, gracias a ello, desaparecen decenas de hectáreas de tierra cultivable. Los ríos transportan una gran cantidad de suelo fértil, cuya formación en condiciones naturales lleva cientos de años. Por lo tanto, la gente intenta por todos los medios combatir la erosión.

La dirección principal de esta lucha es prevenir la erosión del suelo. Si no hay cobertura vegetal en el suelo, el viento y el agua se llevan fácilmente la capa fértil y la tierra se vuelve infértil. Por ello, en zonas con vientos intensos se utilizan métodos de conservación de la tierra, por ejemplo, el arado sin vertedera.

Además, se está llevando a cabo la lucha contra los barrancos. Para ello se plantan diversas plantas en las riberas de los ríos y se refuerzan las pendientes. En las costas marinas y fluviales, donde se produce una fuerte erosión de la costa, se construye un vertedero de grava especial y se instalan presas protectoras para evitar el trasvase de arena.