diff options
author | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
---|---|---|
committer | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
commit | 6352e8e3196f78388b6c771073f9e03eaa612673 (patch) | |
tree | e23772f79a7fbc41bc9108951e9e136857484bf4 /source/rendering | |
parent | 6741a057e5639280d85d0f88ba26f000baa58f61 (diff) |
everything everywhere
all at once
Diffstat (limited to 'source/rendering')
-rw-r--r-- | source/rendering/CMakeLists.txt | 35 | ||||
-rw-r--r-- | source/rendering/StarAnchorTypes.cpp | 14 | ||||
-rw-r--r-- | source/rendering/StarAnchorTypes.hpp | 24 | ||||
-rw-r--r-- | source/rendering/StarAssetTextureGroup.cpp | 85 | ||||
-rw-r--r-- | source/rendering/StarAssetTextureGroup.hpp | 50 | ||||
-rw-r--r-- | source/rendering/StarDrawablePainter.cpp | 57 | ||||
-rw-r--r-- | source/rendering/StarDrawablePainter.hpp | 27 | ||||
-rw-r--r-- | source/rendering/StarEnvironmentPainter.cpp | 443 | ||||
-rw-r--r-- | source/rendering/StarEnvironmentPainter.hpp | 81 | ||||
-rw-r--r-- | source/rendering/StarFontTextureGroup.cpp | 40 | ||||
-rw-r--r-- | source/rendering/StarFontTextureGroup.hpp | 40 | ||||
-rw-r--r-- | source/rendering/StarTextPainter.cpp | 402 | ||||
-rw-r--r-- | source/rendering/StarTextPainter.hpp | 109 | ||||
-rw-r--r-- | source/rendering/StarTilePainter.cpp | 539 | ||||
-rw-r--r-- | source/rendering/StarTilePainter.hpp | 140 | ||||
-rw-r--r-- | source/rendering/StarWorldCamera.cpp | 38 | ||||
-rw-r--r-- | source/rendering/StarWorldCamera.hpp | 117 | ||||
-rw-r--r-- | source/rendering/StarWorldPainter.cpp | 296 | ||||
-rw-r--r-- | source/rendering/StarWorldPainter.hpp | 67 |
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 |