This commit is contained in:
Aria 2025-03-21 22:23:30 +11:00
commit 9c94d113d3
Signed by untrusted user who does not match committer: aria
GPG key ID: 19AB7AA462B8AB3B
10260 changed files with 1237388 additions and 0 deletions

View file

@ -0,0 +1,35 @@
INCLUDE_DIRECTORIES (
${STAR_EXTERN_INCLUDES}
${STAR_CORE_INCLUDES}
${STAR_BASE_INCLUDES}
${STAR_GAME_INCLUDES}
${STAR_PLATFORM_INCLUDES}
${STAR_APPLICATION_INCLUDES}
${STAR_RENDERING_INCLUDES}
)
SET (star_rendering_HEADERS
StarAnchorTypes.hpp
StarAssetTextureGroup.hpp
StarDrawablePainter.hpp
StarEnvironmentPainter.hpp
StarFontTextureGroup.hpp
StarTextPainter.hpp
StarTilePainter.hpp
StarWorldCamera.hpp
StarWorldPainter.hpp
)
SET (star_rendering_SOURCES
StarAnchorTypes.cpp
StarAssetTextureGroup.cpp
StarDrawablePainter.cpp
StarEnvironmentPainter.cpp
StarFontTextureGroup.cpp
StarTextPainter.cpp
StarTilePainter.cpp
StarWorldCamera.cpp
StarWorldPainter.cpp
)
ADD_LIBRARY (star_rendering OBJECT ${star_rendering_SOURCES} ${star_rendering_HEADERS})

View file

@ -0,0 +1,14 @@
#include "StarAnchorTypes.hpp"
namespace Star {
EnumMap<HorizontalAnchor> const HorizontalAnchorNames{
{HorizontalAnchor::LeftAnchor, "left"},
{HorizontalAnchor::HMidAnchor, "mid"},
{HorizontalAnchor::RightAnchor, "right"},
};
EnumMap<VerticalAnchor> const VerticalAnchorNames{
{VerticalAnchor::BottomAnchor, "bottom"}, {VerticalAnchor::VMidAnchor, "mid"}, {VerticalAnchor::TopAnchor, "top"},
};
}

View file

@ -0,0 +1,24 @@
#ifndef STAR_ANCHOR_TYPES_HPP
#define STAR_ANCHOR_TYPES_HPP
#include "StarBiMap.hpp"
namespace Star {
enum class HorizontalAnchor {
LeftAnchor,
HMidAnchor,
RightAnchor
};
extern EnumMap<HorizontalAnchor> const HorizontalAnchorNames;
enum class VerticalAnchor {
BottomAnchor,
VMidAnchor,
TopAnchor
};
extern EnumMap<VerticalAnchor> const VerticalAnchorNames;
}
#endif

View file

@ -0,0 +1,85 @@
#include "StarAssetTextureGroup.hpp"
#include "StarIterator.hpp"
#include "StarTime.hpp"
#include "StarRoot.hpp"
#include "StarAssets.hpp"
#include "StarImageMetadataDatabase.hpp"
namespace Star {
AssetTextureGroup::AssetTextureGroup(TextureGroupPtr textureGroup)
: m_textureGroup(move(textureGroup)) {
m_reloadTracker = make_shared<TrackerListener>();
Root::singleton().registerReloadListener(m_reloadTracker);
}
TexturePtr AssetTextureGroup::loadTexture(String const& imageName) {
return loadTexture(imageName, false);
}
TexturePtr AssetTextureGroup::tryTexture(String const& imageName) {
return loadTexture(imageName, true);
}
bool AssetTextureGroup::textureLoaded(String const& imageName) const {
return m_textureMap.contains(imageName);
}
void AssetTextureGroup::cleanup(int64_t textureTimeout) {
if (m_reloadTracker->pullTriggered()) {
m_textureMap.clear();
m_textureDeduplicationMap.clear();
} else {
int64_t time = Time::monotonicMilliseconds();
List<Texture const*> liveTextures;
filter(m_textureMap, [&](auto const& pair) {
if (time - pair.second.second < textureTimeout) {
liveTextures.append(pair.second.first.get());
return true;
}
return false;
});
liveTextures.sort();
eraseWhere(m_textureDeduplicationMap, [&](auto const& p) {
return !liveTextures.containsSorted(p.second.get());
});
}
}
TexturePtr AssetTextureGroup::loadTexture(String const& imageName, bool tryTexture) {
if (auto p = m_textureMap.ptr(imageName)) {
p->second = Time::monotonicMilliseconds();
return p->first;
}
auto assets = Root::singleton().assets();
ImageConstPtr image;
if (tryTexture)
image = assets->tryImage(imageName);
else
image = assets->image(imageName);
if (!image)
return {};
// Assets will return the same image ptr if two different asset paths point
// to the same underlying cached image. We should not make duplicate entries
// in the texture group for these, so we keep track of the image pointers
// returned to deduplicate them.
if (auto existingTexture = m_textureDeduplicationMap.value(image)) {
m_textureMap.add(imageName, {existingTexture, Time::monotonicMilliseconds()});
return existingTexture;
} else {
auto texture = m_textureGroup->create(*image);
m_textureMap.add(imageName, {texture, Time::monotonicMilliseconds()});
m_textureDeduplicationMap.add(image, texture);
return texture;
}
}
}

View file

@ -0,0 +1,50 @@
#ifndef STAR_ASSET_TEXTURE_GROUP_HPP
#define STAR_ASSET_TEXTURE_GROUP_HPP
#include "StarMaybe.hpp"
#include "StarString.hpp"
#include "StarBiMap.hpp"
#include "StarListener.hpp"
#include "StarRenderer.hpp"
namespace Star {
STAR_CLASS(AssetTextureGroup);
// Creates a renderer texture group for textures loaded directly from Assets.
class AssetTextureGroup {
public:
// Creates a texture group using the given renderer and textureFiltering for
// the managed textures.
AssetTextureGroup(TextureGroupPtr textureGroup);
// Load the given texture into the texture group if it is not loaded, and
// return the texture pointer.
TexturePtr loadTexture(String const& imageName);
// If the texture is loaded and ready, returns the texture pointer, otherwise
// queues the texture using Assets::tryImage and returns nullptr.
TexturePtr tryTexture(String const& imageName);
// Has the texture been loaded?
bool textureLoaded(String const& imageName) const;
// Frees textures that haven't been used in more than 'textureTimeout' time.
// If Root has been reloaded, will simply clear the texture group.
void cleanup(int64_t textureTimeout);
private:
// Returns the texture parameters. If tryTexture is true, then returns none
// if the texture is not loaded, and queues it, otherwise loads texture
// immediately
TexturePtr loadTexture(String const& imageName, bool tryTexture);
TextureGroupPtr m_textureGroup;
StringMap<pair<TexturePtr, int64_t>> m_textureMap;
HashMap<ImageConstPtr, TexturePtr> m_textureDeduplicationMap;
TrackerListenerPtr m_reloadTracker;
};
}
#endif

View file

@ -0,0 +1,57 @@
#include "StarDrawablePainter.hpp"
namespace Star {
DrawablePainter::DrawablePainter(RendererPtr renderer, AssetTextureGroupPtr textureGroup) {
m_renderer = move(renderer);
m_textureGroup = move(textureGroup);
}
void DrawablePainter::drawDrawable(Drawable const& drawable) {
Vec4B color = drawable.color.toRgba();
if (auto linePart = drawable.part.ptr<Drawable::LinePart>()) {
auto line = linePart->line;
line.translate(drawable.position);
Vec2F left = Vec2F(vnorm(line.diff())).rot90() * linePart->width / 2.0f;
m_renderer->render(RenderQuad{{},
RenderVertex{Vec2F(line.min()) + left, Vec2F(), color, drawable.fullbright ? 0.0f : 1.0f},
RenderVertex{Vec2F(line.min()) - left, Vec2F(), color, drawable.fullbright ? 0.0f : 1.0f},
RenderVertex{Vec2F(line.max()) - left, Vec2F(), color, drawable.fullbright ? 0.0f : 1.0f},
RenderVertex{Vec2F(line.max()) + left, Vec2F(), color, drawable.fullbright ? 0.0f : 1.0f}
});
} else if (auto polyPart = drawable.part.ptr<Drawable::PolyPart>()) {
auto poly = polyPart->poly;
poly.translate(drawable.position);
m_renderer->render(renderFlatPoly(poly, color, 0.0f));
} else if (auto imagePart = drawable.part.ptr<Drawable::ImagePart>()) {
TexturePtr texture = m_textureGroup->loadTexture(imagePart->image);
Vec2F textureSize(texture->size());
RectF imageRect(Vec2F(), textureSize);
Mat3F transformation = Mat3F::translation(drawable.position) * imagePart->transformation;
Vec2F lowerLeft = transformation.transformVec2(Vec2F(imageRect.xMin(), imageRect.yMin()));
Vec2F lowerRight = transformation.transformVec2(Vec2F(imageRect.xMax(), imageRect.yMin()));
Vec2F upperRight = transformation.transformVec2(Vec2F(imageRect.xMax(), imageRect.yMax()));
Vec2F upperLeft = transformation.transformVec2(Vec2F(imageRect.xMin(), imageRect.yMax()));
m_renderer->render(RenderQuad{move(texture),
{lowerLeft, {0, 0}, color, drawable.fullbright ? 0.0f : 1.0f},
{lowerRight, {textureSize[0], 0}, color, drawable.fullbright ? 0.0f : 1.0f},
{upperRight, {textureSize[0], textureSize[1]}, color, drawable.fullbright ? 0.0f : 1.0f},
{upperLeft, {0, textureSize[1]}, color, drawable.fullbright ? 0.0f : 1.0f}
});
}
}
void DrawablePainter::cleanup(int64_t textureTimeout) {
m_textureGroup->cleanup(textureTimeout);
}
}

View file

@ -0,0 +1,27 @@
#ifndef STAR_DRAWABLE_PAINTER_HPP
#define STAR_DRAWABLE_PAINTER_HPP
#include "StarDrawable.hpp"
#include "StarRenderer.hpp"
#include "StarAssetTextureGroup.hpp"
namespace Star {
STAR_CLASS(DrawablePainter);
class DrawablePainter {
public:
DrawablePainter(RendererPtr renderer, AssetTextureGroupPtr textureGroup);
void drawDrawable(Drawable const& drawable);
void cleanup(int64_t textureTimeout);
private:
RendererPtr m_renderer;
AssetTextureGroupPtr m_textureGroup;
};
}
#endif

View file

