diff --git a/game/game/compatibility/cpu32.cpp b/game/game/compatibility/cpu32.cpp index c5f9a5fdf..5f2e0766c 100644 --- a/game/game/compatibility/cpu32.cpp +++ b/game/game/compatibility/cpu32.cpp @@ -255,7 +255,7 @@ void Cpu32::callFunction(ptr32_t func) { std::string Cpu32::popString() { if(stack.size()==0) - return 0; + return {}; auto ptr = stack.back(); stack.pop_back(); diff --git a/game/graphics/drawbuckets.cpp b/game/graphics/drawbuckets.cpp index 9ee539d29..28966e7e3 100644 --- a/game/graphics/drawbuckets.cpp +++ b/game/graphics/drawbuckets.cpp @@ -128,6 +128,7 @@ bool DrawBuckets::commit(Encoder& cmd, uint8_t fId) { bx.waveMaxAmplitude = i.mat.waveMaxAmplitude; bx.alphaWeight = i.mat.alphaWeight; bx.envMapping = i.mat.envMapping; + bx.g1BarrierLayer = i.mat.g1BarrierLayer; if(i.staticMesh!=nullptr) { auto& bbox = i.staticMesh->bbox.bbox; bx.bboxRadius = i.staticMesh->bbox.rConservative; @@ -147,6 +148,8 @@ bool DrawBuckets::commit(Encoder& cmd, uint8_t fId) { bx.flags |= BK_SOLID; if(i.mat.alpha==Material::Water) bx.flags |= BK_WATER; + if(i.mat.isG1Barrier) + bx.flags |= BK_G1_BARRIER; bucket.push_back(bx); } diff --git a/game/graphics/drawbuckets.h b/game/graphics/drawbuckets.h index 9d083669b..1d6955b8b 100644 --- a/game/graphics/drawbuckets.h +++ b/game/graphics/drawbuckets.h @@ -63,6 +63,7 @@ class DrawBuckets { BK_SKIN = 0x2, BK_MORPH = 0x4, BK_WATER = 0x8, + BK_G1_BARRIER = 0x10, }; struct BucketGpu final { @@ -73,7 +74,7 @@ class DrawBuckets { float alphaWeight = 1; float envMapping = 0; uint32_t flags = 0; - uint32_t padd[1] = {}; + uint32_t g1BarrierLayer = 0; }; struct { diff --git a/game/graphics/material.cpp b/game/graphics/material.cpp index 8a83ba164..921fea811 100644 --- a/game/graphics/material.cpp +++ b/game/graphics/material.cpp @@ -22,8 +22,20 @@ Material::Material(const zenkit::Material& m, bool enableAlphaTest) { } loadFrames(m); + if(m.name=="MAGICFRONTIER_BARRIER") { + isG1Barrier = true; + alphaWeight = float(m.color.a)/255.f; + if(m.texture=="BARRIERE.TGA") + g1BarrierLayer = 1; + else if(m.texture.rfind("MAGBA_",0)==0) + g1BarrierLayer = 2; + else if(m.texture.rfind("MAGICFRONTIER_",0)==0) + g1BarrierLayer = 3; + } alpha = loadAlphaFunc(m.alpha_func,m.group,m.color.a,tex,enableAlphaTest); + if(isG1Barrier && m.alpha_func==zenkit::AlphaFunction::BLEND) + alpha = Transparent; if(alpha==Water && m.name=="OWODWFALL_WATERFALL_01") { // NOTE: waterfall heuristics alpha = Solid; @@ -70,6 +82,8 @@ bool Material::operator ==(const Material& other) const { texAniMapDirPeriod==other.texAniMapDirPeriod && texAniFPSInv==other.texAniFPSInv && isGhost==other.isGhost && + isG1Barrier==other.isG1Barrier && + g1BarrierLayer==other.g1BarrierLayer && waveMaxAmplitude==other.waveMaxAmplitude && envMapping==other.envMapping; } diff --git a/game/graphics/material.h b/game/graphics/material.h index a60b78569..6cbba415b 100644 --- a/game/graphics/material.h +++ b/game/graphics/material.h @@ -32,6 +32,8 @@ class Material final { Tempest::Point texAniMapDirPeriod; uint64_t texAniFPSInv = 1; bool isGhost = false; + bool isG1Barrier = false; + uint32_t g1BarrierLayer = 0; float waveMaxAmplitude = 0; float envMapping = 0; @@ -59,4 +61,3 @@ class Material final { void loadFrames(const zenkit::Material& m); void loadFrames(const std::string_view fr, float fps); }; - diff --git a/game/physics/physicmesh.cpp b/game/physics/physicmesh.cpp index b0f5878a7..5a02e63fc 100644 --- a/game/physics/physicmesh.cpp +++ b/game/physics/physicmesh.cpp @@ -5,6 +5,8 @@ #include "graphics/mesh/pose.h" #include "graphics/mesh/protomesh.h" +#include + PhysicMesh::PhysicMesh(const ProtoMesh& proto, DynamicWorld& owner, bool movable) :ani(&proto) { Tempest::Matrix4x4 pos; @@ -20,7 +22,9 @@ PhysicMesh::PhysicMesh(const ProtoMesh& proto, DynamicWorld& owner, bool movable } bool PhysicMesh::isEmpty() const { - return sub.empty(); + return std::all_of(sub.begin(),sub.end(),[](const DynamicWorld::Item& i) { + return i.isEmpty(); + }); } void PhysicMesh::setObjMatrix(const Tempest::Matrix4x4& obj) { diff --git a/game/resources.cpp b/game/resources.cpp index c27f2560e..532f45ed3 100644 --- a/game/resources.cpp +++ b/game/resources.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -51,6 +52,154 @@ static void emplaceTag(char* buf, char tag){ } } +static bool isG1BarrierMesh(std::string_view name) { + return name=="MAGICFRONTIER_OUT.MSH"; + } + +static void prepareG1BarrierMesh(zenkit::Mesh& mesh) { + const bool hasBarrierTexture = Resources::vdfsIndex().find("BARRIERE-C.TEX")!=nullptr; + for(auto& mat:mesh.materials) { + mat.name = "MAGICFRONTIER_BARRIER"; + mat.alpha_func = zenkit::AlphaFunction::ADD; + mat.disable_collision = false; + mat.ignore_sun = true; + mat.texture_anim_fps = 0; + + if(hasBarrierTexture) { + mat.texture = "BARRIERE.TGA"; + mat.color = zenkit::Color(255,255,255,20); + } else { + mat.texture.clear(); + mat.color = zenkit::Color(60,110,210,20); + } + } + } + +static void addG1BarrierLayer(zenkit::Mesh& mesh, const char* assetName, const char* textureName, + zenkit::AlphaFunction alphaFunc, uint8_t alpha, float fps) { + if(mesh.materials.empty() || Resources::vdfsIndex().find(assetName)==nullptr) + return; + + const size_t triCount = std::min({mesh.polygons.material_indices.size(), + mesh.polygons.lightmap_indices.size(), + mesh.polygons.flags.size(), + mesh.polygons.vertex_indices.size()/3, + mesh.polygons.feature_indices.size()/3}); + if(triCount==0) + return; + + zenkit::Material mat = mesh.materials.front(); + mat.name = "MAGICFRONTIER_BARRIER"; + mat.texture = textureName; + mat.color = zenkit::Color(255,255,255,alpha); + mat.alpha_func = alphaFunc; + mat.texture_anim_fps = fps; + mat.disable_collision = true; + mat.ignore_sun = true; + + const uint32_t matId = uint32_t(mesh.materials.size()); + mesh.materials.push_back(std::move(mat)); + + mesh.polygons.material_indices.reserve(mesh.polygons.material_indices.size()+triCount); + mesh.polygons.lightmap_indices.reserve(mesh.polygons.lightmap_indices.size()+triCount); + mesh.polygons.flags.reserve(mesh.polygons.flags.size()+triCount); + mesh.polygons.vertex_indices.reserve(mesh.polygons.vertex_indices.size()+triCount*3); + mesh.polygons.feature_indices.reserve(mesh.polygons.feature_indices.size()+triCount*3); + + for(size_t i=0; i=mesh.materials.size() || + end>mesh.polygon_vertex_indices.size() || end>mesh.polygon_feature_indices.size()) + continue; + triCount += poly.index_count - 2; + } + + mesh.polygons.material_indices.clear(); + mesh.polygons.lightmap_indices.clear(); + mesh.polygons.feature_indices.clear(); + mesh.polygons.vertex_indices.clear(); + mesh.polygons.flags.clear(); + + mesh.polygons.material_indices.reserve(triCount); + mesh.polygons.lightmap_indices.reserve(triCount); + mesh.polygons.feature_indices.reserve(triCount*3); + mesh.polygons.vertex_indices.reserve(triCount*3); + mesh.polygons.flags.reserve(triCount); + + for(const auto& poly:mesh.geometry) { + const size_t end = poly.index_offset + poly.index_count; + if(poly.index_count<3 || poly.material>=mesh.materials.size() || + end>mesh.polygon_vertex_indices.size() || end>mesh.polygon_feature_indices.size()) + continue; + + for(size_t i=2; i Resources::implLoadMeshMain(std::string name) { + if(FileExt::hasExt(name,"MSH")) { + const auto* entry = Resources::vdfsIndex().find(name); + if(entry == nullptr) + return nullptr; + + zenkit::Mesh zmsh; + auto reader = entry->open_read(); + zmsh.load(reader.get(),false); + if(isG1BarrierMesh(name)) + prepareG1BarrierMesh(zmsh); + triangulateStandaloneMesh(zmsh); + if(isG1BarrierMesh(name)) + addG1BarrierLayers(zmsh); + if(isG1BarrierMesh(name)) + makeMeshTwoSided(zmsh); + + if(zmsh.polygons.vertex_indices.empty()) + return nullptr; + + PackedMesh packed(zmsh,PackedMesh::PK_Visual); + if(packed.indices.empty() || packed.subMeshes.empty()) + return nullptr; + + return std::unique_ptr{new ProtoMesh(std::move(packed),name)}; + } + if(FileExt::hasExt(name,"3DS")) { FileExt::exchangeExt(name,"3DS","MRM"); diff --git a/game/world/world.cpp b/game/world/world.cpp index 670b2aad7..cfa52b7eb 100644 --- a/game/world/world.cpp +++ b/game/world/world.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include @@ -63,6 +65,25 @@ const char* materialTag(zenkit::MaterialGroup src) { return "UD"; } +static bool isWorldZen(std::string_view name) { + constexpr std::string_view worldZen = "world.zen"; + + const size_t slash = name.find_last_of("/\\"); + if(slash!=std::string_view::npos) + name.remove_prefix(slash+1); + + if(name.size()!=worldZen.size()) + return false; + + for(size_t i=0; i(name[i]))); + auto b = char(std::tolower(static_cast(worldZen[i]))); + if(a!=b) + return false; + } + return true; + } + World::World(GameSession& game, std::string_view file, bool startup, std::function loadProgress) :wname(std::move(file)), game(game), wsound(game,*this), wobj(*this) { const auto* entry = Resources::vdfsIndex().find(wname); @@ -108,6 +129,8 @@ World::World(GameSession& game, std::string_view file, bool startup, std::functi wdynamic = wdynamicFut.get(); loadProgress(70); + initG1Barrier(); + globFx.reset(new GlobalEffects(*this)); wmatrix.reset(new WayMatrix(*this, *world.way_net)); for(auto& vob:world.world_vobs) @@ -372,12 +395,126 @@ void World::scaleTime(uint64_t& dt) { globFx->scaleTime(dt); } +void World::initG1Barrier() { + if(version().game!=1 || !isWorldZen(wname)) + return; + + auto* proto = Resources::loadMesh("MAGICFRONTIER_OUT.MSH"); + if(proto==nullptr) + return; + + const auto* bbox = proto->bbox(); + if(bbox==nullptr) + return; + + G1Barrier barrier; + constexpr uint64_t ThunderDelay[4] = {8000,6000,14000,4000}; + for(size_t i=0; iisDead()) + return; + + const float rx = g1Barrier->radius.x; + if(g1Barrier->ambientSound.isEmpty()) { + g1Barrier->ambientSound = Sound(*this,Sound::T_Regular,"MFX_BARRIERE_AMBIENT.WAV",pl->position(),60000.f,true); + g1Barrier->ambientSound.setAmbient(true); + g1Barrier->ambientSound.setLooping(true); + g1Barrier->ambientSound.setVolume(0.25f); + g1Barrier->ambientSound.play(); + } else { + g1Barrier->ambientSound.setPosition(pl->position()); + } + + const float rz = g1Barrier->radius.z; + if(rx<=0.f || rz<=0.f) + return; + + { + constexpr uint64_t ThunderDelay[4] = {8000,6000,14000,4000}; + constexpr float QuadX[4] = { 1.f,-1.f,-1.f, 1.f}; + constexpr float QuadZ[4] = { 1.f, 1.f,-1.f,-1.f}; + constexpr float EdgeScale = 0.70f; + constexpr float HeightScale = 0.55f; + const uint64_t now = tickCount(); + for(size_t i=0; ithunderTimeout.size(); ++i) { + if(nowthunderTimeout[i]) + continue; + + auto at = g1Barrier->center; + at.x += g1Barrier->radius.x*QuadX[i]*EdgeScale; + at.y += g1Barrier->radius.y*HeightScale; + at.z += g1Barrier->radius.z*QuadZ[i]*EdgeScale; + + auto thunder = Sound(*this,Sound::T_Regular,"MFX_BARRIERE_SHOOT.WAV",at,60000.f,true); + thunder.setVolume(0.45f); + thunder.play(); + g1Barrier->thunderTimeout[i] = now + ThunderDelay[i]; + } + } + + const auto pos = pl->position(); + const float dx = pos.x - g1Barrier->center.x; + const float dz = pos.z - g1Barrier->center.z; + const float d = (dx*dx)/(rx*rx) + (dz*dz)/(rz*rz); + + constexpr float DamageThreshold = 0.99f; + if(d<=DamageThreshold) + return; + + if(d>1.f) { + auto clamped = pos; + const float s = 0.985f/std::sqrt(d); + clamped.x = g1Barrier->center.x + dx*s; + clamped.z = g1Barrier->center.z + dz*s; + if(pl->setPosition(clamped)) + pl->updateTransform(); + } + + if(tickCount()<=g1Barrier->damageTimeout) + return; + constexpr int32_t BarrierDamage = 25; + constexpr uint64_t BarrierRepeatDelay = 500; + const auto& hnpc = pl->handle(); + const int32_t protection = hnpc.protection[zenkit::DamageType::BARRIER]; + if(protection>=0) + pl->changeAttribute(ATR_HITPOINTS,-std::max(BarrierDamage-protection,0),false); + { + auto warning = Sound(*this,Sound::T_Regular,"MFX_BARRIERE_WARNING.WAV",pos,12000.f,true); + warning.setVolume(0.7f); + warning.play(); + } + g1Barrier->damageTimeout = tickCount() + BarrierRepeatDelay; + } + void World::tick(uint64_t dt) { static bool doTicks=true; if(!doTicks) return; wobj.tick(dt,dt); wdynamic->tick(dt); + tickG1Barrier(); wview->tick(dt); if(auto pl = player()) wsound.tick(*pl); diff --git a/game/world/world.h b/game/world/world.h index ae2719e2a..918f3fe58 100644 --- a/game/world/world.h +++ b/game/world/world.h @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -13,8 +14,10 @@ #include "graphics/meshobjects.h" #include "game/gamescript.h" #include "physics/dynamicworld.h" +#include "physics/physicmesh.h" #include "worldobjects.h" #include "worldsound.h" +#include "world/objects/sound.h" #include "waypoint.h" #include "waymatrix.h" @@ -222,7 +225,22 @@ class World final { auto roomAt(const zenkit::BspNode &node) -> std::string_view; auto portalAt(std::string_view tag) -> BspSector*; + void initG1Barrier(); + void tickG1Barrier(); + void initScripts(bool firstTime); Sound addHitEffect(std::string_view src, std::string_view reciver, std::string_view scheme, const Tempest::Matrix4x4& pos); + + struct G1Barrier final { + MeshObjects::Mesh visual; + PhysicMesh physic; + Tempest::Vec3 center; + Tempest::Vec3 radius; + uint64_t damageTimeout = 0; + Sound ambientSound; + std::array thunderTimeout = {}; + }; + + std::unique_ptr g1Barrier; }; diff --git a/shader/materials/main.frag b/shader/materials/main.frag index 10a84a477..c90a694c8 100644 --- a/shader/materials/main.frag +++ b/shader/materials/main.frag @@ -210,9 +210,159 @@ void mainForward(vec4 t) { #endif #if defined(EMISSIVE) +// --------------------------------------------------------------------------- +// Gothic-1 magic barrier: a procedural electric energy dome. +// +// The dome mesh is flat-shaded, so its per-triangle normals are useless as a +// pattern domain (they produce faceted blobs). Instead we derive a *smooth +// radial direction* from the bucket's bounding box centre: every fragment is +// turned into a unit-sphere direction, giving a seamless, scale-independent +// domain that wraps the whole dome regardless of its size in world units. +// +// Electricity is drawn as thin branching filaments: a fractal scalar field is +// domain-warped and we glow along its iso-contours with a 1/distance falloff, +// giving a bright hot core surrounded by a soft halo - the classic look of an +// electric arc - rather than soft noise clouds. +// +// The barrier is drawn as three coincident additive layers (SrcAlpha/One): +// layer 1 - the membrane (faint translucent shell + fresnel rim) +// layer 2 - the main bolts (thick, slow, branching arcs) +// layer 3 - the crackle (fine fast sparks) +// The on-screen contribution of each layer is color.rgb * alpha. +// --------------------------------------------------------------------------- + +float g1bHash(vec3 p) { + p = fract(p * vec3(0.1031, 0.1030, 0.0973)); + p += dot(p, p.yzx + 19.19); + return fract((p.x + p.y) * p.z); + } + +float g1bNoise(vec3 x) { + vec3 p = floor(x); + vec3 f = fract(x); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(mix(g1bHash(p + vec3(0,0,0)), g1bHash(p + vec3(1,0,0)), f.x), + mix(g1bHash(p + vec3(0,1,0)), g1bHash(p + vec3(1,1,0)), f.x), f.y), + mix(mix(g1bHash(p + vec3(0,0,1)), g1bHash(p + vec3(1,0,1)), f.x), + mix(g1bHash(p + vec3(0,1,1)), g1bHash(p + vec3(1,1,1)), f.x), f.y), f.z); + } + +float g1bFbm(vec3 p) { + float f = 0.0, amp = 0.5; + for(int i=0; i<4; ++i) { + f += amp * g1bNoise(p); + p *= 2.02; + amp *= 0.5; + } + return f; + } + +// Distance to the nearest electric filament. A fractal field is strongly +// domain-warped so its iso-contours become jagged, branching channels; the +// returned value is ~0 on a channel and grows away from it. +float g1bArcField(vec3 p) { + vec3 w = vec3(g1bNoise(p), g1bNoise(p + 4.7), g1bNoise(p + 9.2)); + p += (w - 0.5) * 2.4; // jagged branching displacement + float f = g1bFbm(p * 1.7); + return abs(f - 0.5); + } + +// Turn the filament distance into a glowing arc: a thin white-hot core with a +// soft falloff halo. 'width' controls thickness, 'sharp' the falloff. +float g1bArcGlow(float dist, float width, float sharp) { + return pow(width / (dist + width), sharp); + } + +// Patchy temporal flicker so different regions of the dome strike at different +// moments instead of pulsing as one - real lightning is intermittent. +float g1bFlicker(vec3 dir, float time) { + float n = g1bNoise(vec3(dir.xz * 3.0 + dir.y * 2.0, time)); + return smoothstep(0.45, 0.95, n); + } + void mainEmissive(vec4 t) { - vec3 color = textureEmmisive(t.rgb); - outColor = vec4(color,t.a); + vec3 color = textureEmmisive(t.rgb); + float alpha = t.a; + +#if !defined(SIMPLE_MAT) && (MESH_TYPE!=T_PFX) + if((bucket[bucketId].flags & BK_G1_BARRIER) != 0) { + // world-space fragment position, reconstructed from depth + vec2 ndc = (gl_FragCoord.xy*scene.screenResInv)*2.0 - vec2(1.0); + vec4 wpos = scene.viewProjectInv * vec4(ndc, gl_FragCoord.z, 1.0); + vec3 fragPos = wpos.xyz / wpos.w; + + // Smooth radial direction from the dome's bounding-box centre. The mesh is + // flat-shaded, so this - not the faceted vertex normal - is what gives a + // continuous pattern domain across the whole dome. + vec3 bmin = bucket[bucketId].bbox[0].xyz; + vec3 bmax = bucket[bucketId].bbox[1].xyz; + vec3 center = (bmin + bmax) * 0.5; + vec3 rad = max((bmax - bmin) * 0.5, vec3(1.0)); + vec3 dir = normalize((fragPos - center) / rad); + + vec3 view = normalize(scene.camPos - fragPos); + float ndv = abs(dot(view, dir)); + // grazing-angle fresnel: the dome silhouette glows brightest + float fres = pow(clamp(1.0 - ndv, 0.0, 1.0), 2.5); + + float time = float(scene.tickCount32 % 4194304u) * 0.001; + uint layer = bucket[bucketId].g1BarrierLayer; + + // electric blue-white palette + const vec3 cDeep = vec3(0.01, 0.06, 0.16); // shell tint + const vec3 cArc = vec3(0.15, 0.55, 1.00); // arc halo + const vec3 cHot = vec3(0.75, 0.92, 1.00); // near-core + const vec3 cCore = vec3(0.95, 0.99, 1.00); // white-hot core + + if(layer == 1u) { + // ---- membrane: a clearly visible, slowly drifting energy shell ------- + float e1 = g1bFbm(dir*2.0 + vec3(0.0, -time*0.04, 0.0)); + float e2 = g1bFbm(dir*4.0 + vec3(time*0.02, -time*0.03, 0.0)); + float energy = e1*0.6 + e2*0.4; + + vec3 tint = mix(cDeep, cArc, energy) * 1.2 + cHot*fres*0.9; + // a constant floor keeps the whole dome visible; energy + rim add shape + float glow = 0.30 + energy*0.30 + fres*0.70; + + color = tint; + alpha = clamp(glow, 0.0, 1.0); + } + else if(layer == 2u) { + // ---- main bolts: thick, stable, slowly travelling branching arcs ----- + vec3 p = dir*5.0 + vec3(0.0, -time*0.15, time*0.03); + float d = g1bArcField(p); + float bolt = g1bArcGlow(d, 0.025, 1.6); + // slow, smooth brightening - the arcs stay lit and gently pulse + bolt *= mix(0.55, 1.0, g1bFlicker(dir, time*0.8)); + + float core = smoothstep(0.55, 1.0, bolt); + color = cArc*bolt*1.4 + cCore*core*1.6; + alpha = clamp(bolt*1.1 + core, 0.0, 1.0) * mix(0.8, 1.0, fres); + } + else { + // ---- crackle: secondary finer arcs, drifting slowly ------------------ + vec3 p = dir*9.0 + vec3(3.3, -time*0.30, time*0.06); + float d = g1bArcField(p); + float crack = g1bArcGlow(d, 0.016, 1.8); + crack *= mix(0.35, 1.0, g1bFlicker(dir*2.0, time*1.6)); + + float core = smoothstep(0.6, 1.0, crack); + color = cHot*crack + cCore*core*1.4; + alpha = clamp(crack*0.85 + core, 0.0, 1.0); + } + + // The barrier blends additively, so the contribution is color*alpha. Fold + // it into a premultiplied colour and softly roll off the peak so several + // overlapping bright arcs cannot blow out to a flat white sheet. + vec3 contrib = color * alpha; + float peak = max(contrib.r, max(contrib.g, contrib.b)); + contrib *= 4.0 / max(4.0, peak); + color = contrib; + alpha = 1.0; + } +#endif + + outColor = vec4(color, alpha); } #endif diff --git a/shader/scene.glsl b/shader/scene.glsl index e526380b5..8f239e149 100644 --- a/shader/scene.glsl +++ b/shader/scene.glsl @@ -82,6 +82,7 @@ const uint BK_SOLID = 0x1; const uint BK_SKIN = 0x2; const uint BK_MORPH = 0x4; const uint BK_WATER = 0x8; +const uint BK_G1_BARRIER = 0x10; struct Bucket { vec4 bbox[2]; @@ -91,6 +92,7 @@ struct Bucket { float alphaWeight; float envMapping; uint flags; + uint g1BarrierLayer; }; #endif