osb/source/rendering/StarWorldPainter.cpp

299 lines
13 KiB
C++

#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_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->setProcessingDirectives("");
m_textPainter->setFont("");
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();
}
}