@ -0,0 +1,443 @@
#include "StarEnvironmentPainter.hpp"
#include "StarLexicalCast.hpp"
#include "StarTime.hpp"
#include "StarXXHash.hpp"
#include "StarJsonExtra.hpp"
namespace Star {
float const EnvironmentPainter::SunriseTime = 0.072f;
float const EnvironmentPainter::SunsetTime = 0.42f;
float const EnvironmentPainter::SunFadeRate = 0.07f;
float const EnvironmentPainter::MaxFade = 0.3f;
float const EnvironmentPainter::RayPerlinFrequency = 0.005f; // Arbitrary, part of using the Perlin as a PRNG
float const EnvironmentPainter::RayPerlinAmplitude = 2;
int const EnvironmentPainter::RayCount = 60;
float const EnvironmentPainter::RayMinWidth = 0.8f; // % of its sector
float const EnvironmentPainter::RayWidthVariance = 5.0265f; // % of its sector
float const EnvironmentPainter::RayAngleVariance = 6.2832f; // Radians
float const EnvironmentPainter::SunRadius = 50;
float const EnvironmentPainter::RayColorDependenceLevel = 3.0f;
float const EnvironmentPainter::RayColorDependenceScale = 0.00625f;
float const EnvironmentPainter::RayUnscaledAlphaVariance = 2.0943f;
float const EnvironmentPainter::RayMinUnscaledAlpha = 1;
Vec3B const EnvironmentPainter::RayColor = Vec3B(255, 255, 200);
EnvironmentPainter::EnvironmentPainter(RendererPtr renderer) {
m_renderer = move(renderer);
m_textureGroup = make_shared<AssetTextureGroup>(m_renderer->createTextureGroup(TextureGroupSize::Large));
m_timer = 0;
m_lastTime = 0;
m_rayPerlin = PerlinF(1, RayPerlinFrequency, RayPerlinAmplitude, 0, 2.0f, 2.0f, Random::randu64());
}
void EnvironmentPainter::update() {
// Allows the rays to change alpha with time.
int64_t currentTime = Time::monotonicMilliseconds();
m_timer += (currentTime - m_lastTime) / 1000.0;
m_timer = std::fmod(m_timer, Constants::pi * 100000.0);
m_lastTime = currentTime;
}
void EnvironmentPainter::renderStars(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky) {
if (!sky.settings)
return;
float nightSkyAlpha = 1.0f - min(sky.dayLevel, sky.skyAlpha);
if (nightSkyAlpha <= 0.0f)
return;
Vec4B color(255, 255, 255, 255 * nightSkyAlpha);
Vec2F viewSize = screenSize / pixelRatio;
Vec2F viewCenter = viewSize / 2;
Vec2F viewMin = sky.starOffset - viewCenter;
auto newStarsHash = starsHash(sky, viewSize);
if (newStarsHash != m_starsHash) {
m_starsHash = newStarsHash;
setupStars(sky);
}
float screenBuffer = sky.settings.queryFloat("stars.screenBuffer");
PolyF field = PolyF(RectF::withSize(viewMin, Vec2F(viewSize)).padded(screenBuffer));
field.rotate(-sky.starRotation, Vec2F(sky.starOffset));
Mat3F transform = Mat3F::identity();
transform.translate(-viewMin);
transform.rotate(sky.starRotation, viewCenter);
int starTwinkleMin = sky.settings.queryInt("stars.twinkleMin");
int starTwinkleMax = sky.settings.queryInt("stars.twinkleMax");
size_t starTypesSize = sky.starTypes().size();
auto stars = m_starGenerator->generate(field, [&](RandomSource& rand) {
size_t starType = rand.randu32() % starTypesSize;
float frameOffset = rand.randu32() % sky.starFrames + rand.randf(starTwinkleMin, starTwinkleMax);
return pair<size_t, float>(starType, frameOffset);
});
RectF viewRect = RectF::withSize(Vec2F(), viewSize).padded(screenBuffer);
for (auto star : stars) {
Vec2F screenPos = transform.transformVec2(star.first);
if (viewRect.contains(screenPos)) {
size_t starFrame = (int)(sky.epochTime + star.second.second) % sky.starFrames;
auto const& texture = m_starTextures[star.second.first * sky.starFrames + starFrame];
m_renderer->render(renderTexturedRect(texture, screenPos * pixelRatio - Vec2F(texture->size()) / 2, 1.0, color, 0.0f));
}
}
m_renderer->flush();
}
void EnvironmentPainter::renderDebrisFields(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky) {
if (!sky.settings)
return;
if (sky.type == SkyType::Orbital || sky.type == SkyType::Warp) {
Vec2F viewSize = screenSize / pixelRatio;
Vec2F viewCenter = viewSize / 2;
Vec2F viewMin = sky.starOffset - viewCenter;
Mat3F rotMatrix = Mat3F::rotation(sky.starRotation, viewCenter);
JsonArray debrisFields = sky.settings.queryArray("spaceDebrisFields");
for (size_t i = 0; i < debrisFields.size(); ++i) {
Json debrisField = debrisFields[i];
Vec2F spaceDebrisVelocityRange = jsonToVec2F(debrisField.query("velocityRange"));
float debrisXVel = staticRandomFloatRange(spaceDebrisVelocityRange[0], spaceDebrisVelocityRange[1], sky.skyParameters.seed, i, "DebrisFieldXVel");
float debrisYVel = staticRandomFloatRange(spaceDebrisVelocityRange[0], spaceDebrisVelocityRange[1], sky.skyParameters.seed, i, "DebrisFieldYVel");
// Translate the entire field to make the debris seem as though they are moving
Vec2F velocityOffset = -Vec2F(debrisXVel, debrisYVel) * sky.epochTime;
float screenBuffer = debrisField.queryFloat("screenBuffer");
PolyF field = PolyF(RectF::withSize(viewMin, viewSize).padded(screenBuffer).translated(velocityOffset));
Vec2F debrisAngularVelocityRange = jsonToVec2F(debrisField.query("angularVelocityRange"));
JsonArray imageOptions = debrisField.query("list").toArray();
auto debrisItems = m_debrisGenerators[i]->generate(field,
[&](RandomSource& rand) {
String debrisImage = rand.randFrom(imageOptions).toString();
float debrisAngularVelocity = rand.randf(debrisAngularVelocityRange[0], debrisAngularVelocityRange[1]);
return pair<String, float>(debrisImage, debrisAngularVelocity);
});
Vec2F debrisPositionOffset = -(sky.starOffset + velocityOffset + viewCenter);
for (auto debrisItem : debrisItems) {
Vec2F debrisPosition = rotMatrix.transformVec2(debrisItem.first + debrisPositionOffset);
float debrisAngle = fmod(Constants::deg2rad * debrisItem.second.second * sky.epochTime, Constants::pi * 2) + sky.starRotation;
drawOrbiter(pixelRatio, screenSize, sky, {SkyOrbiterType::SpaceDebris, 1.0f, debrisAngle, debrisItem.second.first, debrisPosition});
}
}
m_renderer->flush();
}
}
void EnvironmentPainter::renderBackOrbiters(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky) {
for (auto const& orbiter : sky.backOrbiters(screenSize / pixelRatio))
drawOrbiter(pixelRatio, screenSize, sky, orbiter);
m_renderer->flush();
}
void EnvironmentPainter::renderPlanetHorizon(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky) {
auto planetHorizon = sky.worldHorizon(screenSize / pixelRatio);
if (planetHorizon.empty())
return;
// Can't bail sooner, need to queue all textures
bool allLoaded = true;
for (auto const& layer : planetHorizon.layers) {
if (!m_textureGroup->tryTexture(layer.first) || !m_textureGroup->tryTexture(layer.second))
allLoaded = false;
}
if (!allLoaded)
return;
float planetPixelRatio = pixelRatio * planetHorizon.scale;
Vec2F center = planetHorizon.center * pixelRatio;
for (auto const& layer : planetHorizon.layers) {
TexturePtr leftTexture = m_textureGroup->loadTexture(layer.first);
Vec2F leftTextureSize(leftTexture->size());
TexturePtr rightTexture = m_textureGroup->loadTexture(layer.second);
Vec2F rightTextureSize(rightTexture->size());
Vec2F leftLayer = center;
leftLayer[0] -= leftTextureSize[0] * planetPixelRatio;
auto leftRect = RectF::withSize(leftLayer, leftTextureSize * planetPixelRatio);
PolyF leftImage = PolyF(leftRect);
leftImage.rotate(planetHorizon.rotation, center);
auto rightRect = RectF::withSize(center, rightTextureSize * planetPixelRatio);
PolyF rightImage = PolyF(rightRect);
rightImage.rotate(planetHorizon.rotation, center);
m_renderer->render(RenderQuad{move(leftTexture),
{leftImage[0], Vec2F(0, 0), {255, 255, 255, 255}, 0.0f},
{leftImage[1], Vec2F(leftTextureSize[0], 0), {255, 255, 255, 255}, 0.0f},
{leftImage[2], Vec2F(leftTextureSize[0], leftTextureSize[1]), {255, 255, 255, 255}, 0.0f},
{leftImage[3], Vec2F(0, leftTextureSize[1]), {255, 255, 255, 255}, 0.0f}});
m_renderer->render(RenderQuad{move(rightTexture),
{rightImage[0], Vec2F(0, 0), {255, 255, 255, 255}, 0.0f},
{rightImage[1], Vec2F(rightTextureSize[0], 0), {255, 255, 255, 255}, 0.0f},
{rightImage[2], Vec2F(rightTextureSize[0], rightTextureSize[1]), {255, 255, 255, 255}, 0.0f},
{rightImage[3], Vec2F(0, rightTextureSize[1]), {255, 255, 255, 255}, 0.0f}});
}
m_renderer->flush();
}
void EnvironmentPainter::renderFrontOrbiters(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky) {
for (auto const& orbiter : sky.frontOrbiters(screenSize / pixelRatio))
drawOrbiter(pixelRatio, screenSize, sky, orbiter);
m_renderer->flush();
}
void EnvironmentPainter::renderSky(Vec2F const& screenSize, SkyRenderData const& sky) {
m_renderer->render(RenderQuad{{},
{Vec2F(0, 0), Vec2F(), sky.bottomRectColor.toRgba(), 0.0f},
{Vec2F(screenSize[0], 0), Vec2F(), sky.bottomRectColor.toRgba(), 0.0f},
{screenSize, Vec2F(), sky.topRectColor.toRgba(), 0.0f},
{Vec2F(0, screenSize[1]), Vec2F(), sky.topRectColor.toRgba(), 0.0f}});
// Flash overlay for Interstellar travel
Vec4B flashColor = sky.flashColor.toRgba();
m_renderer->render(renderFlatRect(RectF(Vec2F(), screenSize), flashColor, 0.0f));
m_renderer->flush();
}
void EnvironmentPainter::renderParallaxLayers(
Vec2F parallaxWorldPosition, WorldCamera const& camera, ParallaxLayers const& layers, SkyRenderData const& sky) {
// Note: the "parallax space" referenced below is a grid where the scale of each cell is the size of the parallax image
for (auto layer : layers) {
if (layer.alpha == 0)
continue;
Vec4B drawColor;
if (layer.unlit || layer.lightMapped)
drawColor = Vec4B(255, 255, 255, floor(255 * layer.alpha));
else
drawColor = Vec4B(sky.environmentLight.toRgb(), floor(255 * layer.alpha));
Vec2F parallaxValue = layer.parallaxValue;
Vec2B parallaxRepeat = layer.repeat;
Vec2F parallaxOrigin = {0.0f, layer.verticalOrigin};
Vec2F parallaxSize =
Vec2F(m_textureGroup->loadTexture(String::joinWith("?", layer.textures.first(), layer.directives))->size());
Vec2F parallaxPixels = parallaxSize * camera.pixelRatio();
// texture offset in *screen pixel space*
Vec2F parallaxOffset = layer.parallaxOffset * camera.pixelRatio();
if (layer.speed != 0) {
double drift = fmod((double)layer.speed * (sky.epochTime / (double)sky.dayLength) * camera.pixelRatio(), (double)parallaxPixels[0]);
parallaxOffset[0] = fmod(parallaxOffset[0] + drift, parallaxPixels[0]);
}
// parallax camera world position in *parallax space*
Vec2F parallaxCameraCenter = parallaxWorldPosition - parallaxOrigin;
parallaxCameraCenter =
Vec2F((((parallaxCameraCenter[0] / parallaxPixels[0]) * TilePixels) * camera.pixelRatio()) / parallaxValue[0],
(((parallaxCameraCenter[1] / parallaxPixels[1]) * TilePixels) * camera.pixelRatio()) / parallaxValue[1]);
// width / height of screen in *parallax space*
float parallaxScreenWidth = camera.screenSize()[0] / parallaxPixels[0];
float parallaxScreenHeight = camera.screenSize()[1] / parallaxPixels[1];
// screen world position in *parallax space*
float parallaxScreenLeft = parallaxCameraCenter[0] - parallaxScreenWidth / 2.0;
float parallaxScreenBottom = parallaxCameraCenter[1] - parallaxScreenHeight / 2.0;
// screen boundary world positions in *parallax space*
Vec2F parallaxScreenOffset = parallaxOffset.piecewiseDivide(parallaxPixels);
int left = floor(parallaxScreenLeft + parallaxScreenOffset[0]);
int bottom = floor(parallaxScreenBottom + parallaxScreenOffset[1]);
int right = ceil(parallaxScreenLeft + parallaxScreenWidth + parallaxScreenOffset[0]);
int top = ceil(parallaxScreenBottom + parallaxScreenHeight + parallaxScreenOffset[1]);
// positions to start tiling in *screen pixel space*
float pixelLeft = (left - parallaxScreenLeft) * parallaxPixels[0] - parallaxOffset[0];
float pixelBottom = (bottom - parallaxScreenBottom) * parallaxPixels[1] - parallaxOffset[1];
// vertical top and bottom cutoff points in *parallax space*
float tileLimitTop = top;
if (layer.tileLimitTop)
tileLimitTop = (layer.parallaxOffset[1] - layer.tileLimitTop.value()) / parallaxSize[1];
float tileLimitBottom = bottom;
if (layer.tileLimitBottom)
tileLimitBottom = (layer.parallaxOffset[1] - layer.tileLimitBottom.value()) / parallaxSize[1];
float lightMapMultiplier = (!layer.unlit && layer.lightMapped) ? 1.0f : 0.0f;
for (int y = bottom; y <= top; ++y) {
if (!(parallaxRepeat[1] || y == 0) || y > tileLimitTop || y + 1 < tileLimitBottom)
continue;
for (int x = left; x <= right; ++x) {
if (!(parallaxRepeat[0] || x == 0))
continue;
float pixelTileLeft = pixelLeft + (x - left) * parallaxPixels[0];
float pixelTileBottom = pixelBottom + (y - bottom) * parallaxPixels[1];
Vec2F anchorPoint(pixelTileLeft, pixelTileBottom);
RectF subImage = RectF::withSize(Vec2F(), parallaxSize);
if (tileLimitTop != top && y + 1 > tileLimitTop)
subImage.setYMin(parallaxSize[1] * (1.0f - fpart(tileLimitTop)));
if (tileLimitBottom != bottom && y < tileLimitBottom)
anchorPoint[1] += fpart(tileLimitBottom) * parallaxPixels[1];
for (String const& textureImage : layer.textures) {
if (auto texture = m_textureGroup->tryTexture(String::joinWith("?", textureImage, layer.directives))) {
RectF drawRect = RectF::withSize(anchorPoint, subImage.size() * camera.pixelRatio());
m_renderer->render(RenderQuad{move(texture),
{{drawRect.xMin(), drawRect.yMin()}, {subImage.xMin(), subImage.yMin()}, drawColor, lightMapMultiplier},
{{drawRect.xMax(), drawRect.yMin()}, {subImage.xMax(), subImage.yMin()}, drawColor, lightMapMultiplier},
{{drawRect.xMax(), drawRect.yMax()}, {subImage.xMax(), subImage.yMax()}, drawColor, lightMapMultiplier},
{{drawRect.xMin(), drawRect.yMax()}, {subImage.xMin(), subImage.yMax()}, drawColor, lightMapMultiplier}});
}
}
}
}
}
m_renderer->flush();
}
void EnvironmentPainter::cleanup(int64_t textureTimeout) {
m_textureGroup->cleanup(textureTimeout);
}
void EnvironmentPainter::drawRays(
float pixelRatio, SkyRenderData const& sky, Vec2F start, float length, double time, float alpha) {
// All magic constants are either 2PI or arbritrary to allow the Perlin to act
// as a PRNG
float sectorWidth = 2 * Constants::pi / RayCount; // Radians
Vec3B color = sky.topRectColor.toRgb();
for (int i = 0; i < RayCount; i++)
drawRay(pixelRatio,
sky,
start,
sectorWidth * (std::abs(m_rayPerlin.get(i * 25)) * RayWidthVariance + RayMinWidth),
length,
i * sectorWidth + m_rayPerlin.get(i * 314) * RayAngleVariance,
time,
color,
alpha);
m_renderer->flush();
}
void EnvironmentPainter::drawRay(float pixelRatio,
SkyRenderData const& sky,
Vec2F start,
float width,
float length,
float angle,
double time,
Vec3B color,
float alpha) {
// All magic constants are arbritrary to allow the Perlin to act as a PRNG
float currentTime = sky.timeOfDay / sky.dayLength;
float timeSinceSunEvent = std::min(std::abs(currentTime - SunriseTime), std::abs(currentTime - SunsetTime));
float percentFaded = MaxFade * (1.0f - std::min(1.0f, std::pow(timeSinceSunEvent / SunFadeRate, 2.0f)));
// Gets the current average sky color
color = (Vec3B)((Vec3F)color * (1 - percentFaded) + (Vec3F)sky.mainSkyColor.toRgb() * percentFaded);
// Sum is used to vary the ray intensity based on sky color
// Rays show up more on darker backgrounds, so this scales to remove that
float sum = std::pow((color[0] + color[1]) * RayColorDependenceScale, RayColorDependenceLevel);
m_renderer->render(RenderQuad{{},
{start + Vec2F(std::cos(angle + width), std::sin(angle + width)) * length, {}, Vec4B(RayColor, 0), 0.0f},
{start + Vec2F(std::cos(angle + width), std::sin(angle + width)) * SunRadius * pixelRatio,
{},
Vec4B(RayColor,
(int)(RayMinUnscaledAlpha + std::abs(m_rayPerlin.get(angle * 896 + time * 30) * RayUnscaledAlphaVariance))
* sum
* alpha), 0.0f},
{start + Vec2F(std::cos(angle), std::sin(angle)) * SunRadius * pixelRatio,
{},
Vec4B(RayColor,
(int)(RayMinUnscaledAlpha + std::abs(m_rayPerlin.get(angle * 626 + time * 30) * RayUnscaledAlphaVariance))
* sum
* alpha), 0.0f},
{start + Vec2F(std::cos(angle), std::sin(angle)) * length, {}, Vec4B(RayColor, 0), 0.0f}});
}
void EnvironmentPainter::drawOrbiter(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky, SkyOrbiter const& orbiter) {
float alpha = 1.0f;
Vec2F position = orbiter.position * pixelRatio;
if (orbiter.type == SkyOrbiterType::Sun) {
alpha = sky.dayLevel;
drawRays(pixelRatio, sky, position, std::max(screenSize[0], screenSize[1]), m_timer, sky.skyAlpha);
}
TexturePtr texture = m_textureGroup->loadTexture(orbiter.image);
Vec2F texSize = Vec2F(texture->size());
Mat3F renderMatrix = Mat3F::rotation(orbiter.angle, position);
RectF renderRect = RectF::withCenter(position, texSize * orbiter.scale * pixelRatio);
Vec4B renderColor = Vec4B(255, 255, 255, 255 * alpha);
m_renderer->render(RenderQuad{move(texture),
{renderMatrix.transformVec2(renderRect.min()), Vec2F(0, 0), renderColor, 0.0f},
{renderMatrix.transformVec2(Vec2F{renderRect.xMax(), renderRect.yMin()}), Vec2F(texSize[0], 0), renderColor, 0.0f},
{renderMatrix.transformVec2(renderRect.max()), Vec2F(texSize[0], texSize[1]), renderColor, 0.0f},
{renderMatrix.transformVec2(Vec2F{renderRect.xMin(), renderRect.yMax()}), Vec2F(0, texSize[1]), renderColor, 0.0f}});
}
uint64_t EnvironmentPainter::starsHash(SkyRenderData const& sky, Vec2F const& viewSize) const {
XXHash64 hasher;
hasher.push(reinterpret_cast<char const*>(&viewSize[0]), sizeof(viewSize[0]));
hasher.push(reinterpret_cast<char const*>(&viewSize[1]), sizeof(viewSize[1]));
hasher.push(reinterpret_cast<char const*>(&sky.skyParameters.seed), sizeof(sky.skyParameters.seed));
hasher.push(reinterpret_cast<char const*>(&sky.type), sizeof(sky.type));
return hasher.digest();
}
void EnvironmentPainter::setupStars(SkyRenderData const& sky) {
if (!sky.settings)
return;
StringList starTypes = sky.starTypes();
size_t starTypesSize = starTypes.size();
m_starTextures.resize(starTypesSize * sky.starFrames);
for (size_t i = 0; i < starTypesSize; ++i) {
for (size_t j = 0; j < sky.starFrames; ++j)
m_starTextures[i * sky.starFrames + j] = m_textureGroup->loadTexture(starTypes[i] + ":" + toString(j));
}
int starCellSize = sky.settings.queryInt("stars.cellSize");
Vec2I starCount = jsonToVec2I(sky.settings.query("stars.cellCount"));
m_starGenerator = make_shared<Random2dPointGenerator<pair<size_t, float>>>(sky.skyParameters.seed, starCellSize, starCount);
JsonArray debrisFields = sky.settings.queryArray("spaceDebrisFields");
m_debrisGenerators.resize(debrisFields.size());
for (size_t i = 0; i < debrisFields.size(); ++i) {
int debrisCellSize = debrisFields[i].getInt("cellSize");
Vec2I debrisCountRange = jsonToVec2I(debrisFields[i].get("cellCountRange"));
uint64_t debrisSeed = staticRandomU64(sky.skyParameters.seed, i, "DebrisFieldSeed");
m_debrisGenerators[i] = make_shared<Random2dPointGenerator<pair<String, float>>>(debrisSeed, debrisCellSize, debrisCountRange);
}
}
}

