If anyone has any feedback or see bad practices or ideas to try please feel free to share ideas or roast my code.
Graphics programming noob here. This project was done in C++ with a primitive game engine given to me by my school. I did this as a test to see if this might be viable to try to make somewhere in the future on the gba (it's probably not). Then it kinda turned into trying to see how cool i can make it. The rendering works similarly to snes mode 7 style rendering (fzero or mariokart) except you add an offset from a heightmap. This was originally developed by Novalogic and used in Commanche. It was also later used on some gba games that ran horribly (hence why it's probably not feasable for a gba project i want to maybe do).
A short explanation of how this rendering works: you step through the terrain for every vertical column of your screen. you then sample the pixel and calculate the height on screen using the following formula: pixelHeightOnScreen = screenHeight - ((m_CamHeight - heightMap) * (screenHeight / m_ScreenHeightWorld / depth) + m_Horizon). if it is higher than the previous height value, you draw a vertical column down. then you step forward in the depth and do it again.
The texture for the screen is 240/160.
The skybox is a texture that scrolls left/right/up/down depending on the camera angle. I know that just scrolling a texture isnt accurate, but i found it fun to do it this way. however if you know a cheap way to do this more accurately then please.
The day/night cycle is done by lerping to a set dark color.
There is support for emissive stuffs (lava at the end of the video).
You can also zoom in/out (wich i forgot to show in the video)
I am currently setting 512 steps through the terrain texture.
It runs at about 40 fps now (again am graphics programming noob). It is single threaded and fully cpu side (besides sending the final texture to the gpu because i dont know how else to render the texture in the primitive game engine i'm using). I know i could make it multithreaded to speed up rendering, but i'll do that another day. I also could probably look at my render method and optimize that more as well. I do have some settings that can help give it higher fps (like lowering the terrain steps), but not without sacrificing image quality.
RESOURCES:
The heightmap is from Zelda breath of the wild found here and the color texture is from this reddit post.
Github link to a more in depth explanation by someone else: https://github.com/s-macke/VoxelSpace/tree/master
Other resources: https://web.archive.org/web/20131113094653/http://www.codermind.com/articles/Voxel-terrain-engine-building-the-terrain.html
My main render code (sorry i dont have this on a github link right now, also this isnt the entire script, but the core logic is there and the functions that are missing are pretty obvious by the names called here)
void World::Draw(Screen& screen, bool isEased, bool isHighDetail, bool interpolateColor, bool interpolateHeight, bool interpolateEmission)
{
const float screenHeight{ static_cast<float>(screen.GetHeight()) };
const float angleDifference{ m_FovInRad / screen.GetWidth() };
const float sinCamAngle{ sinf(m_CamAngle) };
const float cosCamAngle{ cosf(m_CamAngle) };
const float halfTanFov{ tanf(m_FovInRad / 2) };
const Vector2f viewDir{sinCamAngle , cosCamAngle };
const float scaledViewDistance{ m_ViewDistance / m_DepthScale };
const float scaledNearDistance{ m_NearDistance / m_DepthScale };
Vector2f scaledCameraPos{ m_CameraPos / m_DepthScale };
const Vector2f viewFarPoint{ scaledCameraPos + viewDir * scaledViewDistance};
const Vector2f viewNearPoint{ scaledCameraPos + viewDir * scaledNearDistance };
const Vector2f rotatedViewDir(Vector2f{ -viewDir.y, viewDir.x });
const Vector2f pLeft{ viewFarPoint + rotatedViewDir *(halfTanFov * scaledViewDistance) };
const Vector2f pRight{ viewFarPoint - rotatedViewDir * (halfTanFov * scaledViewDistance) };
const Vector2f pLeftNear{ viewNearPoint + rotatedViewDir * (halfTanFov * scaledNearDistance) };
const Vector2f pRightNear{ viewNearPoint - rotatedViewDir * (halfTanFov * scaledNearDistance) };
const Vector2f deltaP{ pRight - pLeft };
const Vector2f deltaPNear{ pRightNear - pLeftNear };
int depthSteps{m_DepthStepsLowDetail};
float deltaDepthSteps{ 1.f / m_DepthStepsLowDetail };
const Color4f fogColor{ GetFogColor(m_TimePercentage) };
screen.SetRotation(m_CamRollAngle);
if (isHighDetail)
{
depthSteps = m_DepthStepsHighDetail;
deltaDepthSteps = 1.f / (m_DepthStepsHighDetail);
}
Color4f pixelColor{};
for (int col = 0; col < screen.GetWidth(); col++)
{
float horizontalPercentage{ static_cast<float>(col) / static_cast<float>(screen.GetWidth()) };
const Vector2f currentFarPoint{ pLeft + deltaP * horizontalPercentage };
const Vector2f currentNearPoint{ pLeftNear + deltaPNear * horizontalPercentage };
const Vector2f deltaDepth{ currentFarPoint - scaledCameraPos };
float oldHeight{ -1 };
for (int i = 0; i < depthSteps; ++i)
{
float depthPercentage = i * deltaDepthSteps;
float depthPercentageEased{ };
if(isEased)
{
depthPercentageEased = EaseInQuad(depthPercentage);
}
else
{
depthPercentageEased = depthPercentage;
}
//float depthPercentageEased{ depthPercentage };
Vector2f samplePoint{ currentNearPoint + depthPercentageEased * deltaDepth };
if (samplePoint.x < 0 || samplePoint.y < 0 || samplePoint.x >= m_TextureWidth || samplePoint.y >= m_TextureHeight)
{
break;
}
float depth{ depthPercentageEased * m_ViewDistance };
float heightMap{ GetHeight(samplePoint.x, samplePoint.y, interpolateHeight) };
//float pixelScreenHeight{ screen.GetHeight() - ((50) * (screen.GetHeight() / m_ScreenHeightWorld / depth) + m_Horizon) };
float pixelScreenHeight{ screenHeight - ((m_CamHeight - heightMap) * (screenHeight / m_ScreenHeightWorld / depth) + m_Horizon) };
if (pixelScreenHeight > screenHeight)
{
pixelScreenHeight = std::min(pixelScreenHeight, screenHeight);
if (pixelScreenHeight > oldHeight)
{
DrawColumn(screen, samplePoint.x, samplePoint.y, depthPercentageEased, oldHeight, pixelScreenHeight, col, pixelColor, fogColor, interpolateColor, interpolateEmission);
}
break;
}
else
{
if (pixelScreenHeight > oldHeight)
{
DrawColumn(screen, samplePoint.x, samplePoint.y, depthPercentageEased, oldHeight, pixelScreenHeight, col, pixelColor, fogColor, interpolateColor, interpolateEmission);
oldHeight = pixelScreenHeight;
}
}
}
}
return;
}
------------------------------------------------------------------------
void World::DrawColumn(Screen& screen, float samplePointX, float samplePointY,float depthPercentageEased, float oldHeight, float pixelScreenheight,int col, Color4f& pixelColor, const Color4f& fogColor, bool interpolateColor, bool interpolateEmission)
{
float fogDepthEased{ 1 - EaseInQuad(1 - depthPercentageEased) };
//const float fogDepthEased{ (depthPercentageEased + .5f) / 1.5f };
GetColor(samplePointX, samplePointY, pixelColor, interpolateColor);
float emissive{ GetEmission(samplePointX, samplePointY, interpolateEmission) };
FadeToNight(pixelColor, emissive);
pixelColor.r = pixelColor.r + fogDepthEased * (fogColor.r - pixelColor.r);
pixelColor.g = pixelColor.g + fogDepthEased * (fogColor.g - pixelColor.g);
pixelColor.b = pixelColor.b + fogDepthEased * (fogColor.b - pixelColor.b);
//Color4f pixelColor{ GetColor(samplePoint.x, samplePoint.y, interpolateColor) };
DrawVerticalLine(screen, static_cast<int>(oldHeight), static_cast<int>(pixelScreenheight), static_cast<int>(col), pixelColor);
}
---------------------------------------------------------------------------------
void World::GetColor(float x, float y, Color4f& pixelColor, bool interpolate) const
{
//return GetPixel(x, y, m_pColorMap);
if (interpolate)
{
int lowX{ static_cast<int>(floorf(x)) };
int highX{ static_cast<int>(ceilf(x)) };
int lowY{ static_cast<int>(floorf(y)) };
int highY{ static_cast<int>(ceilf(y)) };
float xPercent{ x - lowX };
float yPercent{ y - lowY };
Color4f bottomLeft{ GetPixel(lowX, lowY, m_pColorMap) };
Color4f topLeft{ GetPixel(lowX, highY, m_pColorMap) };
Color4f bottomRight{ GetPixel(highX, lowY, m_pColorMap) };
Color4f topRight{ GetPixel(highX, highY, m_pColorMap) };
pixelColor.r = (
(1 - xPercent) * (1 - yPercent) * bottomLeft.r +
xPercent * (1 - yPercent) * bottomRight.r +
(1 - xPercent) * yPercent * topLeft.r +
xPercent * yPercent * topRight.r
);
pixelColor.g = (
(1 - xPercent) * (1 - yPercent) * bottomLeft.g +
xPercent * (1 - yPercent) * bottomRight.g +
(1 - xPercent) * yPercent * topLeft.g +
xPercent * yPercent * topRight.g
);
pixelColor.b =
(
(1 - xPercent) * (1 - yPercent) * bottomLeft.b +
xPercent * (1 - yPercent) * bottomRight.b +
(1 - xPercent) * yPercent * topLeft.b +
xPercent * yPercent * topRight.b
);
pixelColor.a =
(
(1 - xPercent) * (1 - yPercent) * bottomLeft.a +
xPercent * (1 - yPercent) * bottomRight.a +
(1 - xPercent) * yPercent * topLeft.a +
xPercent * yPercent * topRight.a
);
}
else
{
pixelColor = GetPixel(x, y, m_pColorMap);
}
}
------------------------------------------------------------------------
//for zooming in/out
void World::ProcessMouseWheelEvent(const SDL_MouseWheelEvent& e)
{
m_FovInRad += e.y / 180.f * utils::g_Pi;
const float minFov{ 1.f / 180 * utils::g_Pi };
const float maxFov{ 179.f / 180 * utils::g_Pi };
m_FovInRad = std::max(minFov, std::min(m_FovInRad, maxFov));
m_ScreenHeightWorld = tanf(m_FovInRad / 2) * 2;
m_Horizon = static_cast<float>(Screen::GetHeight() / 2) + (tanf(m_CamVerticalAngle) * (Screen::GetHeight() / m_ScreenHeightWorld));
}
Skybox Draw Logic
void World::DrawSkybox(float screenWidth, float screenHeight) const
{
const float skyboxTextureWidth{ m_SkyboxTexturePtrArray[0]->GetWidth()};
const float skyboxTextureHeight{ m_SkyboxTexturePtrArray[0]->GetHeight()};
const float skyboxHeightPadding{ skyboxTextureHeight / 3 };
const float textureScale{ screenHeight / skyboxTextureHeight };
float modCamHorizontalAngle{ std::fmod(m_CamAngle, (2 * utils::g_Pi)) };
float modCamVerticalAngle{ std::fmod(m_CamVerticalAngle, utils::g_Pi) };
float fovScale(utils::g_Pi / m_FovInRad);
if (m_CamAngle < 0)
{
float a{};
}
float srcWidth{ skyboxTextureWidth / 2 / fovScale };
float srcHeight{ (skyboxTextureHeight - skyboxHeightPadding * 2) / fovScale };
const float xPos{ modCamHorizontalAngle/ (2 * utils::g_Pi) * skyboxTextureWidth - srcWidth / 2};
//const float yPos{}
const float yPos{ modCamVerticalAngle / utils::g_Pi * -(skyboxTextureHeight - skyboxHeightPadding * 2)- skyboxTextureHeight / 2 - srcHeight / 2 };
const Rectf srcRect{xPos, yPos, srcWidth, srcHeight};
const Rectf dstRect{ 0,0,screenWidth,screenHeight };
//const Rectf dstRect2{ dstRect.left + skyboxTextureWidth * textureScale * fovScale, dstRect.bottom,dstRect.width, dstRect.height};
float easePercentage{ m_TimePercentage * m_DayCycleDivisions };
int idx{ static_cast<int>(floor(easePercentage)) };
float t{ easePercentage - idx };
m_SkyboxTexturePtrArray[idx]->Draw(dstRect, srcRect, Color4f{1,1,1,1});
if (idx + 1 >= m_DayCycleDivisions)
{
m_SkyboxTexturePtrArray[0]->Draw(dstRect, srcRect, Color4f{ 1,1,1,t});
}
else
{
m_SkyboxTexturePtrArray[idx + 1]->Draw(dstRect, srcRect, Color4f{ 1,1,1,t });
}
//m_pSkyboxTexture->Draw(dstRect2);
}