Aula 16 – Pseudo3D em Jogos de Corrida
Introdução à Engenharia
ENG1000
2018.1
Prof. Pedro Sampaio
Pseudo3D vs 3D
Forza Horizon 3 Top Gear 2•
Câmera fixa;
•
Sprites 2D;
•
Matemática simplificada;
•
Pipeline gráfico pequeno
e simples.
Pseudo3D
•
Câmera livre;
•
Modelos 3D;
•
Matemática robusta;
•
Pipeline gráfico extenso
e complexo.
3D Rendering
Operações matemáticas armazenadas em estruturas de matrizes e vetores
3D Rendering
Projeção 3D -> 2D
Projetando um ponto do universo 3D para uma tela 2D:
(x_world, y_world, z_world) -> (x_screen, y_screen)
Usando semelhança de triângulos calculamos nossa
nova coordenada y (y_screen):
y_screen = y_world * dist / z_world
Pseudo3D - Transformações
Transformação de Escala
Precisamos escalar nosso ponto de acordo com
a resolução e a orientação computacional de
Field of View
Podemos configurar o campo de visão da nossa câmera
alterando a distância entra a câmera e o plano 2D
Estrada – Estrutura de Dados
function buildRoad() segments = {}
for n = 1, 500 do -- tamanho arbitrário para estrada (n segmentos)
seg = {
index = n, -- indíce do segmento na tabela
Estrada – Estrutura de Dados
pSegment = findSegment(playerZ)["index"]segments[pSegment + 2]["color"] = COLORS.START segments[pSegment + 3]["color"] = COLORS.START segments[pSegment + 4]["color"] = COLORS.START segments[pSegment + 5]["color"] = COLORS.START
trackLength = tablelength(segments) * segmentLength end function findSegment(z) return segments[(math.floor(z/segmentLength) % tablelength(segments)) + 1] end playerZ=cameraHeight*cameraDepth
Esquema de Cores
-- cores skyblue = {135, 206, 235} darkgray = {169, 169, 169} greenmedium = {10, 111, 10} whitesmoke = {245, 245, 245} darkgreen = { 0, 100, 0} white = {255, 255, 255} -- esquema de cores COLORS = { SKY = skyblue,LIGHT = { road = darkgray, grass = greenmedium, lane=whitesmoke }, DARK = { road = darkgray, grass = darkgreen },
Movimentação – Update
function love.update(dt)dx = dt * 5000 –- deslocamento no eixo x
if (keyLeft) then -- deslocar para esquerda
playerX = playerX - dx;
elseif (keyRight) then -- deloscar para direita
playerX = playerX + dx; end
if (keyFaster) then -- acelerar movimento
speed = speed + 25
elseif (keySlower) then –- desacelerar movimento
speed = speed - 25
else -- desaceleração natural
if speed > 0 then -- considerando que movimento
speed = speed – 5 -- ocorre para frente e para trás
elseif speed < 0 then speed = speed + 5 else
speed = 0 end
Movimentação – Update
speed = limit(speed, -10000, 10000) -– limita velocidadeplayerX = limit(playerX, -10000, 10000) -- limita pos x
position = increase(position, dt * speed, trackLength) end
function limit(value, minValue, maxValue)
return math.max(minValue, math.min(value, maxValue)) end
function increase(start, increment, maxValue) -- com looping
result = start + increment; -- da estrada
if (result >= maxValue) then
result = result - maxValue; end
if (result < 0) then
result = result + maxValue; end
Estrada – Projeção e Draw
function love.draw()
drawRoad() -- desenha a estrada
end
function project(p, cameraX, cameraY, cameraZ, cameraDepth, width, height, roadWidth)
local proj = {} -- coordeanadas após projeção
-- translação das coordenadas de acordo com a câmera
p.camera.x = (p.world.x or 0) - cameraX p.camera.y = (p.world.y or 0) - cameraY p.camera.z = (p.world.z or 0) - cameraZ
-- escala de acordo com a posição z da ponto no mundo 3D
p.screen.scale = cameraDepth/p.camera.z
-- projeção das coodernadas (x,y) e da largura da metade do segmento
proj.x = p.screen.scale * p.camera.x proj.y = p.screen.scale * p.camera.y proj.w = p.screen.scale * roadWidth
-- escala do plano matemático normalizado para o computacional
p.screen.x = round((width/2) + (proj.x * width/2)) p.screen.y = round((height/2) - (proj.y * height/2)) p.screen.w = round((proj.w * width/2))
end
Estrada - Draw
function drawRoad()
baseSegment = findSegment(position)
-- desenha a partir do baseSegment
for n = 0, (drawDistance)-1 do
segment = segments[((baseSegment["index"] + n) % tablelength(segments)) + 1]
segment["looped"] = segment["index"] < baseSegment["index"]
-- projeta p1 na tela 2D
project(segment["p1"], playerX, cameraHeight,
position -(segment.looped and trackLength or 0), cameraDepth, width, height, roadWidth)
-- projeta p2 na tela 2D
project(segment["p2"], playerX, cameraHeight,
position - (segment.looped and trackLength or 0), cameraDepth, width, height, roadWidth)
-- desenha segmentos com p1 e p2 já projetados
drawSegment(width, lanes, segment["p1"]["screen"]["x"],
segment["p1"]["screen"]["y"], segment["p1"]["screen"]["w"], segment["p2"]["screen"]["x"], segment["p2"]["screen"]["y"], segment["p2"]["screen"]["w"], segment["color"])
end end
Estrada - Draw
function drawSegment (width, lanes, x1, y1, w1, x2, y2, w2, color)
-- desenha piso do cenário (grama) no segmento atual
love.graphics.setColor(color.grass)
love.graphics.rectangle("fill", 0, y2, width, y1 - y2)
-- desenha segmento da rua sobre a grama já desenhada
drawPolygon(x1-w1, y1, x1+w1, y1, x2+w2, y2, x2-w2,y2, color.road)
if (color.lane) then
l1 = w1 / (8*lanes) -- largura inferior da faixa
l2 = w2 / (8*lanes) -- largura superior da faixa
lane_w1 = w1*2/lanes –- largura inferior da pista
lane_w2 = w2*2/lanes –- largura superior da pista
lane_x1 = x1 - w1 + lane_w1 –- posição x inferior da pista
lane_x2 = x2 - w2 + lane_w2 –- posição x superior da pista
for lane = 1, lanes-1 do -- loop de desenho das faixas
drawPolygon(lane_x1 - l1/2, y1, lane_x1 + l1/2, y1, lane_x2 + l2/2, y2, lane_x2 - l2/2, y2, color.lane)
lane_x1 = lane_x1 + lane_w1 -- incrementa posição para
lane_x2 = lane_x2 + lane_w2 -- desenhar próxima faixa
end end end
Cor das faixas (color.lane) só está definida se o segmento possuir esquema de cor LIGHT
A variável “lanes” corresponde ao número de pistas que a estrada possuirá. A função “drawPolygon” utiliza a função
Importante - Desabilitar VSync
function love.load()-- desabilitar VSync para evitar problemas relacionados -- à performance do jogo
love.window.setMode(width, height, {vsync=false})
Exercício 1
1) Adicione o sprite de carro ao exemplo disponibilizado e faça a animação
de acordo com a movimentação do jogador
Resources:
Curvas 3D
Rascunho de curvas em um mundo 3D
•
Mudança real na estrutura física dos
segmentos;
Curvas Pseudo3D
Curvas falsas em um mundo Pseudo3D
•
Não há mudanças na estrutura dos
segmentos, apenas na projeção;
•
Cálculos simples, sem nenhuma rotação
envolvida;
Curvas Pseudo3D
Curvas falsas em um mundo Pseudo3D
•
Como funciona?
- É necessário apenas modificar a linha
central (p1, p2) de cada segmento durante
a projeção;
- Para o efeito de curva, basta inclinar
levemente a linha central de segmentos
consecutivos criando um formato de curva.
p1
p2
Curvas Pseudo3D
Curvas falsas em um mundo Pseudo3D
•
Precisamos armazenar o valor que representa
a inclinação de cada segmento na nossa
estrutura de segmentos;
- Valores negativos indicam curvas à esquerda
- Valores positivos indicam curvas à direita
- Valores mais altos indicam curvas acentuadas
- Valores mais baixos indicam curvas suaves
p1
p2
Estrada – Estrutura Revisada
function addSegment(curve) –- adiciona um segmento da estradan = tablelength(segments) + 1; seg = {
index = n, -- index é o tamanho atual da tabela (+1 -> lua)
p1 = { world = { z = (n-1) * segmentLength }, camera = {}, screen = {} },
p2 = { world = { z = n * segmentLength }, camera = {}, screen = {} } ,
color = randomColorLightness(math.floor(n/rumbleLength)), looped = false,
curve = curve –- valor da inclinação do segmento
}
Estrada – Estrutura Revisada
-- adiciona uma porção de segmentos, suavizando curvas quando necessário
function addRoad(enter, hold, leave, curve) for n = 0, enter-1 do
-- interpola valores para suavizar entrada da curva
addSegment(interpolate(0, curve, n/enter)) end
for n = 0, hold-1 do addSegment(curve) end
for n = 0, leave-1 do
-- interpola valores para suavizar saída da curva
addSegment(interpolate(curve, 0, n/leave)) end
end
-- interpolação - adiciona ao valor inicial uma porcentagem do que resta para o valor final
function interpolate(a,b,percent) return a + (b-a)*percent
Estrada – Estrutura Revisada
-- Parâmetros de uma parcela da estrada (conjunto de segmentos)
ROAD = {
LENGTH = { NONE = 0, SHORT = 25, MEDIUM = 40, LONG = 80 }, CURVE = { NONE = 0, EASY = 2, MEDIUM = 4, HARD = 6 } }
-- Adiciona parcelas de estrada retas
function addStraight(size)
size = size or ROAD.LENGTH.MEDIUM
addRoad(size, size, size, 0) –- sem curva nessa parcela
End
-- Adiciona parcelas de estrada com curvas
function addCurve(size, curve)
size = size or ROAD.LENGTH.MEDIUM curve = curve or ROAD.CURVE.MEDIUM
addRoad(size, size, size, curve) –- parcela com curva
Estrada – Estrutura Revisada
-- Adiciona parcelas de estradas com curvas em S
function addSCurves() addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -ROAD.CURVE.EASY) addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.CURVE.MEDIUM) addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.CURVE.EASY) addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -ROAD.CURVE.EASY) addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -ROAD.CURVE.MEDIUM) end
-- constrói a estrada completa adicionando as parcelas desejadas
function buildRoad() (...)
addStraight(ROAD.LENGTH.SHORT/4) –- reta pequena
addSCurves() -- curva em S
addStraight(ROAD.LENGTH.LONG) -- reta comprida
addSCurves() -- curva em S
addCurve(ROAD.LENGTH.LONG, -ROAD.CURVE.MEDIUM) -- curva à esquerda...
addCurve(ROAD.LENGTH.LONG, ROAD.CURVE.MEDIUM) -- curva à direita...
Estrada – Draw Revisado
function drawRoad()
baseSegment = findSegment(position)
basePercent = percentOf (position, segmentLength) –- evita transições de segmentos bruscas
x = 0; -- deslocamento x em p1
dx = - (baseSegment["curve"] * basePercent) –- deslocamento x em p2 relativo ao x em p1
for n = 0, (drawDistance)-1 do (...)
project(segment["p1"], playerX – x, (...)) -- desloca x da câmera
project(segment["p2"], playerX - x – dx, (...)) -- em p1 e em p2 -- atualiza valores de x e dx para próximo segmento
x = x + dx
dx = dx + segment["curve"] (...)
end end
-- retorna o percentual atingido por um valor em um total
function percentOf(n, total) return (n%total)/total
Exercício 2
1) Adicione ao exemplo disponibilizado uma força centrífuga que simule o
movimento de um veículo durante uma curva
Resources:
Adicionando Sprites
•
Sprites 2D compatíveis com a visão da
câmera
•
Requer posicionamento e escala de acordo
com a perspectiva do mundo
Adicionando Sprites
•
Temos as informações necessárias para o desenho de sprites em cada ponto
projetado!
•
Podemos utilizar a escala, o x e y, e a largura da pista (w) ao projetarmos os sprites em
um ponto de um segmento
•
Passos para adicionar um sprite:
•
Armazenar em um segmento da pista
•
Atualizar o segmento atual do sprite (no caso de sprites móveis)
Armazenando Sprites
function addSegment(…) (…)
seg = { (…)
sprites = {}, -- contém todos os sprites fixos de um segmento
cars = {}, -- contém todos os sprites móveis de um segmento
(…) } (…) end
-- constantes relacionadas aos sprites
SPRITES = {
TREE1 = { x = 0, y = 50, w = 232, h = 153}, CAR01 = { x = 0, y = 0, w = 82, h = 46}, CAR02 = { x = 180, y = 0, w = 82, h = 46} }
Armazenando Sprites
-- função que adiciona sprites fixos a um segmento n
function addSprite(n, sprite, offset)
-- offset é o deslocamento do sprite no eixo x
sprite = {source = sprite, offset = offset}
-- insere o sprite no segmento desejado
table.insert(segments[n]["sprites"], sprite) end
-- função que popula a estrada com sprites fixos
function buildSprites()
addSprite(15, SPRITES.TREE1, -roadWidth) -- adiciona sprite TREE1 nos
addSprite(20, SPRITES.TREE1, -roadWidth) -- segmentos 15 e 20 à -- esquerda da estrada
n = 21 -- a partir do segmento 21, adiciona sprite TREE1 à esquerda -- e à direita da estrada, a cada rumbleLength * 2 segmentos
while n < tablelength(segments) do
addSprite(n, SPRITES.TREE1, -roadWidth) addSprite(n, SPRITES.TREE1, roadWidth) n = n + (rumbleLength * 2)
Armazenando Sprites
-- função que popula a estrada com sprites móveis
function buildCars()
cars = {} -- tabela auxiliar para facilitar a atualização dos sprites -- insere sprites móveis na estrada randomicamente
for n = 0, totalCars do -- totalCars equivale a qtd de sprites móveis -- randomiza um offset no eixo x para o sprite
-- OBS: randomChoice escolhe aleatóriamente um valor de uma tabela
offset = math.random() * randomChoice({-0.8*roadWidth, 0.8*roadWidth})
-- randomiza posição z do sprite
z = math.floor(math.random() * tablelength(segments))*segmentLength
-- randomiza o sprite móvel a ser usado e sua velocidade
sprite = randomChoice(SPRITES.CARS)
carspeed = maxSpeed/4 + math.random() * maxSpeed segment = findSegment(z)
car = { offset = offset, z = z, sprite = sprite, speed = carspeed, percent = 0, segment = segment}
-- insere o carro gerado na estrada e também na tabela auxiliar
table.insert(segment["cars"], car) table.insert(cars, car)
Atualizando Sprites
-- função que atualiza posição dos sprites móveis (no ex. carros)
function updateCars(dt) -- deve ser chamado no love.update()
for n = 1, tablelength(cars) do -- percorre tabela de carros auxiliar
car = cars[n]
oldSegment = car["segment"] -- segmento pré-atualização do carro -- atualiza posição z do carro de acordo com sua velocidade
car["z"] = increase(car["z"], dt * car["speed"], trackLength)
-- atualiza percentual percorrido do segmento atual
car["percent"] = percentOf(car["z"], segmentLength) -- facilita desenho
newSegment = findSegment(car["z"]) -- segmento pós-atualização
car["segment"] = newSegment -- atualiza segmento do carro -- se novo segmento é diferente do antigo
-- troca o segmento do carro na estrutura da estrada
if (oldSegment ~= newSegment) then
table.remove(oldSegment["cars"], indexOf(oldSegment["cars"], car))
-- indexOf retorna indíce de um objeto em uma tabela
table.insert(newSegment["cars"], car) end
Desenhando Sprites
-- função que desenha todos os sprites (deve ser chamada no love.draw())
function drawSprites()
-- loop em ordem contrária: algoritmo do pintor -- desenhamos o que está atrás primeiro!
n = (drawDistance-1) while n > 0 do
-- segmento atual do loop
segment = segments[((baseSegment["index"] + n) % tablelength(segments)) + 1]
(… desenha sprites fixos …) (… desenha sprites móveis …) (… desenha sprite do player …)
-- decrementa iterador do loop: algoritmo do pintor
n = n - 1 end
Desenhando Sprites
(… desenha sprites fixos …)
for i = 1 , tablelength(segment["sprites"]) do sprite = segment["sprites"][i]
spriteScale = segment["p1"]["screen"]["scale"] spriteX = segment["p1"]["screen"]["x"] +
(spriteScale * sprite["offset"] * width/2) spriteY = segment["p1"]["screen"]["y"]
drawSprite(width, height, resolution, roadWidth, sprites,
sprite["source"], spriteScale, spriteX, spriteY, (sprite["offset"] < 0 and -1 or 0), -1)
Desenhando Sprites
(… desenha sprites móveis …)
for i = 1 , tablelength(segment["cars"]) do car = segment["cars"][i] sprite = car["sprite"] spriteScale = interpolate(segment["p1"]["screen"]["scale"], segment["p2"]["screen"]["scale"], car["percent"]) spriteX = interpolate(segment["p1"]["screen"]["x"], segment["p2"]["screen"]["x"], car["percent"]) + (spriteScale * car["offset"] * width/2)
spriteY = interpolate(segment["p1"]["screen"]["y"], segment["p2"]["screen"]["y"], car["percent"]) drawSprite(width, height, resolution, roadWidth, sprites,
Desenhando Sprites
(… desenha sprite do player …)
if (segment == playerSegment) then
drawSprite(width, height, resolution, roadWidth, sprites, SPRITES.CAR02, cameraDepth/playerZ, width/2, (height/2), -0.5, 1); end
-- atualizado dentro do love.update()
playerSegment = findSegment(position+playerZ)
-- position + playerZ eqvuiale à:
Desenhando Sprites
-- função que desenha sprites aplicando escalas e offsets -- de acordo com parâmetros passados
function drawSprite(width, height, resolution, roadWidth, sprites, sprite, scale, destX, destY, offsetX, offsetY) destW = (sprite.w * scale * width) * SPRITES.SCALE
destH = (sprite.h * scale * width) * SPRITES.SCALE destX = destX + (destW * offsetX)
destY = destY + (destH * offsetY)
quad = love.graphics.newQuad(sprite.x, sprite.y, sprite.w, sprite.h, sprites:getDimensions())
love.graphics.draw(sprites, quad, destX, destY, 0, (destW)/sprite.w, (destH )/sprite.h)
Collision Check
n segmentos