View file

@ -0,0 +1,81 @@
#ifndef STAR_ENVIRONMENT_PAINTER_HPP
#define STAR_ENVIRONMENT_PAINTER_HPP
#include "StarParallax.hpp"
#include "StarWorldRenderData.hpp"
#include "StarAssetTextureGroup.hpp"
#include "StarRenderer.hpp"
#include "StarWorldCamera.hpp"
#include "StarPerlin.hpp"
#include "StarRandomPoint.hpp"
namespace Star {
STAR_CLASS(EnvironmentPainter);
class EnvironmentPainter {
public:
EnvironmentPainter(RendererPtr renderer);
void update();
void renderStars(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky);
void renderDebrisFields(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky);
void renderBackOrbiters(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky);
void renderPlanetHorizon(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky);
void renderFrontOrbiters(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky);
void renderSky(Vec2F const& screenSize, SkyRenderData const& sky);
void renderParallaxLayers(Vec2F parallaxWorldPosition, WorldCamera const& camera, ParallaxLayers const& layers, SkyRenderData const& sky);
void cleanup(int64_t textureTimeout);
private:
static float const SunriseTime;
static float const SunsetTime;
static float const SunFadeRate;
static float const MaxFade;
static float const RayPerlinFrequency;
static float const RayPerlinAmplitude;
static int const RayCount;
static float const RayMinWidth;
static float const RayWidthVariance;
static float const RayAngleVariance;
static float const SunRadius;
static float const RayColorDependenceLevel;
static float const RayColorDependenceScale;
static float const RayUnscaledAlphaVariance;
static float const RayMinUnscaledAlpha;
static Vec3B const RayColor;
void drawRays(float pixelRatio, SkyRenderData const& sky, Vec2F start, float length, double time, float alpha);
void drawRay(float pixelRatio,
SkyRenderData const& sky,
Vec2F start,
float width,
float length,
float angle,
double time,
Vec3B color,
float alpha);
void drawOrbiter(float pixelRatio, Vec2F const& screenSize, SkyRenderData const& sky, SkyOrbiter const& orbiter);
uint64_t starsHash(SkyRenderData const& sky, Vec2F const& viewSize) const;
void setupStars(SkyRenderData const& sky);
RendererPtr m_renderer;
AssetTextureGroupPtr m_textureGroup;
double m_timer;
int64_t m_lastTime;
PerlinF m_rayPerlin;
uint64_t m_starsHash;
List<TexturePtr> m_starTextures;
shared_ptr<Random2dPointGenerator<pair<size_t, float>>> m_starGenerator;
List<shared_ptr<Random2dPointGenerator<pair<String, float>>>> m_debrisGenerators;
};
}
#endif

View file

@ -0,0 +1,40 @@
#include "StarFontTextureGroup.hpp"
#include "StarTime.hpp"
#include "StarImageProcessing.hpp"
namespace Star {
FontTextureGroup::FontTextureGroup(FontPtr font, TextureGroupPtr textureGroup)
: m_font(move(font)), m_textureGroup(move(textureGroup)) {}
void FontTextureGroup::cleanup(int64_t timeout) {
int64_t currentTime = Time::monotonicMilliseconds();
eraseWhere(m_glyphs, [&](auto const& p) { return currentTime - p.second.time > timeout; });
}
TexturePtr FontTextureGroup::glyphTexture(String::Char c, unsigned size) {
return glyphTexture(c, size, "");
}
TexturePtr FontTextureGroup::glyphTexture(String::Char c, unsigned size, String const& processingDirectives) {
auto res = m_glyphs.insert(GlyphDescriptor{c, size, processingDirectives}, GlyphTexture());
if (res.second) {
m_font->setPixelSize(size);
Image image = m_font->render(c);
if (!processingDirectives.empty())
image = processImageOperations(parseImageOperations(processingDirectives), image);
res.first->second.texture = m_textureGroup->create(image);
}
res.first->second.time = Time::monotonicMilliseconds();
return res.first->second.texture;
}
unsigned FontTextureGroup::glyphWidth(String::Char c, unsigned fontSize) {
m_font->setPixelSize(fontSize);
return m_font->width(c);
}
}

View file

@ -0,0 +1,40 @@
#ifndef STAR_FONT_TEXTURE_GROUP_HPP
#define STAR_FONT_TEXTURE_GROUP_HPP
#include "StarColor.hpp"
#include "StarFont.hpp"
#include "StarRenderer.hpp"
namespace Star {
STAR_CLASS(FontTextureGroup);
class FontTextureGroup {
public:
FontTextureGroup(FontPtr font, TextureGroupPtr textureGroup);
TexturePtr glyphTexture(String::Char, unsigned fontSize);
TexturePtr glyphTexture(String::Char, unsigned fontSize, String const& processingDirectives);
unsigned glyphWidth(String::Char c, unsigned fontSize);
// Removes glyphs that haven't been used in more than the given time in
// milliseconds
void cleanup(int64_t timeout);
private:
typedef tuple<String::Char, unsigned, String> GlyphDescriptor;
struct GlyphTexture {
TexturePtr texture;
int64_t time;
};
FontPtr m_font;
TextureGroupPtr m_textureGroup;
HashMap<GlyphDescriptor, GlyphTexture> m_glyphs;
};
}
#endif

View file

