Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/rendering
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
committerKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
commit6352e8e3196f78388b6c771073f9e03eaa612673 (patch)
treee23772f79a7fbc41bc9108951e9e136857484bf4 /source/rendering
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/rendering')
-rw-r--r--source/rendering/CMakeLists.txt35
-rw-r--r--source/rendering/StarAnchorTypes.cpp14
-rw-r--r--source/rendering/StarAnchorTypes.hpp24
-rw-r--r--source/rendering/StarAssetTextureGroup.cpp85
-rw-r--r--source/rendering/StarAssetTextureGroup.hpp50
-rw-r--r--source/rendering/StarDrawablePainter.cpp57
-rw-r--r--source/rendering/StarDrawablePainter.hpp27
-rw-r--r--source/rendering/StarEnvironmentPainter.cpp443
-rw-r--r--source/rendering/StarEnvironmentPainter.hpp81
-rw-r--r--source/rendering/StarFontTextureGroup.cpp40
-rw-r--r--source/rendering/StarFontTextureGroup.hpp40
-rw-r--r--source/rendering/StarTextPainter.cpp402
-rw-r--r--source/rendering/StarTextPainter.hpp109
-rw-r--r--source/rendering/StarTilePainter.cpp539
-rw-r--r--source/rendering/StarTilePainter.hpp140
-rw-r--r--source/rendering/StarWorldCamera.cpp38
-rw-r--r--source/rendering/StarWorldCamera.hpp117
-rw-r--r--source/rendering/StarWorldPainter.cpp296
-rw-r--r--source/rendering/StarWorldPainter.hpp67
19 files changed, 2604 insertions, 0 deletions
diff --git a/source/rendering/CMakeLists.txt b/source/rendering/CMakeLists.txt
new file mode 100644
index 0000000..d45153a
--- /dev/null
+++ b/source/rendering/CMakeLists.txt
@@ -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})
diff --git a/source/rendering/StarAnchorTypes.cpp b/source/rendering/StarAnchorTypes.cpp
new file mode 100644
index 0000000..dc86d7a
--- /dev/null
+++ b/source/rendering/StarAnchorTypes.cpp
@@ -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"},
+};
+}
diff --git a/source/rendering/StarAnchorTypes.hpp b/source/rendering/StarAnchorTypes.hpp
new file mode 100644
index 0000000..0e21d43
--- /dev/null
+++ b/source/rendering/StarAnchorTypes.hpp
@@ -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
diff --git a/source/rendering/StarAssetTextureGroup.cpp b/source/rendering/StarAssetTextureGroup.cpp
new file mode 100644
index 0000000..44b614a
--- /dev/null
+++ b/source/rendering/StarAssetTextureGroup.cpp
@@ -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;
+ }
+}
+
+}
diff --git a/source/rendering/StarAssetTextureGroup.hpp b/source/rendering/StarAssetTextureGroup.hpp
new file mode 100644
index 0000000..0941ad0
--- /dev/null
+++ b/source/rendering/StarAssetTextureGroup.hpp
@@ -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
diff --git a/source/rendering/StarDrawablePainter.cpp b/source/rendering/StarDrawablePainter.cpp
new file mode 100644
index 0000000..647d209
--- /dev/null
+++ b/source/rendering/StarDrawablePainter.cpp
@@ -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);
+}
+
+}
diff --git a/source/rendering/StarDrawablePainter.hpp b/source/rendering/StarDrawablePainter.hpp
new file mode 100644
index 0000000..5f0d6d6
--- /dev/null
+++ b/source/rendering/StarDrawablePainter.hpp
@@ -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
diff --git a/source/rendering/StarEnvironmentPainter.cpp b/source/rendering/StarEnvironmentPainter.cpp
new file mode 100644
index 0000000..a495eda
--- /dev/null
+++ b/source/rendering/StarEnvironmentPainter.cpp
@@ -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);
+ }
+}
+
+}
diff --git a/source/rendering/StarEnvironmentPainter.hpp b/source/rendering/StarEnvironmentPainter.hpp
new file mode 100644
index 0000000..3a90b69
--- /dev/null
+++ b/source/rendering/StarEnvironmentPainter.hpp
@@ -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
diff --git a/source/rendering/StarFontTextureGroup.cpp b/source/rendering/StarFontTextureGroup.cpp
new file mode 100644
index 0000000..038b535
--- /dev/null
+++ b/source/rendering/StarFontTextureGroup.cpp
@@ -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);
+}
+
+}
diff --git a/source/rendering/StarFontTextureGroup.hpp b/source/rendering/StarFontTextureGroup.hpp
new file mode 100644
index 0000000..d933f1e
--- /dev/null
+++ b/source/rendering/StarFontTextureGroup.hpp
@@ -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
diff --git a/source/rendering/StarTextPainter.cpp b/source/rendering/StarTextPainter.cpp
new file mode 100644
index 0000000..0ef374d
--- /dev/null
+++ b/source/rendering/StarTextPainter.cpp
@@ -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));
+}
+
+}
diff --git a/source/rendering/StarTextPainter.hpp b/source/rendering/StarTextPainter.hpp
new file mode 100644
index 0000000..64b2748
--- /dev/null
+++ b/source/rendering/StarTextPainter.hpp
@@ -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
diff --git a/source/rendering/StarTilePainter.cpp b/source/rendering/StarTilePainter.cpp
new file mode 100644
index 0000000..4b512de
--- /dev/null
+++ b/source/rendering/StarTilePainter.cpp
@@ -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);
+}
+
+}
diff --git a/source/rendering/StarTilePainter.hpp b/source/rendering/StarTilePainter.hpp
new file mode 100644
index 0000000..9e21850
--- /dev/null
+++ b/source/rendering/StarTilePainter.hpp
@@ -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
diff --git a/source/rendering/StarWorldCamera.cpp b/source/rendering/StarWorldCamera.cpp
new file mode 100644
index 0000000..0344bd6
--- /dev/null
+++ b/source/rendering/StarWorldCamera.cpp
@@ -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);
+}
+
+}
diff --git a/source/rendering/StarWorldCamera.hpp b/source/rendering/StarWorldCamera.hpp
new file mode 100644
index 0000000..fe1dbeb
--- /dev/null
+++ b/source/rendering/StarWorldCamera.hpp
@@ -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
diff --git a/source/rendering/StarWorldPainter.cpp b/source/rendering/StarWorldPainter.cpp
new file mode 100644
index 0000000..a063f36
--- /dev/null
+++ b/source/rendering/StarWorldPainter.cpp
@@ -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();
+}
+
+}
diff --git a/source/rendering/StarWorldPainter.hpp b/source/rendering/StarWorldPainter.hpp
new file mode 100644
index 0000000..92bc45d
--- /dev/null
+++ b/source/rendering/StarWorldPainter.hpp
@@ -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