From 0a9cb8a36f7fe0399f27220d02b7f6309d336ae1 Mon Sep 17 00:00:00 2001 From: flatsponge <104839509+flatsponge@users.noreply.github.com> Date: Tue, 26 May 2026 21:21:56 +0200 Subject: [PATCH 1/5] Implement Gothic 1 magic barrier --- game/physics/physicmesh.cpp | 6 ++- game/resources.cpp | 20 ++++++++ game/world/world.cpp | 96 +++++++++++++++++++++++++++++++++++++ game/world/world.h | 14 ++++++ 4 files changed, 135 insertions(+), 1 deletion(-) 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..9006565e7 100644 --- a/game/resources.cpp +++ b/game/resources.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include #include @@ -441,6 +442,25 @@ ProtoMesh* Resources::implLoadMesh(std::string_view name) { } std::unique_ptr 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(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..e38fc381c 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,85 @@ 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; + barrier.visual = addStaticView(proto,true); + barrier.physic = PhysicMesh(*proto,*wdynamic,false); + barrier.center = (bbox[0] + bbox[1]) * 0.5f; + barrier.radius = (bbox[1] - bbox[0]) * 0.5f; + + Tempest::Matrix4x4 identity; + identity.identity(); + barrier.visual.setObjMatrix(identity); + barrier.physic.setObjMatrix(identity); + + if(barrier.visual.isEmpty() && barrier.physic.isEmpty()) + return; + + g1Barrier.reset(new G1Barrier(std::move(barrier))); + } + +void World::tickG1Barrier() { + if(g1Barrier==nullptr) + return; + + auto* pl = player(); + if(pl==nullptr || pl->isDead()) + return; + + const float rx = g1Barrier->radius.x; + const float rz = g1Barrier->radius.z; + if(rx<=0.f || rz<=0.f) + return; + + 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); + 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..bca84b8a9 100644 --- a/game/world/world.h +++ b/game/world/world.h @@ -13,6 +13,7 @@ #include "graphics/meshobjects.h" #include "game/gamescript.h" #include "physics/dynamicworld.h" +#include "physics/physicmesh.h" #include "worldobjects.h" #include "worldsound.h" #include "waypoint.h" @@ -222,7 +223,20 @@ 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; + }; + + std::unique_ptr g1Barrier; }; From 15ed740fb45e9e727b0caab6045a6fdd763d63c8 Mon Sep 17 00:00:00 2001 From: flatsponge <104839509+flatsponge@users.noreply.github.com> Date: Wed, 27 May 2026 23:01:52 +0200 Subject: [PATCH 2/5] Save current progress --- game/game/compatibility/cpu32.cpp | 2 +- game/graphics/drawbuckets.cpp | 3 + game/graphics/drawbuckets.h | 3 +- game/graphics/material.cpp | 14 +++ game/graphics/material.h | 3 +- game/resources.cpp | 155 ++++++++++++++++++++++++++++++ game/world/world.cpp | 43 ++++++++- game/world/world.h | 4 + shader/materials/main.frag | 77 ++++++++++++++- shader/scene.glsl | 2 + 10 files changed, 300 insertions(+), 6 deletions(-) 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/resources.cpp b/game/resources.cpp index 9006565e7..532f45ed3 100644 --- a/game/resources.cpp +++ b/game/resources.cpp @@ -52,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) { 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; diff --git a/game/world/world.cpp b/game/world/world.cpp index e38fc381c..cfa52b7eb 100644 --- a/game/world/world.cpp +++ b/game/world/world.cpp @@ -408,6 +408,10 @@ void World::initG1Barrier() { return; G1Barrier barrier; + constexpr uint64_t ThunderDelay[4] = {8000,6000,14000,4000}; + for(size_t i=0; iradius.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; @@ -457,13 +494,17 @@ void World::tickG1Barrier() { 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; } diff --git a/game/world/world.h b/game/world/world.h index bca84b8a9..918f3fe58 100644 --- a/game/world/world.h +++ b/game/world/world.h @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -16,6 +17,7 @@ #include "physics/physicmesh.h" #include "worldobjects.h" #include "worldsound.h" +#include "world/objects/sound.h" #include "waypoint.h" #include "waymatrix.h" @@ -236,6 +238,8 @@ class World final { 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..a5840fda8 100644 --- a/shader/materials/main.frag +++ b/shader/materials/main.frag @@ -157,6 +157,21 @@ vec4 diffuseTex() { uint fract = scene.tickCount32 % abs(texAniMapDirPeriod.y); texAnim.y = float(fract)/float(texAniMapDirPeriod.y); } + +#if (MESH_TYPE!=T_PFX) + if((bucket[bucketId].flags & BK_G1_BARRIER) != 0) { + // Continuous UV drift on top of the per-frame texture swap so the + // lightning sheets feel alive instead of stamped on the sky dome. + float time = float(scene.tickCount32 % 65536u) * 0.001; + uint layer = bucket[bucketId].g1BarrierLayer; + vec2 drift = vec2(0); + if(layer==2u) + drift = vec2( 0.18, -0.11) * time; + else if(layer==3u) + drift = vec2(-0.13, 0.21) * time; + texAnim += fract(drift); + } +#endif } const vec2 uv = shInp.uv + texAnim; #else @@ -210,9 +225,67 @@ void mainForward(vec4 t) { #endif #if defined(EMISSIVE) +// Cheap hash → pseudo-random in [0,1) +float g1BarrierHash(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); + } + +// Per-region flicker that makes the dome read as electric arcs +// rather than a steady glow. Returns ~[0..1.6]. +float g1BarrierFlicker(vec3 worldPos, float time) { + // Quantize space so flicker varies across cells, not per pixel. + vec3 cell = floor(worldPos * 0.0015); + float h = g1BarrierHash(cell); + + // Two overlaid pulses at different rates, offset by hash. + float a = sin(time * 7.0 + h * 17.0); + float b = sin(time * 13.0 + h * 31.0); + float pulse = max(a, b); + pulse = pulse*pulse; // sharpen — most of the time low, occasional bright peaks + return 0.4 + pulse * 1.8; // floor + spike (so it's always visible, but really pops on peaks) + } + 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) { + // Reconstruct world-space position (MAT_POSITION is not available in + // the emissive variant) so we can do view-dependent + spatial effects. + 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; + + vec3 view = normalize(scene.camPos - fragPos); + vec3 normal = normalize(shInp.normal); + float ndv = abs(dot(view, normal)); + + // Soft rim: keep some visibility everywhere, much brighter at silhouette. + float rim = pow(1.0 - ndv, 2.0); + + // Electric flicker driven by world position + time. + float time = float(scene.tickCount32 % 65536u) * 0.001; + float flicker = g1BarrierFlicker(fragPos, time); + + // Layer 1 = base BARRIERE haze: subtle, no flicker. + // Layers 2/3 = MAGBA / MAGICFRONTIER lightning sheets: full electric behaviour. + uint layer = bucket[bucketId].g1BarrierLayer; + float intensity; + if(layer==1u) { + intensity = mix(0.15, 0.6, rim); + } else { + intensity = (0.35 + 0.65*rim) * flicker; + } + + alpha *= intensity; + color *= intensity; + } +#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 From 404505ac3dc0f6f84d7cdc9e48732daef2da448a Mon Sep 17 00:00:00 2001 From: flatsponge <104839509+flatsponge@users.noreply.github.com> Date: Wed, 27 May 2026 23:18:03 +0200 Subject: [PATCH 3/5] Save current magic barrier progress --- Gothic.ini | 5 ++ shader/materials/main.frag | 132 ++++++++++++++++++++++++++++--------- 2 files changed, 105 insertions(+), 32 deletions(-) create mode 100644 Gothic.ini diff --git a/Gothic.ini b/Gothic.ini new file mode 100644 index 000000000..3fcc1b64d --- /dev/null +++ b/Gothic.ini @@ -0,0 +1,5 @@ +[INTERNAL] + +vidResIndex=0 + + diff --git a/shader/materials/main.frag b/shader/materials/main.frag index a5840fda8..38312deb2 100644 --- a/shader/materials/main.frag +++ b/shader/materials/main.frag @@ -165,7 +165,9 @@ vec4 diffuseTex() { float time = float(scene.tickCount32 % 65536u) * 0.001; uint layer = bucket[bucketId].g1BarrierLayer; vec2 drift = vec2(0); - if(layer==2u) + if(layer==1u) + drift = vec2( 0.05, 0.03) * time; + else if(layer==2u) drift = vec2( 0.18, -0.11) * time; else if(layer==3u) drift = vec2(-0.13, 0.21) * time; @@ -232,29 +234,56 @@ float g1BarrierHash(vec3 p) { return fract((p.x + p.y) * p.z); } -// Per-region flicker that makes the dome read as electric arcs -// rather than a steady glow. Returns ~[0..1.6]. -float g1BarrierFlicker(vec3 worldPos, float time) { - // Quantize space so flicker varies across cells, not per pixel. - vec3 cell = floor(worldPos * 0.0015); - float h = g1BarrierHash(cell); - - // Two overlaid pulses at different rates, offset by hash. - float a = sin(time * 7.0 + h * 17.0); - float b = sin(time * 13.0 + h * 31.0); - float pulse = max(a, b); - pulse = pulse*pulse; // sharpen — most of the time low, occasional bright peaks - return 0.4 + pulse * 1.8; // floor + spike (so it's always visible, but really pops on peaks) +// Smooth 3D value noise for continuous energy waves +float g1BarrierNoise(vec3 x) { + vec3 p = floor(x); + vec3 f = fract(x); + f = f * f * (3.0 - 2.0 * f); + + return mix(mix(mix( g1BarrierHash(p + vec3(0,0,0)), + g1BarrierHash(p + vec3(1,0,0)), f.x), + mix( g1BarrierHash(p + vec3(0,1,0)), + g1BarrierHash(p + vec3(1,1,0)), f.x), f.y), + mix(mix( g1BarrierHash(p + vec3(0,0,1)), + g1BarrierHash(p + vec3(1,0,1)), f.x), + mix( g1BarrierHash(p + vec3(0,1,1)), + g1BarrierHash(p + vec3(1,1,1)), f.x), f.y), f.z); } +float g1BarrierFBM(vec3 p) { + float f = 0.0; + float amp = 0.5; + for(int i=0; i<4; ++i) { + f += amp * g1BarrierNoise(p); + p *= 2.0; + amp *= 0.5; + } + return f; +} + +float g1BarrierRidged(vec3 p) { + float sum = 0.0; + float amp = 0.5; + float weight = 1.0; + for(int i=0; i<3; ++i) { + float n = g1BarrierNoise(p) * 2.0 - 1.0; + n = 1.0 - abs(n); + n = n * n; + sum += n * amp * weight; + weight = n; + p *= 2.0; + amp *= 0.5; + } + return sum; +} + void mainEmissive(vec4 t) { 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) { - // Reconstruct world-space position (MAT_POSITION is not available in - // the emissive variant) so we can do view-dependent + spatial effects. + // Reconstruct world-space position 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; @@ -263,30 +292,69 @@ void mainEmissive(vec4 t) { vec3 normal = normalize(shInp.normal); float ndv = abs(dot(view, normal)); - // Soft rim: keep some visibility everywhere, much brighter at silhouette. - float rim = pow(1.0 - ndv, 2.0); + float rim = pow(1.0 - ndv, 2.5); - // Electric flicker driven by world position + time. - float time = float(scene.tickCount32 % 65536u) * 0.001; - float flicker = g1BarrierFlicker(fragPos, time); + // time without rapid wrapping to prevent noise popping + float time = float(scene.tickCount32 % 8388608u) * 0.001; - // Layer 1 = base BARRIERE haze: subtle, no flicker. - // Layers 2/3 = MAGBA / MAGICFRONTIER lightning sheets: full electric behaviour. uint layer = bucket[bucketId].g1BarrierLayer; - float intensity; - if(layer==1u) { - intensity = mix(0.15, 0.6, rim); - } else { - intensity = (0.35 + 0.65*rim) * flicker; - } - alpha *= intensity; - color *= intensity; + // Electric blue/cyan palette + vec3 colorDark = vec3(0.02, 0.1, 0.4); + vec3 colorMid = vec3(0.1, 0.4, 0.9); + vec3 colorBright = vec3(0.6, 0.9, 1.0); + + if (layer == 1u) { + // Base layer: Deep, slow-moving energy clouds + vec3 p = fragPos * 0.00015; + p.y += time * 0.015; // Slow upward drift + + float noiseVal = g1BarrierFBM(p + vec3(time * 0.005, 0.0, time * 0.005)); + + // Blend colors based on noise + vec3 cloudColor = mix(colorDark, colorMid, noiseVal); + + // Intensity boosts at the rim + float intensity = mix(0.3, 1.0, rim) * (noiseVal + 0.2); + + color = cloudColor * intensity * 2.0; + alpha = intensity; + + } else if (layer == 2u || layer == 3u) { + // Lightning layers: Sharp, crackling arcs of energy + vec3 p = fragPos * 0.0001; + + // Offset and scale time based on layer + float t = time * (layer == 2u ? 0.04 : 0.06); + p.xz += (layer == 2u ? 50.0 : -50.0); + + // Fast sweeping motion + p.y -= t * 0.8; + p.x += t * 0.2; + + // Ridged noise creates sharp lines + float r = g1BarrierRidged(p + vec3(0.0, t * 0.2, 0.0)); + + // Sharpen the arcs + float arc = pow(r, 4.0) * 4.0; + + // Global pulsing to make it feel alive and crackling + float pulse = sin(time * 5.0 + fragPos.y * 0.002) * 0.5 + 0.5; + float crackle = g1BarrierNoise(vec3(time * 10.0, float(layer), 0.0)); + arc *= mix(0.5, 1.5, pulse * crackle); + + // Keep them bright on the edges + float intensity = arc * mix(0.2, 1.5, rim); + + // Inner hot core of the lightning + color = mix(colorMid, colorBright, clamp(arc * 0.5, 0.0, 1.0)) * intensity * 3.0; + alpha = intensity; } + } #endif outColor = vec4(color, alpha); - } +} #endif #if defined(GHOST) From 48974b3af89724150e13cdfa67585184e7c1626a Mon Sep 17 00:00:00 2001 From: flatsponge <104839509+flatsponge@users.noreply.github.com> Date: Wed, 27 May 2026 23:20:44 +0200 Subject: [PATCH 4/5] Remove accidental Gothic.ini --- Gothic.ini | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 Gothic.ini diff --git a/Gothic.ini b/Gothic.ini deleted file mode 100644 index 3fcc1b64d..000000000 --- a/Gothic.ini +++ /dev/null @@ -1,5 +0,0 @@ -[INTERNAL] - -vidResIndex=0 - - From dc17474e589a4d9d1426968ab4282f5dd5c80997 Mon Sep 17 00:00:00 2001 From: flatsponge <104839509+flatsponge@users.noreply.github.com> Date: Fri, 29 May 2026 14:10:06 +0200 Subject: [PATCH 5/5] Improve G1 magic barrier shader --- shader/materials/main.frag | 257 +++++++++++++++++++------------------ 1 file changed, 133 insertions(+), 124 deletions(-) diff --git a/shader/materials/main.frag b/shader/materials/main.frag index 38312deb2..c90a694c8 100644 --- a/shader/materials/main.frag +++ b/shader/materials/main.frag @@ -157,23 +157,6 @@ vec4 diffuseTex() { uint fract = scene.tickCount32 % abs(texAniMapDirPeriod.y); texAnim.y = float(fract)/float(texAniMapDirPeriod.y); } - -#if (MESH_TYPE!=T_PFX) - if((bucket[bucketId].flags & BK_G1_BARRIER) != 0) { - // Continuous UV drift on top of the per-frame texture swap so the - // lightning sheets feel alive instead of stamped on the sky dome. - float time = float(scene.tickCount32 % 65536u) * 0.001; - uint layer = bucket[bucketId].g1BarrierLayer; - vec2 drift = vec2(0); - if(layer==1u) - drift = vec2( 0.05, 0.03) * time; - else if(layer==2u) - drift = vec2( 0.18, -0.11) * time; - else if(layer==3u) - drift = vec2(-0.13, 0.21) * time; - texAnim += fract(drift); - } -#endif } const vec2 uv = shInp.uv + texAnim; #else @@ -227,55 +210,75 @@ void mainForward(vec4 t) { #endif #if defined(EMISSIVE) -// Cheap hash → pseudo-random in [0,1) -float g1BarrierHash(vec3 p) { - p = fract(p * vec3(0.1031, 0.1030, 0.0973)); +// --------------------------------------------------------------------------- +// 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); } -// Smooth 3D value noise for continuous energy waves -float g1BarrierNoise(vec3 x) { +float g1bNoise(vec3 x) { vec3 p = floor(x); vec3 f = fract(x); f = f * f * (3.0 - 2.0 * f); - - return mix(mix(mix( g1BarrierHash(p + vec3(0,0,0)), - g1BarrierHash(p + vec3(1,0,0)), f.x), - mix( g1BarrierHash(p + vec3(0,1,0)), - g1BarrierHash(p + vec3(1,1,0)), f.x), f.y), - mix(mix( g1BarrierHash(p + vec3(0,0,1)), - g1BarrierHash(p + vec3(1,0,1)), f.x), - mix( g1BarrierHash(p + vec3(0,1,1)), - g1BarrierHash(p + vec3(1,1,1)), f.x), f.y), f.z); + 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 g1BarrierFBM(vec3 p) { - float f = 0.0; - float amp = 0.5; - for(int i=0; i<4; ++i) { - f += amp * g1BarrierNoise(p); - p *= 2.0; - amp *= 0.5; +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; -} - -float g1BarrierRidged(vec3 p) { - float sum = 0.0; - float amp = 0.5; - float weight = 1.0; - for(int i=0; i<3; ++i) { - float n = g1BarrierNoise(p) * 2.0 - 1.0; - n = 1.0 - abs(n); - n = n * n; - sum += n * amp * weight; - weight = n; - p *= 2.0; - amp *= 0.5; - } - return sum; -} + 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); @@ -283,78 +286,84 @@ void mainEmissive(vec4 t) { #if !defined(SIMPLE_MAT) && (MESH_TYPE!=T_PFX) if((bucket[bucketId].flags & BK_G1_BARRIER) != 0) { - // Reconstruct world-space position - 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; - - vec3 view = normalize(scene.camPos - fragPos); - vec3 normal = normalize(shInp.normal); - float ndv = abs(dot(view, normal)); - - float rim = pow(1.0 - ndv, 2.5); - - // time without rapid wrapping to prevent noise popping - float time = float(scene.tickCount32 % 8388608u) * 0.001; - - uint layer = bucket[bucketId].g1BarrierLayer; - - // Electric blue/cyan palette - vec3 colorDark = vec3(0.02, 0.1, 0.4); - vec3 colorMid = vec3(0.1, 0.4, 0.9); - vec3 colorBright = vec3(0.6, 0.9, 1.0); - - if (layer == 1u) { - // Base layer: Deep, slow-moving energy clouds - vec3 p = fragPos * 0.00015; - p.y += time * 0.015; // Slow upward drift - - float noiseVal = g1BarrierFBM(p + vec3(time * 0.005, 0.0, time * 0.005)); - - // Blend colors based on noise - vec3 cloudColor = mix(colorDark, colorMid, noiseVal); - - // Intensity boosts at the rim - float intensity = mix(0.3, 1.0, rim) * (noiseVal + 0.2); - - color = cloudColor * intensity * 2.0; - alpha = intensity; - - } else if (layer == 2u || layer == 3u) { - // Lightning layers: Sharp, crackling arcs of energy - vec3 p = fragPos * 0.0001; - - // Offset and scale time based on layer - float t = time * (layer == 2u ? 0.04 : 0.06); - p.xz += (layer == 2u ? 50.0 : -50.0); - - // Fast sweeping motion - p.y -= t * 0.8; - p.x += t * 0.2; - - // Ridged noise creates sharp lines - float r = g1BarrierRidged(p + vec3(0.0, t * 0.2, 0.0)); - - // Sharpen the arcs - float arc = pow(r, 4.0) * 4.0; - - // Global pulsing to make it feel alive and crackling - float pulse = sin(time * 5.0 + fragPos.y * 0.002) * 0.5 + 0.5; - float crackle = g1BarrierNoise(vec3(time * 10.0, float(layer), 0.0)); - arc *= mix(0.5, 1.5, pulse * crackle); - - // Keep them bright on the edges - float intensity = arc * mix(0.2, 1.5, rim); - - // Inner hot core of the lightning - color = mix(colorMid, colorBright, clamp(arc * 0.5, 0.0, 1.0)) * intensity * 3.0; - alpha = intensity; + // 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 #if defined(GHOST)