@ -0,0 +1,402 @@
#include "StarTextPainter.hpp"
#include "StarJsonExtra.hpp"
#include <regex>
namespace Star {
namespace Text {
String stripEscapeCodes(String const& s) {
String regex = strf("\\%s[^;]*%s", CmdEsc, EndEsc);
return std::regex_replace(s.utf8(), std::regex(regex.utf8()), "");
}
String preprocessEscapeCodes(String const& s) {
bool escape = false;
auto result = s.utf8();
size_t escapeStartIdx = 0;
for (size_t i = 0; i < result.size(); i++) {
auto& c = result[i];
if (c == CmdEsc || c == StartEsc) {
escape = true;
escapeStartIdx = i;
}
if ((c <= SpecialCharLimit) && !(c == StartEsc))
escape = false;
if ((c == EndEsc) && escape)
result[escapeStartIdx] = StartEsc;
}
return {result};
}
String extractCodes(String const& s) {
bool escape = false;
StringList result;
String escapeCode;
for (auto c : preprocessEscapeCodes(s)) {
if (c == StartEsc)
escape = true;
if (c == EndEsc) {
escape = false;
for (auto command : escapeCode.split(','))
result.append(command);
escapeCode = "";
}
if (escape && (c != StartEsc))
escapeCode.append(c);
}
if (!result.size())
return "";
return "^" + result.join(",") + ";";
}
}
TextPositioning::TextPositioning() {
pos = Vec2F();
hAnchor = HorizontalAnchor::LeftAnchor;
vAnchor = VerticalAnchor::BottomAnchor;
}
TextPositioning::TextPositioning(Vec2F pos, HorizontalAnchor hAnchor, VerticalAnchor vAnchor,
Maybe<unsigned> wrapWidth, Maybe<unsigned> charLimit)
: pos(pos), hAnchor(hAnchor), vAnchor(vAnchor), wrapWidth(wrapWidth), charLimit(charLimit) {}
TextPositioning::TextPositioning(Json const& v) {
pos = v.opt("position").apply(jsonToVec2F).value();
hAnchor = HorizontalAnchorNames.getLeft(v.getString("horizontalAnchor", "left"));
vAnchor = VerticalAnchorNames.getLeft(v.getString("verticalAnchor", "top"));
wrapWidth = v.optUInt("wrapWidth");
charLimit = v.optUInt("charLimit");
}
Json TextPositioning::toJson() const {
return JsonObject{
{"position", jsonFromVec2F(pos)},
{"horizontalAnchor", HorizontalAnchorNames.getRight(hAnchor)},
{"verticalAnchor", VerticalAnchorNames.getRight(vAnchor)},
{"wrapWidth", jsonFromMaybe(wrapWidth)}
};
}
TextPositioning TextPositioning::translated(Vec2F translation) const {
return {pos + translation, hAnchor, vAnchor, wrapWidth, charLimit};
}
TextPainter::TextPainter(FontPtr font, RendererPtr renderer, TextureGroupPtr textureGroup)
: m_renderer(renderer),
m_fontTextureGroup(move(font), textureGroup),
m_fontSize(8),
m_lineSpacing(1.30f),
m_renderSettings({FontMode::Normal, Vec4B::filled(255)}),
m_splitIgnore(" \t"),
m_splitForce("\n\v"),
m_nonRenderedCharacters("\n\v\r") {}
RectF TextPainter::renderText(String const& s, TextPositioning const& position) {
if (position.charLimit) {
unsigned charLimit = *position.charLimit;
return doRenderText(s, position, true, &charLimit);
} else {
return doRenderText(s, position, true, nullptr);
}
}
RectF TextPainter::renderLine(String const& s, TextPositioning const& position) {
if (position.charLimit) {
unsigned charLimit = *position.charLimit;
return doRenderLine(s, position, true, &charLimit);
} else {
return doRenderLine(s, position, true, nullptr);
}
}
RectF TextPainter::renderGlyph(String::Char c, TextPositioning const& position) {
return doRenderGlyph(c, position, true);
}
RectF TextPainter::determineTextSize(String const& s, TextPositioning const& position) {
return doRenderText(s, position, false, nullptr);
}
RectF TextPainter::determineLineSize(String const& s, TextPositioning const& position) {
return doRenderLine(s, position, false, nullptr);
}
RectF TextPainter::determineGlyphSize(String::Char c, TextPositioning const& position) {
return doRenderGlyph(c, position, false);
}
int TextPainter::glyphWidth(String::Char c) {
return m_fontTextureGroup.glyphWidth(c, m_fontSize);
}
int TextPainter::stringWidth(String const& s) {
int width = 0;
bool escape = false;
for (String::Char c : Text::preprocessEscapeCodes(s)) {
if (c == Text::StartEsc)
escape = true;
if (!escape)
width += glyphWidth(c);
if (c == Text::EndEsc)
escape = false;
}
return width;
}
StringList TextPainter::wrapText(String const& s, Maybe<unsigned> wrapWidth) {
String text = Text::preprocessEscapeCodes(s);
unsigned lineStart = 0; // Where does this line start ?
unsigned lineCharSize = 0; // how many characters in this line ?
unsigned linePixelWidth = 0; // How wide is this line so far
bool inEscapeSequence = false;
unsigned splitPos = 0; // Where did we last see a place to split the string ?
unsigned splitWidth = 0; // How wide was the string there ?
StringList lines; // list of renderable string lines
// loop through every character in the string
for (auto character : text) {
// this up here to deal with the (common) occurance that the first charcter
// is an escape initiator
if (character == Text::StartEsc)
inEscapeSequence = true;
if (inEscapeSequence) {
lineCharSize++; // just jump straight to the next character, we don't care what it is.
if (character == Text::EndEsc)
inEscapeSequence = false;
} else {
lineCharSize++; // assume at least one character if we get here.
// is this a linefeed / cr / whatever that forces a line split ?
if (m_splitForce.find(String(character)) != NPos) {
// knock one off the end because we don't render the CR
lines.push_back(text.substr(lineStart, lineCharSize - 1));
lineStart += lineCharSize; // next line starts after the CR
lineCharSize = 0; // with no characters in it.
linePixelWidth = 0; // No width
splitPos = 0; // and no known splits.
} else {
int charWidth = glyphWidth(character);
// is it a place where we might want to split the line ?
if (m_splitIgnore.find(String(character)) != NPos) {
splitPos = lineStart + lineCharSize; // this is the character after the space.
splitWidth = linePixelWidth + charWidth; // the width of the string at
// the split point, i.e. after the space.
}
// would the line be too long if we render this next character ?
if (wrapWidth && (linePixelWidth + charWidth) > *wrapWidth) {
// did we find somewhere to split the line ?
if (splitPos) {
lines.push_back(text.substr(lineStart, (splitPos - lineStart) - 1));
unsigned stringEnd = lineStart + lineCharSize;
lineCharSize = stringEnd - splitPos; // next line has the characters after the space.
unsigned stringWidth = (linePixelWidth - splitWidth);
linePixelWidth = stringWidth + charWidth; // and is as wide as the bit after the space.
lineStart = splitPos; // next line starts after the space
splitPos = 0; // split is used up.
} else {
// don't draw the last character that puts us over the edge
lines.push_back(text.substr(lineStart, lineCharSize - 1));
lineStart += lineCharSize - 1; // skip back by one to include that
// character on the next line.
lineCharSize = 1; // next line has that character in
linePixelWidth = charWidth; // and is as wide as that character
}
} else {
linePixelWidth += charWidth;
}
}
}
}
// if we hit the end of the string before hitting the end of the line.
if (lineCharSize > 0)
lines.push_back(text.substr(lineStart, lineCharSize));
return lines;
}
unsigned TextPainter::fontSize() const {
return m_fontSize;
}
void TextPainter::setFontSize(unsigned size) {
m_fontSize = size;
}
void TextPainter::setLineSpacing(float lineSpacing) {
m_lineSpacing = lineSpacing;
}
void TextPainter::setMode(FontMode mode) {
m_renderSettings.mode = mode;
}
void TextPainter::setSplitIgnore(String const& splitIgnore) {
m_splitIgnore = splitIgnore;
}
void TextPainter::setFontColor(Vec4B color) {
m_renderSettings.color = move(color);
}
void TextPainter::setProcessingDirectives(String directives) {
m_processingDirectives = move(directives);
}
void TextPainter::cleanup(int64_t timeout) {
m_fontTextureGroup.cleanup(timeout);
}
RectF TextPainter::doRenderText(String const& s, TextPositioning const& position, bool reallyRender, unsigned* charLimit) {
Vec2F pos = position.pos;
StringList lines = wrapText(s, position.wrapWidth);
int height = (lines.size() - 1) * m_lineSpacing * m_fontSize + m_fontSize;
auto savedRenderSettings = m_renderSettings;
m_savedRenderSettings = m_renderSettings;
if (position.vAnchor == VerticalAnchor::BottomAnchor)
pos[1] += (height - m_fontSize);
else if (position.vAnchor == VerticalAnchor::VMidAnchor)
pos[1] += (height - m_fontSize) / 2;
RectF bounds = RectF::withSize(pos, Vec2F());
for (auto i : lines) {
bounds.combine(doRenderLine(i, { pos, position.hAnchor, position.vAnchor }, reallyRender, charLimit));
pos[1] -= m_fontSize * m_lineSpacing;
if (charLimit && *charLimit == 0)
break;
}
m_renderSettings = savedRenderSettings;
return bounds;
}
RectF TextPainter::doRenderLine(String const& s, TextPositioning const& position, bool reallyRender, unsigned* charLimit) {
String text = s;
TextPositioning pos = position;
if (pos.hAnchor == HorizontalAnchor::RightAnchor) {
auto trimmedString = s;
if (charLimit)
trimmedString = s.slice(0, *charLimit);
pos.pos[0] -= stringWidth(trimmedString);
pos.hAnchor = HorizontalAnchor::LeftAnchor;
} else if (pos.hAnchor == HorizontalAnchor::HMidAnchor) {
auto trimmedString = s;
if (charLimit)
trimmedString = s.slice(0, *charLimit);
unsigned width = stringWidth(trimmedString);
pos.pos[0] -= std::floor(width / 2);
pos.hAnchor = HorizontalAnchor::LeftAnchor;
}
bool escape = false;
String escapeCode;
RectF bounds = RectF::withSize(pos.pos, Vec2F());
for (String::Char c : text) {
if (c == Text::StartEsc)
escape = true;
if (!escape) {
if (charLimit) {
if (*charLimit == 0)
break;
else
--*charLimit;
}
RectF glyphBounds = doRenderGlyph(c, pos, reallyRender);
bounds.combine(glyphBounds);
pos.pos[0] += glyphBounds.width();
} else if (c == Text::EndEsc) {
auto commands = escapeCode.split(',');
for (auto command : commands) {
try {
if (command == "reset") {
m_renderSettings = m_savedRenderSettings;
} else if (command == "set") {
m_savedRenderSettings = m_renderSettings;
} else if (command == "shadow") {
m_renderSettings.mode = (FontMode)((int)m_renderSettings.mode | (int)FontMode::Shadow);
} else if (command == "noshadow") {
m_renderSettings.mode = (FontMode)((int)m_renderSettings.mode & (-1 ^ (int)FontMode::Shadow));
} else {
// expects both #... sequences and plain old color names.
Color c = jsonToColor(command);
c.setAlphaF(c.alphaF() * ((float)m_savedRenderSettings.color[3]) / 255);
m_renderSettings.color = c.toRgba();
}
} catch (JsonException&) {
} catch (ColorException&) {
}
}
escape = false;
escapeCode = "";
}
if (escape && (c != Text::StartEsc))
escapeCode.append(c);
}
return bounds;
}
RectF TextPainter::doRenderGlyph(String::Char c, TextPositioning const& position, bool reallyRender) {
if (m_nonRenderedCharacters.find(String(c)) != NPos)
return RectF();
int width = glyphWidth(c);
// Offset left by width if right anchored.
float hOffset = 0;
if (position.hAnchor == HorizontalAnchor::RightAnchor)
hOffset = -width;
else if (position.hAnchor == HorizontalAnchor::HMidAnchor)
hOffset = -std::floor(width / 2);
float vOffset = 0;
if (position.vAnchor == VerticalAnchor::VMidAnchor)
vOffset = -std::floor((float)m_fontSize / 2);
else if (position.vAnchor == VerticalAnchor::TopAnchor)
vOffset = -(float)m_fontSize;
if (reallyRender) {
if ((int)m_renderSettings.mode & (int)FontMode::Shadow) {
Color shadow = Color::Black;
shadow.setAlpha(m_renderSettings.color[3]);
renderGlyph(c, position.pos + Vec2F(hOffset, vOffset - 2), m_fontSize, 1, shadow.toRgba(), m_processingDirectives);
renderGlyph(c, position.pos + Vec2F(hOffset, vOffset - 1), m_fontSize, 1, shadow.toRgba(), m_processingDirectives);
}
renderGlyph(c, position.pos + Vec2F(hOffset, vOffset), m_fontSize, 1, m_renderSettings.color, m_processingDirectives);
}
return RectF::withSize(position.pos + Vec2F(hOffset, vOffset), {(float)width, (int)m_fontSize});
}
void TextPainter::renderGlyph(String::Char c, Vec2F const& screenPos, unsigned fontSize,
float scale, Vec4B const& color, String const& processingDirectives) {
if (!fontSize)
return;
auto texture = m_fontTextureGroup.glyphTexture(c, fontSize, processingDirectives);
m_renderer->render(renderTexturedRect(move(texture), Vec2F(screenPos), scale, color, 0.0f));
}
}

View file

@ -0,0 +1,109 @@
#ifndef STAR_TEXT_PAINTER_HPP
#define STAR_TEXT_PAINTER_HPP
#include "StarFontTextureGroup.hpp"
#include "StarAnchorTypes.hpp"
namespace Star {
STAR_CLASS(TextPainter);
namespace Text {
unsigned char const StartEsc = '\x1b';
unsigned char const EndEsc = ';';
unsigned char const CmdEsc = '^';
unsigned char const SpecialCharLimit = ' ';
String stripEscapeCodes(String const& s);
String preprocessEscapeCodes(String const& s);
String extractCodes(String const& s);
}
enum class FontMode {
Normal,
Shadow
};
float const DefaultLineSpacing = 1.3f;
struct TextPositioning {
TextPositioning();
TextPositioning(Vec2F pos,
HorizontalAnchor hAnchor = HorizontalAnchor::LeftAnchor,
VerticalAnchor vAnchor = VerticalAnchor::BottomAnchor,
Maybe<unsigned> wrapWidth = {},
Maybe<unsigned> charLimit = {});
TextPositioning(Json const& v);
Json toJson() const;
TextPositioning translated(Vec2F translation) const;
Vec2F pos;
HorizontalAnchor hAnchor;
VerticalAnchor vAnchor;
Maybe<unsigned> wrapWidth;
Maybe<unsigned> charLimit;
};
// Renders text while caching individual glyphs for fast rendering but with *no
// kerning*.
class TextPainter {
public:
TextPainter(FontPtr font, RendererPtr renderer, TextureGroupPtr textureGroup);
RectF renderText(String const& s, TextPositioning const& position);
RectF renderLine(String const& s, TextPositioning const& position);
RectF renderGlyph(String::Char c, TextPositioning const& position);
RectF determineTextSize(String const& s, TextPositioning const& position);
RectF determineLineSize(String const& s, TextPositioning const& position);
RectF determineGlyphSize(String::Char c, TextPositioning const& position);
int glyphWidth(String::Char c);
int stringWidth(String const& s);
StringList wrapText(String const& s, Maybe<unsigned> wrapWidth);
unsigned fontSize() const;
void setFontSize(unsigned size);
void setLineSpacing(float lineSpacing);
void setMode(FontMode mode);
void setSplitIgnore(String const& splitIgnore);
void setFontColor(Vec4B color);
void setProcessingDirectives(String directives);
void cleanup(int64_t textureTimeout);
private:
struct RenderSettings {
FontMode mode;
Vec4B color;
};
RectF doRenderText(String const& s, TextPositioning const& position, bool reallyRender, unsigned* charLimit);
RectF doRenderLine(String const& s, TextPositioning const& position, bool reallyRender, unsigned* charLimit);
RectF doRenderGlyph(String::Char c, TextPositioning const& position, bool reallyRender);
void renderGlyph(String::Char c, Vec2F const& screenPos, unsigned fontSize, float scale, Vec4B const& color, String const& processingDirectives);
RendererPtr m_renderer;
FontTextureGroup m_fontTextureGroup;
unsigned m_fontSize;
float m_lineSpacing;
RenderSettings m_renderSettings;
RenderSettings m_savedRenderSettings;
String m_splitIgnore;
String m_splitForce;
String m_nonRenderedCharacters;
String m_processingDirectives;
};
}
#endif

