pioneer/src/Sfx.cpp

428 lines
12 KiB
C++

// Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
#include "Sfx.h"
#include "Body.h"
#include "FileSystem.h"
#include "Frame.h"
#include "GameSaveError.h"
#include "Json.h"
#include "JsonUtils.h"
#include "ModelBody.h"
#include "Pi.h"
#include "StringF.h"
#include "core/IniConfig.h"
#include "graphics/Drawables.h"
#include "graphics/Graphics.h"
#include "graphics/Material.h"
#include "graphics/RenderState.h"
#include "graphics/Renderer.h"
#include "graphics/TextureBuilder.h"
using namespace Graphics;
namespace {
float SizeToPixels(const vector3f &trans, const float size)
{
//some hand-tweaked scaling, to make the lights seem larger from distance (final size is in pixels)
// gl_PointSize = pixels_per_radian * point_diameter / distance( camera, pointcenter );
const float pixrad = Clamp(Graphics::GetScreenHeight() / trans.Length(), 0.1f, 50.0f);
return (size * Graphics::GetFovFactor()) * pixrad;
}
} // namespace
std::unique_ptr<Graphics::Material> SfxManager::damageParticle;
std::unique_ptr<Graphics::Material> SfxManager::ecmParticle;
std::unique_ptr<Graphics::Material> SfxManager::smokeParticle;
std::unique_ptr<Graphics::Material> SfxManager::explosionParticle;
Graphics::RenderState *SfxManager::alphaState = nullptr;
Graphics::RenderState *SfxManager::additiveAlphaState = nullptr;
Graphics::RenderState *SfxManager::alphaOneState = nullptr;
SfxManager::MaterialData SfxManager::m_materialData[TYPE_NONE];
Sfx::Sfx(const vector3d &pos, const vector3d &vel, const float speed, const SFX_TYPE type) :
m_pos(pos),
m_vel(vel),
m_age(0.0f),
m_speed(speed),
m_type(type)
{
}
Sfx::Sfx(const Json &jsonObj)
{
try {
Json sfxObj = jsonObj["sfx"];
m_pos = jsonObj["pos"];
m_vel = jsonObj["vel"];
m_age = sfxObj["age"];
m_type = sfxObj["type"];
} catch (Json::type_error &) {
throw SavedGameCorruptException();
}
}
void Sfx::SaveToJson(Json &jsonObj)
{
Json sfxObj({}); // Create JSON object to contain sfx data.
sfxObj["pos"] = m_pos;
sfxObj["vel"] = m_vel;
sfxObj["age"] = m_age;
sfxObj["type"] = m_type;
jsonObj["sfx"] = sfxObj; // Add sfx object to supplied object.
}
void Sfx::SetPosition(const vector3d &p)
{
m_pos = p;
}
void Sfx::TimeStepUpdate(const float timeStep)
{
PROFILE_SCOPED()
m_age += timeStep;
m_pos += m_vel * double(timeStep);
switch (m_type) {
case TYPE_EXPLOSION:
if (m_age > 3.2) m_type = TYPE_NONE;
break;
case TYPE_DAMAGE:
if (m_age > 2.0) m_type = TYPE_NONE;
break;
case TYPE_SMOKE:
if (m_age > 8.0) m_type = TYPE_NONE;
break;
case TYPE_NONE: break;
}
}
float Sfx::AgeBlend() const
{
switch (m_type) {
case TYPE_EXPLOSION: return (3.2 - m_age) / 3.2;
case TYPE_DAMAGE: return (2.0 - m_age) / 2.0;
case TYPE_SMOKE: return (8.0 - m_age) / 8.0;
case TYPE_NONE: return 0.0f;
}
return 0.0f;
}
SfxManager::SfxManager()
{
for (size_t t = 0; t < TYPE_NONE; t++) {
m_instances[t].clear();
}
}
void SfxManager::ToJson(Json &jsonObj, const FrameId fId)
{
Frame *f = Frame::GetFrame(fId);
Json sfxArray = Json::array(); // Create JSON array to contain sfx data.
if (f->m_sfx) {
for (size_t t = TYPE_EXPLOSION; t < TYPE_NONE; t++) {
for (size_t i = 0; i < f->m_sfx->GetNumberInstances(SFX_TYPE(t)); i++) {
Sfx &inst(f->m_sfx->GetInstanceByIndex(SFX_TYPE(t), i));
if (inst.m_type != TYPE_NONE) {
Json sfxArrayEl({}); // Create JSON object to contain sfx element.
inst.SaveToJson(sfxArrayEl);
sfxArray.push_back(sfxArrayEl); // Append sfx object to array.
}
}
}
}
jsonObj["sfx_array"] = sfxArray; // Add sfx array to supplied object.
}
void SfxManager::FromJson(const Json &jsonObj, FrameId fId)
{
Json sfxArray = jsonObj["sfx_array"].get<Json::array_t>();
Frame *f = Frame::GetFrame(fId);
if (sfxArray.size()) f->m_sfx.reset(new SfxManager);
for (unsigned int i = 0; i < sfxArray.size(); ++i) {
Sfx inst(sfxArray[i]);
f->m_sfx->AddInstance(inst);
}
}
SfxManager *SfxManager::AllocSfxInFrame(FrameId fId)
{
Frame *f = Frame::GetFrame(fId);
if (!f->m_sfx) {
f->m_sfx.reset(new SfxManager);
}
return f->m_sfx.get();
}
void SfxManager::Add(const Body *b, SFX_TYPE t)
{
assert(t != TYPE_NONE);
SfxManager *sfxman = AllocSfxInFrame(b->GetFrame());
if (!sfxman) return;
vector3d vel(b->GetVelocity() + 200.0 * vector3d(Pi::rng.Double() - 0.5, Pi::rng.Double() - 0.5, Pi::rng.Double() - 0.5));
Sfx sfx(b->GetPosition(), vel, 200, t);
sfxman->AddInstance(sfx);
}
void SfxManager::AddExplosion(Body *b)
{
SfxManager *sfxman = AllocSfxInFrame(b->GetFrame());
if (!sfxman) return;
float speed = 200.0f;
if (b->IsType(ObjectType::SHIP)) {
ModelBody *mb = static_cast<ModelBody *>(b);
speed = mb->GetAabb().radius * 8.0;
}
Sfx sfx(b->GetPosition(), b->GetVelocity(), speed, TYPE_EXPLOSION);
sfxman->AddInstance(sfx);
}
void SfxManager::AddThrustSmoke(const Body *b, const float speed, const vector3d &adjustpos)
{
SfxManager *sfxman = AllocSfxInFrame(b->GetFrame());
if (!sfxman) return;
Sfx sfx(b->GetPosition() + adjustpos, vector3d(0, 0, 0), speed, TYPE_SMOKE);
sfxman->AddInstance(sfx);
}
void SfxManager::TimeStepAll(const float timeStep, FrameId fId)
{
PROFILE_SCOPED()
Frame *f = Frame::GetFrame(fId);
if (f->m_sfx) {
for (size_t t = TYPE_EXPLOSION; t < TYPE_NONE; t++) {
for (size_t i = 0; i < f->m_sfx->GetNumberInstances(SFX_TYPE(t)); i++) {
Sfx &inst(f->m_sfx->GetInstanceByIndex(SFX_TYPE(t), i));
inst.TimeStepUpdate(timeStep);
}
}
f->m_sfx->Cleanup();
}
for (FrameId kid : f->GetChildren()) {
TimeStepAll(timeStep, kid);
}
}
void SfxManager::Cleanup()
{
for (size_t t = TYPE_EXPLOSION; t < TYPE_NONE; t++) {
const size_t numInstances = GetNumberInstances(SFX_TYPE(t));
if (!numInstances)
continue;
for (Sint64 i = Sint64(numInstances - 1); i >= 0; i--) {
Sfx &inst(GetInstanceByIndex(SFX_TYPE(t), i));
if (inst.m_type == TYPE_NONE) {
m_instances[t].erase(m_instances[t].begin() + i);
}
}
}
}
void SfxManager::RenderAll(Renderer *renderer, FrameId fId, FrameId camFrameId)
{
Frame *f = Frame::GetFrame(fId);
PROFILE_SCOPED()
if (f->m_sfx) {
matrix4x4d ftran;
Frame::GetFrameTransform(fId, camFrameId, ftran);
for (size_t t = TYPE_EXPLOSION; t < TYPE_NONE; t++) {
const size_t numInstances = f->m_sfx->GetNumberInstances(SFX_TYPE(t));
if (!numInstances)
continue;
Graphics::RenderState *rs = nullptr;
Graphics::Material *material = nullptr;
std::vector<vector3f> positions;
positions.reserve(numInstances);
std::vector<vector2f> offsets;
offsets.reserve(numInstances);
std::vector<float> sizes;
sizes.reserve(numInstances);
for (size_t i = 0; i < numInstances; i++) {
Sfx &inst(f->m_sfx->GetInstanceByIndex(SFX_TYPE(t), i));
assert(inst.m_type == t);
const vector3d dpos = ftran * inst.m_pos;
const vector3f pos(dpos);
positions.push_back(pos);
float speed = 0.0f;
const vector2f offset(CalculateOffset(SFX_TYPE(t), inst));
switch (t) {
case TYPE_NONE: assert(false); break;
case TYPE_EXPLOSION: {
speed = SizeToPixels(pos, inst.m_speed);
rs = SfxManager::alphaState;
material = explosionParticle.get();
break;
}
case TYPE_DAMAGE:
speed = SizeToPixels(pos, 20.f);
rs = SfxManager::additiveAlphaState;
material = damageParticle.get();
break;
case TYPE_SMOKE:
speed = Clamp(SizeToPixels(pos, (inst.m_speed * inst.m_age)), 0.1f, 50.0f);
rs = SfxManager::alphaState;
material = smokeParticle.get();
break;
}
sizes.push_back(speed);
offsets.push_back(offset);
}
renderer->DrawPointSprites(numInstances, &positions[0], &offsets[0], &sizes[0], rs, material);
}
}
for (FrameId kid : f->GetChildren()) {
RenderAll(renderer, kid, camFrameId);
}
}
vector2f SfxManager::CalculateOffset(const enum SFX_TYPE type, const Sfx &inst)
{
if (m_materialData[type].effect == Graphics::EFFECT_BILLBOARD_ATLAS) {
const int spriteframe = inst.AgeBlend() * (m_materialData[type].num_textures - 1);
const Sint32 numImgsWide = m_materialData[type].num_imgs_wide;
const int u = (spriteframe % numImgsWide); // % is the "modulo operator", the remainder of i / width;
const int v = (spriteframe / numImgsWide); // where "/" is an integer division
return vector2f(
float(u) / float(numImgsWide),
float(v) / float(numImgsWide));
}
return vector2f(0.0f);
}
bool SfxManager::SplitMaterialData(const std::string &spec, MaterialData &output)
{
static const std::string delim(",");
enum dataEntries {
eEFFECT = 0,
eNUM_IMGS_WIDE,
eNUM_TEXTURES,
eCOORD_DOWNSCALE
};
size_t i = 0, start = 0, end = 0;
while (end != std::string::npos) {
// get to the first non-delim char
start = spec.find_first_not_of(delim, end);
// read the end, no more to do
if (start == std::string::npos)
break;
// find the end - next delim or end of string
end = spec.find_first_of(delim, start);
// extract the fragment and remember it
switch (i) {
case eEFFECT:
output.effect = (spec.substr(start, (end == std::string::npos) ? std::string::npos : end - start) == "billboard") ? Graphics::EFFECT_BILLBOARD : Graphics::EFFECT_BILLBOARD_ATLAS;
break;
case eNUM_IMGS_WIDE:
output.num_imgs_wide = atoi(spec.substr(start, (end == std::string::npos) ? std::string::npos : end - start).c_str());
break;
case eNUM_TEXTURES:
output.num_textures = atoi(spec.substr(start, (end == std::string::npos) ? std::string::npos : end - start).c_str());
break;
default:
case eCOORD_DOWNSCALE:
assert(false);
break;
}
i++;
}
output.coord_downscale = 1.0f / float(output.num_imgs_wide);
return i == eCOORD_DOWNSCALE;
}
void SfxManager::Init(Graphics::Renderer *r)
{
PROFILE_SCOPED()
IniConfig cfg;
// set defaults in case they're missing from the file
cfg.SetString("damageFile", "textures/smoke.png");
cfg.SetString("smokeFile", "textures/smoke.png");
cfg.SetString("explosionFile", "textures/explosions/explosions.png");
cfg.SetString("damagePacking", "billboard,1,1");
cfg.SetString("smokePacking", "billboard,1,1");
cfg.SetString("explosionPacking", "atlas,6,32");
// load
cfg.Read(FileSystem::gameDataFiles, "textures/Sfx.ini");
// shared render states
Graphics::RenderStateDesc rsd;
rsd.blendMode = Graphics::BLEND_ALPHA;
rsd.depthWrite = false;
alphaState = r->CreateRenderState(rsd);
rsd.blendMode = Graphics::BLEND_ALPHA_ONE;
additiveAlphaState = r->CreateRenderState(rsd);
rsd.depthWrite = true;
alphaOneState = r->CreateRenderState(rsd);
// materials
Graphics::MaterialDescriptor desc;
desc.textures = 1;
// ECM effect is different, not managed by Sfx at all, should it be factored out?
desc.effect = Graphics::EFFECT_BILLBOARD;
ecmParticle.reset(r->CreateMaterial(desc));
ecmParticle->texture0 = Graphics::TextureBuilder::Billboard("textures/ecm.png").GetOrCreateTexture(r, "billboard");
// load material definition data
SplitMaterialData(cfg.String("explosionPacking"), m_materialData[TYPE_EXPLOSION]);
SplitMaterialData(cfg.String("damagePacking"), m_materialData[TYPE_DAMAGE]);
SplitMaterialData(cfg.String("smokePacking"), m_materialData[TYPE_SMOKE]);
desc.effect = m_materialData[TYPE_DAMAGE].effect;
damageParticle.reset(r->CreateMaterial(desc));
damageParticle->texture0 = Graphics::TextureBuilder::Billboard(cfg.String("damageFile")).GetOrCreateTexture(r, "billboard");
if (desc.effect == Graphics::EFFECT_BILLBOARD_ATLAS)
damageParticle->specialParameter0 = &m_materialData[TYPE_DAMAGE].coord_downscale;
desc.effect = m_materialData[TYPE_SMOKE].effect;
smokeParticle.reset(r->CreateMaterial(desc));
smokeParticle->texture0 = Graphics::TextureBuilder::Billboard(cfg.String("smokeFile")).GetOrCreateTexture(r, "billboard");
if (desc.effect == Graphics::EFFECT_BILLBOARD_ATLAS)
smokeParticle->specialParameter0 = &m_materialData[TYPE_SMOKE].coord_downscale;
desc.effect = m_materialData[TYPE_EXPLOSION].effect;
explosionParticle.reset(r->CreateMaterial(desc));
explosionParticle->texture0 = Graphics::TextureBuilder::Billboard(cfg.String("explosionFile")).GetOrCreateTexture(r, "billboard");
if (desc.effect == Graphics::EFFECT_BILLBOARD_ATLAS)
explosionParticle->specialParameter0 = &m_materialData[TYPE_EXPLOSION].coord_downscale;
}
void SfxManager::Uninit()
{
damageParticle.reset();
ecmParticle.reset();
smokeParticle.reset();
explosionParticle.reset();
}