v1.4.4
This commit is contained in:
commit
9c94d113d3
10260 changed files with 1237388 additions and 0 deletions
35
source/rendering/CMakeLists.txt
Normal file
35
source/rendering/CMakeLists.txt
Normal file
|
@ -0,0 +1,35 @@
|
|||
INCLUDE_DIRECTORIES (
|
||||
${STAR_EXTERN_INCLUDES}
|
||||
${STAR_CORE_INCLUDES}
|
||||
${STAR_BASE_INCLUDES}
|
||||
${STAR_GAME_INCLUDES}
|
||||
${STAR_PLATFORM_INCLUDES}
|
||||
${STAR_APPLICATION_INCLUDES}
|
||||
${STAR_RENDERING_INCLUDES}
|
||||
)
|
||||
|
||||
SET (star_rendering_HEADERS
|
||||
StarAnchorTypes.hpp
|
||||
StarAssetTextureGroup.hpp
|
||||
StarDrawablePainter.hpp
|
||||
StarEnvironmentPainter.hpp
|
||||
StarFontTextureGroup.hpp
|
||||
StarTextPainter.hpp
|
||||
StarTilePainter.hpp
|
||||
StarWorldCamera.hpp
|
||||
StarWorldPainter.hpp
|
||||
)
|
||||
|
||||
SET (star_rendering_SOURCES
|
||||
StarAnchorTypes.cpp
|
||||
StarAssetTextureGroup.cpp
|
||||
StarDrawablePainter.cpp
|
||||
StarEnvironmentPainter.cpp
|
||||
StarFontTextureGroup.cpp
|
||||
StarTextPainter.cpp
|
||||
StarTilePainter.cpp
|
||||
StarWorldCamera.cpp
|
||||
StarWorldPainter.cpp
|
||||
)
|
||||
|
||||
ADD_LIBRARY (star_rendering OBJECT ${star_rendering_SOURCES} ${star_rendering_HEADERS})
|
14
source/rendering/StarAnchorTypes.cpp
Normal file
14
source/rendering/StarAnchorTypes.cpp
Normal file
|
@ -0,0 +1,14 @@
|
|||
#include "StarAnchorTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
EnumMap<HorizontalAnchor> const HorizontalAnchorNames{
|
||||
{HorizontalAnchor::LeftAnchor, "left"},
|
||||
{HorizontalAnchor::HMidAnchor, "mid"},
|
||||
{HorizontalAnchor::RightAnchor, "right"},
|
||||
};
|
||||
|
||||
EnumMap<VerticalAnchor> const VerticalAnchorNames{
|
||||
{VerticalAnchor::BottomAnchor, "bottom"}, {VerticalAnchor::VMidAnchor, "mid"}, {VerticalAnchor::TopAnchor, "top"},
|
||||
};
|
||||
}
|
24
source/rendering/StarAnchorTypes.hpp
Normal file
24
source/rendering/StarAnchorTypes.hpp
Normal file
|
@ -0,0 +1,24 @@
|
|||
#ifndef STAR_ANCHOR_TYPES_HPP
|
||||
#define STAR_ANCHOR_TYPES_HPP
|
||||
|
||||
#include "StarBiMap.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
enum class HorizontalAnchor {
|
||||
LeftAnchor,
|
||||
HMidAnchor,
|
||||
RightAnchor
|
||||
};
|
||||
extern EnumMap<HorizontalAnchor> const HorizontalAnchorNames;
|
||||
|
||||
enum class VerticalAnchor {
|
||||
BottomAnchor,
|
||||
VMidAnchor,
|
||||
TopAnchor
|
||||
};
|
||||
extern EnumMap<VerticalAnchor> const VerticalAnchorNames;
|
||||
|
||||
}
|
||||
|
||||
#endif
|
85
source/rendering/StarAssetTextureGroup.cpp
Normal file
85
source/rendering/StarAssetTextureGroup.cpp
Normal file
|
@ -0,0 +1,85 @@
|
|||
#include "StarAssetTextureGroup.hpp"
|
||||
#include "StarIterator.hpp"
|
||||
#include "StarTime.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarImageMetadataDatabase.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
AssetTextureGroup::AssetTextureGroup(TextureGroupPtr textureGroup)
|
||||
: m_textureGroup(move(textureGroup)) {
|
||||
m_reloadTracker = make_shared<TrackerListener>();
|
||||
Root::singleton().registerReloadListener(m_reloadTracker);
|
||||
}
|
||||
|
||||
TexturePtr AssetTextureGroup::loadTexture(String const& imageName) {
|
||||
return loadTexture(imageName, false);
|
||||
}
|
||||
|
||||
TexturePtr AssetTextureGroup::tryTexture(String const& imageName) {
|
||||
return loadTexture(imageName, true);
|
||||
}
|
||||
|
||||
bool AssetTextureGroup::textureLoaded(String const& imageName) const {
|
||||
return m_textureMap.contains(imageName);
|
||||
}
|
||||
|
||||
void AssetTextureGroup::cleanup(int64_t textureTimeout) {
|
||||
if (m_reloadTracker->pullTriggered()) {
|
||||
m_textureMap.clear();
|
||||
m_textureDeduplicationMap.clear();
|
||||
|
||||
} else {
|
||||
int64_t time = Time::monotonicMilliseconds();
|
||||
|
||||
List<Texture const*> liveTextures;
|
||||
filter(m_textureMap, [&](auto const& pair) {
|
||||
if (time - pair.second.second < textureTimeout) {
|
||||
liveTextures.append(pair.second.first.get());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
liveTextures.sort();
|
||||
|
||||
eraseWhere(m_textureDeduplicationMap, [&](auto const& p) {
|
||||
return !liveTextures.containsSorted(p.second.get());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TexturePtr AssetTextureGroup::loadTexture(String const& imageName, bool tryTexture) {
|
||||
if (auto p = m_textureMap.ptr(imageName)) {
|
||||
p->second = Time::monotonicMilliseconds();
|
||||
return p->first;
|
||||
}
|
||||
|
||||
auto assets = Root::singleton().assets();
|
||||
|
||||
ImageConstPtr image;
|
||||
if (tryTexture)
|
||||
image = assets->tryImage(imageName);
|
||||
else
|
||||
image = assets->image(imageName);
|
||||
|
||||
if (!image)
|
||||
return {};
|
||||
|
||||
// Assets will return the same image ptr if two different asset paths point
|
||||
// to the same underlying cached image. We should not make duplicate entries
|
||||
// in the texture group for these, so we keep track of the image pointers
|
||||
// returned to deduplicate them.
|
||||
if (auto existingTexture = m_textureDeduplicationMap.value(image)) {
|
||||
m_textureMap.add(imageName, {existingTexture, Time::monotonicMilliseconds()});
|
||||
return existingTexture;
|
||||
} else {
|
||||
auto texture = m_textureGroup->create(*image);
|
||||
m_textureMap.add(imageName, {texture, Time::monotonicMilliseconds()});
|
||||
m_textureDeduplicationMap.add(image, texture);
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
50
source/rendering/StarAssetTextureGroup.hpp
Normal file
50
source/rendering/StarAssetTextureGroup.hpp
Normal file
|
@ -0,0 +1,50 @@
|
|||
#ifndef STAR_ASSET_TEXTURE_GROUP_HPP
|
||||
#define STAR_ASSET_TEXTURE_GROUP_HPP
|
||||
|
||||
#include "StarMaybe.hpp"
|
||||
#include "StarString.hpp"
|
||||
#include "StarBiMap.hpp"
|
||||
#include "StarListener.hpp"
|
||||
#include "StarRenderer.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(AssetTextureGroup);
|
||||
|
||||
// Creates a renderer texture group for textures loaded directly from Assets.
|
||||
class AssetTextureGroup {
|
||||
public:
|
||||
// Creates a texture group using the given renderer and textureFiltering for
|
||||
// the managed textures.
|
||||
AssetTextureGroup(TextureGroupPtr textureGroup);
|
||||
|
||||
// Load the given texture into the texture group if it is not loaded, and
|
||||
// return the texture pointer.
|
||||
TexturePtr loadTexture(String const& imageName);
|
||||
|
||||
// If the texture is loaded and ready, returns the texture pointer, otherwise
|
||||
// queues the texture using Assets::tryImage and returns nullptr.
|
||||
TexturePtr tryTexture(String const& imageName);
|
||||
|
||||
// Has the texture been loaded?
|
||||
bool textureLoaded(String const& imageName) const;
|
||||
|
||||
// Frees textures that haven't been used in more than 'textureTimeout' time.
|
||||
// If Root has been reloaded, will simply clear the texture group.
|
||||
void cleanup(int64_t textureTimeout);
|
||||
|
||||
private:
|
||||
// Returns the texture parameters. If tryTexture is true, then returns none
|
||||
// if the texture is not loaded, and queues it, otherwise loads texture
|
||||
// immediately
|
||||
TexturePtr loadTexture(String const& imageName, bool tryTexture);
|
||||
|
||||
TextureGroupPtr m_textureGroup;
|
||||
StringMap<pair<TexturePtr, int64_t>> m_textureMap;
|
||||
HashMap<ImageConstPtr, TexturePtr> m_textureDeduplicationMap;
|
||||
TrackerListenerPtr m_reloadTracker;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
57
source/rendering/StarDrawablePainter.cpp
Normal file
57
source/rendering/StarDrawablePainter.cpp
Normal file
|
@ -0,0 +1,57 @@
|
|||
#include "StarDrawablePainter.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
DrawablePainter::DrawablePainter(RendererPtr renderer, AssetTextureGroupPtr textureGroup) {
|
||||
m_renderer = move(renderer);
|
||||
m_textureGroup = move(textureGroup);
|
||||
}
|
||||
|
||||
void DrawablePainter::drawDrawable(Drawable const& drawable) {
|
||||
Vec4B color = drawable.color.toRgba();
|
||||
|
||||
if (auto linePart = drawable.part.ptr<Drawable::LinePart>()) {
|
||||
auto line = linePart->line;
|
||||
line.translate(drawable.position);
|
||||
|
||||
Vec2F left = Vec2F(vnorm(line.diff())).rot90() * linePart->width / 2.0f;
|
||||
m_renderer->render(RenderQuad{{},
|
||||
RenderVertex{Vec2F(line.min()) + left, Vec2F(), color, drawable.fullbright ? 0.0f : 1.0f},
|
||||
RenderVertex{Vec2F(line.min()) - left, Vec2F(), color, drawable.fullbright ? 0.0f : 1.0f},
|
||||
RenderVertex{Vec2F(line.max()) - left, Vec2F(), color, drawable.fullbright ? 0.0f : 1.0f},
|
||||
RenderVertex{Vec2F(line.max()) + left, Vec2F(), color, drawable.fullbright ? 0.0f : 1.0f}
|
||||
});
|
||||
|
||||
} else if (auto polyPart = drawable.part.ptr<Drawable::PolyPart>()) {
|
||||
auto poly = polyPart->poly;
|
||||
poly.translate(drawable.position);
|
||||
|
||||
m_renderer->render(renderFlatPoly(poly, color, 0.0f));
|
||||
|
||||
} else if (auto imagePart = drawable.part.ptr<Drawable::ImagePart>()) {
|
||||
TexturePtr texture = m_textureGroup->loadTexture(imagePart->image);
|
||||
|
||||
Vec2F textureSize(texture->size());
|
||||
RectF imageRect(Vec2F(), textureSize);
|
||||
|
||||
Mat3F transformation = Mat3F::translation(drawable.position) * imagePart->transformation;
|
||||
|
||||
Vec2F lowerLeft = transformation.transformVec2(Vec2F(imageRect.xMin(), imageRect.yMin()));
|
||||
Vec2F lowerRight = transformation.transformVec2(Vec2F(imageRect.xMax(), imageRect.yMin()));
|
||||
Vec2F upperRight = transformation.transformVec2(Vec2F(imageRect.xMax(), imageRect.yMax()));
|
||||
Vec2F upperLeft = transformation.transformVec2(Vec2F(imageRect.xMin(), imageRect.yMax()));
|
||||
|
||||
m_renderer->render(RenderQuad{move(texture),
|
||||
{lowerLeft, {0, 0}, color, drawable.fullbright ? 0.0f : 1.0f},
|
||||
{lowerRight, {textureSize[0], 0}, color, drawable.fullbright ? 0.0f : 1.0f},
|
||||
{upperRight, {textureSize[0], textureSize[1]}, color, drawable.fullbright ? 0.0f : 1.0f},
|
||||
{upperLeft, {0, textureSize[1]}, color, drawable.fullbright ? 0.0f : 1.0f}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void DrawablePainter::cleanup(int64_t textureTimeout) {
|
||||
m_textureGroup->cleanup(textureTimeout);
|
||||
}
|
||||
|
||||
}
|
27
source/rendering/StarDrawablePainter.hpp
Normal file
27
source/rendering/StarDrawablePainter.hpp
Normal file
|
@ -0,0 +1,27 @@
|
|||
#ifndef STAR_DRAWABLE_PAINTER_HPP
|
||||
#define STAR_DRAWABLE_PAINTER_HPP
|
||||
|
||||
#include "StarDrawable.hpp"
|
||||
#include "StarRenderer.hpp"
|
||||
#include "StarAssetTextureGroup.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(DrawablePainter);
|
||||
|
||||
class DrawablePainter {
|
||||
public:
|
||||
DrawablePainter(RendererPtr renderer, AssetTextureGroupPtr textureGroup);
|
||||
|
||||
void drawDrawable(Drawable const& drawable);
|
||||
|
||||
void cleanup(int64_t textureTimeout);
|
||||
|
||||
private:
|
||||
RendererPtr m_renderer;
|
||||
AssetTextureGroupPtr m_textureGroup;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
443
source/rendering/StarEnvironmentPainter.cpp
Normal file
443
source/rendering/StarEnvironmentPainter.cpp
Normal file
|
@ -0,0 +1,443 @@
|
|||
#include "StarEnvironmentPainter.hpp"
|
||||
#include "StarLexicalCast.hpp"
|
||||
#include "StarTime.hpp"
|
||||
#include "StarXXHash.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
float const EnvironmentPainter::SunriseTime = 0.072f;
|
||||
float const EnvironmentPainter::SunsetTime = 0.42f;
|
||||
float const EnvironmentPainter::SunFadeRate = 0.07f;
|
||||
float const EnvironmentPainter::MaxFade = 0.3f;
|
||||
float const EnvironmentPainter::RayPerlinFrequency = 0.005f; // Arbitrary, part of using the Perlin as a PRNG
|
||||
float const EnvironmentPainter::RayPerlinAmplitude = 2;
|
||||
int const EnvironmentPainter::RayCount = 60;
|
||||
float const EnvironmentPainter::RayMinWidth = 0.8f; // % of its sector
|
||||
float const EnvironmentPainter::RayWidthVariance = 5.0265f; // % of its sector
|
||||
float const EnvironmentPainter::RayAngleVariance = 6.2832f; // Radians
|
||||
float const EnvironmentPainter::SunRadius = 50;
|
||||
float const EnvironmentPainter::RayColorDependenceLevel = 3.0f;
|
||||
float const EnvironmentPainter::RayColorDependenceScale = 0.00625f;
|
||||
float const EnvironmentPainter::RayUnscaledAlphaVariance = 2.0943f;
|
||||
float const EnvironmentPainter::RayMinUnscaledAlpha = 1;
|
||||
Vec3B const EnvironmentPainter::RayColor = Vec3B(255, 255, 200);
|
||||
|
||||
EnvironmentPainter::EnvironmentPainter(RendererPtr renderer) {
|
||||
m_renderer = move(renderer);
|
||||
m_textureGroup = make_shared<AssetTextureGroup>(m_renderer->createTextureGroup(TextureGroupSize::Large));
|
||||
m_timer = 0;
|
||||
m_lastTime = 0;
|
||||
m_rayPerlin = PerlinF(1, RayPerlinFrequency, RayPerlinAmplitude, 0, 2.0f, 2.0f, Random::randu64());
|
||||
}
|
||||
|
||||
void EnvironmentPainter::update() {
|
||||
// Allows the rays to change alpha with time.
|
||||
int64_t currentTime = Time::monotonicMilliseconds();
|
||||
m_timer += (currentTime - m_lastTime) / 1000.0;
|
||||
m_timer = std::fmod(m_timer, Constants::pi * 100000.0);
|
||||
m_lastTime = currentTime;
|
||||
}
|
||||
|
||||
void EnvironmentPainter::renderStars(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky) {
|
||||
if (!sky.settings)
|
||||
return;
|
||||
|
||||
float nightSkyAlpha = 1.0f - min(sky.dayLevel, sky.skyAlpha);
|
||||
if (nightSkyAlpha <= 0.0f)
|
||||
return;
|
||||
|
||||
Vec4B color(255, 255, 255, 255 * nightSkyAlpha);
|
||||
|
||||
Vec2F viewSize = screenSize / pixelRatio;
|
||||
Vec2F viewCenter = viewSize / 2;
|
||||
Vec2F viewMin = sky.starOffset - viewCenter;
|
||||
|
||||
auto newStarsHash = starsHash(sky, viewSize);
|
||||
if (newStarsHash != m_starsHash) {
|
||||
m_starsHash = newStarsHash;
|
||||
setupStars(sky);
|
||||
}
|
||||
|
||||
float screenBuffer = sky.settings.queryFloat("stars.screenBuffer");
|
||||
|
||||
PolyF field = PolyF(RectF::withSize(viewMin, Vec2F(viewSize)).padded(screenBuffer));
|
||||
field.rotate(-sky.starRotation, Vec2F(sky.starOffset));
|
||||
|
||||
Mat3F transform = Mat3F::identity();
|
||||
transform.translate(-viewMin);
|
||||
transform.rotate(sky.starRotation, viewCenter);
|
||||
|
||||
int starTwinkleMin = sky.settings.queryInt("stars.twinkleMin");
|
||||
int starTwinkleMax = sky.settings.queryInt("stars.twinkleMax");
|
||||
size_t starTypesSize = sky.starTypes().size();
|
||||
|
||||
auto stars = m_starGenerator->generate(field, [&](RandomSource& rand) {
|
||||
size_t starType = rand.randu32() % starTypesSize;
|
||||
float frameOffset = rand.randu32() % sky.starFrames + rand.randf(starTwinkleMin, starTwinkleMax);
|
||||
return pair<size_t, float>(starType, frameOffset);
|
||||
});
|
||||
|
||||
RectF viewRect = RectF::withSize(Vec2F(), viewSize).padded(screenBuffer);
|
||||
|
||||
for (auto star : stars) {
|
||||
Vec2F screenPos = transform.transformVec2(star.first);
|
||||
if (viewRect.contains(screenPos)) {
|
||||
size_t starFrame = (int)(sky.epochTime + star.second.second) % sky.starFrames;
|
||||
auto const& texture = m_starTextures[star.second.first * sky.starFrames + starFrame];
|
||||
m_renderer->render(renderTexturedRect(texture, screenPos * pixelRatio - Vec2F(texture->size()) / 2, 1.0, color, 0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
m_renderer->flush();
|
||||
}
|
||||
|
||||
void EnvironmentPainter::renderDebrisFields(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky) {
|
||||
if (!sky.settings)
|
||||
return;
|
||||
|
||||
if (sky.type == SkyType::Orbital || sky.type == SkyType::Warp) {
|
||||
Vec2F viewSize = screenSize / pixelRatio;
|
||||
Vec2F viewCenter = viewSize / 2;
|
||||
Vec2F viewMin = sky.starOffset - viewCenter;
|
||||
|
||||
Mat3F rotMatrix = Mat3F::rotation(sky.starRotation, viewCenter);
|
||||
|
||||
JsonArray debrisFields = sky.settings.queryArray("spaceDebrisFields");
|
||||
for (size_t i = 0; i < debrisFields.size(); ++i) {
|
||||
Json debrisField = debrisFields[i];
|
||||
|
||||
Vec2F spaceDebrisVelocityRange = jsonToVec2F(debrisField.query("velocityRange"));
|
||||
float debrisXVel = staticRandomFloatRange(spaceDebrisVelocityRange[0], spaceDebrisVelocityRange[1], sky.skyParameters.seed, i, "DebrisFieldXVel");
|
||||
float debrisYVel = staticRandomFloatRange(spaceDebrisVelocityRange[0], spaceDebrisVelocityRange[1], sky.skyParameters.seed, i, "DebrisFieldYVel");
|
||||
|
||||
// Translate the entire field to make the debris seem as though they are moving
|
||||
Vec2F velocityOffset = -Vec2F(debrisXVel, debrisYVel) * sky.epochTime;
|
||||
|
||||
float screenBuffer = debrisField.queryFloat("screenBuffer");
|
||||
PolyF field = PolyF(RectF::withSize(viewMin, viewSize).padded(screenBuffer).translated(velocityOffset));
|
||||
|
||||
Vec2F debrisAngularVelocityRange = jsonToVec2F(debrisField.query("angularVelocityRange"));
|
||||
JsonArray imageOptions = debrisField.query("list").toArray();
|
||||
|
||||
auto debrisItems = m_debrisGenerators[i]->generate(field,
|
||||
[&](RandomSource& rand) {
|
||||
String debrisImage = rand.randFrom(imageOptions).toString();
|
||||
float debrisAngularVelocity = rand.randf(debrisAngularVelocityRange[0], debrisAngularVelocityRange[1]);
|
||||
|
||||
return pair<String, float>(debrisImage, debrisAngularVelocity);
|
||||
});
|
||||
|
||||
Vec2F debrisPositionOffset = -(sky.starOffset + velocityOffset + viewCenter);
|
||||
|
||||
for (auto debrisItem : debrisItems) {
|
||||
Vec2F debrisPosition = rotMatrix.transformVec2(debrisItem.first + debrisPositionOffset);
|
||||
float debrisAngle = fmod(Constants::deg2rad * debrisItem.second.second * sky.epochTime, Constants::pi * 2) + sky.starRotation;
|
||||
drawOrbiter(pixelRatio, screenSize, sky, {SkyOrbiterType::SpaceDebris, 1.0f, debrisAngle, debrisItem.second.first, debrisPosition});
|
||||
}
|
||||
}
|
||||
|
||||
m_renderer->flush();
|
||||
}
|
||||
}
|
||||
|
||||
void EnvironmentPainter::renderBackOrbiters(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky) {
|
||||
for (auto const& orbiter : sky.backOrbiters(screenSize / pixelRatio))
|
||||
drawOrbiter(pixelRatio, screenSize, sky, orbiter);
|
||||
|
||||
m_renderer->flush();
|
||||
}
|
||||
|
||||
void EnvironmentPainter::renderPlanetHorizon(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky) {
|
||||
auto planetHorizon = sky.worldHorizon(screenSize / pixelRatio);
|
||||
if (planetHorizon.empty())
|
||||
return;
|
||||
|
||||
// Can't bail sooner, need to queue all textures
|
||||
bool allLoaded = true;
|
||||
for (auto const& layer : planetHorizon.layers) {
|
||||
if (!m_textureGroup->tryTexture(layer.first) || !m_textureGroup->tryTexture(layer.second))
|
||||
allLoaded = false;
|
||||
}
|
||||
|
||||
if (!allLoaded)
|
||||
return;
|
||||
|
||||
float planetPixelRatio = pixelRatio * planetHorizon.scale;
|
||||
Vec2F center = planetHorizon.center * pixelRatio;
|
||||
|
||||
for (auto const& layer : planetHorizon.layers) {
|
||||
TexturePtr leftTexture = m_textureGroup->loadTexture(layer.first);
|
||||
Vec2F leftTextureSize(leftTexture->size());
|
||||
TexturePtr rightTexture = m_textureGroup->loadTexture(layer.second);
|
||||
Vec2F rightTextureSize(rightTexture->size());
|
||||
|
||||
Vec2F leftLayer = center;
|
||||
leftLayer[0] -= leftTextureSize[0] * planetPixelRatio;
|
||||
auto leftRect = RectF::withSize(leftLayer, leftTextureSize * planetPixelRatio);
|
||||
PolyF leftImage = PolyF(leftRect);
|
||||
leftImage.rotate(planetHorizon.rotation, center);
|
||||
|
||||
auto rightRect = RectF::withSize(center, rightTextureSize * planetPixelRatio);
|
||||
PolyF rightImage = PolyF(rightRect);
|
||||
rightImage.rotate(planetHorizon.rotation, center);
|
||||
|
||||
m_renderer->render(RenderQuad{move(leftTexture),
|
||||
{leftImage[0], Vec2F(0, 0), {255, 255, 255, 255}, 0.0f},
|
||||
{leftImage[1], Vec2F(leftTextureSize[0], 0), {255, 255, 255, 255}, 0.0f},
|
||||
{leftImage[2], Vec2F(leftTextureSize[0], leftTextureSize[1]), {255, 255, 255, 255}, 0.0f},
|
||||
{leftImage[3], Vec2F(0, leftTextureSize[1]), {255, 255, 255, 255}, 0.0f}});
|
||||
|
||||
m_renderer->render(RenderQuad{move(rightTexture),
|
||||
{rightImage[0], Vec2F(0, 0), {255, 255, 255, 255}, 0.0f},
|
||||
{rightImage[1], Vec2F(rightTextureSize[0], 0), {255, 255, 255, 255}, 0.0f},
|
||||
{rightImage[2], Vec2F(rightTextureSize[0], rightTextureSize[1]), {255, 255, 255, 255}, 0.0f},
|
||||
{rightImage[3], Vec2F(0, rightTextureSize[1]), {255, 255, 255, 255}, 0.0f}});
|
||||
}
|
||||
|
||||
m_renderer->flush();
|
||||
}
|
||||
|
||||
void EnvironmentPainter::renderFrontOrbiters(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky) {
|
||||
for (auto const& orbiter : sky.frontOrbiters(screenSize / pixelRatio))
|
||||
drawOrbiter(pixelRatio, screenSize, sky, orbiter);
|
||||
|
||||
m_renderer->flush();
|
||||
}
|
||||
|
||||
void EnvironmentPainter::renderSky(Vec2F const& screenSize, SkyRenderData const& sky) {
|
||||
m_renderer->render(RenderQuad{{},
|
||||
{Vec2F(0, 0), Vec2F(), sky.bottomRectColor.toRgba(), 0.0f},
|
||||
{Vec2F(screenSize[0], 0), Vec2F(), sky.bottomRectColor.toRgba(), 0.0f},
|
||||
{screenSize, Vec2F(), sky.topRectColor.toRgba(), 0.0f},
|
||||
{Vec2F(0, screenSize[1]), Vec2F(), sky.topRectColor.toRgba(), 0.0f}});
|
||||
|
||||
// Flash overlay for Interstellar travel
|
||||
Vec4B flashColor = sky.flashColor.toRgba();
|
||||
m_renderer->render(renderFlatRect(RectF(Vec2F(), screenSize), flashColor, 0.0f));
|
||||
|
||||
m_renderer->flush();
|
||||
}
|
||||
|
||||
void EnvironmentPainter::renderParallaxLayers(
|
||||
Vec2F parallaxWorldPosition, WorldCamera const& camera, ParallaxLayers const& layers, SkyRenderData const& sky) {
|
||||
|
||||
// Note: the "parallax space" referenced below is a grid where the scale of each cell is the size of the parallax image
|
||||
|
||||
for (auto layer : layers) {
|
||||
if (layer.alpha == 0)
|
||||
continue;
|
||||
|
||||
Vec4B drawColor;
|
||||
if (layer.unlit || layer.lightMapped)
|
||||
drawColor = Vec4B(255, 255, 255, floor(255 * layer.alpha));
|
||||
else
|
||||
drawColor = Vec4B(sky.environmentLight.toRgb(), floor(255 * layer.alpha));
|
||||
|
||||
Vec2F parallaxValue = layer.parallaxValue;
|
||||
Vec2B parallaxRepeat = layer.repeat;
|
||||
Vec2F parallaxOrigin = {0.0f, layer.verticalOrigin};
|
||||
Vec2F parallaxSize =
|
||||
Vec2F(m_textureGroup->loadTexture(String::joinWith("?", layer.textures.first(), layer.directives))->size());
|
||||
Vec2F parallaxPixels = parallaxSize * camera.pixelRatio();
|
||||
|
||||
// texture offset in *screen pixel space*
|
||||
Vec2F parallaxOffset = layer.parallaxOffset * camera.pixelRatio();
|
||||
if (layer.speed != 0) {
|
||||
double drift = fmod((double)layer.speed * (sky.epochTime / (double)sky.dayLength) * camera.pixelRatio(), (double)parallaxPixels[0]);
|
||||
parallaxOffset[0] = fmod(parallaxOffset[0] + drift, parallaxPixels[0]);
|
||||
}
|
||||
|
||||
// parallax camera world position in *parallax space*
|
||||
Vec2F parallaxCameraCenter = parallaxWorldPosition - parallaxOrigin;
|
||||
parallaxCameraCenter =
|
||||
Vec2F((((parallaxCameraCenter[0] / parallaxPixels[0]) * TilePixels) * camera.pixelRatio()) / parallaxValue[0],
|
||||
(((parallaxCameraCenter[1] / parallaxPixels[1]) * TilePixels) * camera.pixelRatio()) / parallaxValue[1]);
|
||||
|
||||
// width / height of screen in *parallax space*
|
||||
float parallaxScreenWidth = camera.screenSize()[0] / parallaxPixels[0];
|
||||
float parallaxScreenHeight = camera.screenSize()[1] / parallaxPixels[1];
|
||||
|
||||
// screen world position in *parallax space*
|
||||
float parallaxScreenLeft = parallaxCameraCenter[0] - parallaxScreenWidth / 2.0;
|
||||
float parallaxScreenBottom = parallaxCameraCenter[1] - parallaxScreenHeight / 2.0;
|
||||
|
||||
// screen boundary world positions in *parallax space*
|
||||
Vec2F parallaxScreenOffset = parallaxOffset.piecewiseDivide(parallaxPixels);
|
||||
int left = floor(parallaxScreenLeft + parallaxScreenOffset[0]);
|
||||
int bottom = floor(parallaxScreenBottom + parallaxScreenOffset[1]);
|
||||
int right = ceil(parallaxScreenLeft + parallaxScreenWidth + parallaxScreenOffset[0]);
|
||||
int top = ceil(parallaxScreenBottom + parallaxScreenHeight + parallaxScreenOffset[1]);
|
||||
|
||||
// positions to start tiling in *screen pixel space*
|
||||
float pixelLeft = (left - parallaxScreenLeft) * parallaxPixels[0] - parallaxOffset[0];
|
||||
float pixelBottom = (bottom - parallaxScreenBottom) * parallaxPixels[1] - parallaxOffset[1];
|
||||
|
||||
// vertical top and bottom cutoff points in *parallax space*
|
||||
float tileLimitTop = top;
|
||||
if (layer.tileLimitTop)
|
||||
tileLimitTop = (layer.parallaxOffset[1] - layer.tileLimitTop.value()) / parallaxSize[1];
|
||||
float tileLimitBottom = bottom;
|
||||
if (layer.tileLimitBottom)
|
||||
tileLimitBottom = (layer.parallaxOffset[1] - layer.tileLimitBottom.value()) / parallaxSize[1];
|
||||
|
||||
float lightMapMultiplier = (!layer.unlit && layer.lightMapped) ? 1.0f : 0.0f;
|
||||
|
||||
for (int y = bottom; y <= top; ++y) {
|
||||
if (!(parallaxRepeat[1] || y == 0) || y > tileLimitTop || y + 1 < tileLimitBottom)
|
||||
continue;
|
||||
for (int x = left; x <= right; ++x) {
|
||||
if (!(parallaxRepeat[0] || x == 0))
|
||||
continue;
|
||||
float pixelTileLeft = pixelLeft + (x - left) * parallaxPixels[0];
|
||||
float pixelTileBottom = pixelBottom + (y - bottom) * parallaxPixels[1];
|
||||
|
||||
Vec2F anchorPoint(pixelTileLeft, pixelTileBottom);
|
||||
|
||||
RectF subImage = RectF::withSize(Vec2F(), parallaxSize);
|
||||
if (tileLimitTop != top && y + 1 > tileLimitTop)
|
||||
subImage.setYMin(parallaxSize[1] * (1.0f - fpart(tileLimitTop)));
|
||||
if (tileLimitBottom != bottom && y < tileLimitBottom)
|
||||
anchorPoint[1] += fpart(tileLimitBottom) * parallaxPixels[1];
|
||||
|
||||
for (String const& textureImage : layer.textures) {
|
||||
if (auto texture = m_textureGroup->tryTexture(String::joinWith("?", textureImage, layer.directives))) {
|
||||
RectF drawRect = RectF::withSize(anchorPoint, subImage.size() * camera.pixelRatio());
|
||||
m_renderer->render(RenderQuad{move(texture),
|
||||
{{drawRect.xMin(), drawRect.yMin()}, {subImage.xMin(), subImage.yMin()}, drawColor, lightMapMultiplier},
|
||||
{{drawRect.xMax(), drawRect.yMin()}, {subImage.xMax(), subImage.yMin()}, drawColor, lightMapMultiplier},
|
||||
{{drawRect.xMax(), drawRect.yMax()}, {subImage.xMax(), subImage.yMax()}, drawColor, lightMapMultiplier},
|
||||
{{drawRect.xMin(), drawRect.yMax()}, {subImage.xMin(), subImage.yMax()}, drawColor, lightMapMultiplier}});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_renderer->flush();
|
||||
}
|
||||
|
||||
void EnvironmentPainter::cleanup(int64_t textureTimeout) {
|
||||
m_textureGroup->cleanup(textureTimeout);
|
||||
}
|
||||
|
||||
void EnvironmentPainter::drawRays(
|
||||
float pixelRatio, SkyRenderData const& sky, Vec2F start, float length, double time, float alpha) {
|
||||
// All magic constants are either 2PI or arbritrary to allow the Perlin to act
|
||||
// as a PRNG
|
||||
float sectorWidth = 2 * Constants::pi / RayCount; // Radians
|
||||
Vec3B color = sky.topRectColor.toRgb();
|
||||
|
||||
for (int i = 0; i < RayCount; i++)
|
||||
drawRay(pixelRatio,
|
||||
sky,
|
||||
start,
|
||||
sectorWidth * (std::abs(m_rayPerlin.get(i * 25)) * RayWidthVariance + RayMinWidth),
|
||||
length,
|
||||
i * sectorWidth + m_rayPerlin.get(i * 314) * RayAngleVariance,
|
||||
time,
|
||||
color,
|
||||
alpha);
|
||||
|
||||
m_renderer->flush();
|
||||
}
|
||||
|
||||
void EnvironmentPainter::drawRay(float pixelRatio,
|
||||
SkyRenderData const& sky,
|
||||
Vec2F start,
|
||||
float width,
|
||||
float length,
|
||||
float angle,
|
||||
double time,
|
||||
Vec3B color,
|
||||
float alpha) {
|
||||
// All magic constants are arbritrary to allow the Perlin to act as a PRNG
|
||||
|
||||
float currentTime = sky.timeOfDay / sky.dayLength;
|
||||
float timeSinceSunEvent = std::min(std::abs(currentTime - SunriseTime), std::abs(currentTime - SunsetTime));
|
||||
float percentFaded = MaxFade * (1.0f - std::min(1.0f, std::pow(timeSinceSunEvent / SunFadeRate, 2.0f)));
|
||||
// Gets the current average sky color
|
||||
color = (Vec3B)((Vec3F)color * (1 - percentFaded) + (Vec3F)sky.mainSkyColor.toRgb() * percentFaded);
|
||||
// Sum is used to vary the ray intensity based on sky color
|
||||
// Rays show up more on darker backgrounds, so this scales to remove that
|
||||
float sum = std::pow((color[0] + color[1]) * RayColorDependenceScale, RayColorDependenceLevel);
|
||||
m_renderer->render(RenderQuad{{},
|
||||
{start + Vec2F(std::cos(angle + width), std::sin(angle + width)) * length, {}, Vec4B(RayColor, 0), 0.0f},
|
||||
{start + Vec2F(std::cos(angle + width), std::sin(angle + width)) * SunRadius * pixelRatio,
|
||||
{},
|
||||
Vec4B(RayColor,
|
||||
(int)(RayMinUnscaledAlpha + std::abs(m_rayPerlin.get(angle * 896 + time * 30) * RayUnscaledAlphaVariance))
|
||||
* sum
|
||||
* alpha), 0.0f},
|
||||
{start + Vec2F(std::cos(angle), std::sin(angle)) * SunRadius * pixelRatio,
|
||||
{},
|
||||
Vec4B(RayColor,
|
||||
(int)(RayMinUnscaledAlpha + std::abs(m_rayPerlin.get(angle * 626 + time * 30) * RayUnscaledAlphaVariance))
|
||||
* sum
|
||||
* alpha), 0.0f},
|
||||
{start + Vec2F(std::cos(angle), std::sin(angle)) * length, {}, Vec4B(RayColor, 0), 0.0f}});
|
||||
}
|
||||
|
||||
void EnvironmentPainter::drawOrbiter(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky, SkyOrbiter const& orbiter) {
|
||||
float alpha = 1.0f;
|
||||
Vec2F position = orbiter.position * pixelRatio;
|
||||
|
||||
if (orbiter.type == SkyOrbiterType::Sun) {
|
||||
alpha = sky.dayLevel;
|
||||
drawRays(pixelRatio, sky, position, std::max(screenSize[0], screenSize[1]), m_timer, sky.skyAlpha);
|
||||
}
|
||||
|
||||
TexturePtr texture = m_textureGroup->loadTexture(orbiter.image);
|
||||
Vec2F texSize = Vec2F(texture->size());
|
||||
|
||||
Mat3F renderMatrix = Mat3F::rotation(orbiter.angle, position);
|
||||
RectF renderRect = RectF::withCenter(position, texSize * orbiter.scale * pixelRatio);
|
||||
Vec4B renderColor = Vec4B(255, 255, 255, 255 * alpha);
|
||||
|
||||
m_renderer->render(RenderQuad{move(texture),
|
||||
{renderMatrix.transformVec2(renderRect.min()), Vec2F(0, 0), renderColor, 0.0f},
|
||||
{renderMatrix.transformVec2(Vec2F{renderRect.xMax(), renderRect.yMin()}), Vec2F(texSize[0], 0), renderColor, 0.0f},
|
||||
{renderMatrix.transformVec2(renderRect.max()), Vec2F(texSize[0], texSize[1]), renderColor, 0.0f},
|
||||
{renderMatrix.transformVec2(Vec2F{renderRect.xMin(), renderRect.yMax()}), Vec2F(0, texSize[1]), renderColor, 0.0f}});
|
||||
}
|
||||
|
||||
uint64_t EnvironmentPainter::starsHash(SkyRenderData const& sky, Vec2F const& viewSize) const {
|
||||
XXHash64 hasher;
|
||||
|
||||
hasher.push(reinterpret_cast<char const*>(&viewSize[0]), sizeof(viewSize[0]));
|
||||
hasher.push(reinterpret_cast<char const*>(&viewSize[1]), sizeof(viewSize[1]));
|
||||
hasher.push(reinterpret_cast<char const*>(&sky.skyParameters.seed), sizeof(sky.skyParameters.seed));
|
||||
hasher.push(reinterpret_cast<char const*>(&sky.type), sizeof(sky.type));
|
||||
|
||||
return hasher.digest();
|
||||
}
|
||||
|
||||
void EnvironmentPainter::setupStars(SkyRenderData const& sky) {
|
||||
if (!sky.settings)
|
||||
return;
|
||||
|
||||
StringList starTypes = sky.starTypes();
|
||||
size_t starTypesSize = starTypes.size();
|
||||
|
||||
m_starTextures.resize(starTypesSize * sky.starFrames);
|
||||
for (size_t i = 0; i < starTypesSize; ++i) {
|
||||
for (size_t j = 0; j < sky.starFrames; ++j)
|
||||
m_starTextures[i * sky.starFrames + j] = m_textureGroup->loadTexture(starTypes[i] + ":" + toString(j));
|
||||
}
|
||||
|
||||
int starCellSize = sky.settings.queryInt("stars.cellSize");
|
||||
Vec2I starCount = jsonToVec2I(sky.settings.query("stars.cellCount"));
|
||||
|
||||
m_starGenerator = make_shared<Random2dPointGenerator<pair<size_t, float>>>(sky.skyParameters.seed, starCellSize, starCount);
|
||||
|
||||
JsonArray debrisFields = sky.settings.queryArray("spaceDebrisFields");
|
||||
m_debrisGenerators.resize(debrisFields.size());
|
||||
for (size_t i = 0; i < debrisFields.size(); ++i) {
|
||||
int debrisCellSize = debrisFields[i].getInt("cellSize");
|
||||
Vec2I debrisCountRange = jsonToVec2I(debrisFields[i].get("cellCountRange"));
|
||||
uint64_t debrisSeed = staticRandomU64(sky.skyParameters.seed, i, "DebrisFieldSeed");
|
||||
m_debrisGenerators[i] = make_shared<Random2dPointGenerator<pair<String, float>>>(debrisSeed, debrisCellSize, debrisCountRange);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
81
source/rendering/StarEnvironmentPainter.hpp
Normal file
81
source/rendering/StarEnvironmentPainter.hpp
Normal file
|
@ -0,0 +1,81 @@
|
|||
#ifndef STAR_ENVIRONMENT_PAINTER_HPP
|
||||
#define STAR_ENVIRONMENT_PAINTER_HPP
|
||||
|
||||
#include "StarParallax.hpp"
|
||||
#include "StarWorldRenderData.hpp"
|
||||
#include "StarAssetTextureGroup.hpp"
|
||||
#include "StarRenderer.hpp"
|
||||
#include "StarWorldCamera.hpp"
|
||||
#include "StarPerlin.hpp"
|
||||
#include "StarRandomPoint.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(EnvironmentPainter);
|
||||
|
||||
class EnvironmentPainter {
|
||||
public:
|
||||
EnvironmentPainter(RendererPtr renderer);
|
||||
|
||||
void update();
|
||||
|
||||
void renderStars(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky);
|
||||
void renderDebrisFields(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky);
|
||||
void renderBackOrbiters(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky);
|
||||
void renderPlanetHorizon(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky);
|
||||
void renderFrontOrbiters(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky);
|
||||
void renderSky(Vec2F const& screenSize, SkyRenderData const& sky);
|
||||
|
||||
void renderParallaxLayers(Vec2F parallaxWorldPosition, WorldCamera const& camera, ParallaxLayers const& layers, SkyRenderData const& sky);
|
||||
|
||||
void cleanup(int64_t textureTimeout);
|
||||
|
||||
private:
|
||||
static float const SunriseTime;
|
||||
static float const SunsetTime;
|
||||
static float const SunFadeRate;
|
||||
static float const MaxFade;
|
||||
static float const RayPerlinFrequency;
|
||||
static float const RayPerlinAmplitude;
|
||||
static int const RayCount;
|
||||
static float const RayMinWidth;
|
||||
static float const RayWidthVariance;
|
||||
static float const RayAngleVariance;
|
||||
static float const SunRadius;
|
||||
static float const RayColorDependenceLevel;
|
||||
static float const RayColorDependenceScale;
|
||||
static float const RayUnscaledAlphaVariance;
|
||||
static float const RayMinUnscaledAlpha;
|
||||
static Vec3B const RayColor;
|
||||
|
||||
void drawRays(float pixelRatio, SkyRenderData const& sky, Vec2F start, float length, double time, float alpha);
|
||||
void drawRay(float pixelRatio,
|
||||
SkyRenderData const& sky,
|
||||
Vec2F start,
|
||||
float width,
|
||||
float length,
|
||||
float angle,
|
||||
double time,
|
||||
Vec3B color,
|
||||
float alpha);
|
||||
void drawOrbiter(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky, SkyOrbiter const& orbiter);
|
||||
|
||||
uint64_t starsHash(SkyRenderData const& sky, Vec2F const& viewSize) const;
|
||||
void setupStars(SkyRenderData const& sky);
|
||||
|
||||
RendererPtr m_renderer;
|
||||
AssetTextureGroupPtr m_textureGroup;
|
||||
|
||||
double m_timer;
|
||||
int64_t m_lastTime;
|
||||
PerlinF m_rayPerlin;
|
||||
|
||||
uint64_t m_starsHash;
|
||||
List<TexturePtr> m_starTextures;
|
||||
shared_ptr<Random2dPointGenerator<pair<size_t, float>>> m_starGenerator;
|
||||
List<shared_ptr<Random2dPointGenerator<pair<String, float>>>> m_debrisGenerators;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
40
source/rendering/StarFontTextureGroup.cpp
Normal file
40
source/rendering/StarFontTextureGroup.cpp
Normal file
|
@ -0,0 +1,40 @@
|
|||
#include "StarFontTextureGroup.hpp"
|
||||
#include "StarTime.hpp"
|
||||
#include "StarImageProcessing.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
FontTextureGroup::FontTextureGroup(FontPtr font, TextureGroupPtr textureGroup)
|
||||
: m_font(move(font)), m_textureGroup(move(textureGroup)) {}
|
||||
|
||||
void FontTextureGroup::cleanup(int64_t timeout) {
|
||||
int64_t currentTime = Time::monotonicMilliseconds();
|
||||
eraseWhere(m_glyphs, [&](auto const& p) { return currentTime - p.second.time > timeout; });
|
||||
}
|
||||
|
||||
TexturePtr FontTextureGroup::glyphTexture(String::Char c, unsigned size) {
|
||||
return glyphTexture(c, size, "");
|
||||
}
|
||||
|
||||
TexturePtr FontTextureGroup::glyphTexture(String::Char c, unsigned size, String const& processingDirectives) {
|
||||
auto res = m_glyphs.insert(GlyphDescriptor{c, size, processingDirectives}, GlyphTexture());
|
||||
|
||||
if (res.second) {
|
||||
m_font->setPixelSize(size);
|
||||
Image image = m_font->render(c);
|
||||
if (!processingDirectives.empty())
|
||||
image = processImageOperations(parseImageOperations(processingDirectives), image);
|
||||
|
||||
res.first->second.texture = m_textureGroup->create(image);
|
||||
}
|
||||
|
||||
res.first->second.time = Time::monotonicMilliseconds();
|
||||
return res.first->second.texture;
|
||||
}
|
||||
|
||||
unsigned FontTextureGroup::glyphWidth(String::Char c, unsigned fontSize) {
|
||||
m_font->setPixelSize(fontSize);
|
||||
return m_font->width(c);
|
||||
}
|
||||
|
||||
}
|
40
source/rendering/StarFontTextureGroup.hpp
Normal file
40
source/rendering/StarFontTextureGroup.hpp
Normal file
|
@ -0,0 +1,40 @@
|
|||
#ifndef STAR_FONT_TEXTURE_GROUP_HPP
|
||||
#define STAR_FONT_TEXTURE_GROUP_HPP
|
||||
|
||||
#include "StarColor.hpp"
|
||||
#include "StarFont.hpp"
|
||||
#include "StarRenderer.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(FontTextureGroup);
|
||||
|
||||
class FontTextureGroup {
|
||||
public:
|
||||
FontTextureGroup(FontPtr font, TextureGroupPtr textureGroup);
|
||||
|
||||
TexturePtr glyphTexture(String::Char, unsigned fontSize);
|
||||
TexturePtr glyphTexture(String::Char, unsigned fontSize, String const& processingDirectives);
|
||||
|
||||
unsigned glyphWidth(String::Char c, unsigned fontSize);
|
||||
|
||||
// Removes glyphs that haven't been used in more than the given time in
|
||||
// milliseconds
|
||||
void cleanup(int64_t timeout);
|
||||
|
||||
private:
|
||||
typedef tuple<String::Char, unsigned, String> GlyphDescriptor;
|
||||
|
||||
struct GlyphTexture {
|
||||
TexturePtr texture;
|
||||
int64_t time;
|
||||
};
|
||||
|
||||
FontPtr m_font;
|
||||
TextureGroupPtr m_textureGroup;
|
||||
HashMap<GlyphDescriptor, GlyphTexture> m_glyphs;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
402
source/rendering/StarTextPainter.cpp
Normal file
402
source/rendering/StarTextPainter.cpp
Normal file
|
@ -0,0 +1,402 @@
|
|||
#include "StarTextPainter.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
|
||||
#include <regex>
|
||||
|
||||
namespace Star {
|
||||
|
||||
namespace Text {
|
||||
String stripEscapeCodes(String const& s) {
|
||||
String regex = strf("\\%s[^;]*%s", CmdEsc, EndEsc);
|
||||
return std::regex_replace(s.utf8(), std::regex(regex.utf8()), "");
|
||||
}
|
||||
|
||||
String preprocessEscapeCodes(String const& s) {
|
||||
bool escape = false;
|
||||
auto result = s.utf8();
|
||||
|
||||
size_t escapeStartIdx = 0;
|
||||
for (size_t i = 0; i < result.size(); i++) {
|
||||
auto& c = result[i];
|
||||
if (c == CmdEsc || c == StartEsc) {
|
||||
escape = true;
|
||||
escapeStartIdx = i;
|
||||
}
|
||||
if ((c <= SpecialCharLimit) && !(c == StartEsc))
|
||||
escape = false;
|
||||
if ((c == EndEsc) && escape)
|
||||
result[escapeStartIdx] = StartEsc;
|
||||
}
|
||||
return {result};
|
||||
}
|
||||
|
||||
String extractCodes(String const& s) {
|
||||
bool escape = false;
|
||||
StringList result;
|
||||
String escapeCode;
|
||||
for (auto c : preprocessEscapeCodes(s)) {
|
||||
if (c == StartEsc)
|
||||
escape = true;
|
||||
if (c == EndEsc) {
|
||||
escape = false;
|
||||
for (auto command : escapeCode.split(','))
|
||||
result.append(command);
|
||||
escapeCode = "";
|
||||
}
|
||||
if (escape && (c != StartEsc))
|
||||
escapeCode.append(c);
|
||||
}
|
||||
if (!result.size())
|
||||
return "";
|
||||
return "^" + result.join(",") + ";";
|
||||
}
|
||||
}
|
||||
|
||||
TextPositioning::TextPositioning() {
|
||||
pos = Vec2F();
|
||||
hAnchor = HorizontalAnchor::LeftAnchor;
|
||||
vAnchor = VerticalAnchor::BottomAnchor;
|
||||
}
|
||||
|
||||
TextPositioning::TextPositioning(Vec2F pos, HorizontalAnchor hAnchor, VerticalAnchor vAnchor,
|
||||
Maybe<unsigned> wrapWidth, Maybe<unsigned> charLimit)
|
||||
: pos(pos), hAnchor(hAnchor), vAnchor(vAnchor), wrapWidth(wrapWidth), charLimit(charLimit) {}
|
||||
|
||||
TextPositioning::TextPositioning(Json const& v) {
|
||||
pos = v.opt("position").apply(jsonToVec2F).value();
|
||||
hAnchor = HorizontalAnchorNames.getLeft(v.getString("horizontalAnchor", "left"));
|
||||
vAnchor = VerticalAnchorNames.getLeft(v.getString("verticalAnchor", "top"));
|
||||
wrapWidth = v.optUInt("wrapWidth");
|
||||
charLimit = v.optUInt("charLimit");
|
||||
}
|
||||
|
||||
Json TextPositioning::toJson() const {
|
||||
return JsonObject{
|
||||
{"position", jsonFromVec2F(pos)},
|
||||
{"horizontalAnchor", HorizontalAnchorNames.getRight(hAnchor)},
|
||||
{"verticalAnchor", VerticalAnchorNames.getRight(vAnchor)},
|
||||
{"wrapWidth", jsonFromMaybe(wrapWidth)}
|
||||
};
|
||||
}
|
||||
|
||||
TextPositioning TextPositioning::translated(Vec2F translation) const {
|
||||
return {pos + translation, hAnchor, vAnchor, wrapWidth, charLimit};
|
||||
}
|
||||
|
||||
TextPainter::TextPainter(FontPtr font, RendererPtr renderer, TextureGroupPtr textureGroup)
|
||||
: m_renderer(renderer),
|
||||
m_fontTextureGroup(move(font), textureGroup),
|
||||
m_fontSize(8),
|
||||
m_lineSpacing(1.30f),
|
||||
m_renderSettings({FontMode::Normal, Vec4B::filled(255)}),
|
||||
m_splitIgnore(" \t"),
|
||||
m_splitForce("\n\v"),
|
||||
m_nonRenderedCharacters("\n\v\r") {}
|
||||
|
||||
RectF TextPainter::renderText(String const& s, TextPositioning const& position) {
|
||||
if (position.charLimit) {
|
||||
unsigned charLimit = *position.charLimit;
|
||||
return doRenderText(s, position, true, &charLimit);
|
||||
} else {
|
||||
return doRenderText(s, position, true, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
RectF TextPainter::renderLine(String const& s, TextPositioning const& position) {
|
||||
if (position.charLimit) {
|
||||
unsigned charLimit = *position.charLimit;
|
||||
return doRenderLine(s, position, true, &charLimit);
|
||||
} else {
|
||||
return doRenderLine(s, position, true, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
RectF TextPainter::renderGlyph(String::Char c, TextPositioning const& position) {
|
||||
return doRenderGlyph(c, position, true);
|
||||
}
|
||||
|
||||
RectF TextPainter::determineTextSize(String const& s, TextPositioning const& position) {
|
||||
return doRenderText(s, position, false, nullptr);
|
||||
}
|
||||
|
||||
RectF TextPainter::determineLineSize(String const& s, TextPositioning const& position) {
|
||||
return doRenderLine(s, position, false, nullptr);
|
||||
}
|
||||
|
||||
RectF TextPainter::determineGlyphSize(String::Char c, TextPositioning const& position) {
|
||||
return doRenderGlyph(c, position, false);
|
||||
}
|
||||
|
||||
int TextPainter::glyphWidth(String::Char c) {
|
||||
return m_fontTextureGroup.glyphWidth(c, m_fontSize);
|
||||
}
|
||||
|
||||
int TextPainter::stringWidth(String const& s) {
|
||||
int width = 0;
|
||||
bool escape = false;
|
||||
|
||||
for (String::Char c : Text::preprocessEscapeCodes(s)) {
|
||||
if (c == Text::StartEsc)
|
||||
escape = true;
|
||||
if (!escape)
|
||||
width += glyphWidth(c);
|
||||
if (c == Text::EndEsc)
|
||||
escape = false;
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
StringList TextPainter::wrapText(String const& s, Maybe<unsigned> wrapWidth) {
|
||||
String text = Text::preprocessEscapeCodes(s);
|
||||
|
||||
unsigned lineStart = 0; // Where does this line start ?
|
||||
unsigned lineCharSize = 0; // how many characters in this line ?
|
||||
unsigned linePixelWidth = 0; // How wide is this line so far
|
||||
|
||||
bool inEscapeSequence = false;
|
||||
|
||||
unsigned splitPos = 0; // Where did we last see a place to split the string ?
|
||||
unsigned splitWidth = 0; // How wide was the string there ?
|
||||
|
||||
StringList lines; // list of renderable string lines
|
||||
|
||||
// loop through every character in the string
|
||||
for (auto character : text) {
|
||||
// this up here to deal with the (common) occurance that the first charcter
|
||||
// is an escape initiator
|
||||
if (character == Text::StartEsc)
|
||||
inEscapeSequence = true;
|
||||
|
||||
if (inEscapeSequence) {
|
||||
lineCharSize++; // just jump straight to the next character, we don't care what it is.
|
||||
if (character == Text::EndEsc)
|
||||
inEscapeSequence = false;
|
||||
} else {
|
||||
lineCharSize++; // assume at least one character if we get here.
|
||||
|
||||
// is this a linefeed / cr / whatever that forces a line split ?
|
||||
if (m_splitForce.find(String(character)) != NPos) {
|
||||
// knock one off the end because we don't render the CR
|
||||
lines.push_back(text.substr(lineStart, lineCharSize - 1));
|
||||
|
||||
lineStart += lineCharSize; // next line starts after the CR
|
||||
lineCharSize = 0; // with no characters in it.
|
||||
linePixelWidth = 0; // No width
|
||||
splitPos = 0; // and no known splits.
|
||||
} else {
|
||||
int charWidth = glyphWidth(character);
|
||||
|
||||
// is it a place where we might want to split the line ?
|
||||
if (m_splitIgnore.find(String(character)) != NPos) {
|
||||
splitPos = lineStart + lineCharSize; // this is the character after the space.
|
||||
splitWidth = linePixelWidth + charWidth; // the width of the string at
|
||||
// the split point, i.e. after the space.
|
||||
}
|
||||
|
||||
// would the line be too long if we render this next character ?
|
||||
if (wrapWidth && (linePixelWidth + charWidth) > *wrapWidth) {
|
||||
// did we find somewhere to split the line ?
|
||||
if (splitPos) {
|
||||
lines.push_back(text.substr(lineStart, (splitPos - lineStart) - 1));
|
||||
|
||||
unsigned stringEnd = lineStart + lineCharSize;
|
||||
lineCharSize = stringEnd - splitPos; // next line has the characters after the space.
|
||||
|
||||
unsigned stringWidth = (linePixelWidth - splitWidth);
|
||||
linePixelWidth = stringWidth + charWidth; // and is as wide as the bit after the space.
|
||||
|
||||
lineStart = splitPos; // next line starts after the space
|
||||
splitPos = 0; // split is used up.
|
||||
} else {
|
||||
// don't draw the last character that puts us over the edge
|
||||
lines.push_back(text.substr(lineStart, lineCharSize - 1));
|
||||
|
||||
lineStart += lineCharSize - 1; // skip back by one to include that
|
||||
// character on the next line.
|
||||
lineCharSize = 1; // next line has that character in
|
||||
linePixelWidth = charWidth; // and is as wide as that character
|
||||
}
|
||||
} else {
|
||||
linePixelWidth += charWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we hit the end of the string before hitting the end of the line.
|
||||
if (lineCharSize > 0)
|
||||
lines.push_back(text.substr(lineStart, lineCharSize));
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
unsigned TextPainter::fontSize() const {
|
||||
return m_fontSize;
|
||||
}
|
||||
|
||||
void TextPainter::setFontSize(unsigned size) {
|
||||
m_fontSize = size;
|
||||
}
|
||||
|
||||
void TextPainter::setLineSpacing(float lineSpacing) {
|
||||
m_lineSpacing = lineSpacing;
|
||||
}
|
||||
|
||||
void TextPainter::setMode(FontMode mode) {
|
||||
m_renderSettings.mode = mode;
|
||||
}
|
||||
|
||||
void TextPainter::setSplitIgnore(String const& splitIgnore) {
|
||||
m_splitIgnore = splitIgnore;
|
||||
}
|
||||
|
||||
void TextPainter::setFontColor(Vec4B color) {
|
||||
m_renderSettings.color = move(color);
|
||||
}
|
||||
|
||||
void TextPainter::setProcessingDirectives(String directives) {
|
||||
m_processingDirectives = move(directives);
|
||||
}
|
||||
|
||||
void TextPainter::cleanup(int64_t timeout) {
|
||||
m_fontTextureGroup.cleanup(timeout);
|
||||
}
|
||||
|
||||
RectF TextPainter::doRenderText(String const& s, TextPositioning const& position, bool reallyRender, unsigned* charLimit) {
|
||||
Vec2F pos = position.pos;
|
||||
StringList lines = wrapText(s, position.wrapWidth);
|
||||
|
||||
int height = (lines.size() - 1) * m_lineSpacing * m_fontSize + m_fontSize;
|
||||
|
||||
auto savedRenderSettings = m_renderSettings;
|
||||
m_savedRenderSettings = m_renderSettings;
|
||||
|
||||
if (position.vAnchor == VerticalAnchor::BottomAnchor)
|
||||
pos[1] += (height - m_fontSize);
|
||||
else if (position.vAnchor == VerticalAnchor::VMidAnchor)
|
||||
pos[1] += (height - m_fontSize) / 2;
|
||||
|
||||
RectF bounds = RectF::withSize(pos, Vec2F());
|
||||
for (auto i : lines) {
|
||||
bounds.combine(doRenderLine(i, { pos, position.hAnchor, position.vAnchor }, reallyRender, charLimit));
|
||||
pos[1] -= m_fontSize * m_lineSpacing;
|
||||
|
||||
if (charLimit && *charLimit == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
m_renderSettings = savedRenderSettings;
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
RectF TextPainter::doRenderLine(String const& s, TextPositioning const& position, bool reallyRender, unsigned* charLimit) {
|
||||
String text = s;
|
||||
TextPositioning pos = position;
|
||||
|
||||
if (pos.hAnchor == HorizontalAnchor::RightAnchor) {
|
||||
auto trimmedString = s;
|
||||
if (charLimit)
|
||||
trimmedString = s.slice(0, *charLimit);
|
||||
pos.pos[0] -= stringWidth(trimmedString);
|
||||
pos.hAnchor = HorizontalAnchor::LeftAnchor;
|
||||
} else if (pos.hAnchor == HorizontalAnchor::HMidAnchor) {
|
||||
auto trimmedString = s;
|
||||
if (charLimit)
|
||||
trimmedString = s.slice(0, *charLimit);
|
||||
unsigned width = stringWidth(trimmedString);
|
||||
pos.pos[0] -= std::floor(width / 2);
|
||||
pos.hAnchor = HorizontalAnchor::LeftAnchor;
|
||||
}
|
||||
|
||||
bool escape = false;
|
||||
String escapeCode;
|
||||
RectF bounds = RectF::withSize(pos.pos, Vec2F());
|
||||
for (String::Char c : text) {
|
||||
if (c == Text::StartEsc)
|
||||
escape = true;
|
||||
|
||||
if (!escape) {
|
||||
if (charLimit) {
|
||||
if (*charLimit == 0)
|
||||
break;
|
||||
else
|
||||
--*charLimit;
|
||||
}
|
||||
RectF glyphBounds = doRenderGlyph(c, pos, reallyRender);
|
||||
bounds.combine(glyphBounds);
|
||||
pos.pos[0] += glyphBounds.width();
|
||||
} else if (c == Text::EndEsc) {
|
||||
auto commands = escapeCode.split(',');
|
||||
for (auto command : commands) {
|
||||
try {
|
||||
if (command == "reset") {
|
||||
m_renderSettings = m_savedRenderSettings;
|
||||
} else if (command == "set") {
|
||||
m_savedRenderSettings = m_renderSettings;
|
||||
} else if (command == "shadow") {
|
||||
m_renderSettings.mode = (FontMode)((int)m_renderSettings.mode | (int)FontMode::Shadow);
|
||||
} else if (command == "noshadow") {
|
||||
m_renderSettings.mode = (FontMode)((int)m_renderSettings.mode & (-1 ^ (int)FontMode::Shadow));
|
||||
} else {
|
||||
// expects both #... sequences and plain old color names.
|
||||
Color c = jsonToColor(command);
|
||||
c.setAlphaF(c.alphaF() * ((float)m_savedRenderSettings.color[3]) / 255);
|
||||
m_renderSettings.color = c.toRgba();
|
||||
}
|
||||
} catch (JsonException&) {
|
||||
} catch (ColorException&) {
|
||||
}
|
||||
}
|
||||
escape = false;
|
||||
escapeCode = "";
|
||||
}
|
||||
if (escape && (c != Text::StartEsc))
|
||||
escapeCode.append(c);
|
||||
}
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
RectF TextPainter::doRenderGlyph(String::Char c, TextPositioning const& position, bool reallyRender) {
|
||||
if (m_nonRenderedCharacters.find(String(c)) != NPos)
|
||||
return RectF();
|
||||
int width = glyphWidth(c);
|
||||
// Offset left by width if right anchored.
|
||||
float hOffset = 0;
|
||||
if (position.hAnchor == HorizontalAnchor::RightAnchor)
|
||||
hOffset = -width;
|
||||
else if (position.hAnchor == HorizontalAnchor::HMidAnchor)
|
||||
hOffset = -std::floor(width / 2);
|
||||
|
||||
float vOffset = 0;
|
||||
if (position.vAnchor == VerticalAnchor::VMidAnchor)
|
||||
vOffset = -std::floor((float)m_fontSize / 2);
|
||||
else if (position.vAnchor == VerticalAnchor::TopAnchor)
|
||||
vOffset = -(float)m_fontSize;
|
||||
|
||||
if (reallyRender) {
|
||||
if ((int)m_renderSettings.mode & (int)FontMode::Shadow) {
|
||||
Color shadow = Color::Black;
|
||||
shadow.setAlpha(m_renderSettings.color[3]);
|
||||
renderGlyph(c, position.pos + Vec2F(hOffset, vOffset - 2), m_fontSize, 1, shadow.toRgba(), m_processingDirectives);
|
||||
renderGlyph(c, position.pos + Vec2F(hOffset, vOffset - 1), m_fontSize, 1, shadow.toRgba(), m_processingDirectives);
|
||||
}
|
||||
|
||||
renderGlyph(c, position.pos + Vec2F(hOffset, vOffset), m_fontSize, 1, m_renderSettings.color, m_processingDirectives);
|
||||
}
|
||||
|
||||
return RectF::withSize(position.pos + Vec2F(hOffset, vOffset), {(float)width, (int)m_fontSize});
|
||||
}
|
||||
|
||||
void TextPainter::renderGlyph(String::Char c, Vec2F const& screenPos, unsigned fontSize,
|
||||
float scale, Vec4B const& color, String const& processingDirectives) {
|
||||
if (!fontSize)
|
||||
return;
|
||||
|
||||
auto texture = m_fontTextureGroup.glyphTexture(c, fontSize, processingDirectives);
|
||||
m_renderer->render(renderTexturedRect(move(texture), Vec2F(screenPos), scale, color, 0.0f));
|
||||
}
|
||||
|
||||
}
|
109
source/rendering/StarTextPainter.hpp
Normal file
109
source/rendering/StarTextPainter.hpp
Normal file
|
@ -0,0 +1,109 @@
|
|||
#ifndef STAR_TEXT_PAINTER_HPP
|
||||
#define STAR_TEXT_PAINTER_HPP
|
||||
|
||||
#include "StarFontTextureGroup.hpp"
|
||||
#include "StarAnchorTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(TextPainter);
|
||||
|
||||
namespace Text {
|
||||
unsigned char const StartEsc = '\x1b';
|
||||
unsigned char const EndEsc = ';';
|
||||
unsigned char const CmdEsc = '^';
|
||||
unsigned char const SpecialCharLimit = ' ';
|
||||
|
||||
String stripEscapeCodes(String const& s);
|
||||
String preprocessEscapeCodes(String const& s);
|
||||
String extractCodes(String const& s);
|
||||
}
|
||||
|
||||
enum class FontMode {
|
||||
Normal,
|
||||
Shadow
|
||||
};
|
||||
|
||||
float const DefaultLineSpacing = 1.3f;
|
||||
|
||||
struct TextPositioning {
|
||||
TextPositioning();
|
||||
|
||||
TextPositioning(Vec2F pos,
|
||||
HorizontalAnchor hAnchor = HorizontalAnchor::LeftAnchor,
|
||||
VerticalAnchor vAnchor = VerticalAnchor::BottomAnchor,
|
||||
Maybe<unsigned> wrapWidth = {},
|
||||
Maybe<unsigned> charLimit = {});
|
||||
|
||||
TextPositioning(Json const& v);
|
||||
Json toJson() const;
|
||||
|
||||
TextPositioning translated(Vec2F translation) const;
|
||||
|
||||
Vec2F pos;
|
||||
HorizontalAnchor hAnchor;
|
||||
VerticalAnchor vAnchor;
|
||||
Maybe<unsigned> wrapWidth;
|
||||
Maybe<unsigned> charLimit;
|
||||
};
|
||||
|
||||
// Renders text while caching individual glyphs for fast rendering but with *no
|
||||
// kerning*.
|
||||
class TextPainter {
|
||||
public:
|
||||
TextPainter(FontPtr font, RendererPtr renderer, TextureGroupPtr textureGroup);
|
||||
|
||||
RectF renderText(String const& s, TextPositioning const& position);
|
||||
RectF renderLine(String const& s, TextPositioning const& position);
|
||||
RectF renderGlyph(String::Char c, TextPositioning const& position);
|
||||
|
||||
RectF determineTextSize(String const& s, TextPositioning const& position);
|
||||
RectF determineLineSize(String const& s, TextPositioning const& position);
|
||||
RectF determineGlyphSize(String::Char c, TextPositioning const& position);
|
||||
|
||||
int glyphWidth(String::Char c);
|
||||
int stringWidth(String const& s);
|
||||
|
||||
StringList wrapText(String const& s, Maybe<unsigned> wrapWidth);
|
||||
|
||||
unsigned fontSize() const;
|
||||
void setFontSize(unsigned size);
|
||||
void setLineSpacing(float lineSpacing);
|
||||
void setMode(FontMode mode);
|
||||
void setSplitIgnore(String const& splitIgnore);
|
||||
void setFontColor(Vec4B color);
|
||||
void setProcessingDirectives(String directives);
|
||||
|
||||
void cleanup(int64_t textureTimeout);
|
||||
|
||||
private:
|
||||
struct RenderSettings {
|
||||
FontMode mode;
|
||||
Vec4B color;
|
||||
};
|
||||
|
||||
RectF doRenderText(String const& s, TextPositioning const& position, bool reallyRender, unsigned* charLimit);
|
||||
RectF doRenderLine(String const& s, TextPositioning const& position, bool reallyRender, unsigned* charLimit);
|
||||
RectF doRenderGlyph(String::Char c, TextPositioning const& position, bool reallyRender);
|
||||
|
||||
void renderGlyph(String::Char c, Vec2F const& screenPos, unsigned fontSize, float scale, Vec4B const& color, String const& processingDirectives);
|
||||
|
||||
RendererPtr m_renderer;
|
||||
FontTextureGroup m_fontTextureGroup;
|
||||
|
||||
unsigned m_fontSize;
|
||||
float m_lineSpacing;
|
||||
|
||||
RenderSettings m_renderSettings;
|
||||
RenderSettings m_savedRenderSettings;
|
||||
|
||||
String m_splitIgnore;
|
||||
String m_splitForce;
|
||||
String m_nonRenderedCharacters;
|
||||
|
||||
String m_processingDirectives;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
539
source/rendering/StarTilePainter.cpp
Normal file
539
source/rendering/StarTilePainter.cpp
Normal file
|
@ -0,0 +1,539 @@
|
|||
#include "StarTilePainter.hpp"
|
||||
#include "StarLexicalCast.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
#include "StarXXHash.hpp"
|
||||
#include "StarMaterialDatabase.hpp"
|
||||
#include "StarLiquidsDatabase.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
TilePainter::TilePainter(RendererPtr renderer) {
|
||||
m_renderer = move(renderer);
|
||||
m_textureGroup = m_renderer->createTextureGroup(TextureGroupSize::Large);
|
||||
|
||||
auto& root = Root::singleton();
|
||||
auto assets = root.assets();
|
||||
|
||||
m_terrainChunkCache.setTimeToLive(assets->json("/rendering.config:chunkCacheTimeout").toInt());
|
||||
m_liquidChunkCache.setTimeToLive(assets->json("/rendering.config:chunkCacheTimeout").toInt());
|
||||
m_textureCache.setTimeToLive(assets->json("/rendering.config:textureTimeout").toInt());
|
||||
|
||||
m_backgroundLayerColor = jsonToColor(assets->json("/rendering.config:backgroundLayerColor")).toRgba();
|
||||
m_foregroundLayerColor = jsonToColor(assets->json("/rendering.config:foregroundLayerColor")).toRgba();
|
||||
m_liquidDrawLevels = jsonToVec2F(assets->json("/rendering.config:liquidDrawLevels"));
|
||||
|
||||
for (auto const& liquid : root.liquidsDatabase()->allLiquidSettings()) {
|
||||
m_liquids.set(liquid->id, LiquidInfo{
|
||||
m_renderer->createTexture(*assets->image(liquid->config.getString("texture")), TextureAddressing::Wrap),
|
||||
jsonToColor(liquid->config.get("color")).toRgba(),
|
||||
jsonToColor(liquid->config.get("bottomLightMix")).toRgbF(),
|
||||
liquid->config.getFloat("textureMovementFactor")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void TilePainter::adjustLighting(WorldRenderData& renderData) const {
|
||||
RectI lightRange = RectI::withSize(renderData.lightMinPosition, Vec2I(renderData.lightMap.size()));
|
||||
forEachRenderTile(renderData, lightRange, [&](Vec2I const& pos, RenderTile const& tile) {
|
||||
// Only adjust lighting for full tiles
|
||||
if (liquidDrawLevel(byteToFloat(tile.liquidLevel)) < 1.0f)
|
||||
return;
|
||||
|
||||
auto lightIndex = Vec2U(pos - renderData.lightMinPosition);
|
||||
auto lightValue = renderData.lightMap.get(lightIndex).vec3();
|
||||
|
||||
auto const& liquid = m_liquids[tile.liquidId];
|
||||
Vec3F tileLight = Vec3F(lightValue);
|
||||
float darknessLevel = (1 - tileLight.sum() / (3.0f * 255.0f));
|
||||
lightValue = Vec3B(tileLight.piecewiseMultiply(Vec3F::filled(1 - darknessLevel) + liquid.bottomLightMix * darknessLevel));
|
||||
|
||||
renderData.lightMap.set(lightIndex, lightValue);
|
||||
});
|
||||
}
|
||||
|
||||
void TilePainter::setup(WorldCamera const& camera, WorldRenderData& renderData) {
|
||||
m_pendingTerrainChunks.clear();
|
||||
m_pendingLiquidChunks.clear();
|
||||
|
||||
auto cameraCenter = camera.centerWorldPosition();
|
||||
if (m_lastCameraCenter)
|
||||
m_cameraPan = renderData.geometry.diff(cameraCenter, *m_lastCameraCenter);
|
||||
m_lastCameraCenter = cameraCenter;
|
||||
|
||||
RectI chunkRange = RectI::integral(RectF(camera.worldTileRect()).scaled(1.0f / RenderChunkSize));
|
||||
|
||||
for (int x = chunkRange.xMin(); x < chunkRange.xMax(); ++x) {
|
||||
for (int y = chunkRange.yMin(); y < chunkRange.yMax(); ++y) {
|
||||
m_pendingTerrainChunks.append(getTerrainChunk(renderData, {x, y}));
|
||||
m_pendingLiquidChunks.append(getLiquidChunk(renderData, {x, y}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TilePainter::renderBackground(WorldCamera const& camera) {
|
||||
renderTerrainChunks(camera, TerrainLayer::Background);
|
||||
}
|
||||
|
||||
void TilePainter::renderMidground(WorldCamera const& camera) {
|
||||
renderTerrainChunks(camera, TerrainLayer::Midground);
|
||||
}
|
||||
|
||||
void TilePainter::renderLiquid(WorldCamera const& camera) {
|
||||
Mat3F transformation = Mat3F::identity();
|
||||
transformation.translate(-Vec2F(camera.worldTileRect().min()));
|
||||
transformation.scale(TilePixels * camera.pixelRatio());
|
||||
transformation.translate(camera.tileMinScreen());
|
||||
|
||||
for (auto const& chunk : m_pendingLiquidChunks) {
|
||||
for (auto const& p : *chunk)
|
||||
m_renderer->renderBuffer(p.second, transformation);
|
||||
}
|
||||
|
||||
m_renderer->flush();
|
||||
}
|
||||
|
||||
void TilePainter::renderForeground(WorldCamera const& camera) {
|
||||
renderTerrainChunks(camera, TerrainLayer::Foreground);
|
||||
}
|
||||
|
||||
void TilePainter::cleanup() {
|
||||
m_pendingTerrainChunks.clear();
|
||||
m_pendingLiquidChunks.clear();
|
||||
|
||||
m_textureCache.cleanup();
|
||||
m_terrainChunkCache.cleanup();
|
||||
m_liquidChunkCache.cleanup();
|
||||
}
|
||||
|
||||
size_t TilePainter::TextureKeyHash::operator()(TextureKey const& key) const {
|
||||
if (key.is<MaterialPieceTextureKey>())
|
||||
return hashOf(key.typeIndex(), key.get<MaterialPieceTextureKey>());
|
||||
else
|
||||
return hashOf(key.typeIndex(), key.get<AssetTextureKey>());
|
||||
}
|
||||
|
||||
TilePainter::ChunkHash TilePainter::terrainChunkHash(WorldRenderData& renderData, Vec2I chunkIndex) {
|
||||
XXHash64 hasher;
|
||||
RectI tileRange = RectI::withSize(chunkIndex * RenderChunkSize, Vec2I::filled(RenderChunkSize)).padded(MaterialRenderProfileMaxNeighborDistance);
|
||||
|
||||
forEachRenderTile(renderData, tileRange, [&](Vec2I const&, RenderTile const& renderTile) {
|
||||
renderTile.hashPushTerrain(hasher);
|
||||
});
|
||||
|
||||
return hasher.digest();
|
||||
}
|
||||
|
||||
TilePainter::ChunkHash TilePainter::liquidChunkHash(WorldRenderData& renderData, Vec2I chunkIndex) {
|
||||
XXHash64 hasher;
|
||||
RectI tileRange = RectI::withSize(chunkIndex * RenderChunkSize, Vec2I::filled(RenderChunkSize)).padded(MaterialRenderProfileMaxNeighborDistance);
|
||||
|
||||
forEachRenderTile(renderData, tileRange, [&](Vec2I const&, RenderTile const& renderTile) {
|
||||
renderTile.hashPushLiquid(hasher);
|
||||
});
|
||||
|
||||
return hasher.digest();
|
||||
}
|
||||
|
||||
TilePainter::QuadZLevel TilePainter::materialZLevel(uint32_t zLevel, MaterialId material, MaterialHue hue, MaterialColorVariant colorVariant) {
|
||||
QuadZLevel quadZLevel = 0;
|
||||
quadZLevel |= (uint64_t)colorVariant;
|
||||
quadZLevel |= (uint64_t)hue << 8;
|
||||
quadZLevel |= (uint64_t)material << 16;
|
||||
quadZLevel |= (uint64_t)zLevel << 32;
|
||||
return quadZLevel;
|
||||
}
|
||||
|
||||
TilePainter::QuadZLevel TilePainter::modZLevel(uint32_t zLevel, ModId mod, MaterialHue hue, MaterialColorVariant colorVariant) {
|
||||
QuadZLevel quadZLevel = 0;
|
||||
quadZLevel |= (uint64_t)colorVariant;
|
||||
quadZLevel |= (uint64_t)hue << 8;
|
||||
quadZLevel |= (uint64_t)mod << 16;
|
||||
quadZLevel |= (uint64_t)zLevel << 32;
|
||||
quadZLevel |= (uint64_t)1 << 63;
|
||||
return quadZLevel;
|
||||
}
|
||||
|
||||
TilePainter::QuadZLevel TilePainter::damageZLevel() {
|
||||
return (uint64_t)(-1);
|
||||
}
|
||||
|
||||
RenderTile const& TilePainter::getRenderTile(WorldRenderData const& renderData, Vec2I const& worldPos) {
|
||||
Vec2I arrayPos = renderData.geometry.diff(worldPos, renderData.tileMinPosition);
|
||||
|
||||
Vec2I size = Vec2I(renderData.tiles.size());
|
||||
if (arrayPos[0] >= 0 && arrayPos[1] >= 0 && arrayPos[0] < size[0] && arrayPos[1] < size[1])
|
||||
return renderData.tiles(Vec2S(arrayPos));
|
||||
|
||||
static RenderTile defaultRenderTile = {
|
||||
NullMaterialId,
|
||||
NoModId,
|
||||
NullMaterialId,
|
||||
NoModId,
|
||||
0,
|
||||
0,
|
||||
DefaultMaterialColorVariant,
|
||||
TileDamageType::Protected,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
DefaultMaterialColorVariant,
|
||||
TileDamageType::Protected,
|
||||
0,
|
||||
EmptyLiquidId,
|
||||
0
|
||||
};
|
||||
return defaultRenderTile;
|
||||
}
|
||||
|
||||
void TilePainter::renderTerrainChunks(WorldCamera const& camera, TerrainLayer terrainLayer) {
|
||||
Map<QuadZLevel, List<RenderBufferPtr>> zOrderBuffers;
|
||||
for (auto const& chunk : m_pendingTerrainChunks) {
|
||||
for (auto const& pair : chunk->value(terrainLayer))
|
||||
zOrderBuffers[pair.first].append(pair.second);
|
||||
}
|
||||
|
||||
Mat3F transformation = Mat3F::identity();
|
||||
transformation.translate(-Vec2F(camera.worldTileRect().min()));
|
||||
transformation.scale(TilePixels * camera.pixelRatio());
|
||||
transformation.translate(camera.tileMinScreen());
|
||||
|
||||
for (auto const& pair : zOrderBuffers) {
|
||||
for (auto const& buffer : pair.second)
|
||||
m_renderer->renderBuffer(buffer, transformation);
|
||||
}
|
||||
|
||||
m_renderer->flush();
|
||||
}
|
||||
|
||||
shared_ptr<TilePainter::TerrainChunk const> TilePainter::getTerrainChunk(WorldRenderData& renderData, Vec2I chunkIndex) {
|
||||
pair<Vec2I, ChunkHash> chunkKey = {chunkIndex, terrainChunkHash(renderData, chunkIndex)};
|
||||
return m_terrainChunkCache.get(chunkKey, [&](auto const&) {
|
||||
HashMap<TerrainLayer, HashMap<QuadZLevel, List<RenderPrimitive>>> terrainPrimitives;
|
||||
|
||||
RectI tileRange = RectI::withSize(chunkIndex * RenderChunkSize, Vec2I::filled(RenderChunkSize));
|
||||
for (int x = tileRange.xMin(); x < tileRange.xMax(); ++x) {
|
||||
for (int y = tileRange.yMin(); y < tileRange.yMax(); ++y) {
|
||||
bool occluded = this->produceTerrainPrimitives(terrainPrimitives[TerrainLayer::Foreground], TerrainLayer::Foreground, {x, y}, renderData);
|
||||
occluded = this->produceTerrainPrimitives(terrainPrimitives[TerrainLayer::Midground], TerrainLayer::Midground, {x, y}, renderData) || occluded;
|
||||
if (!occluded)
|
||||
this->produceTerrainPrimitives(terrainPrimitives[TerrainLayer::Background], TerrainLayer::Background, {x, y}, renderData);
|
||||
}
|
||||
}
|
||||
|
||||
auto chunk = make_shared<TerrainChunk>();
|
||||
|
||||
for (auto& layerPair : terrainPrimitives) {
|
||||
for (auto& zLevelPair : layerPair.second) {
|
||||
auto rb = m_renderer->createRenderBuffer();
|
||||
rb->set(move(zLevelPair.second));
|
||||
(*chunk)[layerPair.first][zLevelPair.first] = move(rb);
|
||||
}
|
||||
}
|
||||
|
||||
return chunk;
|
||||
});
|
||||
}
|
||||
|
||||
shared_ptr<TilePainter::LiquidChunk const> TilePainter::getLiquidChunk(WorldRenderData& renderData, Vec2I chunkIndex) {
|
||||
pair<Vec2I, ChunkHash> chunkKey = {chunkIndex, liquidChunkHash(renderData, chunkIndex)};
|
||||
return m_liquidChunkCache.get(chunkKey, [&](auto const&) {
|
||||
HashMap<LiquidId, List<RenderPrimitive>> liquidPrimitives;
|
||||
|
||||
RectI tileRange = RectI::withSize(chunkIndex * RenderChunkSize, Vec2I::filled(RenderChunkSize));
|
||||
for (int x = tileRange.xMin(); x < tileRange.xMax(); ++x) {
|
||||
for (int y = tileRange.yMin(); y < tileRange.yMax(); ++y)
|
||||
this->produceLiquidPrimitives(liquidPrimitives, {x, y}, renderData);
|
||||
}
|
||||
|
||||
auto chunk = make_shared<LiquidChunk>();
|
||||
|
||||
for (auto& p : liquidPrimitives) {
|
||||
auto rb = m_renderer->createRenderBuffer();
|
||||
rb->set(move(p.second));
|
||||
chunk->set(p.first, move(rb));
|
||||
}
|
||||
|
||||
return chunk;
|
||||
});
|
||||
}
|
||||
|
||||
bool TilePainter::produceTerrainPrimitives(HashMap<QuadZLevel, List<RenderPrimitive>>& primitives,
|
||||
TerrainLayer terrainLayer, Vec2I const& pos, WorldRenderData const& renderData) {
|
||||
auto& root = Root::singleton();
|
||||
auto assets = Root::singleton().assets();
|
||||
auto materialDatabase = root.materialDatabase();
|
||||
|
||||
RenderTile const& tile = getRenderTile(renderData, pos);
|
||||
|
||||
MaterialId material = EmptyMaterialId;
|
||||
MaterialHue materialHue = 0;
|
||||
MaterialHue materialColorVariant = 0;
|
||||
ModId mod = NoModId;
|
||||
MaterialHue modHue = 0;
|
||||
float damageLevel = 0.0f;
|
||||
TileDamageType damageType = TileDamageType::Protected;
|
||||
Vec4B color;
|
||||
|
||||
bool occlude = false;
|
||||
|
||||
if (terrainLayer == TerrainLayer::Background) {
|
||||
material = tile.background;
|
||||
materialHue = tile.backgroundHueShift;
|
||||
materialColorVariant = tile.backgroundColorVariant;
|
||||
mod = tile.backgroundMod;
|
||||
modHue = tile.backgroundModHueShift;
|
||||
damageLevel = byteToFloat(tile.backgroundDamageLevel);
|
||||
damageType = tile.backgroundDamageType;
|
||||
color = m_backgroundLayerColor;
|
||||
} else {
|
||||
material = tile.foreground;
|
||||
materialHue = tile.foregroundHueShift;
|
||||
materialColorVariant = tile.foregroundColorVariant;
|
||||
mod = tile.foregroundMod;
|
||||
modHue = tile.foregroundModHueShift;
|
||||
damageLevel = byteToFloat(tile.foregroundDamageLevel);
|
||||
damageType = tile.foregroundDamageType;
|
||||
color = m_foregroundLayerColor;
|
||||
}
|
||||
|
||||
// render non-block colliding things in the midground
|
||||
bool isBlock = BlockCollisionSet.contains(materialDatabase->materialCollisionKind(material));
|
||||
if ((isBlock && terrainLayer == TerrainLayer::Midground) || (!isBlock && terrainLayer == TerrainLayer::Foreground))
|
||||
return false;
|
||||
|
||||
auto getPieceTexture = [this, assets](MaterialId material, MaterialRenderPieceConstPtr const& piece, MaterialHue hue, bool mod) {
|
||||
return m_textureCache.get(MaterialPieceTextureKey(material, piece->pieceId, hue, mod), [&](auto const&) {
|
||||
String texture;
|
||||
if (hue == 0)
|
||||
texture = piece->texture;
|
||||
else
|
||||
texture = strf("%s?hueshift=%s", piece->texture, materialHueToDegrees(hue));
|
||||
|
||||
return m_textureGroup->create(*assets->image(texture));
|
||||
});
|
||||
};
|
||||
|
||||
auto materialRenderProfile = materialDatabase->materialRenderProfile(material);
|
||||
|
||||
auto modRenderProfile = materialDatabase->modRenderProfile(mod);
|
||||
|
||||
if (materialRenderProfile) {
|
||||
occlude = materialRenderProfile->occludesBehind;
|
||||
|
||||
uint32_t variance = staticRandomU32(renderData.geometry.xwrap(pos[0]), pos[1], (int)terrainLayer, "main");
|
||||
auto& quadList = primitives[materialZLevel(materialRenderProfile->zLevel, material, materialHue, materialColorVariant)];
|
||||
|
||||
MaterialPieceResultList pieces;
|
||||
determineMatchingPieces(pieces, &occlude, materialDatabase, materialRenderProfile->mainMatchList, renderData, pos,
|
||||
terrainLayer == TerrainLayer::Background ? TileLayer::Background : TileLayer::Foreground, false);
|
||||
for (auto const& piecePair : pieces) {
|
||||
TexturePtr texture = getPieceTexture(material, piecePair.first, materialHue, false);
|
||||
RectF textureCoords = piecePair.first->variants.get(materialColorVariant).wrap(variance);
|
||||
RectF worldCoords = RectF::withSize(piecePair.second / TilePixels + Vec2F(pos), textureCoords.size() / TilePixels);
|
||||
quadList.append(RenderQuad{
|
||||
move(texture),
|
||||
RenderVertex{Vec2F(worldCoords.xMin(), worldCoords.yMin()), Vec2F(textureCoords.xMin(), textureCoords.yMin()), color, 1.0f},
|
||||
RenderVertex{Vec2F(worldCoords.xMax(), worldCoords.yMin()), Vec2F(textureCoords.xMax(), textureCoords.yMin()), color, 1.0f},
|
||||
RenderVertex{Vec2F(worldCoords.xMax(), worldCoords.yMax()), Vec2F(textureCoords.xMax(), textureCoords.yMax()), color, 1.0f},
|
||||
RenderVertex{Vec2F(worldCoords.xMin(), worldCoords.yMax()), Vec2F(textureCoords.xMin(), textureCoords.yMax()), color, 1.0f}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (modRenderProfile) {
|
||||
auto modColorVariant = modRenderProfile->multiColor ? materialColorVariant : 0;
|
||||
uint32_t variance = staticRandomU32(renderData.geometry.xwrap(pos[0]), pos[1], (int)terrainLayer, "mod");
|
||||
auto& quadList = primitives[modZLevel(modRenderProfile->zLevel, mod, modHue, modColorVariant)];
|
||||
|
||||
MaterialPieceResultList pieces;
|
||||
determineMatchingPieces(pieces, &occlude, materialDatabase, modRenderProfile->mainMatchList, renderData, pos,
|
||||
terrainLayer == TerrainLayer::Background ? TileLayer::Background : TileLayer::Foreground, true);
|
||||
for (auto const& piecePair : pieces) {
|
||||
auto texture = getPieceTexture(mod, piecePair.first, modHue, true);
|
||||
auto textureCoords = piecePair.first->variants.get(modColorVariant).wrap(variance);
|
||||
RectF worldCoords = RectF::withSize(piecePair.second / TilePixels + Vec2F(pos), textureCoords.size() / TilePixels);
|
||||
quadList.append(RenderQuad{
|
||||
move(texture),
|
||||
RenderVertex{Vec2F(worldCoords.xMin(), worldCoords.yMin()), Vec2F(textureCoords.xMin(), textureCoords.yMin()), color, 1.0f},
|
||||
RenderVertex{Vec2F(worldCoords.xMax(), worldCoords.yMin()), Vec2F(textureCoords.xMax(), textureCoords.yMin()), color, 1.0f},
|
||||
RenderVertex{Vec2F(worldCoords.xMax(), worldCoords.yMax()), Vec2F(textureCoords.xMax(), textureCoords.yMax()), color, 1.0f},
|
||||
RenderVertex{Vec2F(worldCoords.xMin(), worldCoords.yMax()), Vec2F(textureCoords.xMin(), textureCoords.yMax()), color, 1.0f}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (materialRenderProfile && damageLevel > 0 && isBlock) {
|
||||
auto& quadList = primitives[damageZLevel()];
|
||||
auto const& crackingImage = materialRenderProfile->damageImage(damageLevel, damageType);
|
||||
|
||||
TexturePtr texture = m_textureCache.get(AssetTextureKey(crackingImage.first),
|
||||
[&](auto const&) { return m_textureGroup->create(*assets->image(crackingImage.first)); });
|
||||
|
||||
Vec2F textureSize(texture->size());
|
||||
RectF textureCoords = RectF::withSize(Vec2F(), textureSize);
|
||||
RectF worldCoords = RectF::withSize(crackingImage.second / TilePixels + Vec2F(pos), textureCoords.size() / TilePixels);
|
||||
|
||||
quadList.append(RenderQuad{
|
||||
move(texture),
|
||||
RenderVertex{Vec2F(worldCoords.xMin(), worldCoords.yMin()), Vec2F(textureCoords.xMin(), textureCoords.yMin()), color, 1.0f},
|
||||
RenderVertex{Vec2F(worldCoords.xMax(), worldCoords.yMin()), Vec2F(textureCoords.xMax(), textureCoords.yMin()), color, 1.0f},
|
||||
RenderVertex{Vec2F(worldCoords.xMax(), worldCoords.yMax()), Vec2F(textureCoords.xMax(), textureCoords.yMax()), color, 1.0f},
|
||||
RenderVertex{Vec2F(worldCoords.xMin(), worldCoords.yMax()), Vec2F(textureCoords.xMin(), textureCoords.yMax()), color, 1.0f}
|
||||
});
|
||||
}
|
||||
|
||||
return occlude;
|
||||
}
|
||||
|
||||
void TilePainter::produceLiquidPrimitives(HashMap<LiquidId, List<RenderPrimitive>>& primitives, Vec2I const& pos, WorldRenderData const& renderData) {
|
||||
RenderTile const& tile = getRenderTile(renderData, pos);
|
||||
|
||||
float drawLevel = liquidDrawLevel(byteToFloat(tile.liquidLevel));
|
||||
if (drawLevel <= 0.0f)
|
||||
return;
|
||||
|
||||
RenderTile const& tileBottom = getRenderTile(renderData, pos - Vec2I(0, 1));
|
||||
float bottomDrawLevel = liquidDrawLevel(byteToFloat(tileBottom.liquidLevel));
|
||||
|
||||
RectF worldRect;
|
||||
if (tileBottom.foreground == EmptyMaterialId && bottomDrawLevel < 1.0f)
|
||||
worldRect = RectF::withSize(Vec2F(pos), Vec2F::filled(1.0f)).expanded(drawLevel);
|
||||
else
|
||||
worldRect = RectF::withSize(Vec2F(pos), Vec2F(1.0f, drawLevel));
|
||||
|
||||
auto texRect = worldRect.scaled(TilePixels);
|
||||
|
||||
auto const& liquid = m_liquids[tile.liquidId];
|
||||
primitives[tile.liquidId].append(RenderQuad{
|
||||
liquid.texture,
|
||||
RenderVertex{Vec2F(worldRect.xMin(), worldRect.yMin()), Vec2F(texRect.xMin(), texRect.yMin()), liquid.color, 1.0f},
|
||||
RenderVertex{Vec2F(worldRect.xMax(), worldRect.yMin()), Vec2F(texRect.xMax(), texRect.yMin()), liquid.color, 1.0f},
|
||||
RenderVertex{Vec2F(worldRect.xMax(), worldRect.yMax()), Vec2F(texRect.xMax(), texRect.yMax()), liquid.color, 1.0f},
|
||||
RenderVertex{Vec2F(worldRect.xMin(), worldRect.yMax()), Vec2F(texRect.xMin(), texRect.yMax()), liquid.color, 1.0f}
|
||||
});
|
||||
}
|
||||
|
||||
bool TilePainter::determineMatchingPieces(MaterialPieceResultList& resultList, bool* occlude, MaterialDatabaseConstPtr const& materialDb, MaterialRenderMatchList const& matchList,
|
||||
WorldRenderData const& renderData, Vec2I const& basePos, TileLayer layer, bool isMod) {
|
||||
RenderTile const& tile = getRenderTile(renderData, basePos);
|
||||
|
||||
auto matchSetMatches = [&](MaterialRenderMatchConstPtr const& match) -> bool {
|
||||
if (match->requiredLayer && *match->requiredLayer != layer)
|
||||
return false;
|
||||
|
||||
if (match->matchPoints.empty())
|
||||
return true;
|
||||
|
||||
bool matchValid = match->matchJoin == MaterialJoinType::All;
|
||||
for (auto const& matchPoint : match->matchPoints) {
|
||||
auto const& neighborTile = getRenderTile(renderData, basePos + matchPoint.position);
|
||||
|
||||
bool neighborShadowing = false;
|
||||
if (layer == TileLayer::Background) {
|
||||
if (auto profile = materialDb->materialRenderProfile(neighborTile.foreground))
|
||||
neighborShadowing = !profile->foregroundLightTransparent;
|
||||
}
|
||||
|
||||
MaterialHue baseHue = layer == TileLayer::Foreground ? tile.foregroundHueShift : tile.backgroundHueShift;
|
||||
MaterialHue neighborHue = layer == TileLayer::Foreground ? neighborTile.foregroundHueShift : neighborTile.backgroundHueShift;
|
||||
MaterialHue baseModHue = layer == TileLayer::Foreground ? tile.foregroundModHueShift : tile.backgroundModHueShift;
|
||||
MaterialHue neighborModHue = layer == TileLayer::Foreground ? neighborTile.foregroundModHueShift : neighborTile.backgroundModHueShift;
|
||||
MaterialId baseMaterial = layer == TileLayer::Foreground ? tile.foreground : tile.background;
|
||||
MaterialId neighborMaterial = layer == TileLayer::Foreground ? neighborTile.foreground : neighborTile.background;
|
||||
ModId baseMod = layer == TileLayer::Foreground ? tile.foregroundMod : tile.backgroundMod;
|
||||
ModId neighborMod = layer == TileLayer::Foreground ? neighborTile.foregroundMod : neighborTile.backgroundMod;
|
||||
|
||||
bool rulesValid = matchPoint.rule->join == MaterialJoinType::All;
|
||||
for (auto const& ruleEntry : matchPoint.rule->entries) {
|
||||
bool valid = true;
|
||||
if (isMod) {
|
||||
if (ruleEntry.rule.is<MaterialRule::RuleEmpty>()) {
|
||||
valid = neighborMod == NoModId;
|
||||
} else if (ruleEntry.rule.is<MaterialRule::RuleConnects>()) {
|
||||
valid = isConnectableMaterial(neighborMaterial);
|
||||
} else if (ruleEntry.rule.is<MaterialRule::RuleShadows>()) {
|
||||
valid = neighborShadowing;
|
||||
} else if (auto equalsSelf = ruleEntry.rule.ptr<MaterialRule::RuleEqualsSelf>()) {
|
||||
valid = neighborMod == baseMod;
|
||||
if (equalsSelf->matchHue)
|
||||
valid = valid && baseModHue == neighborModHue;
|
||||
} else if (auto equalsId = ruleEntry.rule.ptr<MaterialRule::RuleEqualsId>()) {
|
||||
valid = neighborMod == equalsId->id;
|
||||
} else if (auto propertyEquals = ruleEntry.rule.ptr<MaterialRule::RulePropertyEquals>()) {
|
||||
if (auto profile = materialDb->modRenderProfile(neighborMod))
|
||||
valid = profile->ruleProperties.get(propertyEquals->propertyName, Json()) == propertyEquals->compare;
|
||||
else
|
||||
valid = false;
|
||||
}
|
||||
} else {
|
||||
if (ruleEntry.rule.is<MaterialRule::RuleEmpty>()) {
|
||||
valid = neighborMaterial == EmptyMaterialId;
|
||||
} else if (ruleEntry.rule.is<MaterialRule::RuleConnects>()) {
|
||||
valid = isConnectableMaterial(neighborMaterial);
|
||||
} else if (ruleEntry.rule.is<MaterialRule::RuleShadows>()) {
|
||||
valid = neighborShadowing;
|
||||
} else if (auto equalsSelf = ruleEntry.rule.ptr<MaterialRule::RuleEqualsSelf>()) {
|
||||
valid = neighborMaterial == baseMaterial;
|
||||
if (equalsSelf->matchHue)
|
||||
valid = valid && baseHue == neighborHue;
|
||||
} else if (auto equalsId = ruleEntry.rule.ptr<MaterialRule::RuleEqualsId>()) {
|
||||
valid = neighborMaterial == equalsId->id;
|
||||
} else if (auto propertyEquals = ruleEntry.rule.ptr<MaterialRule::RulePropertyEquals>()) {
|
||||
if (auto profile = materialDb->materialRenderProfile(neighborMaterial))
|
||||
valid = profile->ruleProperties.get(propertyEquals->propertyName) == propertyEquals->compare;
|
||||
else
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
if (ruleEntry.inverse)
|
||||
valid = !valid;
|
||||
|
||||
if (matchPoint.rule->join == MaterialJoinType::All) {
|
||||
rulesValid = valid && rulesValid;
|
||||
if (!rulesValid)
|
||||
break;
|
||||
} else {
|
||||
rulesValid = valid || rulesValid;
|
||||
}
|
||||
}
|
||||
|
||||
if (match->matchJoin == MaterialJoinType::All) {
|
||||
matchValid = matchValid && rulesValid;
|
||||
if (!matchValid)
|
||||
return matchValid;
|
||||
} else {
|
||||
matchValid = matchValid || rulesValid;
|
||||
}
|
||||
}
|
||||
return matchValid;
|
||||
};
|
||||
|
||||
bool subMatchResult = false;
|
||||
for (auto const& match : matchList) {
|
||||
if (matchSetMatches(match)) {
|
||||
if (match->occlude)
|
||||
*occlude = match->occlude.get();
|
||||
|
||||
subMatchResult = true;
|
||||
|
||||
for (auto const& piecePair : match->resultingPieces)
|
||||
resultList.append({piecePair.first, piecePair.second});
|
||||
|
||||
if (determineMatchingPieces(resultList, occlude, materialDb, match->subMatches, renderData, basePos, layer, isMod) && match->haltOnSubMatch)
|
||||
break;
|
||||
|
||||
if (match->haltOnMatch)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return subMatchResult;
|
||||
}
|
||||
|
||||
float TilePainter::liquidDrawLevel(float liquidLevel) const {
|
||||
return clamp((liquidLevel - m_liquidDrawLevels[0]) / (m_liquidDrawLevels[1] - m_liquidDrawLevels[0]), 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
}
|
140
source/rendering/StarTilePainter.hpp
Normal file
140
source/rendering/StarTilePainter.hpp
Normal file
|
@ -0,0 +1,140 @@
|
|||
#ifndef STAR_NEW_TILE_PAINTER_HPP
|
||||
#define STAR_NEW_TILE_PAINTER_HPP
|
||||
|
||||
#include "StarTtlCache.hpp"
|
||||
#include "StarWorldRenderData.hpp"
|
||||
#include "StarMaterialRenderProfile.hpp"
|
||||
#include "StarRenderer.hpp"
|
||||
#include "StarWorldCamera.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(Assets);
|
||||
STAR_CLASS(MaterialDatabase);
|
||||
STAR_CLASS(TilePainter);
|
||||
|
||||
class TilePainter {
|
||||
public:
|
||||
// The rendered tiles are split and cached in chunks of RenderChunkSize x
|
||||
// RenderChunkSize. This means that, around the border, there may be as many
|
||||
// as RenderChunkSize - 1 tiles rendered outside of the viewing area from
|
||||
// chunk alignment. In addition to this, there is also a region around each
|
||||
// tile that is used for neighbor based rendering rules which has a max of
|
||||
// MaterialRenderProfileMaxNeighborDistance. If the given tile data does not
|
||||
// extend RenderChunkSize + MaterialRenderProfileMaxNeighborDistance - 1
|
||||
// around the viewing area, then border chunks can continuously change hash,
|
||||
// and will be recomputed too often.
|
||||
static unsigned const RenderChunkSize = 16;
|
||||
static unsigned const BorderTileSize = RenderChunkSize + MaterialRenderProfileMaxNeighborDistance - 1;
|
||||
|
||||
TilePainter(RendererPtr renderer);
|
||||
|
||||
// Adjusts lighting levels for liquids.
|
||||
void adjustLighting(WorldRenderData& renderData) const;
|
||||
|
||||
// Sets up chunk data for every chunk that intersects the rendering region
|
||||
// and prepares it for rendering. Do not call cleanup in between calling
|
||||
// setup and each render method.
|
||||
void setup(WorldCamera const& camera, WorldRenderData& renderData);
|
||||
|
||||
void renderBackground(WorldCamera const& camera);
|
||||
void renderMidground(WorldCamera const& camera);
|
||||
void renderLiquid(WorldCamera const& camera);
|
||||
void renderForeground(WorldCamera const& camera);
|
||||
|
||||
// Clears any render data, as well as cleaning up old cached textures and
|
||||
// chunks.
|
||||
void cleanup();
|
||||
|
||||
private:
|
||||
typedef uint64_t QuadZLevel;
|
||||
typedef uint64_t ChunkHash;
|
||||
|
||||
enum class TerrainLayer { Background, Midground, Foreground };
|
||||
|
||||
struct LiquidInfo {
|
||||
TexturePtr texture;
|
||||
Vec4B color;
|
||||
Vec3F bottomLightMix;
|
||||
float textureMovementFactor;
|
||||
};
|
||||
|
||||
typedef HashMap<TerrainLayer, HashMap<QuadZLevel, RenderBufferPtr>> TerrainChunk;
|
||||
typedef HashMap<LiquidId, RenderBufferPtr> LiquidChunk;
|
||||
|
||||
typedef size_t MaterialRenderPieceIndex;
|
||||
typedef tuple<MaterialId, MaterialRenderPieceIndex, MaterialHue, bool> MaterialPieceTextureKey;
|
||||
typedef String AssetTextureKey;
|
||||
typedef Variant<MaterialPieceTextureKey, AssetTextureKey> TextureKey;
|
||||
|
||||
typedef List<pair<MaterialRenderPieceConstPtr, Vec2F>> MaterialPieceResultList;
|
||||
|
||||
struct TextureKeyHash {
|
||||
size_t operator()(TextureKey const& key) const;
|
||||
};
|
||||
|
||||
// chunkIndex here is the index of the render chunk such that chunkIndex *
|
||||
// RenderChunkSize results in the coordinate of the lower left most tile in
|
||||
// the render chunk.
|
||||
|
||||
static ChunkHash terrainChunkHash(WorldRenderData& renderData, Vec2I chunkIndex);
|
||||
static ChunkHash liquidChunkHash(WorldRenderData& renderData, Vec2I chunkIndex);
|
||||
|
||||
static QuadZLevel materialZLevel(uint32_t zLevel, MaterialId material, MaterialHue hue, MaterialColorVariant colorVariant);
|
||||
static QuadZLevel modZLevel(uint32_t zLevel, ModId mod, MaterialHue hue, MaterialColorVariant colorVariant);
|
||||
static QuadZLevel damageZLevel();
|
||||
|
||||
static RenderTile const& getRenderTile(WorldRenderData const& renderData, Vec2I const& worldPos);
|
||||
|
||||
template <typename Function>
|
||||
static void forEachRenderTile(WorldRenderData const& renderData, RectI const& worldCoordRange, Function&& function);
|
||||
|
||||
void renderTerrainChunks(WorldCamera const& camera, TerrainLayer terrainLayer);
|
||||
|
||||
shared_ptr<TerrainChunk const> getTerrainChunk(WorldRenderData& renderData, Vec2I chunkIndex);
|
||||
shared_ptr<LiquidChunk const> getLiquidChunk(WorldRenderData& renderData, Vec2I chunkIndex);
|
||||
|
||||
bool produceTerrainPrimitives(HashMap<QuadZLevel, List<RenderPrimitive>>& primitives,
|
||||
TerrainLayer terrainLayer, Vec2I const& pos, WorldRenderData const& renderData);
|
||||
void produceLiquidPrimitives(HashMap<LiquidId, List<RenderPrimitive>>& primitives, Vec2I const& pos, WorldRenderData const& renderData);
|
||||
|
||||
bool determineMatchingPieces(MaterialPieceResultList& resultList, bool* occlude, MaterialDatabaseConstPtr const& materialDb, MaterialRenderMatchList const& matchList,
|
||||
WorldRenderData const& renderData, Vec2I const& basePos, TileLayer layer, bool isMod);
|
||||
|
||||
float liquidDrawLevel(float liquidLevel) const;
|
||||
|
||||
List<LiquidInfo> m_liquids;
|
||||
|
||||
Vec4B m_backgroundLayerColor;
|
||||
Vec4B m_foregroundLayerColor;
|
||||
Vec2F m_liquidDrawLevels;
|
||||
|
||||
RendererPtr m_renderer;
|
||||
TextureGroupPtr m_textureGroup;
|
||||
|
||||
HashTtlCache<TextureKey, TexturePtr, TextureKeyHash> m_textureCache;
|
||||
HashTtlCache<pair<Vec2I, ChunkHash>, shared_ptr<TerrainChunk const>> m_terrainChunkCache;
|
||||
HashTtlCache<pair<Vec2I, ChunkHash>, shared_ptr<LiquidChunk const>> m_liquidChunkCache;
|
||||
|
||||
List<shared_ptr<TerrainChunk const>> m_pendingTerrainChunks;
|
||||
List<shared_ptr<LiquidChunk const>> m_pendingLiquidChunks;
|
||||
|
||||
Maybe<Vec2F> m_lastCameraCenter;
|
||||
Vec2F m_cameraPan;
|
||||
};
|
||||
|
||||
template <typename Function>
|
||||
void TilePainter::forEachRenderTile(WorldRenderData const& renderData, RectI const& worldCoordRange, Function&& function) {
|
||||
RectI indexRect = RectI::withSize(renderData.geometry.diff(worldCoordRange.min(), renderData.tileMinPosition), worldCoordRange.size());
|
||||
indexRect.limit(RectI::withSize(Vec2I(0, 0), Vec2I(renderData.tiles.size())));
|
||||
|
||||
if (!indexRect.isEmpty()) {
|
||||
renderData.tiles.forEach(Array2S(indexRect.min()), Array2S(indexRect.size()), [&](Array2S const& index, RenderTile const& tile) {
|
||||
return function(worldCoordRange.min() + (Vec2I(index) - indexRect.min()), tile);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
38
source/rendering/StarWorldCamera.cpp
Normal file
38
source/rendering/StarWorldCamera.cpp
Normal file
|
@ -0,0 +1,38 @@
|
|||
#include "StarWorldCamera.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
void WorldCamera::setCenterWorldPosition(Vec2F const& position) {
|
||||
// Only actually move the world center if a half pixel distance has been
|
||||
// moved in any direction. This is sort of arbitrary, but helps prevent
|
||||
// judder if the camera is at a boundary and floating point inaccuracy is
|
||||
// causing the focus to jitter back and forth across the boundary.
|
||||
if (fabs(position[0] - m_worldCenter[0]) < 1.0f / (TilePixels * m_pixelRatio * 2) && fabs(position[1] - m_worldCenter[1]) < 1.0f / (TilePixels * m_pixelRatio * 2))
|
||||
return;
|
||||
|
||||
// First, make sure the camera center position is inside the main x
|
||||
// coordinate bounds, and that the top and bototm of the screen are not
|
||||
// outside of the y coordinate bounds.
|
||||
m_worldCenter = m_worldGeometry.xwrap(position);
|
||||
m_worldCenter[1] = clamp(m_worldCenter[1],
|
||||
(float)m_screenSize[1] / (TilePixels * m_pixelRatio * 2),
|
||||
m_worldGeometry.height() - (float)m_screenSize[1] / (TilePixels * m_pixelRatio * 2));
|
||||
|
||||
// Then, position the camera center position so that the tile grid is as
|
||||
// close as possible aligned to whole pixel boundaries. This is incredibly
|
||||
// important, because this means that even without any complicated rounding,
|
||||
// elements drawn in world space that are aligned with TilePixels will
|
||||
// eventually also be aligned to real screen pixels.
|
||||
|
||||
if (m_screenSize[0] % 2 == 0)
|
||||
m_worldCenter[0] = round(m_worldCenter[0] * (TilePixels * m_pixelRatio)) / (TilePixels * m_pixelRatio);
|
||||
else
|
||||
m_worldCenter[0] = (round(m_worldCenter[0] * (TilePixels * m_pixelRatio) + 0.5f) - 0.5f) / (TilePixels * m_pixelRatio);
|
||||
|
||||
if (m_screenSize[1] % 2 == 0)
|
||||
m_worldCenter[1] = round(m_worldCenter[1] * (TilePixels * m_pixelRatio)) / (TilePixels * m_pixelRatio);
|
||||
else
|
||||
m_worldCenter[1] = (round(m_worldCenter[1] * (TilePixels * m_pixelRatio) + 0.5f) - 0.5f) / (TilePixels * m_pixelRatio);
|
||||
}
|
||||
|
||||
}
|
117
source/rendering/StarWorldCamera.hpp
Normal file
117
source/rendering/StarWorldCamera.hpp
Normal file
|
@ -0,0 +1,117 @@
|
|||
#ifndef STAR_WORLD_CAMERA_HPP
|
||||
#define STAR_WORLD_CAMERA_HPP
|
||||
|
||||
#include "StarWorldGeometry.hpp"
|
||||
#include "StarGameTypes.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
class WorldCamera {
|
||||
public:
|
||||
void setScreenSize(Vec2U screenSize);
|
||||
Vec2U screenSize() const;
|
||||
|
||||
void setPixelRatio(unsigned pixelRatio);
|
||||
unsigned pixelRatio() const;
|
||||
|
||||
void setWorldGeometry(WorldGeometry geometry);
|
||||
WorldGeometry worldGeometry() const;
|
||||
|
||||
// Set the camera center position (in world space) to as close to the given
|
||||
// location as possible while keeping the screen within world bounds.
|
||||
// Returns the actual camera position.
|
||||
void setCenterWorldPosition(Vec2F const& position);
|
||||
Vec2F centerWorldPosition() const;
|
||||
|
||||
// Transforms world coordinates into one set of screen coordinates. Since
|
||||
// the world is non-euclidean, one world coordinate can transform to
|
||||
// potentially an infinite number of screen coordinates. This will retrun
|
||||
// the closest to the center of the screen.
|
||||
Vec2F worldToScreen(Vec2F const& worldCoord) const;
|
||||
|
||||
// Assumes top left corner of screen is (0, 0) in screen coordinates.
|
||||
Vec2F screenToWorld(Vec2F const& screen) const;
|
||||
|
||||
// Returns screen dimensions in world space.
|
||||
RectF worldScreenRect() const;
|
||||
|
||||
// Returns tile dimensions of the tiles that overlap with the screen
|
||||
RectI worldTileRect() const;
|
||||
|
||||
// Returns the position of the lower left corner of the lower left tile of
|
||||
// worldTileRect, in screen coordinates.
|
||||
Vec2F tileMinScreen() const;
|
||||
|
||||
private:
|
||||
WorldGeometry m_worldGeometry;
|
||||
Vec2U m_screenSize;
|
||||
unsigned m_pixelRatio = 1;
|
||||
Vec2F m_worldCenter;
|
||||
};
|
||||
|
||||
inline void WorldCamera::setScreenSize(Vec2U screenSize) {
|
||||
m_screenSize = screenSize;
|
||||
}
|
||||
|
||||
inline Vec2U WorldCamera::screenSize() const {
|
||||
return m_screenSize;
|
||||
}
|
||||
|
||||
inline void WorldCamera::setPixelRatio(unsigned pixelRatio) {
|
||||
m_pixelRatio = pixelRatio;
|
||||
}
|
||||
|
||||
inline unsigned WorldCamera::pixelRatio() const {
|
||||
return m_pixelRatio;
|
||||
}
|
||||
|
||||
inline void WorldCamera::setWorldGeometry(WorldGeometry geometry) {
|
||||
m_worldGeometry = move(geometry);
|
||||
}
|
||||
|
||||
inline WorldGeometry WorldCamera::worldGeometry() const {
|
||||
return m_worldGeometry;
|
||||
}
|
||||
|
||||
inline Vec2F WorldCamera::centerWorldPosition() const {
|
||||
return Vec2F(m_worldCenter);
|
||||
}
|
||||
|
||||
inline Vec2F WorldCamera::worldToScreen(Vec2F const& worldCoord) const {
|
||||
Vec2F wrappedCoord = m_worldGeometry.nearestTo(Vec2F(m_worldCenter), worldCoord);
|
||||
return Vec2F(
|
||||
(wrappedCoord[0] - m_worldCenter[0]) * (TilePixels * m_pixelRatio) + m_screenSize[0] / 2.0,
|
||||
(wrappedCoord[1] - m_worldCenter[1]) * (TilePixels * m_pixelRatio) + m_screenSize[1] / 2.0
|
||||
);
|
||||
}
|
||||
|
||||
inline Vec2F WorldCamera::screenToWorld(Vec2F const& screen) const {
|
||||
return Vec2F(
|
||||
(screen[0] - m_screenSize[0] / 2.0) / (TilePixels * m_pixelRatio) + m_worldCenter[0],
|
||||
(screen[1] - m_screenSize[1] / 2.0) / (TilePixels * m_pixelRatio) + m_worldCenter[1]
|
||||
);
|
||||
}
|
||||
|
||||
inline RectF WorldCamera::worldScreenRect() const {
|
||||
// screen dimensions in world space
|
||||
float w = (float)m_screenSize[0] / (TilePixels * m_pixelRatio);
|
||||
float h = (float)m_screenSize[1] / (TilePixels * m_pixelRatio);
|
||||
return RectF::withSize(Vec2F(m_worldCenter[0] - w / 2, m_worldCenter[1] - h / 2), Vec2F(w, h));
|
||||
}
|
||||
|
||||
inline RectI WorldCamera::worldTileRect() const {
|
||||
RectF screen = worldScreenRect();
|
||||
Vec2I min = Vec2I::floor(screen.min());
|
||||
Vec2I size = Vec2I::ceil(Vec2F(m_screenSize) / (TilePixels * m_pixelRatio) + (screen.min() - Vec2F(min)));
|
||||
return RectI::withSize(min, size);
|
||||
}
|
||||
|
||||
inline Vec2F WorldCamera::tileMinScreen() const {
|
||||
RectF screenRect = worldScreenRect();
|
||||
RectI tileRect = worldTileRect();
|
||||
return (Vec2F(tileRect.min()) - screenRect.min()) * (TilePixels * m_pixelRatio);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
296
source/rendering/StarWorldPainter.cpp
Normal file
296
source/rendering/StarWorldPainter.cpp
Normal file
|
@ -0,0 +1,296 @@
|
|||
#include "StarWorldPainter.hpp"
|
||||
#include "StarAnimation.hpp"
|
||||
#include "StarRoot.hpp"
|
||||
#include "StarConfiguration.hpp"
|
||||
#include "StarAssets.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
WorldPainter::WorldPainter() {
|
||||
m_assets = Root::singleton().assets();
|
||||
|
||||
m_camera.setScreenSize({800, 600});
|
||||
m_camera.setCenterWorldPosition(Vec2F());
|
||||
m_camera.setPixelRatio(Root::singleton().configuration()->get("zoomLevel").toFloat());
|
||||
|
||||
m_highlightConfig = m_assets->json("/highlights.config");
|
||||
for (auto p : m_highlightConfig.get("highlightDirectives").iterateObject())
|
||||
m_highlightDirectives.set(EntityHighlightEffectTypeNames.getLeft(p.first), {p.second.getString("underlay", ""), p.second.getString("overlay", "")});
|
||||
|
||||
m_entityBarOffset = jsonToVec2F(m_assets->json("/rendering.config:entityBarOffset"));
|
||||
m_entityBarSpacing = jsonToVec2F(m_assets->json("/rendering.config:entityBarSpacing"));
|
||||
m_entityBarSize = jsonToVec2F(m_assets->json("/rendering.config:entityBarSize"));
|
||||
m_entityBarIconOffset = jsonToVec2F(m_assets->json("/rendering.config:entityBarIconOffset"));
|
||||
m_preloadTextureChance = m_assets->json("/rendering.config:preloadTextureChance").toFloat();
|
||||
}
|
||||
|
||||
void WorldPainter::renderInit(RendererPtr renderer) {
|
||||
m_assets = Root::singleton().assets();
|
||||
|
||||
m_renderer = move(renderer);
|
||||
auto textureGroup = m_renderer->createTextureGroup(TextureGroupSize::Large);
|
||||
m_textPainter = make_shared<TextPainter>(m_assets->font("/hobo.ttf")->clone(), m_renderer, textureGroup);
|
||||
m_tilePainter = make_shared<TilePainter>(m_renderer);
|
||||
m_drawablePainter = make_shared<DrawablePainter>(m_renderer, make_shared<AssetTextureGroup>(textureGroup));
|
||||
m_environmentPainter = make_shared<EnvironmentPainter>(m_renderer);
|
||||
}
|
||||
|
||||
void WorldPainter::setCameraPosition(WorldGeometry const& geometry, Vec2F const& position) {
|
||||
m_camera.setWorldGeometry(geometry);
|
||||
m_camera.setCenterWorldPosition(position);
|
||||
}
|
||||
|
||||
WorldCamera const& WorldPainter::camera() const {
|
||||
return m_camera;
|
||||
}
|
||||
|
||||
void WorldPainter::render(WorldRenderData& renderData) {
|
||||
m_camera.setScreenSize(m_renderer->screenSize());
|
||||
m_camera.setPixelRatio(Root::singleton().configuration()->get("zoomLevel").toFloat());
|
||||
|
||||
m_assets = Root::singleton().assets();
|
||||
|
||||
m_environmentPainter->update();
|
||||
|
||||
m_tilePainter->setup(m_camera, renderData);
|
||||
|
||||
if (renderData.isFullbright) {
|
||||
m_renderer->setEffectTexture("lightMap", Image::filled(Vec2U(1, 1), {255, 255, 255, 255}, PixelFormat::RGB24));
|
||||
m_renderer->setEffectParameter("lightMapMultiplier", 1.0f);
|
||||
} else {
|
||||
m_tilePainter->adjustLighting(renderData);
|
||||
|
||||
m_renderer->setEffectParameter("lightMapMultiplier", m_assets->json("/rendering.config:lightMapMultiplier").toFloat());
|
||||
m_renderer->setEffectParameter("lightMapScale", Vec2F::filled(TilePixels * m_camera.pixelRatio()));
|
||||
m_renderer->setEffectParameter("lightMapOffset", m_camera.worldToScreen(Vec2F(renderData.lightMinPosition)));
|
||||
m_renderer->setEffectTexture("lightMap", renderData.lightMap);
|
||||
}
|
||||
|
||||
// Stars, Debris Fields, Sky, and Orbiters
|
||||
|
||||
m_environmentPainter->renderStars(m_camera.pixelRatio(), Vec2F(m_camera.screenSize()), renderData.skyRenderData);
|
||||
m_environmentPainter->renderDebrisFields(m_camera.pixelRatio(), Vec2F(m_camera.screenSize()), renderData.skyRenderData);
|
||||
m_environmentPainter->renderBackOrbiters(m_camera.pixelRatio(), Vec2F(m_camera.screenSize()), renderData.skyRenderData);
|
||||
m_environmentPainter->renderPlanetHorizon(m_camera.pixelRatio(), Vec2F(m_camera.screenSize()), renderData.skyRenderData);
|
||||
m_environmentPainter->renderSky(Vec2F(m_camera.screenSize()), renderData.skyRenderData);
|
||||
m_environmentPainter->renderFrontOrbiters(m_camera.pixelRatio(), Vec2F(m_camera.screenSize()), renderData.skyRenderData);
|
||||
|
||||
// Parallax layers
|
||||
|
||||
auto parallaxDelta = m_camera.worldGeometry().diff(m_camera.centerWorldPosition(), m_previousCameraCenter);
|
||||
if (parallaxDelta.magnitude() > 10)
|
||||
m_parallaxWorldPosition = m_camera.centerWorldPosition();
|
||||
else
|
||||
m_parallaxWorldPosition += parallaxDelta;
|
||||
m_previousCameraCenter = m_camera.centerWorldPosition();
|
||||
m_parallaxWorldPosition[1] = m_camera.centerWorldPosition()[1];
|
||||
|
||||
if (!renderData.parallaxLayers.empty())
|
||||
m_environmentPainter->renderParallaxLayers(m_parallaxWorldPosition, m_camera, renderData.parallaxLayers, renderData.skyRenderData);
|
||||
|
||||
// Main world layers
|
||||
|
||||
Map<EntityRenderLayer, List<pair<EntityHighlightEffect, List<Drawable>>>> entityDrawables;
|
||||
for (auto& ed : renderData.entityDrawables) {
|
||||
for (auto& p : ed.layers)
|
||||
entityDrawables[p.first].append({ed.highlightEffect, move(p.second)});
|
||||
}
|
||||
|
||||
auto entityDrawableIterator = entityDrawables.begin();
|
||||
auto renderEntitiesUntil = [this, &entityDrawables, &entityDrawableIterator](Maybe<EntityRenderLayer> until) {
|
||||
while (true) {
|
||||
if (entityDrawableIterator == entityDrawables.end())
|
||||
break;
|
||||
if (until && entityDrawableIterator->first >= *until)
|
||||
break;
|
||||
for (auto& edl : entityDrawableIterator->second)
|
||||
drawEntityLayer(move(edl.second), edl.first);
|
||||
++entityDrawableIterator;
|
||||
}
|
||||
|
||||
m_renderer->flush();
|
||||
};
|
||||
|
||||
renderEntitiesUntil(RenderLayerBackgroundOverlay);
|
||||
drawDrawableSet(renderData.backgroundOverlays);
|
||||
renderEntitiesUntil(RenderLayerBackgroundTile);
|
||||
m_tilePainter->renderBackground(m_camera);
|
||||
renderEntitiesUntil(RenderLayerPlatform);
|
||||
m_tilePainter->renderMidground(m_camera);
|
||||
renderEntitiesUntil(RenderLayerBackParticle);
|
||||
renderParticles(renderData, Particle::Layer::Back);
|
||||
renderEntitiesUntil(RenderLayerLiquid);
|
||||
m_tilePainter->renderLiquid(m_camera);
|
||||
renderEntitiesUntil(RenderLayerMiddleParticle);
|
||||
renderParticles(renderData, Particle::Layer::Middle);
|
||||
renderEntitiesUntil(RenderLayerForegroundTile);
|
||||
m_tilePainter->renderForeground(m_camera);
|
||||
renderEntitiesUntil(RenderLayerForegroundOverlay);
|
||||
drawDrawableSet(renderData.foregroundOverlays);
|
||||
renderEntitiesUntil(RenderLayerFrontParticle);
|
||||
renderParticles(renderData, Particle::Layer::Front);
|
||||
renderEntitiesUntil(RenderLayerOverlay);
|
||||
drawDrawableSet(renderData.nametags);
|
||||
renderBars(renderData);
|
||||
renderEntitiesUntil({});
|
||||
|
||||
auto dimLevel = round(renderData.dimLevel * 255);
|
||||
if (dimLevel != 0)
|
||||
m_renderer->render(renderFlatRect(RectF::withSize({}, Vec2F(m_camera.screenSize())), Vec4B(renderData.dimColor, dimLevel), 0.0f));
|
||||
|
||||
int64_t textureTimeout = m_assets->json("/rendering.config:textureTimeout").toInt();
|
||||
m_textPainter->cleanup(textureTimeout);
|
||||
m_drawablePainter->cleanup(textureTimeout);
|
||||
m_environmentPainter->cleanup(textureTimeout);
|
||||
m_tilePainter->cleanup();
|
||||
}
|
||||
|
||||
void WorldPainter::renderParticles(WorldRenderData& renderData, Particle::Layer layer) {
|
||||
const int textParticleFontSize = m_assets->json("/rendering.config:textParticleFontSize").toInt();
|
||||
const RectF particleRenderWindow = RectF::withSize(Vec2F(), Vec2F(m_camera.screenSize())).padded(m_assets->json("/rendering.config:particleRenderWindowPadding").toInt());
|
||||
|
||||
for (Particle const& particle : renderData.particles) {
|
||||
if (layer != particle.layer)
|
||||
continue;
|
||||
|
||||
Vec2F position = m_camera.worldToScreen(particle.position);
|
||||
|
||||
if (!particleRenderWindow.contains(position))
|
||||
continue;
|
||||
|
||||
Vec2I size = Vec2I::filled(particle.size * m_camera.pixelRatio());
|
||||
|
||||
if (particle.type == Particle::Type::Ember) {
|
||||
m_renderer->render(renderFlatRect(RectF(position - Vec2F(size) / 2, position + Vec2F(size) / 2), particle.color.toRgba(), particle.fullbright ? 0.0f : 1.0f));
|
||||
|
||||
} else if (particle.type == Particle::Type::Streak) {
|
||||
// Draw a rotated quad streaking in the direction the particle is coming from.
|
||||
// Sadly this looks awful.
|
||||
Vec2F dir = particle.velocity.normalized();
|
||||
Vec2F sideHalf = dir.rot90() * m_camera.pixelRatio() * particle.size / 2;
|
||||
float length = particle.length * m_camera.pixelRatio();
|
||||
Vec4B color = particle.color.toRgba();
|
||||
float lightMapMultiplier = particle.fullbright ? 0.0f : 1.0f;
|
||||
m_renderer->render(RenderQuad{{},
|
||||
{position - sideHalf, {}, color, lightMapMultiplier},
|
||||
{position + sideHalf, {}, color, lightMapMultiplier},
|
||||
{position - dir * length + sideHalf, {}, color, lightMapMultiplier},
|
||||
{position - dir * length - sideHalf, {}, color, lightMapMultiplier}
|
||||
});
|
||||
|
||||
} else if (particle.type == Particle::Type::Textured || particle.type == Particle::Type::Animated) {
|
||||
Drawable drawable;
|
||||
if (particle.type == Particle::Type::Textured)
|
||||
drawable = Drawable::makeImage(particle.string, 1.0f / TilePixels, true, Vec2F(0, 0));
|
||||
else
|
||||
drawable = particle.animation->drawable(1.0f / TilePixels);
|
||||
|
||||
if (particle.flip && particle.flippable)
|
||||
drawable.scale(Vec2F(-1, 1));
|
||||
if (drawable.isImage())
|
||||
drawable.imagePart().addDirectives(particle.directives, true);
|
||||
drawable.fullbright = particle.fullbright;
|
||||
drawable.color = particle.color;
|
||||
drawable.rotate(particle.rotation);
|
||||
drawable.scale(particle.size);
|
||||
drawable.translate(particle.position);
|
||||
drawDrawable(move(drawable));
|
||||
|
||||
} else if (particle.type == Particle::Type::Text) {
|
||||
Vec2F position = m_camera.worldToScreen(particle.position);
|
||||
unsigned size = textParticleFontSize * m_camera.pixelRatio() * particle.size;
|
||||
if (size > 0) {
|
||||
m_textPainter->setFontSize(size);
|
||||
m_textPainter->setFontColor(particle.color.toRgba());
|
||||
m_textPainter->renderText(particle.string, {position, HorizontalAnchor::HMidAnchor, VerticalAnchor::VMidAnchor});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_renderer->flush();
|
||||
}
|
||||
|
||||
void WorldPainter::renderBars(WorldRenderData& renderData) {
|
||||
auto offset = m_entityBarOffset;
|
||||
for (auto const& bar : renderData.overheadBars) {
|
||||
auto position = bar.entityPosition + offset;
|
||||
offset += m_entityBarSpacing;
|
||||
if (bar.icon) {
|
||||
auto iconDrawPosition = position - (m_entityBarSize / 2).round() + m_entityBarIconOffset;
|
||||
drawDrawable(Drawable::makeImage(*bar.icon, 1.0f / TilePixels, true, iconDrawPosition));
|
||||
}
|
||||
|
||||
if (!bar.detailOnly) {
|
||||
auto fullBar = RectF({}, {m_entityBarSize.x() * bar.percentage, m_entityBarSize.y()});
|
||||
auto emptyBar = RectF({m_entityBarSize.x() * bar.percentage, 0.0f}, m_entityBarSize);
|
||||
auto fullColor = bar.color;
|
||||
auto emptyColor = Color::Black;
|
||||
|
||||
drawDrawable(Drawable::makePoly(PolyF(emptyBar), emptyColor, position));
|
||||
drawDrawable(Drawable::makePoly(PolyF(fullBar), fullColor, position));
|
||||
}
|
||||
}
|
||||
|
||||
m_renderer->flush();
|
||||
}
|
||||
|
||||
void WorldPainter::drawEntityLayer(List<Drawable> drawables, EntityHighlightEffect highlightEffect) {
|
||||
highlightEffect.level *= m_highlightConfig.getFloat("maxHighlightLevel", 1.0);
|
||||
if (m_highlightDirectives.contains(highlightEffect.type) && highlightEffect.level > 0) {
|
||||
// first pass, draw underlay
|
||||
auto underlayDirectives = m_highlightDirectives[highlightEffect.type].first;
|
||||
if (!underlayDirectives.empty()) {
|
||||
for (auto& d : drawables) {
|
||||
if (d.isImage()) {
|
||||
auto underlayDrawable = Drawable(d);
|
||||
underlayDrawable.fullbright = true;
|
||||
underlayDrawable.color = Color::rgbaf(1, 1, 1, highlightEffect.level);
|
||||
underlayDrawable.imagePart().addDirectives(underlayDirectives, true);
|
||||
drawDrawable(move(underlayDrawable));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// second pass, draw main drawables and overlays
|
||||
auto overlayDirectives = m_highlightDirectives[highlightEffect.type].second;
|
||||
for (auto& d : drawables) {
|
||||
drawDrawable(d);
|
||||
if (!overlayDirectives.empty() && d.isImage()) {
|
||||
auto overlayDrawable = Drawable(d);
|
||||
overlayDrawable.fullbright = true;
|
||||
overlayDrawable.color = Color::rgbaf(1, 1, 1, highlightEffect.level);
|
||||
overlayDrawable.imagePart().addDirectives(overlayDirectives, true);
|
||||
drawDrawable(move(overlayDrawable));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (auto& d : drawables)
|
||||
drawDrawable(move(d));
|
||||
}
|
||||
}
|
||||
|
||||
void WorldPainter::drawDrawable(Drawable drawable) {
|
||||
drawable.position = m_camera.worldToScreen(drawable.position);
|
||||
drawable.scale(m_camera.pixelRatio() * TilePixels, drawable.position);
|
||||
|
||||
if (drawable.isLine())
|
||||
drawable.linePart().width *= m_camera.pixelRatio();
|
||||
|
||||
// draw the drawable if it's on screen
|
||||
// if it's not on screen, there's a random chance to pre-load
|
||||
// pre-load is not done on every tick because it's expensive to look up images with long paths
|
||||
if (RectF::withSize(Vec2F(), Vec2F(m_camera.screenSize())).intersects(drawable.boundBox(false)))
|
||||
m_drawablePainter->drawDrawable(drawable);
|
||||
else if (drawable.isImage() && Random::randf() < m_preloadTextureChance)
|
||||
m_assets->tryImage(drawable.imagePart().image);
|
||||
}
|
||||
|
||||
void WorldPainter::drawDrawableSet(List<Drawable>& drawables) {
|
||||
for (Drawable& drawable : drawables)
|
||||
drawDrawable(move(drawable));
|
||||
|
||||
m_renderer->flush();
|
||||
}
|
||||
|
||||
}
|
67
source/rendering/StarWorldPainter.hpp
Normal file
67
source/rendering/StarWorldPainter.hpp
Normal file
|
@ -0,0 +1,67 @@
|
|||
#ifndef STAR_WORLD_PAINTER_HPP
|
||||
#define STAR_WORLD_PAINTER_HPP
|
||||
|
||||
#include "StarWorldRenderData.hpp"
|
||||
#include "StarTilePainter.hpp"
|
||||
#include "StarEnvironmentPainter.hpp"
|
||||
#include "StarTextPainter.hpp"
|
||||
#include "StarDrawablePainter.hpp"
|
||||
#include "StarRenderer.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(WorldPainter);
|
||||
|
||||
// Will update client rendering window internally
|
||||
class WorldPainter {
|
||||
public:
|
||||
WorldPainter();
|
||||
|
||||
void renderInit(RendererPtr renderer);
|
||||
|
||||
void setCameraPosition(WorldGeometry const& worldGeometry, Vec2F const& position);
|
||||
|
||||
WorldCamera const& camera() const;
|
||||
|
||||
void render(WorldRenderData& renderData);
|
||||
|
||||
private:
|
||||
void renderParticles(WorldRenderData& renderData, Particle::Layer layer);
|
||||
void renderBars(WorldRenderData& renderData);
|
||||
|
||||
void drawEntityLayer(List<Drawable> drawables, EntityHighlightEffect highlightEffect = EntityHighlightEffect());
|
||||
|
||||
void drawDrawable(Drawable drawable);
|
||||
void drawDrawableSet(List<Drawable>& drawable);
|
||||
|
||||
WorldCamera m_camera;
|
||||
|
||||
RendererPtr m_renderer;
|
||||
|
||||
TextPainterPtr m_textPainter;
|
||||
DrawablePainterPtr m_drawablePainter;
|
||||
EnvironmentPainterPtr m_environmentPainter;
|
||||
TilePainterPtr m_tilePainter;
|
||||
|
||||
Json m_highlightConfig;
|
||||
Map<EntityHighlightEffectType, pair<String, String>> m_highlightDirectives;
|
||||
|
||||
Vec2F m_entityBarOffset;
|
||||
Vec2F m_entityBarSpacing;
|
||||
Vec2F m_entityBarSize;
|
||||
Vec2F m_entityBarIconOffset;
|
||||
|
||||
// Updated every frame
|
||||
|
||||
AssetsConstPtr m_assets;
|
||||
RectF m_worldScreenRect;
|
||||
|
||||
Vec2F m_previousCameraCenter;
|
||||
Vec2F m_parallaxWorldPosition;
|
||||
|
||||
float m_preloadTextureChance;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
Loading…
Add table
Add a link
Reference in a new issue