View file

@ -0,0 +1,539 @@
#include "StarTilePainter.hpp"
#include "StarLexicalCast.hpp"
#include "StarJsonExtra.hpp"
#include "StarXXHash.hpp"
#include "StarMaterialDatabase.hpp"
#include "StarLiquidsDatabase.hpp"
#include "StarAssets.hpp"
#include "StarRoot.hpp"
namespace Star {
TilePainter::TilePainter(RendererPtr renderer) {
m_renderer = move(renderer);
m_textureGroup = m_renderer->createTextureGroup(TextureGroupSize::Large);
auto& root = Root::singleton();
auto assets = root.assets();
m_terrainChunkCache.setTimeToLive(assets->json("/rendering.config:chunkCacheTimeout").toInt());
m_liquidChunkCache.setTimeToLive(assets->json("/rendering.config:chunkCacheTimeout").toInt());
m_textureCache.setTimeToLive(assets->json("/rendering.config:textureTimeout").toInt());
m_backgroundLayerColor = jsonToColor(assets->json("/rendering.config:backgroundLayerColor")).toRgba();
m_foregroundLayerColor = jsonToColor(assets->json("/rendering.config:foregroundLayerColor")).toRgba();
m_liquidDrawLevels = jsonToVec2F(assets->json("/rendering.config:liquidDrawLevels"));
for (auto const& liquid : root.liquidsDatabase()->allLiquidSettings()) {
m_liquids.set(liquid->id, LiquidInfo{
m_renderer->createTexture(*assets->image(liquid->config.getString("texture")), TextureAddressing::Wrap),
jsonToColor(liquid->config.get("color")).toRgba(),
jsonToColor(liquid->config.get("bottomLightMix")).toRgbF(),
liquid->config.getFloat("textureMovementFactor")
});
}
}
void TilePainter::adjustLighting(WorldRenderData& renderData) const {
RectI lightRange = RectI::withSize(renderData.lightMinPosition, Vec2I(renderData.lightMap.size()));
forEachRenderTile(renderData, lightRange, [&](Vec2I const& pos, RenderTile const& tile) {
// Only adjust lighting for full tiles
if (liquidDrawLevel(byteToFloat(tile.liquidLevel)) < 1.0f)
return;
auto lightIndex = Vec2U(pos - renderData.lightMinPosition);
auto lightValue = renderData.lightMap.get(lightIndex).vec3();
auto const& liquid = m_liquids[tile.liquidId];
Vec3F tileLight = Vec3F(lightValue);
float darknessLevel = (1 - tileLight.sum() / (3.0f * 255.0f));
lightValue = Vec3B(tileLight.piecewiseMultiply(Vec3F::filled(1 - darknessLevel) + liquid.bottomLightMix * darknessLevel));
renderData.lightMap.set(lightIndex, lightValue);
});
}
void TilePainter::setup(WorldCamera const& camera, WorldRenderData& renderData) {
m_pendingTerrainChunks.clear();
m_pendingLiquidChunks.clear();
auto cameraCenter = camera.centerWorldPosition();
if (m_lastCameraCenter)
m_cameraPan = renderData.geometry.diff(cameraCenter, *m_lastCameraCenter);
m_lastCameraCenter = cameraCenter;
RectI chunkRange = RectI::integral(RectF(camera.worldTileRect()).scaled(1.0f / RenderChunkSize));
for (int x = chunkRange.xMin(); x < chunkRange.xMax(); ++x) {
for (int y = chunkRange.yMin(); y < chunkRange.yMax(); ++y) {
m_pendingTerrainChunks.append(getTerrainChunk(renderData, {x, y}));
m_pendingLiquidChunks.append(getLiquidChunk(renderData, {x, y}));
}
}
}
void TilePainter::renderBackground(WorldCamera const& camera) {
renderTerrainChunks(camera, TerrainLayer::Background);
}
void TilePainter::renderMidground(WorldCamera const& camera) {
renderTerrainChunks(camera, TerrainLayer::Midground);
}
void TilePainter::renderLiquid(WorldCamera const& camera) {
Mat3F transformation = Mat3F::identity();
transformation.translate(-Vec2F(camera.worldTileRect().min()));
transformation.scale(TilePixels * camera.pixelRatio());
transformation.translate(camera.tileMinScreen());
for (auto const& chunk : m_pendingLiquidChunks) {
for (auto const& p : *chunk)
m_renderer->renderBuffer(p.second, transformation);
}
m_renderer->flush();
}
void TilePainter::renderForeground(WorldCamera const& camera) {
renderTerrainChunks(camera, TerrainLayer::Foreground);
}
void TilePainter::cleanup() {
m_pendingTerrainChunks.clear();
m_pendingLiquidChunks.clear();
m_textureCache.cleanup();
m_terrainChunkCache.cleanup();
m_liquidChunkCache.cleanup();
}
size_t TilePainter::TextureKeyHash::operator()(TextureKey const& key) const {
if (key.is<MaterialPieceTextureKey>())
return hashOf(key.typeIndex(), key.get<MaterialPieceTextureKey>());
else
return hashOf(key.typeIndex(), key.get<AssetTextureKey>());
}
TilePainter::ChunkHash TilePainter::terrainChunkHash(WorldRenderData& renderData, Vec2I chunkIndex) {
XXHash64 hasher;
RectI tileRange = RectI::withSize(chunkIndex * RenderChunkSize, Vec2I::filled(RenderChunkSize)).padded(MaterialRenderProfileMaxNeighborDistance);
forEachRenderTile(renderData, tileRange, [&](Vec2I const&, RenderTile const& renderTile) {
renderTile.hashPushTerrain(hasher);
});
return hasher.digest();
}
TilePainter::ChunkHash TilePainter::liquidChunkHash(WorldRenderData& renderData, Vec2I chunkIndex) {
XXHash64 hasher;
RectI tileRange = RectI::withSize(chunkIndex * RenderChunkSize, Vec2I::filled(RenderChunkSize)).padded(MaterialRenderProfileMaxNeighborDistance);
forEachRenderTile(renderData, tileRange, [&](Vec2I const&, RenderTile const& renderTile) {
renderTile.hashPushLiquid(hasher);
});
return hasher.digest();
}
TilePainter::QuadZLevel TilePainter::materialZLevel(uint32_t zLevel, MaterialId material, MaterialHue hue, MaterialColorVariant colorVariant) {
QuadZLevel quadZLevel = 0;
quadZLevel |= (uint64_t)colorVariant;
quadZLevel |= (uint64_t)hue << 8;
quadZLevel |= (uint64_t)material << 16;
quadZLevel |= (uint64_t)zLevel << 32;
return quadZLevel;
}
TilePainter::QuadZLevel TilePainter::modZLevel(uint32_t zLevel, ModId mod, MaterialHue hue, MaterialColorVariant colorVariant) {
QuadZLevel quadZLevel = 0;
quadZLevel |= (uint64_t)colorVariant;
quadZLevel |= (uint64_t)hue << 8;
quadZLevel |= (uint64_t)mod << 16;
quadZLevel |= (uint64_t)zLevel << 32;
quadZLevel |= (uint64_t)1 << 63;
return quadZLevel;
}
TilePainter::QuadZLevel TilePainter::damageZLevel() {
return (uint64_t)(-1);
}
RenderTile const& TilePainter::getRenderTile(WorldRenderData const& renderData, Vec2I const& worldPos) {
Vec2I arrayPos = renderData.geometry.diff(worldPos, renderData.tileMinPosition);
Vec2I size = Vec2I(renderData.tiles.size());
if (arrayPos[0] >= 0 && arrayPos[1] >= 0 && arrayPos[0] < size[0] && arrayPos[1] < size[1])
return renderData.tiles(Vec2S(arrayPos));
static RenderTile defaultRenderTile = {
NullMaterialId,
NoModId,
NullMaterialId,
NoModId,
0,
0,
DefaultMaterialColorVariant,
TileDamageType::Protected,
0,
0,
0,
DefaultMaterialColorVariant,
TileDamageType::Protected,
0,
EmptyLiquidId,
0
};
return defaultRenderTile;
}
void TilePainter::renderTerrainChunks(WorldCamera const& camera, TerrainLayer terrainLayer) {
Map<QuadZLevel, List<RenderBufferPtr>> zOrderBuffers;
for (auto const& chunk : m_pendingTerrainChunks) {
for (auto const& pair : chunk->value(terrainLayer))
zOrderBuffers[pair.first].append(pair.second);
}
Mat3F transformation = Mat3F::identity();
transformation.translate(-Vec2F(camera.worldTileRect().min()));
transformation.scale(TilePixels * camera.pixelRatio());
transformation.translate(camera.tileMinScreen());
for (auto const& pair : zOrderBuffers) {
for (auto const& buffer : pair.second)
m_renderer->renderBuffer(buffer, transformation);
}
m_renderer->flush();
}
shared_ptr<TilePainter::TerrainChunk const> TilePainter::getTerrainChunk(WorldRenderData& renderData, Vec2I chunkIndex) {
pair<Vec2I, ChunkHash> chunkKey = {chunkIndex, terrainChunkHash(renderData, chunkIndex)};
return m_terrainChunkCache.get(chunkKey, [&](auto const&) {
HashMap<TerrainLayer, HashMap<QuadZLevel, List<RenderPrimitive>>> terrainPrimitives;
RectI tileRange = RectI::withSize(chunkIndex * RenderChunkSize, Vec2I::filled(RenderChunkSize));
for (int x = tileRange.xMin(); x < tileRange.xMax(); ++x) {
for (int y = tileRange.yMin(); y < tileRange.yMax(); ++y) {
bool occluded = this->produceTerrainPrimitives(terrainPrimitives[TerrainLayer::Foreground], TerrainLayer::Foreground, {x, y}, renderData);
occluded = this->produceTerrainPrimitives(terrainPrimitives[TerrainLayer::Midground], TerrainLayer::Midground, {x, y}, renderData) || occluded;
if (!occluded)
this->produceTerrainPrimitives(terrainPrimitives[TerrainLayer::Background], TerrainLayer::Background, {x, y}, renderData);
}
}
auto chunk = make_shared<TerrainChunk>();
for (auto& layerPair : terrainPrimitives) {
for (auto& zLevelPair : layerPair.second) {
auto rb = m_renderer->createRenderBuffer();
rb->set(move(zLevelPair.second));
(*chunk)[layerPair.first][zLevelPair.first] = move(rb);
}
}
return chunk;
});
}
shared_ptr<TilePainter::LiquidChunk const> TilePainter::getLiquidChunk(WorldRenderData& renderData, Vec2I chunkIndex) {
pair<Vec2I, ChunkHash> chunkKey = {chunkIndex, liquidChunkHash(renderData, chunkIndex)};
return m_liquidChunkCache.get(chunkKey, [&](auto const&) {
HashMap<LiquidId, List<RenderPrimitive>> liquidPrimitives;
RectI tileRange = RectI::withSize(chunkIndex * RenderChunkSize, Vec2I::filled(RenderChunkSize));
for (int x = tileRange.xMin(); x < tileRange.xMax(); ++x) {
for (int y = tileRange.yMin(); y < tileRange.yMax(); ++y)
this->produceLiquidPrimitives(liquidPrimitives, {x, y}, renderData);
}
auto chunk = make_shared<LiquidChunk>();
for (auto& p : liquidPrimitives) {
auto rb = m_renderer->createRenderBuffer();
rb->set(move(p.second));
chunk->set(p.first, move(rb));
}
return chunk;
});
}
bool TilePainter::produceTerrainPrimitives(HashMap<QuadZLevel, List<RenderPrimitive>>& primitives,
TerrainLayer terrainLayer, Vec2I const& pos, WorldRenderData const& renderData) {
auto& root = Root::singleton();
auto assets = Root::singleton().assets();
auto materialDatabase = root.materialDatabase();
RenderTile const& tile = getRenderTile(renderData, pos);
MaterialId material = EmptyMaterialId;
MaterialHue materialHue = 0;
MaterialHue materialColorVariant = 0;
ModId mod = NoModId;
MaterialHue modHue = 0;
float damageLevel = 0.0f;
TileDamageType damageType = TileDamageType::Protected;
Vec4B color;
bool occlude = false;
if (terrainLayer == TerrainLayer::Background) {
material = tile.background;
materialHue = tile.backgroundHueShift;
materialColorVariant = tile.backgroundColorVariant;
mod = tile.backgroundMod;
modHue = tile.backgroundModHueShift;
damageLevel = byteToFloat(tile.backgroundDamageLevel);
damageType = tile.backgroundDamageType;
color = m_backgroundLayerColor;
} else {
material = tile.foreground;
materialHue = tile.foregroundHueShift;
materialColorVariant = tile.foregroundColorVariant;
mod = tile.foregroundMod;
modHue = tile.foregroundModHueShift;
damageLevel = byteToFloat(tile.foregroundDamageLevel);
damageType = tile.foregroundDamageType;
color = m_foregroundLayerColor;
}
// render non-block colliding things in the midground
bool isBlock = BlockCollisionSet.contains(materialDatabase->materialCollisionKind(material));
if ((isBlock && terrainLayer == TerrainLayer::Midground) || (!isBlock && terrainLayer == TerrainLayer::Foreground))
return false;
auto getPieceTexture = [this, assets](MaterialId material, MaterialRenderPieceConstPtr const& piece, MaterialHue hue, bool mod) {
return m_textureCache.get(MaterialPieceTextureKey(material, piece->pieceId, hue, mod), [&](auto const&) {
String texture;
if (hue == 0)
texture = piece->texture;
else
texture = strf("%s?hueshift=%s", piece->texture, materialHueToDegrees(hue));
return m_textureGroup->create(*assets->image(texture));
});
};
auto materialRenderProfile = materialDatabase->materialRenderProfile(material);
auto modRenderProfile = materialDatabase->modRenderProfile(mod);
if (materialRenderProfile) {
occlude = materialRenderProfile->occludesBehind;
uint32_t variance = staticRandomU32(renderData.geometry.xwrap(pos[0]), pos[1], (int)terrainLayer, "main");
auto& quadList = primitives[materialZLevel(materialRenderProfile->zLevel, material, materialHue, materialColorVariant)];
MaterialPieceResultList pieces;
determineMatchingPieces(pieces, &occlude, materialDatabase, materialRenderProfile->mainMatchList, renderData, pos,
terrainLayer == TerrainLayer::Background ? TileLayer::Background : TileLayer::Foreground, false);
for (auto const& piecePair : pieces) {
TexturePtr texture = getPieceTexture(material, piecePair.first, materialHue, false);
RectF textureCoords = piecePair.first->variants.get(materialColorVariant).wrap(variance);
RectF worldCoords = RectF::withSize(piecePair.second / TilePixels + Vec2F(pos), textureCoords.size() / TilePixels);
quadList.append(RenderQuad{
move(texture),
RenderVertex{Vec2F(worldCoords.xMin(), worldCoords.yMin()), Vec2F(textureCoords.xMin(), textureCoords.yMin()), color, 1.0f},
RenderVertex{Vec2F(worldCoords.xMax(), worldCoords.yMin()), Vec2F(textureCoords.xMax(), textureCoords.yMin()), color, 1.0f},
RenderVertex{Vec2F(worldCoords.xMax(), worldCoords.yMax()), Vec2F(textureCoords.xMax(), textureCoords.yMax()), color, 1.0f},
RenderVertex{Vec2F(worldCoords.xMin(), worldCoords.yMax()), Vec2F(textureCoords.xMin(), textureCoords.yMax()), color, 1.0f}
});
}
}
if (modRenderProfile) {
auto modColorVariant = modRenderProfile->multiColor ? materialColorVariant : 0;
uint32_t variance = staticRandomU32(renderData.geometry.xwrap(pos[0]), pos[1], (int)terrainLayer, "mod");
auto& quadList = primitives[modZLevel(modRenderProfile->zLevel, mod, modHue, modColorVariant)];
MaterialPieceResultList pieces;
determineMatchingPieces(pieces, &occlude, materialDatabase, modRenderProfile->mainMatchList, renderData, pos,
terrainLayer == TerrainLayer::Background ? TileLayer::Background : TileLayer::Foreground, true);
for (auto const& piecePair : pieces) {
auto texture = getPieceTexture(mod, piecePair.first, modHue, true);
auto textureCoords = piecePair.first->variants.get(modColorVariant).wrap(variance);
RectF worldCoords = RectF::withSize(piecePair.second / TilePixels + Vec2F(pos), textureCoords.size() / TilePixels);
quadList.append(RenderQuad{
move(texture),
RenderVertex{Vec2F(worldCoords.xMin(), worldCoords.yMin()), Vec2F(textureCoords.xMin(), textureCoords.yMin()), color, 1.0f},
RenderVertex{Vec2F(worldCoords.xMax(), worldCoords.yMin()), Vec2F(textureCoords.xMax(), textureCoords.yMin()), color, 1.0f},
RenderVertex{Vec2F(worldCoords.xMax(), worldCoords.yMax()), Vec2F(textureCoords.xMax(), textureCoords.yMax()), color, 1.0f},
RenderVertex{Vec2F(worldCoords.xMin(), worldCoords.yMax()), Vec2F(textureCoords.xMin(), textureCoords.yMax()), color, 1.0f}
});
}
}
if (materialRenderProfile && damageLevel > 0 && isBlock) {
auto& quadList = primitives[damageZLevel()];
auto const& crackingImage = materialRenderProfile->damageImage(damageLevel, damageType);
TexturePtr texture = m_textureCache.get(AssetTextureKey(crackingImage.first),
[&](auto const&) { return m_textureGroup->create(*assets->image(crackingImage.first)); });
Vec2F textureSize(texture->size());
RectF textureCoords = RectF::withSize(Vec2F(), textureSize);
RectF worldCoords = RectF::withSize(crackingImage.second / TilePixels + Vec2F(pos), textureCoords.size() / TilePixels);
quadList.append(RenderQuad{
move(texture),
RenderVertex{Vec2F(worldCoords.xMin(), worldCoords.yMin()), Vec2F(textureCoords.xMin(), textureCoords.yMin()), color, 1.0f},
RenderVertex{Vec2F(worldCoords.xMax(), worldCoords.yMin()), Vec2F(textureCoords.xMax(), textureCoords.yMin()), color, 1.0f},
RenderVertex{Vec2F(worldCoords.xMax(), worldCoords.yMax()), Vec2F(textureCoords.xMax(), textureCoords.yMax()), color, 1.0f},
RenderVertex{Vec2F(worldCoords.xMin(), worldCoords.yMax()), Vec2F(textureCoords.xMin(), textureCoords.yMax()), color, 1.0f}
});
}
return occlude;
}
void TilePainter::produceLiquidPrimitives(HashMap<LiquidId, List<RenderPrimitive>>& primitives, Vec2I const& pos, WorldRenderData const& renderData) {
RenderTile const& tile = getRenderTile(renderData, pos);
float drawLevel = liquidDrawLevel(byteToFloat(tile.liquidLevel));
if (drawLevel <= 0.0f)
return;
RenderTile const& tileBottom = getRenderTile(renderData, pos - Vec2I(0, 1));
float bottomDrawLevel = liquidDrawLevel(byteToFloat(tileBottom.liquidLevel));
RectF worldRect;
if (tileBottom.foreground == EmptyMaterialId && bottomDrawLevel < 1.0f)
worldRect = RectF::withSize(Vec2F(pos), Vec2F::filled(1.0f)).expanded(drawLevel);
else
worldRect = RectF::withSize(Vec2F(pos), Vec2F(1.0f, drawLevel));
auto texRect = worldRect.scaled(TilePixels);
auto const& liquid = m_liquids[tile.liquidId];
primitives[tile.liquidId].append(RenderQuad{
liquid.texture,
RenderVertex{Vec2F(worldRect.xMin(), worldRect.yMin()), Vec2F(texRect.xMin(), texRect.yMin()), liquid.color, 1.0f},
RenderVertex{Vec2F(worldRect.xMax(), worldRect.yMin()), Vec2F(texRect.xMax(), texRect.yMin()), liquid.color, 1.0f},
RenderVertex{Vec2F(worldRect.xMax(), worldRect.yMax()), Vec2F(texRect.xMax(), texRect.yMax()), liquid.color, 1.0f},
RenderVertex{Vec2F(worldRect.xMin(), worldRect.yMax()), Vec2F(texRect.xMin(), texRect.yMax()), liquid.color, 1.0f}
});
}
bool TilePainter::determineMatchingPieces(MaterialPieceResultList& resultList, bool* occlude, MaterialDatabaseConstPtr const& materialDb, MaterialRenderMatchList const& matchList,
WorldRenderData const& renderData, Vec2I const& basePos, TileLayer layer, bool isMod) {
RenderTile const& tile = getRenderTile(renderData, basePos);
auto matchSetMatches = [&](MaterialRenderMatchConstPtr const& match) -> bool {
if (match->requiredLayer && *match->requiredLayer != layer)
return false;
if (match->matchPoints.empty())
return true;
bool matchValid = match->matchJoin == MaterialJoinType::All;
for (auto const& matchPoint : match->matchPoints) {
auto const& neighborTile = getRenderTile(renderData, basePos + matchPoint.position);
bool neighborShadowing = false;
if (layer == TileLayer::Background) {
if (auto profile = materialDb->materialRenderProfile(neighborTile.foreground))
neighborShadowing = !profile->foregroundLightTransparent;
}
MaterialHue baseHue = layer == TileLayer::Foreground ? tile.foregroundHueShift : tile.backgroundHueShift;
MaterialHue neighborHue = layer == TileLayer::Foreground ? neighborTile.foregroundHueShift : neighborTile.backgroundHueShift;
MaterialHue baseModHue = layer == TileLayer::Foreground ? tile.foregroundModHueShift : tile.backgroundModHueShift;
MaterialHue neighborModHue = layer == TileLayer::Foreground ? neighborTile.foregroundModHueShift : neighborTile.backgroundModHueShift;
MaterialId baseMaterial = layer == TileLayer::Foreground ? tile.foreground : tile.background;
MaterialId neighborMaterial = layer == TileLayer::Foreground ? neighborTile.foreground : neighborTile.background;
ModId baseMod = layer == TileLayer::Foreground ? tile.foregroundMod : tile.backgroundMod;
ModId neighborMod = layer == TileLayer::Foreground ? neighborTile.foregroundMod : neighborTile.backgroundMod;
bool rulesValid = matchPoint.rule->join == MaterialJoinType::All;
for (auto const& ruleEntry : matchPoint.rule->entries) {
bool valid = true;
if (isMod) {
if (ruleEntry.rule.is<MaterialRule::RuleEmpty>()) {
valid = neighborMod == NoModId;
} else if (ruleEntry.rule.is<MaterialRule::RuleConnects>()) {
valid = isConnectableMaterial(neighborMaterial);
} else if (ruleEntry.rule.is<MaterialRule::RuleShadows>()) {
valid = neighborShadowing;
} else if (auto equalsSelf = ruleEntry.rule.ptr<MaterialRule::RuleEqualsSelf>()) {
valid = neighborMod == baseMod;
if (equalsSelf->matchHue)
valid = valid && baseModHue == neighborModHue;
} else if (auto equalsId = ruleEntry.rule.ptr<MaterialRule::RuleEqualsId>()) {
valid = neighborMod == equalsId->id;
} else if (auto propertyEquals = ruleEntry.rule.ptr<MaterialRule::RulePropertyEquals>()) {
if (auto profile = materialDb->modRenderProfile(neighborMod))
valid = profile->ruleProperties.get(propertyEquals->propertyName, Json()) == propertyEquals->compare;
else
valid = false;
}
} else {
if (ruleEntry.rule.is<MaterialRule::RuleEmpty>()) {
valid = neighborMaterial == EmptyMaterialId;
} else if (ruleEntry.rule.is<MaterialRule::RuleConnects>()) {
valid = isConnectableMaterial(neighborMaterial);
} else if (ruleEntry.rule.is<MaterialRule::RuleShadows>()) {
valid = neighborShadowing;
} else if (auto equalsSelf = ruleEntry.rule.ptr<MaterialRule::RuleEqualsSelf>()) {
valid = neighborMaterial == baseMaterial;
if (equalsSelf->matchHue)
valid = valid && baseHue == neighborHue;
} else if (auto equalsId = ruleEntry.rule.ptr<MaterialRule::RuleEqualsId>()) {
valid = neighborMaterial == equalsId->id;
} else if (auto propertyEquals = ruleEntry.rule.ptr<MaterialRule::RulePropertyEquals>()) {
if (auto profile = materialDb->materialRenderProfile(neighborMaterial))
valid = profile->ruleProperties.get(propertyEquals->propertyName) == propertyEquals->compare;
else
valid = false;
}
}
if (ruleEntry.inverse)
valid = !valid;
if (matchPoint.rule->join == MaterialJoinType::All) {
rulesValid = valid && rulesValid;
if (!rulesValid)
break;
} else {
rulesValid = valid || rulesValid;
}
}
if (match->matchJoin == MaterialJoinType::All) {
matchValid = matchValid && rulesValid;
if (!matchValid)
return matchValid;
} else {
matchValid = matchValid || rulesValid;
}
}
return matchValid;
};
bool subMatchResult = false;
for (auto const& match : matchList) {
if (matchSetMatches(match)) {
if (match->occlude)
*occlude = match->occlude.get();
subMatchResult = true;
for (auto const& piecePair : match->resultingPieces)
resultList.append({piecePair.first, piecePair.second});
if (determineMatchingPieces(resultList, occlude, materialDb, match->subMatches, renderData, basePos, layer, isMod) && match->haltOnSubMatch)
break;
if (match->haltOnMatch)
break;
}
}
return subMatchResult;
}
float TilePainter::liquidDrawLevel(float liquidLevel) const {
return clamp((liquidLevel - m_liquidDrawLevels[0]) / (m_liquidDrawLevels[1] - m_liquidDrawLevels[0]), 0.0f, 1.0f);
}
}

