From e2d3e03f59558d093584fc5a9e4de85363ef8e8f Mon Sep 17 00:00:00 2001 From: XProger Date: Tue, 8 Nov 2016 02:22:40 +0300 Subject: [PATCH] #7 draw/hide weapons (hand animations and mesh swap), shotting, muzzle flash, tracing bullets for walls #8 combat mode, underwater sound --- bin/OpenLara.exe | Bin 94720 -> 100352 bytes src/camera.h | 230 ++------------- src/controller.h | 233 ++++++++++++++- src/format.h | 39 ++- src/frustum.h | 152 ++++++++++ src/lara.h | 474 ++++++++++++++++++++---------- src/level.h | 178 ++--------- src/platform/web/index.html | 2 +- src/platform/win/OpenLara.vcxproj | 1 + src/platform/win/main.cpp | 2 +- src/utils.h | 11 + 11 files changed, 785 insertions(+), 537 deletions(-) create mode 100644 src/frustum.h diff --git a/bin/OpenLara.exe b/bin/OpenLara.exe index 49b3094f70d338deee829338c86d0bd4b5ddec6f..c350d1d5708122a7fcc42b9994ff0c756b4aad2f 100644 GIT binary patch delta 36084 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 delta 31168 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%0DWpbs!mIsv`f1u|P`2wrBmQKs`$@M)#s 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⋙x;!5s`!y*;> z`wHuV#lorH><8O{b)W~Z4lMe-z>2I mAFi*i|IGsX|Ka)ym(s5n*v7eQxOX+ua`o~?&04OH`@aCLg;S&e diff --git a/src/camera.h b/src/camera.h index 954f4b2..f66efd7 100644 --- a/src/camera.h +++ b/src/camera.h @@ -2,158 +2,12 @@ #define H_CAMERA #include "core.h" +#include "frustum.h" #include "controller.h" #include "lara.h" -#define MAX_CLIP_PLANES 16 - #define CAMERA_OFFSET (1024.0f + 256.0f) -struct Frustum { - - struct Poly { - vec3 vertices[MAX_CLIP_PLANES]; - int count; - }; - - vec3 pos; - vec4 planes[MAX_CLIP_PLANES * 2]; // + buffer for OBB visibility test - int start, count; -#ifdef _DEBUG - int dbg; - Poly debugPoly; -#endif - - void calcPlanes(const mat4 &m) { - #ifdef _DEBUG - dbg = 0; - #endif - start = 0; - count = 5; - planes[0] = vec4(m.e30 - m.e20, m.e31 - m.e21, m.e32 - m.e22, m.e33 - m.e23); // near - planes[1] = vec4(m.e30 - m.e10, m.e31 - m.e11, m.e32 - m.e12, m.e33 - m.e13); // top - planes[2] = vec4(m.e30 - m.e00, m.e31 - m.e01, m.e32 - m.e02, m.e33 - m.e03); // right - planes[3] = vec4(m.e30 + m.e10, m.e31 + m.e11, m.e32 + m.e12, m.e33 + m.e13); // bottom - planes[4] = vec4(m.e30 + m.e00, m.e31 + m.e01, m.e32 + m.e02, m.e33 + m.e03); // left - for (int i = 0; i < count; i++) - planes[i] *= 1.0f / planes[i].xyz.length(); - } - - void calcPlanes(const Poly &poly) { - count = 1 + poly.count; // add one for near plane (not changing) - ASSERT(count < MAX_CLIP_PLANES); - if (count < 4) return; - - vec3 e1 = poly.vertices[0] - pos; - for (int i = 1; i < count; i++) { - vec3 e2 = poly.vertices[i % poly.count] - pos; - planes[i].xyz = e1.cross(e2).normal(); - planes[i].w = -(pos.dot(planes[i].xyz)); - e1 = e2; - } - } - - void clipPlane(const Poly &src, Poly &dst, const vec4 &plane) { - dst.count = 0; - if (!src.count) return; - - float t1 = src.vertices[0].dot(plane.xyz) + plane.w; - - for (int i = 0; i < src.count; i++) { - const vec3 &v1 = src.vertices[i]; - const vec3 &v2 = src.vertices[(i + 1) % src.count]; - - float t2 = v2.dot(plane.xyz) + plane.w; - - // hack for big float numbers - int s1 = (int)t1; - int s2 = (int)t2; - - if (s1 >= 0) { - dst.vertices[dst.count++] = v1; - ASSERT(dst.count < MAX_CLIP_PLANES); - } - - if ((s1 ^ s2) < 0) { // check for opposite signs - float k1 = t2 / (t2 - t1); - float k2 = t1 / (t2 - t1); - dst.vertices[dst.count++] = v1 * (float)k1 - v2 * (float)k2; - ASSERT(dst.count < MAX_CLIP_PLANES); - } - - t1 = t2; - } - } - - bool clipByPortal(const vec3 *vertices, int vCount, const vec3 &normal) { - if (normal.dot(pos - vertices[0]) < 0.0f) // check portal winding order - return false; - - Poly poly[2]; - - poly[0].count = vCount; - memmove(poly[0].vertices, vertices, sizeof(vec3) * poly[0].count); -#ifdef _DEBUG - debugPoly.count = 0; -#endif - int j = 0; - for (int i = 1; i < count; i++, j ^= 1) - clipPlane(poly[j], poly[j ^ 1], planes[i]); - - calcPlanes(poly[j]); - return count >= 4; - } - - // AABB visibility check - bool isVisible(const vec3 &min, const vec3 &max) const { - if (count < 4) return false; - - for (int i = start; i < start + count; i++) { - const vec3 &n = planes[i].xyz; - const float d = -planes[i].w; - - if (n.dot(max) < d && - n.dot(min) < d && - n.dot(vec3(min.x, max.y, max.z)) < d && - n.dot(vec3(max.x, min.y, max.z)) < d && - n.dot(vec3(min.x, min.y, max.z)) < d && - n.dot(vec3(max.x, max.y, min.z)) < d && - n.dot(vec3(min.x, max.y, min.z)) < d && - n.dot(vec3(max.x, min.y, min.z)) < d) - return false; - } - return true; - } - - // OBB visibility check - bool isVisible(const mat4 &matrix, const vec3 &min, const vec3 &max) { - start = count; - // transform clip planes (relative) - mat4 m = matrix.inverse(); - for (int i = 0; i < count; i++) { - vec4 &p = planes[i]; - vec4 o = m * vec4(p.xyz * (-p.w), 1.0f); - vec4 n = m * vec4(p.xyz, 0.0f); - planes[start + i] = vec4(n.xyz, -n.xyz.dot(o.xyz)); - } - bool visible = isVisible(min, max); - start = 0; - return visible; - } - - // Sphere visibility check - bool isVisible(const vec3 ¢er, float radius) { - if (count < 4) return false; - - for (int i = 0; i < count; i++) - if (planes[i].xyz.dot(center) + planes[i].w < -radius) - return false; - return true; - } - -}; - - struct Camera : Controller { Lara *owner; Frustum *frustum; @@ -165,6 +19,7 @@ struct Camera : Controller { float timer; int actTargetEntity, actCamera; + vec3 viewOffset; Camera(TR::Level *level, Lara *owner) : Controller(level, owner ? owner->entity : 0), owner(owner), frustum(new Frustum()), timer(0.0f), actTargetEntity(-1), actCamera(-1) { fov = 75.0f; @@ -176,6 +31,7 @@ struct Camera : Controller { room = owner->getEntity().room; pos = pos - owner->getDir() * 1024.0f; } + viewOffset = owner->getViewOffset(); } virtual ~Camera() { @@ -234,8 +90,11 @@ struct Camera : Controller { angle.z = 0.0f; //angle.x = min(max(angle.x, -80 * DEG2RAD), 80 * DEG2RAD); + float lerpFactor = (actTargetEntity == -1) ? 4.0f : 10.0f; + viewOffset = viewOffset.lerp(owner->getViewOffset(), Core::deltaTime * lerpFactor); + vec3 dir; - target = vec3(owner->pos.x, owner->pos.y, owner->pos.z) + owner->getViewOffset(); + target = vec3(owner->pos.x, owner->pos.y, owner->pos.z) + viewOffset; if (actCamera > -1) { TR::Camera &c = level->cameras[actCamera]; @@ -253,24 +112,25 @@ struct Camera : Controller { } else dir = getDir(); - if (owner->state != Lara::STATE_BACK_JUMP || actTargetEntity > -1) { + int destRoom; + if ((owner->wpnState != Lara::Weapon::IS_HIDDEN || owner->state != Lara::STATE_BACK_JUMP) || actTargetEntity > -1) { vec3 eye = target - dir * CAMERA_OFFSET; - destPos = trace(owner->getRoomIndex(), target, eye); + destPos = trace(owner->getRoomIndex(), target, eye, destRoom, true); lastDest = destPos; } else { vec3 eye = lastDest + dir.cross(vec3(0, 1, 0)).normal() * 2048.0f - vec3(0.0f, 512.0f, 0.0f); - destPos = trace(owner->getRoomIndex(), target, eye); + destPos = trace(owner->getRoomIndex(), target, eye, destRoom, true); } } - float lerpFactor = (actTargetEntity == -1) ? 2.0f : 10.0f; - pos = pos.lerp(destPos, Core::deltaTime * lerpFactor); if (actCamera <= -1) { TR::Level::FloorInfo info; level->getFloorInfo(room, (int)pos.x, (int)pos.z, info); + int lastRoom = room; + if (info.roomNext != 255) room = info.roomNext; @@ -289,72 +149,16 @@ struct Camera : Controller { if (info.floor != 0xffff8100) pos.y = info.floor; } + + // play underwater sound when camera goes under water + if (lastRoom != room && !level->rooms[lastRoom].flags.water && level->rooms[room].flags.water) + playSound(TR::SND_UNDERWATER, vec3(0.0f), 0); } mViewInv = mat4(pos, target, vec3(0, -1, 0)); Sound::listener.matrix = mViewInv; } - vec3 trace(int fromRoom, const vec3 &from, const vec3 &to) { // TODO: use Bresenham - int room = fromRoom; - - vec3 pos = from, dir = to - from; - int px = (int)pos.x, py = (int)pos.y, pz = (int)pos.z; - - float dist = dir.length(); - dir = dir * (1.0f / dist); - - int lr = -1, lx = -1, lz = -1; - TR::Level::FloorInfo info; - while (dist > 1.0f) { - int sx = px / 1024 * 1024 + 512, - sz = pz / 1024 * 1024 + 512; - - if (lr != room || lx != sx || lz != sz) { - level->getFloorInfo(room, sx, sz, info); - if (info.roomNext != 0xFF) { - room = info.roomNext; - level->getFloorInfo(room, sx, sz, info); - } - lr = room; - lx = sx; - lz = sz; - } - - if (py > info.floor && info.roomBelow != 0xFF) - room = info.roomBelow; - else if (py < info.ceiling && info.roomAbove != 0xFF) - room = info.roomAbove; - else if (py > info.floor || py < info.ceiling) { - int minX = px / 1024 * 1024; - int minZ = pz / 1024 * 1024; - int maxX = minX + 1024; - int maxZ = minZ + 1024; - - pos = vec3(clamp(px, minX, maxX), pos.y, clamp(pz, minZ, maxZ)) + boxNormal(px, pz) * 256.0f; - dir = (pos - from).normal(); - } - - float d = min(dist, 128.0f); // STEP = 128 - dist -= d; - pos = pos + dir * d; - - px = (int)pos.x, py = (int)pos.y, pz = (int)pos.z; - } - - return pos; - } - - vec3 boxNormal(int x, int z) { - x %= 1024; - z %= 1024; - - if (x > 1024 - z) - return x < z ? vec3(0, 0, 1) : vec3(1, 0, 0); - else - return x < z ? vec3(-1, 0, 0) : vec3(0, 0, -1); - } - virtual void setup() { Core::mViewInv = mViewInv; Core::mView = Core::mViewInv.inverse(); diff --git a/src/controller.h b/src/controller.h index b286e72..02aac82 100644 --- a/src/controller.h +++ b/src/controller.h @@ -2,6 +2,8 @@ #define H_CONTROLLER #include "format.h" +#include "frustum.h" +#include "mesh.h" #define GRAVITY 6.0f #define NO_OVERLAP 0x7FFFFFFF @@ -43,6 +45,9 @@ struct Controller { int *meshes; int mCount; + quat *animOverrides; // left & right arms animation frames + int animOverrideMask; + mat4 *joints; struct ActionCommand { TR::Action action; @@ -54,7 +59,7 @@ struct Controller { ActionCommand(TR::Action action, int value, float timer, ActionCommand *next = NULL) : action(action), value(value), timer(timer), next(next) {} } *actionCommand; - Controller(TR::Level *level, int entity) : level(level), entity(entity), velocity(0.0f), animTime(0.0f), animPrevFrame(0), health(100), turnTime(0.0f), actionCommand(NULL) { + Controller(TR::Level *level, int entity) : level(level), entity(entity), velocity(0.0f), animTime(0.0f), animPrevFrame(0), health(100), turnTime(0.0f), actionCommand(NULL), mCount(0), meshes(NULL), animOverrides(NULL), animOverrideMask(0), joints(NULL) { TR::Entity &e = getEntity(); pos = vec3((float)e.x, (float)e.y, (float)e.z); angle = vec3(0.0f, e.rotation, 0.0f); @@ -62,14 +67,25 @@ struct Controller { animIndex = e.modelIndex > 0 ? getModel().animation : 0; state = level->anims[animIndex].state; TR::Model &model = getModel(); - mCount = model.mCount; - meshes = mCount ? new int[mCount] : NULL; - for (int i = 0; i < mCount; i++) - meshes[i] = model.mStart + i; } virtual ~Controller() { delete[] meshes; + delete[] animOverrides; + delete[] joints; + } + + void initMeshOverrides() { + TR::Model &model = getModel(); + mCount = model.mCount; + meshes = mCount ? new int[mCount] : NULL; + for (int i = 0; i < mCount; i++) + meshes[i] = model.mStart + i; + + animOverrides = new quat[model.mCount]; + animOverrideMask = 0; + + joints = new mat4[model.mCount]; } void meshSwap(TR::Model &model, int mask) { @@ -269,6 +285,77 @@ struct Controller { return box; } + vec3 trace(int fromRoom, const vec3 &from, const vec3 &to, int &room, bool isCamera) { // TODO: use Bresenham + room = fromRoom; + + vec3 pos = from, dir = to - from; + int px = (int)pos.x, py = (int)pos.y, pz = (int)pos.z; + + float dist = dir.length(); + dir = dir * (1.0f / dist); + + int lr = -1, lx = -1, lz = -1; + TR::Level::FloorInfo info; + while (dist > 1.0f) { + int sx = px / 1024 * 1024 + 512, + sz = pz / 1024 * 1024 + 512; + + if (lr != room || lx != sx || lz != sz) { + level->getFloorInfo(room, sx, sz, info); + if (info.roomNext != 0xFF) { + room = info.roomNext; + level->getFloorInfo(room, sx, sz, info); + } + lr = room; + lx = sx; + lz = sz; + } + + if (isCamera) { + if (py > info.floor && info.roomBelow != 0xFF) + room = info.roomBelow; + else if (py < info.ceiling && info.roomAbove != 0xFF) + room = info.roomAbove; + else if (py > info.floor || py < info.ceiling) { + int minX = px / 1024 * 1024; + int minZ = pz / 1024 * 1024; + int maxX = minX + 1024; + int maxZ = minZ + 1024; + + pos = vec3(clamp(px, minX, maxX), pos.y, clamp(pz, minZ, maxZ)) + boxNormal(px, pz) * 256.0f; + dir = (pos - from).normal(); + } + } else { + if (py > info.floor) { + if (info.roomBelow != 0xFF) + room = info.roomBelow; + else + break; + } + + if (py < info.ceiling) { + if (info.roomAbove != 0xFF) + room = info.roomAbove; + else + break; + } + } + + float d = min(dist, 32.0f); // STEP = 32 + dist -= d; + pos = pos + dir * d; + + px = (int)pos.x, py = (int)pos.y, pz = (int)pos.z; + } + + return pos; + } + + void doBubbles() { + if (rand() % 10 <= 6) return; + playSound(TR::SND_BUBBLE, pos, Sound::Flags::PAN); + } + void collide() { TR::Entity &entity = getEntity(); @@ -482,7 +569,7 @@ struct Controller { if (cmd == TR::ANIM_CMD_EFFECT) { switch (id) { case TR::EFFECT_ROTATE_180 : angle.y = angle.y + PI; break; - case TR::EFFECT_LARA_BUBBLES : if (rand() % 10 > 6) playSound(TR::SND_BUBBLE, pos, Sound::Flags::PAN); break; + case TR::EFFECT_LARA_BUBBLES : doBubbles(); break; case TR::EFFECT_LARA_HANDSFREE : break; default : LOG("unknown special cmd %d (anim %d)\n", id, animIndex); } @@ -511,6 +598,122 @@ struct Controller { updateVelocity(); updateEnd(); } + + void renderMesh(MeshBuilder *mesh, uint32 offsetIndex) { + MeshBuilder::MeshInfo *m = mesh->meshMap[offsetIndex]; + if (!m) return; // invisible mesh (offsetIndex > 0 && level.meshOffsets[offsetIndex] == 0) camera target entity etc. + + Core::active.shader->setParam(uModel, Core::mModel); + mesh->renderMesh(m); + } + + void renderShadow(MeshBuilder *mesh, const vec3 &pos, const vec3 &offset, const vec3 &size, float angle) { + mat4 m; + m.identity(); + m.translate(pos); + m.rotateY(angle); + m.translate(vec3(offset.x, 0.0f, offset.z)); + m.scale(vec3(size.x, 0.0f, size.z) * (1.0f / 1024.0f)); + + Core::active.shader->setParam(uModel, m); + Core::active.shader->setParam(uColor, vec4(0.0f, 0.0f, 0.0f, 0.5f)); + mesh->renderShadowSpot(); + } + + virtual void render(Frustum *frustum, MeshBuilder *mesh) { + PROFILE_MARKER("MDL"); + TR::Entity &entity = getEntity(); + TR::Model &model = getModel(); + + TR::Animation *anim; + float fTime; + vec3 angle; + + Controller *controller = (Controller*)entity.controller; + + anim = &level->anims[controller->animIndex]; + angle = controller->angle; + fTime = controller->animTime; + + if (angle.y != 0.0f) Core::mModel.rotateY(angle.y); + if (angle.x != 0.0f) Core::mModel.rotateX(angle.x); + if (angle.z != 0.0f) Core::mModel.rotateZ(angle.z); + + float k = fTime * 30.0f / anim->frameRate; + int fIndex = (int)k; + int fCount = (anim->frameEnd - anim->frameStart) / anim->frameRate + 1; + + int fSize = sizeof(TR::AnimFrame) + model.mCount * sizeof(uint16) * 2; + k = k - fIndex; + + int fIndexA = fIndex % fCount, fIndexB = (fIndex + 1) % fCount; + TR::AnimFrame *frameA = (TR::AnimFrame*)&level->frameData[(anim->frameOffset + fIndexA * fSize) >> 1]; + + TR::Animation *nextAnim = NULL; + + vec3 move(0.0f); + if (fIndexB == 0) { + move = getAnimMove(); + nextAnim = &level->anims[anim->nextAnimation]; + fIndexB = (anim->nextFrame - nextAnim->frameStart) / nextAnim->frameRate; + } else + nextAnim = anim; + + TR::AnimFrame *frameB = (TR::AnimFrame*)&level->frameData[(nextAnim->frameOffset + fIndexB * fSize) >> 1]; + + vec3 bmin = frameA->box.min().lerp(frameB->box.min(), k); + vec3 bmax = frameA->box.max().lerp(frameB->box.max(), k); + if (frustum && !frustum->isVisible(Core::mModel, bmin, bmax)) + return; + + TR::Node *node = (int)model.node < level->nodesDataSize ? (TR::Node*)&level->nodesData[model.node] : NULL; + + mat4 m; + m.identity(); + m.translate(((vec3)frameA->pos).lerp(move + frameB->pos, k)); + + int sIndex = 0; + mat4 stack[20]; + + for (int i = 0; i < model.mCount; i++) { + + if (i > 0 && node) { + TR::Node &t = node[i - 1]; + + if (t.flags & 0x01) m = stack[--sIndex]; + if (t.flags & 0x02) stack[sIndex++] = m; + + ASSERT(sIndex >= 0 && sIndex < 20); + + m.translate(vec3(t.x, t.y, t.z)); + } + + quat q; + if (animOverrideMask & (1 << i)) + q = animOverrides[i]; + else + q = lerpAngle(frameA->getAngle(i), frameB->getAngle(i), k); + m = m * mat4(q, vec3(0.0f)); + + mat4 tmp = Core::mModel; + Core::mModel = Core::mModel * m; + if (meshes) + renderMesh(mesh, meshes[i]); + else + renderMesh(mesh, model.mStart + i); + + if (joints) + joints[i] = Core::mModel; + + Core::mModel = tmp; + } + + if (TR::castShadow(entity.type)) { + TR::Level::FloorInfo info; + level->getFloorInfo(entity.room, entity.x, entity.z, info, true); + renderShadow(mesh, vec3(entity.x, info.floor - 16.0f, entity.z), (bmax + bmin) * 0.5f, (bmax - bmin) * 0.8f, entity.rotation); + } + } }; @@ -521,10 +724,10 @@ struct SpriteController : Controller { FRAME_RANDOM = -2, }; - int frame; - bool instant, animated; + int frame, flag; + bool instant; - SpriteController(TR::Level *level, int entity, bool instant = true, int frame = FRAME_ANIMATED) : Controller(level, entity), instant(instant), animated(frame == FRAME_ANIMATED) { + SpriteController(TR::Level *level, int entity, bool instant = true, int frame = FRAME_ANIMATED) : Controller(level, entity), instant(instant), flag(frame) { if (frame >= 0) { // specific frame this->frame = frame; } else if (frame == FRAME_RANDOM) { // random frame @@ -539,10 +742,12 @@ struct SpriteController : Controller { } void update() { + if (flag >= 0) return; + bool remove = false; animTime += Core::deltaTime; - if (animated) { + if (flag == FRAME_ANIMATED) { frame = int(animTime * SPRITE_FPS); TR::SpriteSequence &seq = getSequence(); if (instant && frame >= seq.sCount) @@ -558,6 +763,12 @@ struct SpriteController : Controller { delete this; } } + + virtual void render(Frustum *frustum, MeshBuilder *mesh) { + PROFILE_MARKER("SPR"); + Core::active.shader->setParam(uModel, Core::mModel); + mesh->renderSprite(-(getEntity().modelIndex + 1), frame); + } }; void addSprite(TR::Level *level, TR::Entity::Type type, int room, int x, int y, int z, int frame = -1) { @@ -565,7 +776,7 @@ void addSprite(TR::Level *level, TR::Entity::Type type, int room, int x, int y, if (index > -1) { level->entities[index].intensity = 0x1FFF - level->rooms[room].ambient; level->entities[index].controller = new SpriteController(level, index, true, frame); - } + } } #endif \ No newline at end of file diff --git a/src/format.h b/src/format.h index d52459c..c4698c7 100644 --- a/src/format.h +++ b/src/format.h @@ -70,11 +70,40 @@ namespace TR { }; enum { - SND_NO = 2, - SND_LANDING = 4, - SND_BUBBLE = 37, - SND_DART = 151, - SND_SECRET = 173, + SND_NO = 2, + + SND_LANDING = 4, + + SND_UNHOLSTER = 6, + SND_HOLSTER = 7, + SND_PISTOLS_SHOT = 8, + SND_SHOTGUN_RELOAD = 9, + SND_RICOCHET = 10, + + SND_SCREAM = 30, + SND_HIT = 31, + + SND_BUBBLE = 37, + + SND_UZIS_SHOT = 43, + SND_MAGNUMS_SHOT = 44, + SND_SHOTGUN_SHOT = 45, + + SND_UNDERWATER = 60, + + SND_MENU_SPIN = 108, + SND_MENU_HOME = 109, + SND_MENU_CONTROLS = 110, + SND_MENU_SHOW = 111, + SND_MENU_HIDE = 112, + SND_MENU_COMPASS = 113, + SND_MENU_WEAPON = 114, + SND_MENU_PAGE = 115, + SND_HEALTH = 116, + + SND_DART = 151, + + SND_SECRET = 173, }; enum Action : uint16 { diff --git a/src/frustum.h b/src/frustum.h new file mode 100644 index 0000000..2e5a523 --- /dev/null +++ b/src/frustum.h @@ -0,0 +1,152 @@ +#ifndef H_FRUSTUM +#define H_FRUSTUM + +#include "utils.h" + +#define MAX_CLIP_PLANES 16 + +struct Frustum { + + struct Poly { + vec3 vertices[MAX_CLIP_PLANES]; + int count; + }; + + vec3 pos; + vec4 planes[MAX_CLIP_PLANES * 2]; // + buffer for OBB visibility test + int start, count; +#ifdef _DEBUG + int dbg; + Poly debugPoly; +#endif + + void calcPlanes(const mat4 &m) { + #ifdef _DEBUG + dbg = 0; + #endif + start = 0; + count = 5; + planes[0] = vec4(m.e30 - m.e20, m.e31 - m.e21, m.e32 - m.e22, m.e33 - m.e23); // near + planes[1] = vec4(m.e30 - m.e10, m.e31 - m.e11, m.e32 - m.e12, m.e33 - m.e13); // top + planes[2] = vec4(m.e30 - m.e00, m.e31 - m.e01, m.e32 - m.e02, m.e33 - m.e03); // right + planes[3] = vec4(m.e30 + m.e10, m.e31 + m.e11, m.e32 + m.e12, m.e33 + m.e13); // bottom + planes[4] = vec4(m.e30 + m.e00, m.e31 + m.e01, m.e32 + m.e02, m.e33 + m.e03); // left + for (int i = 0; i < count; i++) + planes[i] *= 1.0f / planes[i].xyz.length(); + } + + void calcPlanes(const Poly &poly) { + count = 1 + poly.count; // add one for near plane (not changing) + ASSERT(count < MAX_CLIP_PLANES); + if (count < 4) return; + + vec3 e1 = poly.vertices[0] - pos; + for (int i = 1; i < count; i++) { + vec3 e2 = poly.vertices[i % poly.count] - pos; + planes[i].xyz = e1.cross(e2).normal(); + planes[i].w = -(pos.dot(planes[i].xyz)); + e1 = e2; + } + } + + void clipPlane(const Poly &src, Poly &dst, const vec4 &plane) { + dst.count = 0; + if (!src.count) return; + + float t1 = src.vertices[0].dot(plane.xyz) + plane.w; + + for (int i = 0; i < src.count; i++) { + const vec3 &v1 = src.vertices[i]; + const vec3 &v2 = src.vertices[(i + 1) % src.count]; + + float t2 = v2.dot(plane.xyz) + plane.w; + + // hack for big float numbers + int s1 = (int)t1; + int s2 = (int)t2; + + if (s1 >= 0) { + dst.vertices[dst.count++] = v1; + ASSERT(dst.count < MAX_CLIP_PLANES); + } + + if ((s1 ^ s2) < 0) { // check for opposite signs + float k1 = t2 / (t2 - t1); + float k2 = t1 / (t2 - t1); + dst.vertices[dst.count++] = v1 * (float)k1 - v2 * (float)k2; + ASSERT(dst.count < MAX_CLIP_PLANES); + } + + t1 = t2; + } + } + + bool clipByPortal(const vec3 *vertices, int vCount, const vec3 &normal) { + if (normal.dot(pos - vertices[0]) < 0.0f) // check portal winding order + return false; + + Poly poly[2]; + + poly[0].count = vCount; + memmove(poly[0].vertices, vertices, sizeof(vec3) * poly[0].count); +#ifdef _DEBUG + debugPoly.count = 0; +#endif + int j = 0; + for (int i = 1; i < count; i++, j ^= 1) + clipPlane(poly[j], poly[j ^ 1], planes[i]); + + calcPlanes(poly[j]); + return count >= 4; + } + + // AABB visibility check + bool isVisible(const vec3 &min, const vec3 &max) const { + if (count < 4) return false; + + for (int i = start; i < start + count; i++) { + const vec3 &n = planes[i].xyz; + const float d = -planes[i].w; + + if (n.dot(max) < d && + n.dot(min) < d && + n.dot(vec3(min.x, max.y, max.z)) < d && + n.dot(vec3(max.x, min.y, max.z)) < d && + n.dot(vec3(min.x, min.y, max.z)) < d && + n.dot(vec3(max.x, max.y, min.z)) < d && + n.dot(vec3(min.x, max.y, min.z)) < d && + n.dot(vec3(max.x, min.y, min.z)) < d) + return false; + } + return true; + } + + // OBB visibility check + bool isVisible(const mat4 &matrix, const vec3 &min, const vec3 &max) { + start = count; + // transform clip planes (relative) + mat4 m = matrix.inverse(); + for (int i = 0; i < count; i++) { + vec4 &p = planes[i]; + vec4 o = m * vec4(p.xyz * (-p.w), 1.0f); + vec4 n = m * vec4(p.xyz, 0.0f); + planes[start + i] = vec4(n.xyz, -n.xyz.dot(o.xyz)); + } + bool visible = isVisible(min, max); + start = 0; + return visible; + } + + // Sphere visibility check + bool isVisible(const vec3 ¢er, float radius) { + if (count < 4) return false; + + for (int i = 0; i < count; i++) + if (planes[i].xyz.dot(center) + planes[i].w < -radius) + return false; + return true; + } + +}; + +#endif \ No newline at end of file diff --git a/src/lara.h b/src/lara.h index 768d274..69676e9 100644 --- a/src/lara.h +++ b/src/lara.h @@ -165,7 +165,7 @@ struct Lara : Controller { struct Weapon { enum Type { EMPTY, PISTOLS, SHOTGUN, MAGNUMS, UZIS, MAX }; - enum State { IS_CLEAN, IS_ARMED, IS_FIRING }; + enum State { IS_HIDDEN, IS_ARMED, IS_FIRING }; enum Anim { NONE, PREPARE, UNHOLSTER, HOLSTER, HOLD, AIM, FIRE }; int ammo; // if -1 weapon is not available @@ -176,16 +176,20 @@ struct Lara : Controller { Weapon::Anim wpnAnim; float wpnAnimTime; float wpnAnimDir; - quat animOverrides[15]; // left & right arms animation frames - int animOverrideMask; + int wpnLastFrame; + Weapon::Type wpnNext; + float wpnShotTime[2]; - Lara(TR::Level *level, int entity) : Controller(level, entity), wpnCurrent(Weapon::EMPTY), animOverrideMask(0) { + Lara(TR::Level *level, int entity) : Controller(level, entity), wpnCurrent(Weapon::EMPTY), wpnNext(Weapon::EMPTY) { + initMeshOverrides(); + wpnShotTime[0] = wpnShotTime[1] = 0.0f; memset(weapons, -1, sizeof(weapons)); weapons[Weapon::PISTOLS].ammo = 0; - weapons[Weapon::SHOTGUN].ammo = 8; - - setWeapon(Weapon::PISTOLS, Weapon::IS_CLEAN, Weapon::Anim::NONE, 1.0f); + weapons[Weapon::SHOTGUN].ammo = 9000; + weapons[Weapon::MAGNUMS].ammo = 9000; + weapons[Weapon::UZIS ].ammo = 9000; + setWeapon(Weapon::PISTOLS, Weapon::IS_HIDDEN); #ifdef _DEBUG /* // gym @@ -212,7 +216,7 @@ struct Lara : Controller { pos = vec3(31400, -2560, 25200); angle = vec3(0.0f, PI, 0.0f); getEntity().room = 43; - + // level 2 (medikit) pos = vec3(30800, -7936, 22131); angle = vec3(0.0f, 0.0f, 0.0f); @@ -237,13 +241,14 @@ struct Lara : Controller { #endif } - void setWeapon(Weapon::Type wType, Weapon::State wState, Weapon::Anim wAnim, float wAnimDir) { + void setWeapon(Weapon::Type wType, Weapon::State wState, Weapon::Anim wAnim = Weapon::Anim::NONE, float wAnimDir = 0.0f) { wpnAnimDir = wAnimDir; if (wAnim != wpnAnim) { wpnAnim = wAnim; TR::Animation *anim = &level->anims[getWeaponAnimIndex(wpnAnim)]; wpnAnimTime = wpnAnimDir >= 0.0f ? 0.0f : ((anim->frameEnd - anim->frameStart) / 30.0f); + wpnLastFrame = 0xFFFF; } if (wpnCurrent == wType && wpnState == wState) @@ -256,22 +261,30 @@ struct Lara : Controller { case Weapon::MAGNUMS : case Weapon::UZIS : switch (wState) { - case Weapon::IS_CLEAN : mask = BODY_LEG_L1 | BODY_LEG_R1; break; + case Weapon::IS_HIDDEN : mask = BODY_LEG_L1 | BODY_LEG_R1; break; case Weapon::IS_ARMED : mask = BODY_ARM_L3 | BODY_ARM_R3; break; case Weapon::IS_FIRING : mask = BODY_ARM_L3 | BODY_ARM_R3 | BODY_HEAD; break; } break; case Weapon::SHOTGUN : switch (wState) { - case Weapon::IS_CLEAN : mask = BODY_CHEST; break; + case Weapon::IS_HIDDEN : mask = BODY_CHEST; break; case Weapon::IS_ARMED : mask = BODY_ARM_L3 | BODY_ARM_R3; break; case Weapon::IS_FIRING : mask = BODY_ARM_L3 | BODY_ARM_R3 | BODY_HEAD; break; } break; + default : ; } + if (wpnState == Weapon::IS_HIDDEN && wState == Weapon::IS_ARMED) playSound(TR::SND_UNHOLSTER, pos, Sound::Flags::PAN); + if (wpnState == Weapon::IS_ARMED && wState == Weapon::IS_HIDDEN) playSound(TR::SND_HOLSTER, pos, Sound::Flags::PAN); + + int resetMask = BODY_HEAD | BODY_UPPER | BODY_LOWER; + if (wType == Weapon::SHOTGUN) + resetMask &= ~(BODY_LEG_L1 | BODY_LEG_R1); + // restore original meshes first - meshSwap(level->models[Weapon::EMPTY], BODY_HEAD | BODY_UPPER | BODY_LOWER); + meshSwap(level->models[Weapon::EMPTY], resetMask); // replace some parts meshSwap(level->models[wType], mask); // have a shotgun in inventory place it on the back if another weapon is in use @@ -285,8 +298,72 @@ struct Lara : Controller { wpnState = wState; } + bool emptyHands() { + return wpnState == Weapon::IS_HIDDEN; + } + + bool canDrawWeapon() { + return wpnCurrent != Weapon::EMPTY + && state != STATE_DEATH + && state != STATE_HANG + && state != STATE_REACH + && state != STATE_TREAD + && state != STATE_SWIM + && state != STATE_GLIDE + && state != STATE_HANG_UP + && state != STATE_FALL_BACK + && state != STATE_HANG_LEFT + && state != STATE_HANG_RIGHT + && state != STATE_SURF_TREAD + && state != STATE_SURF_SWIM + && state != STATE_DIVE + && state != STATE_PUSH_BLOCK + && state != STATE_PULL_BLOCK + && state != STATE_PUSH_PULL_READY + && state != STATE_PICK_UP + && state != STATE_SWITCH_DOWN + && state != STATE_SWITCH_UP + && state != STATE_USE_KEY + && state != STATE_USE_PUZZLE + && state != STATE_UNDERWATER_DEATH + && state != STATE_SPECIAL + && state != STATE_SURF_BACK + && state != STATE_SURF_LEFT + && state != STATE_SURF_RIGHT + && state != STATE_SWAN_DIVE + && state != STATE_FAST_DIVE + && state != STATE_HANDSTAND + && state != STATE_WATER_OUT; + } + + void drawWeapon() { + if (!canDrawWeapon()) return; + + if (wpnAnim != Weapon::Anim::PREPARE && wpnAnim != Weapon::Anim::UNHOLSTER && wpnAnim != Weapon::Anim::HOLSTER && emptyHands()) { + bool isRifle = wpnCurrent == Weapon::SHOTGUN; + setWeapon(wpnCurrent, wpnState, isRifle ? Weapon::Anim::UNHOLSTER : Weapon::Anim::PREPARE, 1.0f); + } + } + + void hideWeapon() { + if (wpnAnim != Weapon::Anim::PREPARE && wpnAnim != Weapon::Anim::UNHOLSTER && wpnAnim != Weapon::Anim::HOLSTER && !emptyHands()) { + bool isRifle = wpnCurrent == Weapon::SHOTGUN; + + if (isRifle) + setWeapon(wpnCurrent, wpnState, Weapon::Anim::HOLSTER, 1.0f); + else + setWeapon(wpnCurrent, wpnState, Weapon::Anim::UNHOLSTER, -1.0f); + } + } + + void changeWeapon(Weapon::Type wType) { + if (wpnCurrent == wType) return; + wpnNext = wType; + hideWeapon(); + } + int getWeaponAnimIndex(Weapon::Anim wAnim) { - int baseAnim = level->models[wpnCurrent].animation; + int baseAnim = level->models[wpnCurrent == Weapon::SHOTGUN ? Weapon::SHOTGUN : Weapon::PISTOLS].animation; if (wpnCurrent == Weapon::SHOTGUN) { switch (wAnim) { @@ -296,6 +373,7 @@ struct Lara : Controller { case Weapon::Anim::HOLD : case Weapon::Anim::AIM : return baseAnim; case Weapon::Anim::FIRE : return baseAnim + 2; + default : ; } } else switch (wAnim) { @@ -305,10 +383,202 @@ struct Lara : Controller { case Weapon::Anim::HOLD : case Weapon::Anim::AIM : return baseAnim; case Weapon::Anim::FIRE : return baseAnim + 3; + default : ; } return 0; } + int getWeaponSound() { + switch (wpnCurrent) { + case Weapon::PISTOLS : return TR::SND_PISTOLS_SHOT; + case Weapon::SHOTGUN : return TR::SND_SHOTGUN_SHOT; + case Weapon::MAGNUMS : return TR::SND_MAGNUMS_SHOT; + case Weapon::UZIS : return TR::SND_UZIS_SHOT; + default : return TR::SND_NO; + } + } + + void doShot() { + playSound(getWeaponSound(), pos, Sound::Flags::PAN); + + int count = wpnCurrent == Weapon::SHOTGUN ? 6 : 2; + + float nearDist = 32.0f * 1024.0f; + vec3 nearPos; + + for (int i = 0; i < count; i++) { + vec3 p = pos - vec3(0.0f, LARA_HANG_OFFSET, 0.0f); + vec3 d = getDir(); + vec3 r = d.cross(vec3(0, -1, 0)); // right dir + + if (wpnCurrent != Weapon::SHOTGUN) + p += r.normal() * ((i * 2 - 1) * 48); + + vec3 t = p + d * (24.0f * 1024.0f) + ((vec3(randf(), randf(), randf()) * 2.0f) - 1.0f) * 1024.0f; + + int room; + vec3 hit = trace(getRoomIndex(), p, t, room, false); + hit -= d * 64.0f; + addSprite(level, TR::Entity::SPARK, room, (int)hit.x, (int)hit.y, (int)hit.z, SpriteController::FRAME_RANDOM); + + float dist = (hit - p).length(); + if (dist < nearDist) { + nearPos = hit; + nearDist = dist; + } + } + + playSound(TR::SND_RICOCHET, nearPos, Sound::Flags::PAN); + + wpnShotTime[0] = wpnShotTime[1] = 0.0f; + } + + void updateWeapon() { + wpnShotTime[0] += Core::deltaTime; + wpnShotTime[1] += Core::deltaTime; + + TR::Animation *anim = &level->anims[getWeaponAnimIndex(wpnAnim)]; + + if (Input::down[ik1]) changeWeapon(Weapon::PISTOLS); + if (Input::down[ik2]) changeWeapon(Weapon::SHOTGUN); + if (Input::down[ik3]) changeWeapon(Weapon::MAGNUMS); + if (Input::down[ik4]) changeWeapon(Weapon::UZIS); + + if (wpnNext != Weapon::EMPTY && wpnState == Weapon::IS_HIDDEN) { + setWeapon(wpnNext, Weapon::IS_HIDDEN); + drawWeapon(); + wpnNext = Weapon::EMPTY; + } + + // apply weapon state changes + if (wpnCurrent == Weapon::EMPTY) { + animOverrideMask &= ~(BODY_ARM_L | BODY_ARM_R); + return; + } + + Weapon::Anim nextAnim = wpnAnim; + + bool isRifle = wpnCurrent == Weapon::SHOTGUN; + + if (mask & WEAPON) { + if (emptyHands()) + drawWeapon(); + else + hideWeapon(); + } + + if (!emptyHands()) { + if (mask & ACTION) { + if (wpnAnim == Weapon::Anim::HOLD) + setWeapon(wpnCurrent, wpnState, Weapon::Anim::AIM, 1.0f); + } else + if (wpnAnim == Weapon::Anim::AIM) + wpnAnimDir = -1.0f; + } + + anim = &level->anims[getWeaponAnimIndex(wpnAnim)]; + float maxTime = (anim->frameEnd - anim->frameStart) / 30.0f; + + if (wpnAnim == Weapon::Anim::NONE) { + animOverrideMask &= ~(BODY_ARM_L | BODY_ARM_R); + return; + } + animOverrideMask |= BODY_ARM_L | BODY_ARM_R; + + Weapon::Anim prevAnim = wpnAnim; // cache before changes + + wpnAnimTime += Core::deltaTime * wpnAnimDir; + + if (isRifle) { + if (wpnAnimDir > 0.0f) + switch (wpnAnim) { + case Weapon::Anim::UNHOLSTER : + if (wpnAnimTime >= maxTime) + setWeapon(wpnCurrent, Weapon::IS_ARMED, Weapon::Anim::HOLD, 0.0f); + else if (wpnAnimTime >= maxTime * 0.3f) + setWeapon(wpnCurrent, Weapon::IS_ARMED, wpnAnim, 1.0f); + break; + case Weapon::Anim::HOLSTER : + if (wpnAnimTime >= maxTime) + setWeapon(wpnCurrent, Weapon::IS_HIDDEN, Weapon::Anim::NONE, wpnAnimDir); + else if (wpnAnimTime >= maxTime * 0.7f) + setWeapon(wpnCurrent, Weapon::IS_HIDDEN, wpnAnim, 1.0f); + break; + case Weapon::Anim::AIM : + if (wpnAnimTime >= maxTime) + setWeapon(wpnCurrent, Weapon::IS_FIRING, Weapon::Anim::FIRE, wpnAnimDir); + break; + default : ; + }; + + if (wpnAnimDir < 0.0f && wpnAnimTime <= 0.0f) + if (wpnAnim == Weapon::Anim::AIM) { + setWeapon(wpnCurrent, wpnState, Weapon::Anim::HOLD, 0.0f); + }; + } else { + if (wpnAnimDir > 0.0f && wpnAnimTime >= maxTime) + switch (wpnAnim) { + case Weapon::Anim::PREPARE : setWeapon(wpnCurrent, Weapon::IS_ARMED, Weapon::Anim::UNHOLSTER, wpnAnimDir); break; + case Weapon::Anim::UNHOLSTER : setWeapon(wpnCurrent, wpnState, Weapon::Anim::HOLD, 0.0f); break; + case Weapon::Anim::AIM : setWeapon(wpnCurrent, Weapon::IS_FIRING, Weapon::Anim::FIRE, wpnCurrent == Weapon::UZIS ? 2.0f : 1.0f); break; + default : ; + }; + + if (wpnAnimDir < 0.0f && wpnAnimTime <= 0.0f) + switch (wpnAnim) { + case Weapon::Anim::PREPARE : setWeapon(wpnCurrent, wpnState, Weapon::Anim::NONE, wpnAnimDir); break; + case Weapon::Anim::UNHOLSTER : setWeapon(wpnCurrent, Weapon::IS_HIDDEN, Weapon::Anim::PREPARE, wpnAnimDir); break; + case Weapon::Anim::AIM : setWeapon(wpnCurrent, wpnState, Weapon::Anim::HOLD, 0.0f); break; + default : ; + }; + } + + if (prevAnim != wpnAnim) // check by cache + anim = &level->anims[getWeaponAnimIndex(wpnAnim)]; + + // make a shot + int frameIndex = int(wpnAnimTime * 30.0f / anim->frameRate) % ((anim->frameEnd - anim->frameStart) / anim->frameRate + 1); + if (wpnAnim == Weapon::Anim::FIRE) { + if (frameIndex < wpnLastFrame) { + if (mask & ACTION) { + doShot(); + } else + setWeapon(wpnCurrent, Weapon::IS_ARMED, Weapon::Anim::AIM, -1.0f); + } + // shotgun reload sound + if (isRifle && frameIndex >= 10 && wpnLastFrame < 10) + playSound(TR::SND_SHOTGUN_RELOAD, pos, Sound::Flags::PAN); + } + wpnLastFrame = frameIndex; + + + if (wpnAnim == Weapon::Anim::NONE) { + animOverrideMask &= ~(BODY_ARM_L | BODY_ARM_R); + return; + } + + // update animation overrides + float k = wpnAnimTime * 30.0f / anim->frameRate; + int fIndex = (int)k; + int fCount = (anim->frameEnd - anim->frameStart) / anim->frameRate + 1; + + int fSize = sizeof(TR::AnimFrame) + getModel().mCount * sizeof(uint16) * 2; + k = k - fIndex; + + int fIndexA = fIndex % fCount, fIndexB = (fIndex + 1) % fCount; + TR::AnimFrame *frameA = (TR::AnimFrame*)&level->frameData[(anim->frameOffset + fIndexA * fSize) >> 1]; + TR::AnimFrame *frameB = (TR::AnimFrame*)&level->frameData[(anim->frameOffset + fIndexB * fSize) >> 1]; + + // left arm + animOverrides[ 8] = lerpFrames(frameA, frameB, k, 8); + animOverrides[ 9] = lerpFrames(frameA, frameB, k, 9); + animOverrides[10] = lerpFrames(frameA, frameB, k, 10); + // right arm + animOverrides[11] = lerpFrames(frameA, frameB, k, 11); + animOverrides[12] = lerpFrames(frameA, frameB, k, 12); + animOverrides[13] = lerpFrames(frameA, frameB, k, 13); + } + bool waterOut(int &outState) { // TODO: playSound 36 vec3 dst = pos + getDir() * 32.0f; @@ -411,14 +681,14 @@ struct Lara : Controller { break; case TR::Level::Trigger::SWITCH : actionState = (isActive && stand == STAND_GROUND) ? STATE_SWITCH_UP : STATE_SWITCH_DOWN; - if ((mask & ACTION) == 0 || state == actionState) + if ((mask & ACTION) == 0 || state == actionState || !emptyHands()) return; if (!checkAngle(level->entities[info.trigCmd[0].args].rotation)) return; break; case TR::Level::Trigger::KEY : actionState = STATE_USE_KEY; - if (isActive || (mask & ACTION) == 0 || state == actionState) // TODO: STATE_USE_PUZZLE + if (isActive || (mask & ACTION) == 0 || state == actionState || !emptyHands()) // TODO: STATE_USE_PUZZLE return; if (!checkAngle(level->entities[info.trigCmd[0].args].rotation)) return; @@ -511,6 +781,8 @@ struct Lara : Controller { case Controller::STAND_SLIDE : case Controller::STAND_HANG : h -= 256.0f; + if (wpnState != Weapon::IS_HIDDEN) + h -= 256.0f; break; case Controller::STAND_UNDERWATER : case Controller::STAND_ONWATER : @@ -521,10 +793,6 @@ struct Lara : Controller { return vec3(0.0f, h, 0.0f); } - bool emptyHands() { - return wpnState == Weapon::IS_CLEAN; - } - virtual Stand getStand() { if (state == STATE_HANG || state == STATE_HANG_LEFT || state == STATE_HANG_RIGHT) { if (mask & ACTION) @@ -542,8 +810,10 @@ struct Lara : Controller { if (stand == STAND_ONWATER && state != STATE_DIVE && state != STATE_STOP) return stand; - if (getRoom().flags.water) - return STAND_UNDERWATER; // TODO: ONWATER + if (getRoom().flags.water) { + hideWeapon(); + return STAND_UNDERWATER; + } TR::Entity &e = getEntity(); TR::Level::FloorInfo info; @@ -663,7 +933,7 @@ struct Lara : Controller { virtual int getStateGround() { angle.x = 0.0f; - if ((mask & ACTION) && doPickUp() && emptyHands()) + if ((mask & ACTION) && emptyHands() && doPickUp()) return STATE_PICK_UP; if ( (mask & (FORTH | ACTION)) == (FORTH | ACTION) && (animIndex == ANIM_STAND || animIndex == ANIM_STAND_NORMAL) && emptyHands()) { @@ -897,18 +1167,17 @@ struct Lara : Controller { float rot = 0.0f; #ifdef _DEBUG - // show state transitions for current animation + // show state transitions for current animation static bool lState = false; if (Input::down[ikEnter]) { if (!lState) { lState = true; - static int snd_id = 0;//160; - /*playSound(snd_id); - */ - //setAnimation(snd_id); - //LOG("sound: %d\n", snd_id++); + static int snd_id = 0; + //playSound(snd_id, pos, 0); + LOG("sound: %d\n", snd_id++); + /* LOG("state: %d\n", anim->state); for (int i = 0; i < anim->scCount; i++) { auto &sc = level->states[anim->scOffset + i]; @@ -919,7 +1188,7 @@ struct Lara : Controller { } LOG("\n"); } - + */ } } else @@ -946,7 +1215,7 @@ struct Lara : Controller { else if (state == STATE_TREAD || state == STATE_SURF_TREAD || state == STATE_SURF_SWIM || state == STATE_SURF_BACK) w = TURN_WATER_SLOW; else if (state == STATE_RUN || state == STATE_FAST_TURN) - w = TURN_FAST; + w = TURN_FAST; // TODO: modulate angular speed by turnTime factor else if (state == STATE_FAST_BACK) w = TURN_FAST_BACK; else if (state == STATE_TURN_LEFT || state == STATE_TURN_RIGHT || state == STATE_WALK) @@ -1004,127 +1273,7 @@ struct Lara : Controller { virtual void updateAnimation(bool commands) { Controller::updateAnimation(commands); - - // apply weapon state changes - if (wpnCurrent == Weapon::EMPTY) { - animOverrideMask &= ~(BODY_ARM_L | BODY_ARM_R); - return; - } - - Weapon::Anim nextAnim = wpnAnim; - - bool isRifle = wpnCurrent == Weapon::SHOTGUN; - - if ((mask & WEAPON) && wpnAnim != Weapon::Anim::PREPARE && wpnAnim != Weapon::Anim::UNHOLSTER && wpnAnim != Weapon::Anim::HOLSTER) { - if (wpnState == Weapon::IS_CLEAN) - setWeapon(wpnCurrent, wpnState, isRifle ? Weapon::Anim::UNHOLSTER : Weapon::Anim::PREPARE, 1.0f); - else - if (isRifle) - setWeapon(wpnCurrent, wpnState, Weapon::Anim::HOLSTER, 1.0f); - else - setWeapon(wpnCurrent, wpnState, Weapon::Anim::UNHOLSTER, -1.0f); - } - - if (wpnState != Weapon::IS_CLEAN) { - if (mask & ACTION) { - if (wpnAnim == Weapon::Anim::HOLD) - setWeapon(wpnCurrent, wpnState, Weapon::Anim::AIM, 1.0f); - } else - if (wpnAnim == Weapon::Anim::AIM) - wpnAnimDir = -1.0f; - } - - TR::Animation *anim = &level->anims[getWeaponAnimIndex(wpnAnim)]; - float maxTime = (anim->frameEnd - anim->frameStart) / 30.0f; - - if (wpnAnim == Weapon::Anim::NONE) { - animOverrideMask &= ~(BODY_ARM_L | BODY_ARM_R); - return; - } - animOverrideMask |= BODY_ARM_L | BODY_ARM_R; - - Weapon::Anim prevAnim = wpnAnim; // cache before changes - - wpnAnimTime += Core::deltaTime * wpnAnimDir; - - /* - case Weapon::Anim::FIRE : - if (!(mask & ACTION)) - setWeapon(wpnCurrent, Weapon::IS_ARMED, Weapon::Anim::AIM, -1.0f); - break; - */ - - if (wpnAnim == Weapon::Anim::FIRE && wpnAnimTime >= maxTime) - if (!(mask & ACTION)) - setWeapon(wpnCurrent, Weapon::IS_ARMED, Weapon::Anim::AIM, -1.0f); - - - if (isRifle) { - if (wpnAnimDir > 0.0f) - switch (wpnAnim) { - case Weapon::Anim::UNHOLSTER : - if (wpnAnimTime >= maxTime) - setWeapon(wpnCurrent, Weapon::IS_ARMED, Weapon::Anim::HOLD, 0.0f); - else if (wpnAnimTime >= maxTime * 0.3f) - setWeapon(wpnCurrent, Weapon::IS_ARMED, wpnAnim, 1.0f); - break; - case Weapon::Anim::HOLSTER : - if (wpnAnimTime >= maxTime) - setWeapon(wpnCurrent, Weapon::IS_CLEAN, Weapon::Anim::NONE, wpnAnimDir); - else if (wpnAnimTime >= maxTime * 0.7f) - setWeapon(wpnCurrent, Weapon::IS_CLEAN, wpnAnim, 1.0f); break; - case Weapon::Anim::AIM : if (wpnAnimTime >= maxTime) setWeapon(wpnCurrent, Weapon::IS_FIRING, Weapon::Anim::FIRE, wpnAnimDir); break; - }; - - if (wpnAnimDir < 0.0f && wpnAnimTime <= 0.0f) - switch (wpnAnim) { - case Weapon::Anim::AIM : setWeapon(wpnCurrent, wpnState, Weapon::Anim::HOLD, 0.0f); break; - }; - } else { - if (wpnAnimDir > 0.0f && wpnAnimTime >= maxTime) - switch (wpnAnim) { - case Weapon::Anim::PREPARE : setWeapon(wpnCurrent, Weapon::IS_ARMED, Weapon::Anim::UNHOLSTER, wpnAnimDir); break; - case Weapon::Anim::UNHOLSTER : setWeapon(wpnCurrent, wpnState, Weapon::Anim::HOLD, 0.0f); break; - case Weapon::Anim::AIM : setWeapon(wpnCurrent, Weapon::IS_FIRING, Weapon::Anim::FIRE, wpnAnimDir); break; - }; - - if (wpnAnimDir < 0.0f && wpnAnimTime <= 0.0f) - switch (wpnAnim) { - case Weapon::Anim::PREPARE : setWeapon(wpnCurrent, wpnState, Weapon::Anim::NONE, wpnAnimDir); break; - case Weapon::Anim::UNHOLSTER : setWeapon(wpnCurrent, Weapon::IS_CLEAN, Weapon::Anim::PREPARE, wpnAnimDir); break; - case Weapon::Anim::AIM : setWeapon(wpnCurrent, wpnState, Weapon::Anim::HOLD, 0.0f); break; - }; - } - - if (prevAnim != wpnAnim) // check by cache - anim = &level->anims[getWeaponAnimIndex(wpnAnim)]; - - if (wpnAnim == Weapon::Anim::NONE) { - animOverrideMask &= ~(BODY_ARM_L | BODY_ARM_R); - return; - } - - // update animation overrides - float k = wpnAnimTime * 30.0f / anim->frameRate; - int fIndex = (int)k; - int fCount = (anim->frameEnd - anim->frameStart) / anim->frameRate + 1; - - int fSize = sizeof(TR::AnimFrame) + getModel().mCount * sizeof(uint16) * 2; - k = k - fIndex; - - int fIndexA = fIndex % fCount, fIndexB = (fIndex + 1) % fCount; - TR::AnimFrame *frameA = (TR::AnimFrame*)&level->frameData[(anim->frameOffset + fIndexA * fSize) >> 1]; - TR::AnimFrame *frameB = (TR::AnimFrame*)&level->frameData[(anim->frameOffset + fIndexB * fSize) >> 1]; - LOG("%d %d %f\n", getWeaponAnimIndex(wpnAnim), fIndexA, wpnAnimTime); - - // left arm - animOverrides[ 8] = lerpFrames(frameA, frameB, k, 8); - animOverrides[ 9] = lerpFrames(frameA, frameB, k, 9); - animOverrides[10] = lerpFrames(frameA, frameB, k, 10); - // right arm - animOverrides[11] = lerpFrames(frameA, frameB, k, 11); - animOverrides[12] = lerpFrames(frameA, frameB, k, 12); - animOverrides[13] = lerpFrames(frameA, frameB, k, 13); + updateWeapon(); } quat lerpFrames(TR::AnimFrame *frameA, TR::AnimFrame *frameB, float t, int index) { @@ -1385,6 +1534,29 @@ struct Lara : Controller { updateEntity(); checkRoom(); } + + void renderMuzzleFlash(MeshBuilder *mesh, const mat4 &matrix, const vec3 &offset, float time) { + if (time > 0.1f) return; + float alpha = min(1.0f, (0.1f - time) * 20.0f); + float lum = 3.0f; + + mat4 tmp = Core::mModel; + Core::mModel = matrix; + Core::mModel.rotateX(-PI * 0.5f); + Core::mModel.translate(offset); + Core::active.shader->setParam(uColor, vec4(lum, lum, lum, alpha)); + renderMesh(mesh, level->models[47].mStart); + Core::active.shader->setParam(uColor, Core::color); + Core::mModel = tmp; + } + + virtual void render(Frustum *frustum, MeshBuilder *mesh) { + Controller::render(frustum, mesh); + if (wpnCurrent != Weapon::SHOTGUN) { + renderMuzzleFlash(mesh, joints[10], vec3(-10, -50, 150), wpnShotTime[0]); + renderMuzzleFlash(mesh, joints[13], vec3( 10, -50, 150), wpnShotTime[1]); + } + } }; #endif \ No newline at end of file diff --git a/src/level.h b/src/level.h index 25a9353..d10a685 100644 --- a/src/level.h +++ b/src/level.h @@ -40,7 +40,7 @@ struct Level { initShaders(); initOverrides(); - for (int i = 0; i < level.entitiesCount; i++) { + for (int i = 0; i < level.entitiesBaseCount; i++) { TR::Entity &entity = level.entities[i]; switch (entity.type) { case TR::Entity::LARA : @@ -104,7 +104,11 @@ struct Level { case TR::Entity::HOLE_KEY : entity.controller = new Trigger(&level, i, false); break; - default : ; + default : + if (entity.modelIndex > 0) + entity.controller = new Controller(&level, i); + else + entity.controller = new SpriteController(&level, i, 0); } } @@ -261,7 +265,8 @@ struct Level { mat4 mTemp = Core::mModel; Core::mModel.translate(offset); Core::mModel.rotateY(rMesh.rotation); - renderMesh(sMesh->mesh); + Core::active.shader->setParam(uModel, Core::mModel); + mesh->renderMesh(mesh->meshMap[sMesh->mesh]); Core::mModel = mTemp; } } @@ -324,141 +329,6 @@ struct Level { camera->frustum = camFrustum; // pop camera frustum } - void renderMesh(uint32 offsetIndex) { - MeshBuilder::MeshInfo *m = mesh->meshMap[offsetIndex]; - if (!m) return; // invisible mesh (offsetIndex > 0 && level.meshOffsets[offsetIndex] == 0) camera target entity etc. - - Core::active.shader->setParam(uModel, Core::mModel); - mesh->renderMesh(m); - } - - void renderShadow(const vec3 &pos, const vec3 &offset, const vec3 &size, float angle) { - mat4 m; - m.identity(); - m.translate(pos); - m.rotateY(angle); - m.translate(vec3(offset.x, 0.0f, offset.z)); - m.scale(vec3(size.x, 0.0f, size.z) * (1.0f / 1024.0f)); - - Core::active.shader->setParam(uModel, m); - Core::active.shader->setParam(uColor, vec4(0.0f, 0.0f, 0.0f, 0.5f)); - mesh->renderShadowSpot(); - } - - void renderModel(const TR::Model &model, const TR::Entity &entity) { - TR::Animation *anim; - float fTime; - vec3 angle; - - Controller *controller = (Controller*)entity.controller; - - if (controller) { - anim = &level.anims[controller->animIndex]; - angle = controller->angle; - fTime = controller->animTime; - } else { - anim = &level.anims[model.animation]; - angle = vec3(0.0f, entity.rotation, 0.0f); - fTime = time; - } - - if (angle.y != 0.0f) Core::mModel.rotateY(angle.y); - if (angle.x != 0.0f) Core::mModel.rotateX(angle.x); - if (angle.z != 0.0f) Core::mModel.rotateZ(angle.z); - - float k = fTime * 30.0f / anim->frameRate; - int fIndex = (int)k; - int fCount = (anim->frameEnd - anim->frameStart) / anim->frameRate + 1; - - int fSize = sizeof(TR::AnimFrame) + model.mCount * sizeof(uint16) * 2; - k = k - fIndex; - - int fIndexA = fIndex % fCount, fIndexB = (fIndex + 1) % fCount; - TR::AnimFrame *frameA = (TR::AnimFrame*)&level.frameData[(anim->frameOffset + fIndexA * fSize) >> 1]; - - TR::Animation *nextAnim = NULL; - - vec3 move(0.0f); - if (fIndexB == 0) { - if (controller) - move = controller->getAnimMove(); - nextAnim = &level.anims[anim->nextAnimation]; - fIndexB = (anim->nextFrame - nextAnim->frameStart) / nextAnim->frameRate; - } else - nextAnim = anim; - - TR::AnimFrame *frameB = (TR::AnimFrame*)&level.frameData[(nextAnim->frameOffset + fIndexB * fSize) >> 1]; - - vec3 bmin = frameA->box.min().lerp(frameB->box.min(), k); - vec3 bmax = frameA->box.max().lerp(frameB->box.max(), k); - if (!camera->frustum->isVisible(Core::mModel, bmin, bmax)) - return; - - TR::Node *node = (int)model.node < level.nodesDataSize ? (TR::Node*)&level.nodesData[model.node] : NULL; - - mat4 m; - m.identity(); - m.translate(((vec3)frameA->pos).lerp(move + frameB->pos, k)); - - int sIndex = 0; - mat4 stack[20]; - - for (int i = 0; i < model.mCount; i++) { - - if (i > 0 && node) { - TR::Node &t = node[i - 1]; - - if (t.flags & 0x01) m = stack[--sIndex]; - if (t.flags & 0x02) stack[sIndex++] = m; - - ASSERT(sIndex >= 0 && sIndex < 20); - - m.translate(vec3(t.x, t.y, t.z)); - } - - quat q; - if (entity.type == TR::Entity::LARA && (((Lara*)controller)->animOverrideMask & (1 << i))) - q = ((Lara*)controller)->animOverrides[i]; - else - q = lerpAngle(frameA->getAngle(i), frameB->getAngle(i), k); - m = m * mat4(q, vec3(0.0f)); - - - // vec3 angle = lerpAngle(getAngle(frameA, i), getAngle(frameB, i), k); - // m.rotateY(angle.y); - // m.rotateX(angle.x); - // m.rotateZ(angle.z); - - mat4 tmp = Core::mModel; - Core::mModel = Core::mModel * m; - if (controller) - renderMesh(controller->meshes[i]); - else - renderMesh(model.mStart + i); - Core::mModel = tmp; - } - - if (TR::castShadow(entity.type)) { - TR::Level::FloorInfo info; - level.getFloorInfo(entity.room, entity.x, entity.z, info, true); - renderShadow(vec3(entity.x, info.floor - 16.0f, entity.z), (bmax + bmin) * 0.5f, (bmax - bmin) * 0.8f, entity.rotation); - } - } - - void renderSequence(const TR::Entity &entity) { - shaders[shSprite]->bind(); - Core::active.shader->setParam(uModel, Core::mModel); - Core::active.shader->setParam(uColor, Core::color); - - int sIndex = -(entity.modelIndex + 1); - int sFrame; - if (entity.controller) - sFrame = ((SpriteController*)entity.controller)->frame; - else - sFrame = int(time * 10.0f) % level.spriteSequences[sIndex].sCount; - mesh->renderSprite(sIndex, sFrame); - } - int getLightIndex(const vec3 &pos, int &room) { int idx = -1; float dist; @@ -498,6 +368,7 @@ struct Level { void renderEntity(const TR::Entity &entity) { if (entity.type == TR::Entity::NONE) return; + ASSERT(entity.controller); TR::Room &room = level.rooms[entity.room]; if (!room.flags.rendered || entity.flags.invisible) // check for room visibility @@ -509,31 +380,28 @@ struct Level { float c = (entity.intensity > -1) ? (1.0f - entity.intensity / (float)0x1FFF) : 1.0f; float l = 1.0f; - // set shader - setRoomShader(room, c)->bind(); + if (entity.modelIndex > 0) { // model + // set shader + setRoomShader(room, c)->bind(); + Core::active.shader->setParam(uColor, Core::color); + // get light parameters for entity + getLight(vec3(entity.x, entity.y, entity.z), entity.room); + } + + if (entity.modelIndex < 0) { // sprite + shaders[shSprite]->bind(); + Core::color = vec4(c, c, c, 1.0f); + } Core::active.shader->setParam(uColor, Core::color); - // get light parameters for entity - getLight(vec3(entity.x, entity.y, entity.z), entity.room); - - // render entity models - if (entity.modelIndex > 0) { - PROFILE_MARKER("MDL"); - renderModel(level.models[entity.modelIndex - 1], entity); - } - // if entity is billboard - if (entity.modelIndex < 0) { - PROFILE_MARKER("SPR"); - Core::color = vec4(c, c, c, 1.0f); - renderSequence(entity); - } + ((Controller*)entity.controller)->render(camera->frustum, mesh); Core::mModel = m; } void update() { time += Core::deltaTime; - + for (int i = 0; i < level.entitiesCount; i++) if (level.entities[i].type != TR::Entity::NONE) { Controller *controller = (Controller*)level.entities[i].controller; diff --git a/src/platform/web/index.html b/src/platform/web/index.html index 974340e..d9f5488 100644 --- a/src/platform/web/index.html +++ b/src/platform/web/index.html @@ -5,7 +5,7 @@ Starting...


- OpenLara on github
(controls -> gamepad or keyboad: move - WASD / arrows, jump - Space, action - E/Ctrl, walk - Shift, side steps - ZX/walk+direction, camera - MouseR)
+ OpenLara on github
controls:
keyboad: move - WASD / arrows, jump - Space, action - E/Ctrl, draw weapon - Q, change weapon - 1-4, walk - Shift, side steps - ZX/walk+direction, camera - MouseR)
gamepad: PSX controls on XBox controller