View file

@ -0,0 +1,140 @@
#ifndef STAR_NEW_TILE_PAINTER_HPP
#define STAR_NEW_TILE_PAINTER_HPP
#include "StarTtlCache.hpp"
#include "StarWorldRenderData.hpp"
#include "StarMaterialRenderProfile.hpp"
#include "StarRenderer.hpp"
#include "StarWorldCamera.hpp"
namespace Star {
STAR_CLASS(Assets);
STAR_CLASS(MaterialDatabase);
STAR_CLASS(TilePainter);
class TilePainter {
public:
// The rendered tiles are split and cached in chunks of RenderChunkSize x
// RenderChunkSize. This means that, around the border, there may be as many
// as RenderChunkSize - 1 tiles rendered outside of the viewing area from
// chunk alignment. In addition to this, there is also a region around each
// tile that is used for neighbor based rendering rules which has a max of
// MaterialRenderProfileMaxNeighborDistance. If the given tile data does not
// extend RenderChunkSize + MaterialRenderProfileMaxNeighborDistance - 1
// around the viewing area, then border chunks can continuously change hash,
// and will be recomputed too often.
static unsigned const RenderChunkSize = 16;
static unsigned const BorderTileSize = RenderChunkSize + MaterialRenderProfileMaxNeighborDistance - 1;
TilePainter(RendererPtr renderer);
// Adjusts lighting levels for liquids.
void adjustLighting(WorldRenderData& renderData) const;
// Sets up chunk data for every chunk that intersects the rendering region
// and prepares it for rendering. Do not call cleanup in between calling
// setup and each render method.
void setup(WorldCamera const& camera, WorldRenderData& renderData);
void renderBackground(WorldCamera const& camera);
void renderMidground(WorldCamera const& camera);
void renderLiquid(WorldCamera const& camera);
void renderForeground(WorldCamera const& camera);
// Clears any render data, as well as cleaning up old cached textures and
// chunks.
void cleanup();
private:
typedef uint64_t QuadZLevel;
typedef uint64_t ChunkHash;
enum class TerrainLayer { Background, Midground, Foreground };
struct LiquidInfo {
TexturePtr texture;
Vec4B color;
Vec3F bottomLightMix;
float textureMovementFactor;
};
typedef HashMap<TerrainLayer, HashMap<QuadZLevel, RenderBufferPtr>> TerrainChunk;
typedef HashMap<LiquidId, RenderBufferPtr> LiquidChunk;
typedef size_t MaterialRenderPieceIndex;
typedef tuple<MaterialId, MaterialRenderPieceIndex, MaterialHue, bool> MaterialPieceTextureKey;
typedef String AssetTextureKey;
typedef Variant<MaterialPieceTextureKey, AssetTextureKey> TextureKey;
typedef List<pair<MaterialRenderPieceConstPtr, Vec2F>> MaterialPieceResultList;
struct TextureKeyHash {
size_t operator()(TextureKey const& key) const;
};
// chunkIndex here is the index of the render chunk such that chunkIndex *
// RenderChunkSize results in the coordinate of the lower left most tile in
// the render chunk.
static ChunkHash terrainChunkHash(WorldRenderData& renderData, Vec2I chunkIndex);
static ChunkHash liquidChunkHash(WorldRenderData& renderData, Vec2I chunkIndex);
static QuadZLevel materialZLevel(uint32_t zLevel, MaterialId material, MaterialHue hue, MaterialColorVariant colorVariant);
static QuadZLevel modZLevel(uint32_t zLevel, ModId mod, MaterialHue hue, MaterialColorVariant colorVariant);
static QuadZLevel damageZLevel();
static RenderTile const& getRenderTile(WorldRenderData const& renderData, Vec2I const& worldPos);
template <typename Function>
static void forEachRenderTile(WorldRenderData const& renderData, RectI const& worldCoordRange, Function&& function);
void renderTerrainChunks(WorldCamera const& camera, TerrainLayer terrainLayer);
shared_ptr<TerrainChunk const> getTerrainChunk(WorldRenderData& renderData, Vec2I chunkIndex);
shared_ptr<LiquidChunk const> getLiquidChunk(WorldRenderData& renderData, Vec2I chunkIndex);
bool produceTerrainPrimitives(HashMap<QuadZLevel, List<RenderPrimitive>>& primitives,
TerrainLayer terrainLayer, Vec2I const& pos, WorldRenderData const& renderData);
void produceLiquidPrimitives(HashMap<LiquidId, List<RenderPrimitive>>& primitives, Vec2I const& pos, WorldRenderData const& renderData);
bool determineMatchingPieces(MaterialPieceResultList& resultList, bool* occlude, MaterialDatabaseConstPtr const& materialDb, MaterialRenderMatchList const& matchList,
WorldRenderData const& renderData, Vec2I const& basePos, TileLayer layer, bool isMod);
float liquidDrawLevel(float liquidLevel) const;
List<LiquidInfo> m_liquids;
Vec4B m_backgroundLayerColor;
Vec4B m_foregroundLayerColor;
Vec2F m_liquidDrawLevels;
RendererPtr m_renderer;
TextureGroupPtr m_textureGroup;
HashTtlCache<TextureKey, TexturePtr, TextureKeyHash> m_textureCache;
HashTtlCache<pair<Vec2I, ChunkHash>, shared_ptr<TerrainChunk const>> m_terrainChunkCache;
HashTtlCache<pair<Vec2I, ChunkHash>, shared_ptr<LiquidChunk const>> m_liquidChunkCache;
List<shared_ptr<TerrainChunk const>> m_pendingTerrainChunks;
List<shared_ptr<LiquidChunk const>> m_pendingLiquidChunks;
Maybe<Vec2F> m_lastCameraCenter;
Vec2F m_cameraPan;
};
template <typename Function>
void TilePainter::forEachRenderTile(WorldRenderData const& renderData, RectI const& worldCoordRange, Function&& function) {
RectI indexRect = RectI::withSize(renderData.geometry.diff(worldCoordRange.min(), renderData.tileMinPosition), worldCoordRange.size());
indexRect.limit(RectI::withSize(Vec2I(0, 0), Vec2I(renderData.tiles.size())));
if (!indexRect.isEmpty()) {
renderData.tiles.forEach(Array2S(indexRect.min()), Array2S(indexRect.size()), [&](Array2S const& index, RenderTile const& tile) {
return function(worldCoordRange.min() + (Vec2I(index) - indexRect.min()), tile);
});
}
}
}
#endif

View file

@ -0,0 +1,38 @@
#include "StarWorldCamera.hpp"
namespace Star {
void WorldCamera::setCenterWorldPosition(Vec2F const& position) {
// Only actually move the world center if a half pixel distance has been
// moved in any direction. This is sort of arbitrary, but helps prevent
// judder if the camera is at a boundary and floating point inaccuracy is
// causing the focus to jitter back and forth across the boundary.
if (fabs(position[0] - m_worldCenter[0]) < 1.0f / (TilePixels * m_pixelRatio * 2) && fabs(position[1] - m_worldCenter[1]) < 1.0f / (TilePixels * m_pixelRatio * 2))
return;
// First, make sure the camera center position is inside the main x
// coordinate bounds, and that the top and bototm of the screen are not
// outside of the y coordinate bounds.
m_worldCenter = m_worldGeometry.xwrap(position);
m_worldCenter[1] = clamp(m_worldCenter[1],
(float)m_screenSize[1] / (TilePixels * m_pixelRatio * 2),
m_worldGeometry.height() - (float)m_screenSize[1] / (TilePixels * m_pixelRatio * 2));
// Then, position the camera center position so that the tile grid is as
// close as possible aligned to whole pixel boundaries. This is incredibly
// important, because this means that even without any complicated rounding,
// elements drawn in world space that are aligned with TilePixels will
// eventually also be aligned to real screen pixels.
if (m_screenSize[0] % 2 == 0)
m_worldCenter[0] = round(m_worldCenter[0] * (TilePixels * m_pixelRatio)) / (TilePixels * m_pixelRatio);
else
m_worldCenter[0] = (round(m_worldCenter[0] * (TilePixels * m_pixelRatio) + 0.5f) - 0.5f) / (TilePixels * m_pixelRatio);
if (m_screenSize[1] % 2 == 0)
m_worldCenter[1] = round(m_worldCenter[1] * (TilePixels * m_pixelRatio)) / (TilePixels * m_pixelRatio);
else
m_worldCenter[1] = (round(m_worldCenter[1] * (TilePixels * m_pixelRatio) + 0.5f) - 0.5f) / (TilePixels * m_pixelRatio);
}
}

View file

@ -0,0 +1,117 @@
#ifndef STAR_WORLD_CAMERA_HPP
#define STAR_WORLD_CAMERA_HPP
#include "StarWorldGeometry.hpp"
#include "StarGameTypes.hpp"
namespace Star {
class WorldCamera {
public:
void setScreenSize(Vec2U screenSize);
Vec2U screenSize() const;
void setPixelRatio(unsigned pixelRatio);
unsigned pixelRatio() const;
void setWorldGeometry(WorldGeometry geometry);
WorldGeometry worldGeometry() const;
// Set the camera center position (in world space) to as close to the given
// location as possible while keeping the screen within world bounds.
// Returns the actual camera position.
void setCenterWorldPosition(Vec2F const& position);
Vec2F centerWorldPosition() const;
// Transforms world coordinates into one set of screen coordinates. Since
// the world is non-euclidean, one world coordinate can transform to
// potentially an infinite number of screen coordinates. This will retrun
// the closest to the center of the screen.
Vec2F worldToScreen(Vec2F const& worldCoord) const;
// Assumes top left corner of screen is (0, 0) in screen coordinates.
Vec2F screenToWorld(Vec2F const& screen) const;
// Returns screen dimensions in world space.
RectF worldScreenRect() const;
// Returns tile dimensions of the tiles that overlap with the screen
RectI worldTileRect() const;
// Returns the position of the lower left corner of the lower left tile of
// worldTileRect, in screen coordinates.
Vec2F tileMinScreen() const;
private:
WorldGeometry m_worldGeometry;
Vec2U m_screenSize;
unsigned m_pixelRatio = 1;
Vec2F m_worldCenter;
};
inline void WorldCamera::setScreenSize(Vec2U screenSize) {
m_screenSize = screenSize;
}
inline Vec2U WorldCamera::screenSize() const {
return m_screenSize;
}
inline void WorldCamera::setPixelRatio(unsigned pixelRatio) {
m_pixelRatio = pixelRatio;
}
inline unsigned WorldCamera::pixelRatio() const {
return m_pixelRatio;
}
inline void WorldCamera::setWorldGeometry(WorldGeometry geometry) {
m_worldGeometry = move(geometry);
}
inline WorldGeometry WorldCamera::worldGeometry() const {
return m_worldGeometry;
}
inline Vec2F WorldCamera::centerWorldPosition() const {
return Vec2F(m_worldCenter);
}
inline Vec2F WorldCamera::worldToScreen(Vec2F const& worldCoord) const {
Vec2F wrappedCoord = m_worldGeometry.nearestTo(Vec2F(m_worldCenter), worldCoord);
return Vec2F(
(wrappedCoord[0] - m_worldCenter[0]) * (TilePixels * m_pixelRatio) + m_screenSize[0] / 2.0,
(wrappedCoord[1] - m_worldCenter[1]) * (TilePixels * m_pixelRatio) + m_screenSize[1] / 2.0
);
}
inline Vec2F WorldCamera::screenToWorld(Vec2F const& screen) const {
return Vec2F(
(screen[0] - m_screenSize[0] / 2.0) / (TilePixels * m_pixelRatio) + m_worldCenter[0],
(screen[1] - m_screenSize[1] / 2.0) / (TilePixels * m_pixelRatio) + m_worldCenter[1]
);
}
inline RectF WorldCamera::worldScreenRect() const {
// screen dimensions in world space
float w = (float)m_screenSize[0] / (TilePixels * m_pixelRatio);
float h = (float)m_screenSize[1] / (TilePixels * m_pixelRatio);
return RectF::withSize(Vec2F(m_worldCenter[0] - w / 2, m_worldCenter[1] - h / 2), Vec2F(w, h));
}
inline RectI WorldCamera::worldTileRect() const {
RectF screen = worldScreenRect();
Vec2I min = Vec2I::floor(screen.min());
Vec2I size = Vec2I::ceil(Vec2F(m_screenSize) / (TilePixels * m_pixelRatio) + (screen.min() - Vec2F(min)));
return RectI::withSize(min, size);
}
inline Vec2F WorldCamera::tileMinScreen() const {
RectF screenRect = worldScreenRect();
RectI tileRect = worldTileRect();
return (Vec2F(tileRect.min()) - screenRect.min()) * (TilePixels * m_pixelRatio);
}
}
#endif

View file

@ -0,0 +1,296 @@
#include "StarWorldPainter.hpp"
#include "StarAnimation.hpp"
#include "StarRoot.hpp"
#include "StarConfiguration.hpp"
#include "StarAssets.hpp"
#include "StarJsonExtra.hpp"
namespace Star {
WorldPainter::WorldPainter() {
m_assets = Root::singleton().assets();
m_camera.setScreenSize({800, 600});
m_camera.setCenterWorldPosition(Vec2F());
m_camera.setPixelRatio(Root::singleton().configuration()->get("zoomLevel").toFloat());
m_highlightConfig = m_assets->json("/highlights.config");
for (auto p : m_highlightConfig.get("highlightDirectives").iterateObject())
m_highlightDirectives.set(EntityHighlightEffectTypeNames.getLeft(p.first), {p.second.getString("underlay", ""), p.second.getString("overlay", "")});
m_entityBarOffset = jsonToVec2F(m_assets->json("/rendering.config:entityBarOffset"));
m_entityBarSpacing = jsonToVec2F(m_assets->json("/rendering.config:entityBarSpacing"));
m_entityBarSize = jsonToVec2F(m_assets->json("/rendering.config:entityBarSize"));
m_entityBarIconOffset = jsonToVec2F(m_assets->json("/rendering.config:entityBarIconOffset"));
m_preloadTextureChance = m_assets->json("/rendering.config:preloadTextureChance").toFloat();
}
void WorldPainter::renderInit(RendererPtr renderer) {
m_assets = Root::singleton().assets();
m_renderer = move(renderer);
auto textureGroup = m_renderer->createTextureGroup(TextureGroupSize::Large);
m_textPainter = make_shared<TextPainter>(m_assets->font("/hobo.ttf")->clone(), m_renderer, textureGroup);
m_tilePainter = make_shared<TilePainter>(m_renderer);
m_drawablePainter = make_shared<DrawablePainter>(m_renderer, make_shared<AssetTextureGroup>(textureGroup));
m_environmentPainter = make_shared<EnvironmentPainter>(m_renderer);
}
void WorldPainter::setCameraPosition(WorldGeometry const& geometry, Vec2F const& position) {
m_camera.setWorldGeometry(geometry);
m_camera.setCenterWorldPosition(position);
}
WorldCamera const& WorldPainter::camera() const {
return m_camera;
}
void WorldPainter::render(WorldRenderData& renderData) {
m_camera.setScreenSize(m_renderer->screenSize());
m_camera.setPixelRatio(Root::singleton().configuration()->get("zoomLevel").toFloat());
m_assets = Root::singleton().assets();
m_environmentPainter->update();
m_tilePainter->setup(m_camera, renderData);
if (renderData.isFullbright) {
m_renderer->setEffectTexture("lightMap", Image::filled(Vec2U(1, 1), {255, 255, 255, 255}, PixelFormat::RGB24));
m_renderer->setEffectParameter("lightMapMultiplier", 1.0f);
} else {
m_tilePainter->adjustLighting(renderData);
m_renderer->setEffectParameter("lightMapMultiplier", m_assets->json("/rendering.config:lightMapMultiplier").toFloat());
m_renderer->setEffectParameter("lightMapScale", Vec2F::filled(TilePixels * m_camera.pixelRatio()));
m_renderer->setEffectParameter("lightMapOffset", m_camera.worldToScreen(Vec2F(renderData.lightMinPosition)));
m_renderer->setEffectTexture("lightMap", renderData.lightMap);
}
// Stars, Debris Fields, Sky, and Orbiters
m_environmentPainter->renderStars(m_camera.pixelRatio(), Vec2F(m_camera.screenSize()), renderData.skyRenderData);
m_environmentPainter->renderDebrisFields(m_camera.pixelRatio(), Vec2F(m_camera.screenSize()), renderData.skyRenderData);
m_environmentPainter->renderBackOrbiters(m_camera.pixelRatio(), Vec2F(m_camera.screenSize()), renderData.skyRenderData);
m_environmentPainter->renderPlanetHorizon(m_camera.pixelRatio(), Vec2F(m_camera.screenSize()), renderData.skyRenderData);
m_environmentPainter->renderSky(Vec2F(m_camera.screenSize()), renderData.skyRenderData);
m_environmentPainter->renderFrontOrbiters(m_camera.pixelRatio(), Vec2F(m_camera.screenSize()), renderData.skyRenderData);
// Parallax layers
auto parallaxDelta = m_camera.worldGeometry().diff(m_camera.centerWorldPosition(), m_previousCameraCenter);
if (parallaxDelta.magnitude() > 10)
m_parallaxWorldPosition = m_camera.centerWorldPosition();
else
m_parallaxWorldPosition += parallaxDelta;
m_previousCameraCenter = m_camera.centerWorldPosition();
m_parallaxWorldPosition[1] = m_camera.centerWorldPosition()[1];
if (!renderData.parallaxLayers.empty())
m_environmentPainter->renderParallaxLayers(m_parallaxWorldPosition, m_camera, renderData.parallaxLayers, renderData.skyRenderData);
// Main world layers
Map<EntityRenderLayer, List<pair<EntityHighlightEffect, List<Drawable>>>> entityDrawables;
for (auto& ed : renderData.entityDrawables) {
for (auto& p : ed.layers)
entityDrawables[p.first].append({ed.highlightEffect, move(p.second)});
}
auto entityDrawableIterator = entityDrawables.begin();
auto renderEntitiesUntil = [this, &entityDrawables, &entityDrawableIterator](Maybe<EntityRenderLayer> until) {
while (true) {
if (entityDrawableIterator == entityDrawables.end())
break;
if (until && entityDrawableIterator->first >= *until)
break;
for (auto& edl : entityDrawableIterator->second)
drawEntityLayer(move(edl.second), edl.first);
++entityDrawableIterator;
}
m_renderer->flush();
};
renderEntitiesUntil(RenderLayerBackgroundOverlay);
drawDrawableSet(renderData.backgroundOverlays);
renderEntitiesUntil(RenderLayerBackgroundTile);
m_tilePainter->renderBackground(m_camera);
renderEntitiesUntil(RenderLayerPlatform);
m_tilePainter->renderMidground(m_camera);
renderEntitiesUntil(RenderLayerBackParticle);
renderParticles(renderData, Particle::Layer::Back);
renderEntitiesUntil(RenderLayerLiquid);
m_tilePainter->renderLiquid(m_camera);
renderEntitiesUntil(RenderLayerMiddleParticle);
renderParticles(renderData, Particle::Layer::Middle);
renderEntitiesUntil(RenderLayerForegroundTile);
m_tilePainter->renderForeground(m_camera);
renderEntitiesUntil(RenderLayerForegroundOverlay);
drawDrawableSet(renderData.foregroundOverlays);
renderEntitiesUntil(RenderLayerFrontParticle);
renderParticles(renderData, Particle::Layer::Front);
renderEntitiesUntil(RenderLayerOverlay);
drawDrawableSet(renderData.nametags);
renderBars(renderData);
renderEntitiesUntil({});
auto dimLevel = round(renderData.dimLevel * 255);
if (dimLevel != 0)
m_renderer->render(renderFlatRect(RectF::withSize({}, Vec2F(m_camera.screenSize())), Vec4B(renderData.dimColor, dimLevel), 0.0f));
int64_t textureTimeout = m_assets->json("/rendering.config:textureTimeout").toInt();
m_textPainter->cleanup(textureTimeout);
m_drawablePainter->cleanup(textureTimeout);
m_environmentPainter->cleanup(textureTimeout);
m_tilePainter->cleanup();
}
void WorldPainter::renderParticles(WorldRenderData& renderData, Particle::Layer layer) {
const int textParticleFontSize = m_assets->json("/rendering.config:textParticleFontSize").toInt();
const RectF particleRenderWindow = RectF::withSize(Vec2F(), Vec2F(m_camera.screenSize())).padded(m_assets->json("/rendering.config:particleRenderWindowPadding").toInt());
for (Particle const& particle : renderData.particles) {
if (layer != particle.layer)
continue;
Vec2F position = m_camera.worldToScreen(particle.position);
if (!particleRenderWindow.contains(position))
continue;
Vec2I size = Vec2I::filled(particle.size * m_camera.pixelRatio());
if (particle.type == Particle::Type::Ember) {
m_renderer->render(renderFlatRect(RectF(position - Vec2F(size) / 2, position + Vec2F(size) / 2), particle.color.toRgba(), particle.fullbright ? 0.0f : 1.0f));
} else if (particle.type == Particle::Type::Streak) {
// Draw a rotated quad streaking in the direction the particle is coming from.
// Sadly this looks awful.
Vec2F dir = particle.velocity.normalized();
Vec2F sideHalf = dir.rot90() * m_camera.pixelRatio() * particle.size / 2;
float length = particle.length * m_camera.pixelRatio();
Vec4B color = particle.color.toRgba();
float lightMapMultiplier = particle.fullbright ? 0.0f : 1.0f;
m_renderer->render(RenderQuad{{},
{position - sideHalf, {}, color, lightMapMultiplier},
{position + sideHalf, {}, color, lightMapMultiplier},
{position - dir * length + sideHalf, {}, color, lightMapMultiplier},
{position - dir * length - sideHalf, {}, color, lightMapMultiplier}
});
} else if (particle.type == Particle::Type::Textured || particle.type == Particle::Type::Animated) {
Drawable drawable;
if (particle.type == Particle::Type::Textured)
drawable = Drawable::makeImage(particle.string, 1.0f / TilePixels, true, Vec2F(0, 0));
else
drawable = particle.animation->drawable(1.0f / TilePixels);
if (particle.flip && particle.flippable)
drawable.scale(Vec2F(-1, 1));
if (drawable.isImage())
drawable.imagePart().addDirectives(particle.directives, true);
drawable.fullbright = particle.fullbright;
drawable.color = particle.color;
drawable.rotate(particle.rotation);
drawable.scale(particle.size);
drawable.translate(particle.position);
drawDrawable(move(drawable));
} else if (particle.type == Particle::Type::Text) {
Vec2F position = m_camera.worldToScreen(particle.position);
unsigned size = textParticleFontSize * m_camera.pixelRatio() * particle.size;
if (size > 0) {
m_textPainter->setFontSize(size);
m_textPainter->setFontColor(particle.color.toRgba());
m_textPainter->renderText(particle.string, {position, HorizontalAnchor::HMidAnchor, VerticalAnchor::VMidAnchor});
}
}
}
m_renderer->flush();
}
void WorldPainter::renderBars(WorldRenderData& renderData) {
auto offset = m_entityBarOffset;
for (auto const& bar : renderData.overheadBars) {
auto position = bar.entityPosition + offset;
offset += m_entityBarSpacing;
if (bar.icon) {
auto iconDrawPosition = position - (m_entityBarSize / 2).round() + m_entityBarIconOffset;
drawDrawable(Drawable::makeImage(*bar.icon, 1.0f / TilePixels, true, iconDrawPosition));
}
if (!bar.detailOnly) {
auto fullBar = RectF({}, {m_entityBarSize.x() * bar.percentage, m_entityBarSize.y()});
auto emptyBar = RectF({m_entityBarSize.x() * bar.percentage, 0.0f}, m_entityBarSize);
auto fullColor = bar.color;
auto emptyColor = Color::Black;
drawDrawable(Drawable::makePoly(PolyF(emptyBar), emptyColor, position));
drawDrawable(Drawable::makePoly(PolyF(fullBar), fullColor, position));
}
}
m_renderer->flush();
}
void WorldPainter::drawEntityLayer(List<Drawable> drawables, EntityHighlightEffect highlightEffect) {
highlightEffect.level *= m_highlightConfig.getFloat("maxHighlightLevel", 1.0);
if (m_highlightDirectives.contains(highlightEffect.type) && highlightEffect.level > 0) {
// first pass, draw underlay
auto underlayDirectives = m_highlightDirectives[highlightEffect.type].first;
if (!underlayDirectives.empty()) {
for (auto& d : drawables) {
if (d.isImage()) {
auto underlayDrawable = Drawable(d);
underlayDrawable.fullbright = true;
underlayDrawable.color = Color::rgbaf(1, 1, 1, highlightEffect.level);
underlayDrawable.imagePart().addDirectives(underlayDirectives, true);
drawDrawable(move(underlayDrawable));
}
}
}
// second pass, draw main drawables and overlays
auto overlayDirectives = m_highlightDirectives[highlightEffect.type].second;
for (auto& d : drawables) {
drawDrawable(d);
if (!overlayDirectives.empty() && d.isImage()) {
auto overlayDrawable = Drawable(d);
overlayDrawable.fullbright = true;
overlayDrawable.color = Color::rgbaf(1, 1, 1, highlightEffect.level);
overlayDrawable.imagePart().addDirectives(overlayDirectives, true);
drawDrawable(move(overlayDrawable));
}
}
} else {
for (auto& d : drawables)
drawDrawable(move(d));
}
}
void WorldPainter::drawDrawable(Drawable drawable) {
drawable.position = m_camera.worldToScreen(drawable.position);
drawable.scale(m_camera.pixelRatio() * TilePixels, drawable.position);
if (drawable.isLine())
drawable.linePart().width *= m_camera.pixelRatio();
// draw the drawable if it's on screen
// if it's not on screen, there's a random chance to pre-load
// pre-load is not done on every tick because it's expensive to look up images with long paths
if (RectF::withSize(Vec2F(), Vec2F(m_camera.screenSize())).intersects(drawable.boundBox(false)))
m_drawablePainter->drawDrawable(drawable);
else if (drawable.isImage() && Random::randf() < m_preloadTextureChance)
m_assets->tryImage(drawable.imagePart().image);
}
void WorldPainter::drawDrawableSet(List<Drawable>& drawables) {
for (Drawable& drawable : drawables)
drawDrawable(move(drawable));
m_renderer->flush();
}
}

View file

@ -0,0 +1,67 @@
#ifndef STAR_WORLD_PAINTER_HPP
#define STAR_WORLD_PAINTER_HPP
#include "StarWorldRenderData.hpp"
#include "StarTilePainter.hpp"
#include "StarEnvironmentPainter.hpp"
#include "StarTextPainter.hpp"
#include "StarDrawablePainter.hpp"
#include "StarRenderer.hpp"
namespace Star {
STAR_CLASS(WorldPainter);
// Will update client rendering window internally
class WorldPainter {
public:
WorldPainter();
void renderInit(RendererPtr renderer);
void setCameraPosition(WorldGeometry const& worldGeometry, Vec2F const& position);
WorldCamera const& camera() const;
void render(WorldRenderData& renderData);
private:
void renderParticles(WorldRenderData& renderData, Particle::Layer layer);
void renderBars(WorldRenderData& renderData);
void drawEntityLayer(List<Drawable> drawables, EntityHighlightEffect highlightEffect = EntityHighlightEffect());
void drawDrawable(Drawable drawable);
void drawDrawableSet(List<Drawable>& drawable);
WorldCamera m_camera;
RendererPtr m_renderer;
TextPainterPtr m_textPainter;
DrawablePainterPtr m_drawablePainter;
EnvironmentPainterPtr m_environmentPainter;
TilePainterPtr m_tilePainter;
Json m_highlightConfig;
Map<EntityHighlightEffectType, pair<String, String>> m_highlightDirectives;
Vec2F m_entityBarOffset;
Vec2F m_entityBarSpacing;
Vec2F m_entityBarSize;
Vec2F m_entityBarIconOffset;
// Updated every frame
AssetsConstPtr m_assets;
RectF m_worldScreenRect;
Vec2F m_previousCameraCenter;
Vec2F m_parallaxWorldPosition;
float m_preloadTextureChance;
};
}
#endif