From 7f372076df83bf63c1fa0917d1809e23c4e64ec4 Mon Sep 17 00:00:00 2001 From: XProger Date: Mon, 14 Nov 2016 07:43:49 +0300 Subject: [PATCH 1/4] #7 pistols aiming --- src/camera.h | 4 +- src/controller.h | 141 +++++++---- src/format.h | 4 + src/lara.h | 632 ++++++++++++++++++++++++++++++----------------- src/utils.h | 32 +-- 5 files changed, 528 insertions(+), 285 deletions(-) diff --git a/src/camera.h b/src/camera.h index f66efd7..bcc0884 100644 --- a/src/camera.h +++ b/src/camera.h @@ -22,7 +22,7 @@ struct Camera : Controller { 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; + fov = 80.0f; znear = 128; zfar = 100.0f * 1024.0f; angleAdv = vec3(0.0f); @@ -57,6 +57,8 @@ struct Camera : Controller { } virtual void update() { + actTargetEntity = owner->target; + if (timer > 0.0f) { timer -= Core::deltaTime; if (timer <= 0.0f) { diff --git a/src/controller.h b/src/controller.h index 02aac82..9162104 100644 --- a/src/controller.h +++ b/src/controller.h @@ -96,6 +96,89 @@ struct Controller { } } + int getFrameIndex(int animIndex, float t) { + TR::Animation &anim = level->anims[animIndex]; + return int(t * 30.0f / anim.frameRate) % ((anim.frameEnd - anim.frameStart) / anim.frameRate + 1); + } + + void getFrames(TR::AnimFrame **frameA, TR::AnimFrame **frameB, float &t, int animIndex, float animTime, bool nextAnim = false, vec3 *move = NULL) { + TR::Animation *anim = &level->anims[animIndex]; + + t = animTime * 30.0f / anim->frameRate; + int fIndex = (int)t; + int fCount = (anim->frameEnd - anim->frameStart) / anim->frameRate + 1; + + int fSize = sizeof(TR::AnimFrame) + getModel().mCount * sizeof(uint16) * 2; + t -= fIndex; + + int fIndexA = fIndex % fCount, fIndexB = (fIndex + 1) % fCount; + *frameA = (TR::AnimFrame*)&level->frameData[(anim->frameOffset + fIndexA * fSize) >> 1]; + + if (!fIndexB) { + if (move) + *move = getAnimMove(); + if (nextAnim) { + int nextFrame = anim->nextFrame; + anim = &level->anims[anim->nextAnimation]; + fIndexB = (nextFrame - anim->frameStart) / anim->frameRate; + } + } + *frameB = (TR::AnimFrame*)&level->frameData[(anim->frameOffset + fIndexB * fSize) >> 1]; + } + + mat4 getJoint(int index, bool postRot = false) { + mat4 matrix; + matrix.identity(); + + matrix.translate(pos); + if (angle.y != 0.0f) matrix.rotateY(angle.y); + if (angle.x != 0.0f) matrix.rotateX(angle.x); + if (angle.z != 0.0f) matrix.rotateZ(angle.z); + + TR::Animation *anim = &level->anims[animIndex]; + TR::Model &model = getModel(); + + float t; + vec3 move(0.0f); + TR::AnimFrame *frameA, *frameB; + getFrames(&frameA, &frameB, t, animIndex, animTime, true, &move); + + TR::Node *node = (int)model.node < level->nodesDataSize ? (TR::Node*)&level->nodesData[model.node] : NULL; + + matrix.translate(((vec3)frameA->pos).lerp(move + frameB->pos, t)); + + 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) matrix = stack[--sIndex]; + if (t.flags & 0x02) stack[sIndex++] = matrix; + + ASSERT(sIndex >= 0 && sIndex < 20); + + matrix.translate(vec3(t.x, t.y, t.z)); + } + + if (i == index && !postRot) + return matrix; + + quat q; + if (animOverrideMask & (1 << i)) + q = animOverrides[i]; + else + q = lerpAngle(frameA->getAngle(i), frameB->getAngle(i), t); + matrix = matrix * mat4(q, vec3(0.0f)); + + if (i == index && postRot) + return matrix; + } + return matrix; + } + void updateEntity() { TR::Entity &e = getEntity(); e.x = int(pos.x); @@ -265,20 +348,11 @@ struct Controller { } virtual Box getBoundingBox() { - TR::Animation *anim = &level->anims[animIndex]; - TR::Model &model = getModel(); + float t; + TR::AnimFrame *frameA, *frameB; + getFrames(&frameA, &frameB, t, animIndex, animTime, true); - float k = animTime * 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 *fA = (TR::AnimFrame*)&level->frameData[(anim->frameOffset + fIndexA * fSize) >> 1]; - TR::AnimFrame *fB = (TR::AnimFrame*)&level->frameData[(anim->frameOffset + fIndexB * fSize) >> 1]; - Box box(fA->box.min().lerp(fB->box.min(), k), fA->box.max().lerp(fB->box.max(), k)); + Box box(frameA->box.min().lerp(frameB->box.min(), t), frameA->box.max().lerp(frameB->box.max(), t)); box.rotate90(getEntity().rotation.value / 0x4000); box.min += pos; box.max += pos; @@ -625,44 +699,19 @@ struct Controller { 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; + TR::Animation *anim = &level->anims[animIndex]; 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; - + float t; 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]; + TR::AnimFrame *frameA, *frameB; + getFrames(&frameA, &frameB, t, animIndex, animTime, true, &move); - vec3 bmin = frameA->box.min().lerp(frameB->box.min(), k); - vec3 bmax = frameA->box.max().lerp(frameB->box.max(), k); + vec3 bmin = frameA->box.min().lerp(frameB->box.min(), t); + vec3 bmax = frameA->box.max().lerp(frameB->box.max(), t); if (frustum && !frustum->isVisible(Core::mModel, bmin, bmax)) return; @@ -670,7 +719,7 @@ struct Controller { mat4 m; m.identity(); - m.translate(((vec3)frameA->pos).lerp(move + frameB->pos, k)); + m.translate(((vec3)frameA->pos).lerp(move + frameB->pos, t)); int sIndex = 0; mat4 stack[20]; @@ -692,7 +741,7 @@ struct Controller { if (animOverrideMask & (1 << i)) q = animOverrides[i]; else - q = lerpAngle(frameA->getAngle(i), frameB->getAngle(i), k); + q = lerpAngle(frameA->getAngle(i), frameB->getAngle(i), t); m = m * mat4(q, vec3(0.0f)); mat4 tmp = Core::mModel; diff --git a/src/format.h b/src/format.h index c4698c7..746e34b 100644 --- a/src/format.h +++ b/src/format.h @@ -415,6 +415,10 @@ namespace TR { uint16 align; int16 modelIndex; // index of representation in models (index + 1) or spriteSequences (-(index + 1)) arrays void *controller; // Controller implementation or NULL + + bool isEnemy() { + return type >= ENEMY_TWIN && type <= ENEMY_LARSON; + } }; struct Animation { diff --git a/src/lara.h b/src/lara.h index 69676e9..cb86096 100644 --- a/src/lara.h +++ b/src/lara.h @@ -25,6 +25,8 @@ #define MAX_TRIGGER_ACTIONS 64 #define DESCENT_SPEED 2048.0f +#define MUZZLE_FLASH_TIME 0.1f +#define TARGET_MAX_DIST (8.0f * 1024.0f) struct Lara : Controller { @@ -141,19 +143,19 @@ struct Lara : Controller { enum : int { BODY_HIP = 0x0001, - BODY_LEG_R1 = 0x0002, - BODY_LEG_R2 = 0x0004, - BODY_LEG_R3 = 0x0008, - BODY_LEG_L1 = 0x0010, - BODY_LEG_L2 = 0x0020, - BODY_LEG_L3 = 0x0040, + BODY_LEG_L1 = 0x0002, + BODY_LEG_L2 = 0x0004, + BODY_LEG_L3 = 0x0008, + BODY_LEG_R1 = 0x0010, + BODY_LEG_R2 = 0x0020, + BODY_LEG_R3 = 0x0040, BODY_CHEST = 0x0080, - BODY_ARM_L1 = 0x0100, - BODY_ARM_L2 = 0x0200, - BODY_ARM_L3 = 0x0400, - BODY_ARM_R1 = 0x0800, - BODY_ARM_R2 = 0x1000, - BODY_ARM_R3 = 0x2000, + BODY_ARM_R1 = 0x0100, + BODY_ARM_R2 = 0x0200, + BODY_ARM_R3 = 0x0400, + BODY_ARM_L1 = 0x0800, + BODY_ARM_L2 = 0x1000, + BODY_ARM_L3 = 0x2000, BODY_HEAD = 0x4000, BODY_ARM_L = BODY_ARM_L1 | BODY_ARM_L2 | BODY_ARM_L3, BODY_ARM_R = BODY_ARM_R1 | BODY_ARM_R2 | BODY_ARM_R3, @@ -172,24 +174,43 @@ struct Lara : Controller { } weapons[Weapon::MAX]; Weapon::Type wpnCurrent; - Weapon::State wpnState; - Weapon::Anim wpnAnim; - float wpnAnimTime; - float wpnAnimDir; - int wpnLastFrame; Weapon::Type wpnNext; - float wpnShotTime[2]; + Weapon::State wpnState; + vec3 chestOffset; - Lara(TR::Level *level, int entity) : Controller(level, entity), wpnCurrent(Weapon::EMPTY), wpnNext(Weapon::EMPTY) { + struct Arm { + int target; + int lastFrame; + float shotTimer; + float weight; + float animDir; + float animTime; + float animMaxTime; + int animIndex; + quat rot; + Weapon::Anim anim; + } arms[2]; + + int target; + quat rotHead, rotChest; + + + Lara(TR::Level *level, int entity) : Controller(level, entity), wpnCurrent(Weapon::EMPTY), wpnNext(Weapon::EMPTY), chestOffset(pos), target(-1) { initMeshOverrides(); - wpnShotTime[0] = wpnShotTime[1] = 0.0f; + for (int i = 0; i < 2; i++) { + arms[i].shotTimer = MUZZLE_FLASH_TIME + 1.0f; + arms[i].animTime = 0.0f; + arms[i].rot = quat(0, 0, 0, 1); + } + + rotHead = rotChest = quat(0, 0, 0, 1); memset(weapons, -1, sizeof(weapons)); weapons[Weapon::PISTOLS].ammo = 0; weapons[Weapon::SHOTGUN].ammo = 9000; weapons[Weapon::MAGNUMS].ammo = 9000; weapons[Weapon::UZIS ].ammo = 9000; - setWeapon(Weapon::PISTOLS, Weapon::IS_HIDDEN); + wpnSet(Weapon::PISTOLS); #ifdef _DEBUG /* // gym @@ -211,7 +232,7 @@ struct Lara : Controller { pos = vec3(75671, -1024, 22862); angle = vec3(0.0f, -PI * 0.25f, 0.0f); getEntity().room = 13; - + // level 2 (room 1) pos = vec3(31400, -2560, 25200); angle = vec3(0.0f, PI, 0.0f); @@ -241,21 +262,38 @@ struct Lara : Controller { #endif } - void setWeapon(Weapon::Type wType, Weapon::State wState, Weapon::Anim wAnim = Weapon::Anim::NONE, float wAnimDir = 0.0f) { - wpnAnimDir = wAnimDir; + void wpnSet(Weapon::Type wType) { + wpnCurrent = wType; + wpnState = Weapon::IS_FIRING; + wpnSetAnim(arms[0], Weapon::IS_HIDDEN, Weapon::Anim::NONE, 0.0f, 0.0f); + wpnSetAnim(arms[1], Weapon::IS_HIDDEN, Weapon::Anim::NONE, 0.0f, 0.0f); + } - 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; - } + void wpnSetAnim(Arm &arm, Weapon::State wState, Weapon::Anim wAnim, float wAnimTime, float wAnimDir, bool playing = true) { + if (arm.anim != wAnim) + arm.lastFrame = 0xFFFF; - if (wpnCurrent == wType && wpnState == wState) - return; + arm.anim = wAnim; + arm.animIndex = wpnGetAnimIndex(wAnim); + TR::Animation &anim = level->anims[arm.animIndex]; + + arm.animDir = playing ? wAnimDir : 0.0f; + arm.animMaxTime = (anim.frameEnd - anim.frameStart) / 30.0f; + + if (wAnimDir > 0.0f) + arm.animTime = wAnimTime; + else + if (wAnimDir < 0.0f) + arm.animTime = arm.animMaxTime + wAnimTime; + + wpnSetState(wState); + } + + void wpnSetState(Weapon::State wState) { + if (wpnState == wState) return; int mask = 0; - switch (wType) { + switch (wpnCurrent) { case Weapon::EMPTY : break; case Weapon::PISTOLS : case Weapon::MAGNUMS : @@ -280,30 +318,56 @@ struct Lara : Controller { 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) + if (wpnCurrent == Weapon::SHOTGUN) resetMask &= ~(BODY_LEG_L1 | BODY_LEG_R1); // restore original meshes first meshSwap(level->models[Weapon::EMPTY], resetMask); // replace some parts - meshSwap(level->models[wType], mask); + meshSwap(level->models[wpnCurrent], mask); // have a shotgun in inventory place it on the back if another weapon is in use - if (wType != Weapon::SHOTGUN && weapons[Weapon::SHOTGUN].ammo != -1) + if (wpnCurrent != Weapon::SHOTGUN && weapons[Weapon::SHOTGUN].ammo != -1) meshSwap(level->models[Weapon::SHOTGUN], BODY_CHEST); // mesh swap to angry Lara's head while firing (from uzis model) if (wState == Weapon::IS_FIRING) meshSwap(level->models[Weapon::UZIS], BODY_HEAD); - wpnCurrent = wType; wpnState = wState; } + /* + void setWeaponAnim(Weapon::Anim wAnim, float wAnimDir, Arm *arm) { + arm->animDir = wAnimDir; + + if (wAnim != arm->anim) { + arm->anim = wAnim; + TR::Animation *anim = &level->anims[getWeaponAnimIndex(arm->anim)]; + arm->animTime = arm->animDir >= 0.0f ? 0.0f : ((anim->frameEnd - anim->frameStart) / 30.0f); + arm->lastFrame = 0xFFFF; + } + } + + void setWeapon(Weapon::Type wType, Weapon::State wState, Weapon::Anim wAnim = Weapon::Anim::NONE, float wAnimDir = 0.0f, Arm *arm = NULL) { + + if (wpnCurrent != Weapon::SHOTGUN && wAnim != Weapon::Anim::HOLD && wAnim != Weapon::Anim::AIM && wAnim != Weapon::Anim::FIRE) { + setWeaponAnim(wAnim, wAnimDir, &arms[0]); + setWeaponAnim(wAnim, wAnimDir, &arms[1]); + } else + setWeaponAnim(wAnim, wAnimDir, !arm ? &arms[1] : arm); + + if (wpnCurrent == wType && wpnState == wState) + return; + wpnCurrent = wType; + wpnSetState(wState); + } +*/ bool emptyHands() { - return wpnState == Weapon::IS_HIDDEN; + return arms[0].anim == Weapon::Anim::NONE; } bool canDrawWeapon() { return wpnCurrent != Weapon::EMPTY + && emptyHands() && state != STATE_DEATH && state != STATE_HANG && state != STATE_REACH @@ -336,33 +400,39 @@ struct Lara : Controller { && state != STATE_WATER_OUT; } - void drawWeapon() { + bool wpnReady() { + return arms[0].anim != Weapon::Anim::PREPARE && arms[0].anim != Weapon::Anim::UNHOLSTER && arms[0].anim != Weapon::Anim::HOLSTER; + } + + void wpnDraw() { 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); + if (wpnReady() && emptyHands()) { + if (wpnCurrent != Weapon::SHOTGUN) { + wpnSetAnim(arms[0], wpnState, Weapon::Anim::PREPARE, 0.0f, 1.0f); + wpnSetAnim(arms[1], wpnState, Weapon::Anim::PREPARE, 0.0f, 1.0f); + } else + wpnSetAnim(arms[0], wpnState, Weapon::Anim::UNHOLSTER, 0.0f, 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 wpnHide() { + if (wpnReady() && !emptyHands()) { + if (wpnCurrent != Weapon::SHOTGUN) { + wpnSetAnim(arms[0], wpnState, Weapon::Anim::UNHOLSTER, 0.0f, -1.0f); + wpnSetAnim(arms[1], wpnState, Weapon::Anim::UNHOLSTER, 0.0f, -1.0f); + } else + wpnSetAnim(arms[0], wpnState, Weapon::Anim::UNHOLSTER, 0.0f, 1.0f); } } - void changeWeapon(Weapon::Type wType) { + void wpnChange(Weapon::Type wType) { if (wpnCurrent == wType) return; wpnNext = wType; - hideWeapon(); + wpnHide(); } - int getWeaponAnimIndex(Weapon::Anim wAnim) { + int wpnGetAnimIndex(Weapon::Anim wAnim) { int baseAnim = level->models[wpnCurrent == Weapon::SHOTGUN ? Weapon::SHOTGUN : Weapon::PISTOLS].animation; if (wpnCurrent == Weapon::SHOTGUN) { @@ -388,7 +458,7 @@ struct Lara : Controller { return 0; } - int getWeaponSound() { + int wpnGetSound() { switch (wpnCurrent) { case Weapon::PISTOLS : return TR::SND_PISTOLS_SHOT; case Weapon::SHOTGUN : return TR::SND_SHOTGUN_SHOT; @@ -398,23 +468,62 @@ struct Lara : Controller { } } - void doShot() { - playSound(getWeaponSound(), pos, Sound::Flags::PAN); + void wpnFire() { + bool armShot[2] = { false, false }; + for (int i = 0; i < 2; i++) { + int frameIndex = getFrameIndex(arms[i].animIndex, arms[i].animTime); + if (arms[i].anim == Weapon::Anim::FIRE) { + if (frameIndex < arms[i].lastFrame) { + if (target == -1 || ((mask & ACTION) && arms[i].target > -1)) { + armShot[i] = true; + } else + wpnSetAnim(arms[i], Weapon::IS_ARMED, Weapon::Anim::AIM, 0.0f, -1.0f, target == -1); + } + // shotgun reload sound + if (wpnCurrent == Weapon::SHOTGUN) { + if (frameIndex >= 10 && arms[i].lastFrame < 10) + playSound(TR::SND_SHOTGUN_RELOAD, pos, Sound::Flags::PAN); + } + } + arms[i].lastFrame = frameIndex; + if (wpnCurrent == Weapon::SHOTGUN) break; + } + + if (armShot[0] || armShot[1]) + doShot(armShot[0], armShot[1]); + } + + void doShot(bool rightHand, bool leftHand) { int count = wpnCurrent == Weapon::SHOTGUN ? 6 : 2; float nearDist = 32.0f * 1024.0f; vec3 nearPos; + bool hasShot = false; 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); + Arm *arm; + if (wpnCurrent == Weapon::SHOTGUN) { + if (!rightHand) continue; + arm = &arms[0]; + } else { + if (!(i ? leftHand : rightHand)) continue; + arm = &arms[i]; + } - vec3 t = p + d * (24.0f * 1024.0f) + ((vec3(randf(), randf(), randf()) * 2.0f) - 1.0f) * 1024.0f; + arm->shotTimer = 0.0f; + hasShot = true; + + int joint = wpnCurrent == Weapon::SHOTGUN ? 8 : (i ? 11 : 8); + + quat tmp = animOverrides[joint]; + animOverrides[joint] = arm->rot * quat(vec3(1, 0, 0), PI * 0.5f) ; + mat4 m = getJoint(joint, true); + animOverrides[joint] = tmp; + + vec3 p = m.getPos(); + vec3 d = (m * vec4(0, 1, 0, 0)).xyz; + vec3 t = p + d * (24.0f * 1024.0f) + ((vec3(randf(), randf(), randf()) * 2.0f) - vec3(1.0f)) * 1024.0f; int room; vec3 hit = trace(getRoomIndex(), p, t, room, false); @@ -428,155 +537,264 @@ struct Lara : Controller { } } - playSound(TR::SND_RICOCHET, nearPos, Sound::Flags::PAN); - - wpnShotTime[0] = wpnShotTime[1] = 0.0f; + if (hasShot) { + playSound(wpnGetSound(), pos, Sound::Flags::PAN); + playSound(TR::SND_RICOCHET, nearPos, Sound::Flags::PAN); + } } void updateWeapon() { - wpnShotTime[0] += Core::deltaTime; - wpnShotTime[1] += Core::deltaTime; + updateTargets(); + updateOverrides(); - TR::Animation *anim = &level->anims[getWeaponAnimIndex(wpnAnim)]; + if (Input::down[ik1]) wpnChange(Weapon::PISTOLS); + if (Input::down[ik2]) wpnChange(Weapon::SHOTGUN); + if (Input::down[ik3]) wpnChange(Weapon::MAGNUMS); + if (Input::down[ik4]) wpnChange(Weapon::UZIS); - 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(); + if (wpnNext != Weapon::EMPTY && emptyHands()) { + wpnSet(wpnNext); + wpnDraw(); 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(); + wpnDraw(); else - hideWeapon(); + wpnHide(); } 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; + bool isRifle = wpnCurrent == Weapon::SHOTGUN; + + for (int i = 0; i < 2; i++) { + if (arms[i].target > -1 || ((mask & ACTION) && target == -1)) { + if (arms[i].anim == Weapon::Anim::HOLD) + wpnSetAnim(arms[i], wpnState, Weapon::Anim::AIM, 0.0f, 1.0f); + } else + if (arms[i].anim == Weapon::Anim::AIM) + arms[i].animDir = -1.0f; + + if (isRifle) break; + } + + for (int i = 0; i < 2; i++){ + arms[i].animTime += Core::deltaTime * arms[i].animDir; + arms[i].shotTimer += Core::deltaTime; + } + + if (isRifle) + animateShotgun(); + else + animatePistols(); + + wpnFire(); // make a shot } + } - anim = &level->anims[getWeaponAnimIndex(wpnAnim)]; - float maxTime = (anim->frameEnd - anim->frameStart) / 30.0f; + void animatePistols() { + for (int i = 0; i < 2; i++) { + Arm &arm = arms[i]; - 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); + if (arm.animDir >= 0.0f && arm.animTime >= arm.animMaxTime) + switch (arm.anim) { + case Weapon::Anim::PREPARE : wpnSetAnim(arm, Weapon::IS_ARMED, Weapon::Anim::UNHOLSTER, arm.animTime - arm.animMaxTime, 1.0f); break; + case Weapon::Anim::UNHOLSTER : wpnSetAnim(arm, Weapon::IS_ARMED, Weapon::Anim::HOLD, 0.0f, 1.0f, false); break; + case Weapon::Anim::AIM : + if (mask & ACTION) + wpnSetAnim(arm, Weapon::IS_FIRING, Weapon::Anim::FIRE, arm.animTime - arm.animMaxTime, wpnCurrent == Weapon::UZIS ? 2.0f : 1.0f); + else + wpnSetAnim(arm, Weapon::IS_ARMED, Weapon::Anim::AIM, 0.0f, -1.0f, false); 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; + if (arm.animDir < 0.0f && arm.animTime <= 0.0f) + switch (arm.anim) { + case Weapon::Anim::PREPARE : wpnSetAnim(arm, Weapon::IS_HIDDEN, Weapon::Anim::NONE, 0.0f, 1.0f, false); break; + case Weapon::Anim::UNHOLSTER : wpnSetAnim(arm, Weapon::IS_HIDDEN, Weapon::Anim::PREPARE, arm.animTime, -1.0f); break; + case Weapon::Anim::AIM : wpnSetAnim(arm, Weapon::IS_ARMED, Weapon::Anim::HOLD, 0.0f, 1.0f, false); break; default : ; }; } + } - if (prevAnim != wpnAnim) // check by cache - anim = &level->anims[getWeaponAnimIndex(wpnAnim)]; + void animateShotgun() { + /* + float &wpnAnimDir = arms[0].animDir; + float &wpnAnimTime = arms[0].animTime; + Weapon::Anim &wpnAnim = arms[0].anim; + TR::Animation *anim = &level->anims[getWeaponAnimIndex(arms[0].anim)]; + float maxTime = (anim->frameEnd - anim->frameStart) / 30.0f; - // 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); + 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); + }; + */ + } + + void updateOverrides() { + // update animation overrides + TR::AnimFrame *frameA, *frameB; + float t; + + // head & chest + animOverrideMask |= BODY_CHEST | BODY_HEAD; + + getFrames(&frameA, &frameB, t, animIndex, animTime, true); + animOverrides[ 7] = lerpFrames(frameA, frameB, t, 7); + animOverrides[14] = lerpFrames(frameA, frameB, t, 14); + + // right arm + if (arms[0].anim != Weapon::Anim::NONE) { + getFrames(&frameA, &frameB, t, arms[0].animIndex, arms[0].animTime); + animOverrides[ 8] = lerpFrames(frameA, frameB, t, 8); + animOverrides[ 9] = lerpFrames(frameA, frameB, t, 9); + animOverrides[10] = lerpFrames(frameA, frameB, t, 10); + animOverrideMask |= BODY_ARM_R; + } else + animOverrideMask &= ~BODY_ARM_R; + + // left arm + if (arms[1].anim != Weapon::Anim::NONE) { + getFrames(&frameA, &frameB, t, arms[1].animIndex, arms[1].animTime); + animOverrides[11] = lerpFrames(frameA, frameB, t, 11); + animOverrides[12] = lerpFrames(frameA, frameB, t, 12); + animOverrides[13] = lerpFrames(frameA, frameB, t, 13); + animOverrideMask |= BODY_ARM_L; + } else + animOverrideMask &= ~BODY_ARM_L; + + lookAt(target); + + if (wpnCurrent != Weapon::SHOTGUN) + aimPistols(); + } + + void lookAt(int target) { + float speed = 8.0f * Core::deltaTime; + quat rot; + + // chest + if ((stand == STAND_GROUND || stand == STAND_SLIDE) && aim(target, 7, vec4(-PI * 0.4f, PI * 0.4f, -PI * 0.9f, PI * 0.9f), rot)) + rotChest = rotChest.slerp(quat(0, 0, 0, 1).slerp(rot, 0.5f), speed); + else + rotChest = rotChest.slerp(quat(0, 0, 0, 1), speed); + animOverrides[7] = rotChest * animOverrides[7]; + + // head + if (aim(target, 14, vec4(-PI * 0.25f, PI * 0.25f, -PI * 0.5f, PI * 0.5f), rot)) + rotHead = rotHead.slerp(rot, speed); + else + rotHead = rotHead.slerp(quat(0, 0, 0, 1), speed); + animOverrides[14] = rotHead * animOverrides[14]; + } + + void aimPistols() { + float speed = 8.0f * Core::deltaTime; + + for (int i = 0; i < 2; i++) { + int joint = i ? 11 : 8; + vec4 range = i ? vec4(-PI * 0.4f, PI * 0.4f, -PI * 0.5f, PI * 0.2f) : vec4(-PI * 0.4f, PI * 0.4f, -PI * 0.2f, PI * 0.5f); + + Arm *arm = &arms[i]; + quat rot; + if (!aim(target, joint, range, rot)) { + arm->target = -1; + rot = quat(0, 0, 0, 1); + } else + arm->target = target; + + float t; + if (arm->anim == Weapon::Anim::FIRE) + t = 1.0f; + else if (arm->anim == Weapon::Anim::AIM) + t = arm->animTime / arm->animMaxTime; + else + t = 0.0f; + + arm->rot = arm->rot.slerp(rot, speed); + animOverrides[joint] = animOverrides[joint].slerp(arm->rot * animOverrides[joint], t); } - wpnLastFrame = frameIndex; + } + bool aim(int target, int joint, const vec4 &angleRange, quat &rot) { + if (target == -1) return false; - if (wpnAnim == Weapon::Anim::NONE) { - animOverrideMask &= ~(BODY_ARM_L | BODY_ARM_R); + TR::Entity &e = level->entities[target]; + vec3 t(e.x, e.y, e.z); + + mat4 m = getJoint(joint); + vec3 delta = (m.inverse() * t).normal(); + + float angleY = clampAngle(atan2(delta.x, delta.z)); + float angleX = clampAngle(asinf(delta.y)); + + if (angleX < angleRange.x || angleX > angleRange.y || + angleY < angleRange.z || angleY > angleRange.w) + return false; + + quat ax(vec3(1, 0, 0), -angleX); + quat ay(vec3(0, 1, 0), angleY); + + rot = ay * ax; + return true; + } + + void updateTargets() { + if (emptyHands() || !wpnReady()) { + target = arms[0].target = arms[1].target = -1; return; } - // update animation overrides - float k = wpnAnimTime * 30.0f / anim->frameRate; - int fIndex = (int)k; - int fCount = (anim->frameEnd - anim->frameStart) / anim->frameRate + 1; + if (!(mask & ACTION) || target == -1) { + target = getTarget(); + arms[0].target = target; + arms[1].target = target; + } + } - int fSize = sizeof(TR::AnimFrame) + getModel().mCount * sizeof(uint16) * 2; - k = k - fIndex; + int getTarget() { + int dist = TARGET_MAX_DIST * 3.0f; + + int index = -1; + TR::Entity &entity = getEntity(); + for (int i = 0; i < level->entitiesCount; i++) { + TR::Entity &e = level->entities[i]; + if (!e.isEnemy()) continue; - 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]; + int d = abs(e.x - entity.x) + abs(e.y - entity.y) + abs(e.z - entity.z); + if (d < dist) { + index = i; + dist = d; + } + } - // 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); + return index; } bool waterOut(int &outState) { @@ -748,49 +966,13 @@ struct Lara : Controller { } vec3 getViewOffset() { - TR::Animation *anim = &level->anims[animIndex]; - TR::Model &model = getModel(); + vec3 offset = vec3(0.0f); + if (stand != STAND_UNDERWATER) + offset.y -= 256.0f; + if (wpnState != Weapon::IS_HIDDEN) + offset.y -= 256.0f; - float k = animTime * 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]; - - float h = ((vec3)frameA->pos).lerp(move + frameB->pos, k).y; - - switch (stand) { - case Controller::STAND_AIR : - case Controller::STAND_GROUND : - 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 : - h -= 128.0f; - break; - } - - return vec3(0.0f, h, 0.0f); + return chestOffset - pos + offset; } virtual Stand getStand() { @@ -811,7 +993,7 @@ struct Lara : Controller { return stand; if (getRoom().flags.water) { - hideWeapon(); + wpnHide(); return STAND_UNDERWATER; } @@ -1173,8 +1355,8 @@ struct Lara : Controller { if (!lState) { lState = true; - static int snd_id = 0; - //playSound(snd_id, pos, 0); + static int snd_id = 81; + playSound(snd_id, pos, 0); LOG("sound: %d\n", snd_id++); /* @@ -1536,7 +1718,7 @@ struct Lara : Controller { } void renderMuzzleFlash(MeshBuilder *mesh, const mat4 &matrix, const vec3 &offset, float time) { - if (time > 0.1f) return; + if (time > MUZZLE_FLASH_TIME) return; float alpha = min(1.0f, (0.1f - time) * 20.0f); float lum = 3.0f; @@ -1552,9 +1734,11 @@ struct Lara : Controller { virtual void render(Frustum *frustum, MeshBuilder *mesh) { Controller::render(frustum, mesh); + chestOffset = joints[7].getPos(); + if (wpnCurrent != Weapon::SHOTGUN) { - renderMuzzleFlash(mesh, joints[10], vec3(-10, -50, 150), wpnShotTime[0]); - renderMuzzleFlash(mesh, joints[13], vec3( 10, -50, 150), wpnShotTime[1]); + renderMuzzleFlash(mesh, joints[10], vec3(-10, -50, 150), arms[0].shotTimer); + renderMuzzleFlash(mesh, joints[13], vec3( 10, -50, 150), arms[1].shotTimer); } } }; diff --git a/src/utils.h b/src/utils.h index 44aa637..3728077 100644 --- a/src/utils.h +++ b/src/utils.h @@ -141,6 +141,10 @@ struct vec3 { float s = sinf(angle), c = cosf(angle); return vec3(x*c - z*s, y, x*s + z*c); } + + float angle(const vec3 &v) { + return dot(v) / (length() * v.length()); + } }; struct vec4 { @@ -158,7 +162,10 @@ struct vec4 { }; struct quat { - float x, y, z, w; + union { + struct { float x, y, z, w; }; + struct { vec3 xyz; }; + }; quat() {} quat(float x, float y, float z, float w) : x(x), y(y), z(z), w(w) {} @@ -492,20 +499,17 @@ struct mat4 { } }; +quat rotYXZ(const vec3 &a) { + mat4 m; + m.identity(); + m.rotateY(a.y); + m.rotateX(a.x); + m.rotateZ(a.z); + return m.getRot(); +} + quat lerpAngle(const vec3 &a, const vec3 &b, float t) { // TODO: optimization - mat4 ma, mb; - ma.identity(); - mb.identity(); - - ma.rotateY(a.y); - ma.rotateX(a.x); - ma.rotateZ(a.z); - - mb.rotateY(b.y); - mb.rotateX(b.x); - mb.rotateZ(b.z); - - return ma.getRot().slerp(mb.getRot(), t).normal(); + return rotYXZ(a).slerp(rotYXZ(b), t).normal(); } vec3 boxNormal(int x, int z) { From 08dc561617910b2678a2a0e2f27b7e9768abc34f Mon Sep 17 00:00:00 2001 From: XProger Date: Tue, 15 Nov 2016 03:45:16 +0300 Subject: [PATCH 2/4] #7 auto-aiming, enemies hit; #8 camera look at enemy, view point on Lara's chest moving while moving blocks; #3 head & chest IK; #15 preventing closing page by Ctrl + W (request) --- bin/OpenLara.exe | Bin 100352 -> 107008 bytes src/camera.h | 9 +- src/controller.h | 60 ++++---- src/enemy.h | 9 +- src/lara.h | 275 +++++++++++++++++++----------------- src/level.h | 10 +- src/platform/web/index.html | 29 ++-- src/utils.h | 25 +++- 8 files changed, 230 insertions(+), 187 deletions(-) diff --git a/bin/OpenLara.exe b/bin/OpenLara.exe index c350d1d5708122a7fcc42b9994ff0c756b4aad2f..54b450cc73fabe846bcfb59e8032f99054a22ef8 100644 GIT binary patch delta 35540 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 V?BXDex-+7R

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 : 0), owner(owner), frustum(new Frustum()), timer(0.0f), actTargetEntity(-1), actCamera(-1) { fov = 80.0f; @@ -30,8 +29,8 @@ struct Camera : Controller { if (owner) { room = owner->getEntity().room; pos = pos - owner->getDir() * 1024.0f; + target = owner->getViewPoint(); } - viewOffset = owner->getViewOffset(); } virtual ~Camera() { @@ -92,11 +91,9 @@ 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); - + float lerpFactor = (actTargetEntity == -1) ? 6.0f : 10.0f; vec3 dir; - target = vec3(owner->pos.x, owner->pos.y, owner->pos.z) + viewOffset; + target = target.lerp(owner->getViewPoint(), lerpFactor * Core::deltaTime); if (actCamera > -1) { TR::Camera &c = level->cameras[actCamera]; diff --git a/src/controller.h b/src/controller.h index 9162104..aa96ffd 100644 --- a/src/controller.h +++ b/src/controller.h @@ -39,10 +39,6 @@ struct Controller { float angleExt; - int health; - - float turnTime; - int *meshes; int mCount; quat *animOverrides; // left & right arms animation frames @@ -59,7 +55,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), mCount(0), meshes(NULL), animOverrides(NULL), animOverrideMask(0), joints(NULL) { + Controller(TR::Level *level, int entity) : level(level), entity(entity), velocity(0.0f), animTime(0.0f), animPrevFrame(0), 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); @@ -528,6 +524,7 @@ struct Controller { virtual int getStateDeath() { return state; } virtual int getStateDefault() { return state; } virtual int getInputMask() { return 0; } + virtual void hit(int damage) { }; virtual int getState(Stand stand) { TR::Animation *anim = &level->anims[animIndex]; @@ -673,12 +670,12 @@ struct Controller { 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 renderMesh(const mat4 &matrix, MeshBuilder *mesh, uint32 offsetIndex) { + MeshBuilder::MeshInfo *mInfo = mesh->meshMap[offsetIndex]; + if (!mInfo) return; // invisible mesh (offsetIndex > 0 && level.meshOffsets[offsetIndex] == 0) camera target entity etc. + + Core::active.shader->setParam(uModel, matrix); + mesh->renderMesh(mInfo); } void renderShadow(MeshBuilder *mesh, const vec3 &pos, const vec3 &offset, const vec3 &size, float angle) { @@ -695,15 +692,16 @@ struct Controller { } virtual void render(Frustum *frustum, MeshBuilder *mesh) { - PROFILE_MARKER("MDL"); TR::Entity &entity = getEntity(); TR::Model &model = getModel(); TR::Animation *anim = &level->anims[animIndex]; - 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); + mat4 matrix(Core::mModel); + matrix.translate(pos); + if (angle.y != 0.0f) matrix.rotateY(angle.y); + if (angle.x != 0.0f) matrix.rotateX(angle.x); + if (angle.z != 0.0f) matrix.rotateZ(angle.z); float t; vec3 move(0.0f); @@ -712,14 +710,13 @@ struct Controller { vec3 bmin = frameA->box.min().lerp(frameB->box.min(), t); vec3 bmax = frameA->box.max().lerp(frameB->box.max(), t); - if (frustum && !frustum->isVisible(Core::mModel, bmin, bmax)) + if (frustum && !frustum->isVisible(matrix, bmin, bmax)) return; + entity.flags.rendered = true; 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, t)); + matrix.translate(((vec3)frameA->pos).lerp(move + frameB->pos, t)); int sIndex = 0; mat4 stack[20]; @@ -729,12 +726,12 @@ struct Controller { if (i > 0 && node) { TR::Node &t = node[i - 1]; - if (t.flags & 0x01) m = stack[--sIndex]; - if (t.flags & 0x02) stack[sIndex++] = m; + if (t.flags & 0x01) matrix = stack[--sIndex]; + if (t.flags & 0x02) stack[sIndex++] = matrix; ASSERT(sIndex >= 0 && sIndex < 20); - m.translate(vec3(t.x, t.y, t.z)); + matrix.translate(vec3(t.x, t.y, t.z)); } quat q; @@ -742,19 +739,15 @@ struct Controller { q = animOverrides[i]; else q = lerpAngle(frameA->getAngle(i), frameB->getAngle(i), t); - m = m * mat4(q, vec3(0.0f)); - - mat4 tmp = Core::mModel; - Core::mModel = Core::mModel * m; + matrix = matrix * mat4(q, vec3(0.0f)); + if (meshes) - renderMesh(mesh, meshes[i]); + renderMesh(matrix, mesh, meshes[i]); else - renderMesh(mesh, model.mStart + i); + renderMesh(matrix, mesh, model.mStart + i); if (joints) - joints[i] = Core::mModel; - - Core::mModel = tmp; + joints[i] = matrix; } if (TR::castShadow(entity.type)) { @@ -814,8 +807,9 @@ struct SpriteController : Controller { } virtual void render(Frustum *frustum, MeshBuilder *mesh) { - PROFILE_MARKER("SPR"); - Core::active.shader->setParam(uModel, Core::mModel); + mat4 m(Core::mModel); + m.translate(pos); + Core::active.shader->setParam(uModel, m); mesh->renderSprite(-(getEntity().modelIndex + 1), frame); } }; diff --git a/src/enemy.h b/src/enemy.h index 8dfb9a9..9968fa2 100644 --- a/src/enemy.h +++ b/src/enemy.h @@ -4,11 +4,18 @@ #include "controller.h" struct Enemy : Controller { - Enemy(TR::Level *level, int entity) : Controller(level, entity) {} + int health; + + Enemy(TR::Level *level, int entity) : Controller(level, entity), health(100) {} virtual Stand getStand() { return STAND_GROUND; } + + virtual void hit(int damage) { + health -= damage; + }; + }; diff --git a/src/lara.h b/src/lara.h index cb86096..4df92e2 100644 --- a/src/lara.h +++ b/src/lara.h @@ -187,20 +187,23 @@ struct Lara : Controller { float animTime; float animMaxTime; int animIndex; - quat rot; + quat rot, rotAbs; Weapon::Anim anim; } arms[2]; int target; quat rotHead, rotChest; + int health; + float turnTime; - Lara(TR::Level *level, int entity) : Controller(level, entity), wpnCurrent(Weapon::EMPTY), wpnNext(Weapon::EMPTY), chestOffset(pos), target(-1) { + Lara(TR::Level *level, int entity) : Controller(level, entity), wpnCurrent(Weapon::EMPTY), wpnNext(Weapon::EMPTY), chestOffset(pos), target(-1), health(100), turnTime(0.0f) { initMeshOverrides(); for (int i = 0; i < 2; i++) { arms[i].shotTimer = MUZZLE_FLASH_TIME + 1.0f; arms[i].animTime = 0.0f; arms[i].rot = quat(0, 0, 0, 1); + arms[i].rotAbs = quat(0, 0, 0, 1); } rotHead = rotChest = quat(0, 0, 0, 1); @@ -261,7 +264,7 @@ struct Lara : Controller { updateEntity(); #endif } - + void wpnSet(Weapon::Type wType) { wpnCurrent = wType; wpnState = Weapon::IS_FIRING; @@ -289,6 +292,16 @@ struct Lara : Controller { wpnSetState(wState); } + int wpnGetDamage() { + switch (wpnCurrent) { + case Weapon::PISTOLS : return 10; + case Weapon::SHOTGUN : return 5; + case Weapon::MAGNUMS : return 20; + case Weapon::UZIS : return 5; + default : return 0; + } + } + void wpnSetState(Weapon::State wState) { if (wpnState == wState) return; @@ -335,32 +348,6 @@ struct Lara : Controller { wpnState = wState; } - /* - void setWeaponAnim(Weapon::Anim wAnim, float wAnimDir, Arm *arm) { - arm->animDir = wAnimDir; - - if (wAnim != arm->anim) { - arm->anim = wAnim; - TR::Animation *anim = &level->anims[getWeaponAnimIndex(arm->anim)]; - arm->animTime = arm->animDir >= 0.0f ? 0.0f : ((anim->frameEnd - anim->frameStart) / 30.0f); - arm->lastFrame = 0xFFFF; - } - } - - void setWeapon(Weapon::Type wType, Weapon::State wState, Weapon::Anim wAnim = Weapon::Anim::NONE, float wAnimDir = 0.0f, Arm *arm = NULL) { - - if (wpnCurrent != Weapon::SHOTGUN && wAnim != Weapon::Anim::HOLD && wAnim != Weapon::Anim::AIM && wAnim != Weapon::Anim::FIRE) { - setWeaponAnim(wAnim, wAnimDir, &arms[0]); - setWeaponAnim(wAnim, wAnimDir, &arms[1]); - } else - setWeaponAnim(wAnim, wAnimDir, !arm ? &arms[1] : arm); - - if (wpnCurrent == wType && wpnState == wState) - return; - wpnCurrent = wType; - wpnSetState(wState); - } -*/ bool emptyHands() { return arms[0].anim == Weapon::Anim::NONE; } @@ -422,7 +409,7 @@ struct Lara : Controller { wpnSetAnim(arms[0], wpnState, Weapon::Anim::UNHOLSTER, 0.0f, -1.0f); wpnSetAnim(arms[1], wpnState, Weapon::Anim::UNHOLSTER, 0.0f, -1.0f); } else - wpnSetAnim(arms[0], wpnState, Weapon::Anim::UNHOLSTER, 0.0f, 1.0f); + wpnSetAnim(arms[0], wpnState, Weapon::Anim::HOLSTER, 0.0f, 1.0f); } } @@ -474,7 +461,7 @@ struct Lara : Controller { int frameIndex = getFrameIndex(arms[i].animIndex, arms[i].animTime); if (arms[i].anim == Weapon::Anim::FIRE) { if (frameIndex < arms[i].lastFrame) { - if (target == -1 || ((mask & ACTION) && arms[i].target > -1)) { + if ((mask & ACTION) && (target == -1 || (target > -1 && arms[i].target > -1))) { armShot[i] = true; } else wpnSetAnim(arms[i], Weapon::IS_ARMED, Weapon::Anim::AIM, 0.0f, -1.0f, target == -1); @@ -516,24 +503,25 @@ struct Lara : Controller { int joint = wpnCurrent == Weapon::SHOTGUN ? 8 : (i ? 11 : 8); - quat tmp = animOverrides[joint]; - animOverrides[joint] = arm->rot * quat(vec3(1, 0, 0), PI * 0.5f) ; - mat4 m = getJoint(joint, true); - animOverrides[joint] = tmp; - - vec3 p = m.getPos(); - vec3 d = (m * vec4(0, 1, 0, 0)).xyz; + vec3 p = getJoint(joint, false).getPos(); + vec3 d = arm->rotAbs * vec3(0, 0, 1); vec3 t = p + d * (24.0f * 1024.0f) + ((vec3(randf(), randf(), randf()) * 2.0f) - vec3(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); + if (target > -1 && checkHit(target, p, hit, hit)) { + ((Controller*)level->entities[target].controller)->hit(wpnGetDamage()); + hit -= d * 64.0f; + addSprite(level, TR::Entity::BLOOD, room, (int)hit.x, (int)hit.y, (int)hit.z, SpriteController::FRAME_ANIMATED); + } else { + 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; + float dist = (hit - p).length(); + if (dist < nearDist) { + nearPos = hit; + nearDist = dist; + } } } @@ -622,39 +610,35 @@ struct Lara : Controller { } void animateShotgun() { - /* - float &wpnAnimDir = arms[0].animDir; - float &wpnAnimTime = arms[0].animTime; - Weapon::Anim &wpnAnim = arms[0].anim; - TR::Animation *anim = &level->anims[getWeaponAnimIndex(arms[0].anim)]; - float maxTime = (anim->frameEnd - anim->frameStart) / 30.0f; - - if (wpnAnimDir > 0.0f) - switch (wpnAnim) { + Arm &arm = arms[0]; + if (arm.animDir >= 0.0f) + switch (arm.anim) { 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); + if (arm.animTime >= arm.animMaxTime) + wpnSetAnim(arm, Weapon::IS_ARMED, Weapon::Anim::HOLD, 0.0f, 1.0f, false); + else if (arm.animTime >= arm.animMaxTime * 0.3f) + wpnSetAnim(arm, Weapon::IS_ARMED, arm.anim, arm.animTime, 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); + if (arm.animTime >= arm.animMaxTime) + wpnSetAnim(arm, Weapon::IS_HIDDEN, Weapon::Anim::NONE, 0.0f, 1.0f, false); + else if (arm.animTime >= arm.animMaxTime * 0.7f) + wpnSetAnim(arm, Weapon::IS_HIDDEN, arm.anim, arm.animTime, 1.0f); break; case Weapon::Anim::AIM : - if (wpnAnimTime >= maxTime) - setWeapon(wpnCurrent, Weapon::IS_FIRING, Weapon::Anim::FIRE, wpnAnimDir); + if (arm.animTime >= arm.animMaxTime) { + if (mask & ACTION) + wpnSetAnim(arm, Weapon::IS_FIRING, Weapon::Anim::FIRE, arm.animTime - arm.animMaxTime, 1.0f); + else + wpnSetAnim(arm, Weapon::IS_ARMED, Weapon::Anim::AIM, 0.0f, -1.0f, false); + } break; default : ; }; - if (wpnAnimDir < 0.0f && wpnAnimTime <= 0.0f) - if (wpnAnim == Weapon::Anim::AIM) { - setWeapon(wpnCurrent, wpnState, Weapon::Anim::HOLD, 0.0f); - }; - */ + if (arm.animDir < 0.0f && arm.animTime <= 0.0f) + if (arm.anim == Weapon::Anim::AIM) + wpnSetAnim(arm, Weapon::IS_ARMED, Weapon::Anim::HOLD, 0.0f, 1.0f, false); } void updateOverrides() { @@ -669,29 +653,30 @@ struct Lara : Controller { animOverrides[ 7] = lerpFrames(frameA, frameB, t, 7); animOverrides[14] = lerpFrames(frameA, frameB, t, 14); - // right arm - if (arms[0].anim != Weapon::Anim::NONE) { - getFrames(&frameA, &frameB, t, arms[0].animIndex, arms[0].animTime); + + if (!emptyHands()) { + // right arm + Arm *arm = &arms[0]; + getFrames(&frameA, &frameB, t, arm->animIndex, arm->animTime); animOverrides[ 8] = lerpFrames(frameA, frameB, t, 8); animOverrides[ 9] = lerpFrames(frameA, frameB, t, 9); animOverrides[10] = lerpFrames(frameA, frameB, t, 10); - animOverrideMask |= BODY_ARM_R; - } else - animOverrideMask &= ~BODY_ARM_R; - - // left arm - if (arms[1].anim != Weapon::Anim::NONE) { - getFrames(&frameA, &frameB, t, arms[1].animIndex, arms[1].animTime); + // left arm + if (wpnCurrent != Weapon::SHOTGUN) arm = &arms[1]; + getFrames(&frameA, &frameB, t, arm->animIndex, arm->animTime); animOverrides[11] = lerpFrames(frameA, frameB, t, 11); animOverrides[12] = lerpFrames(frameA, frameB, t, 12); animOverrides[13] = lerpFrames(frameA, frameB, t, 13); - animOverrideMask |= BODY_ARM_L; + + animOverrideMask |= BODY_ARM_R | BODY_ARM_L; } else - animOverrideMask &= ~BODY_ARM_L; + animOverrideMask &= ~(BODY_ARM_R | BODY_ARM_L); lookAt(target); - - if (wpnCurrent != Weapon::SHOTGUN) + + if (wpnCurrent == Weapon::SHOTGUN) + aimShotgun(); + else aimPistols(); } @@ -714,6 +699,12 @@ struct Lara : Controller { animOverrides[14] = rotHead * animOverrides[14]; } + void aimShotgun() { + quat rot; + if (!aim(target, 14, vec4(-PI * 0.4f, PI * 0.4f, -PI * 0.25f, PI * 0.25f), rot, &arms[0].rotAbs)) + arms[0].target = -1; + } + void aimPistols() { float speed = 8.0f * Core::deltaTime; @@ -721,48 +712,53 @@ struct Lara : Controller { int joint = i ? 11 : 8; vec4 range = i ? vec4(-PI * 0.4f, PI * 0.4f, -PI * 0.5f, PI * 0.2f) : vec4(-PI * 0.4f, PI * 0.4f, -PI * 0.2f, PI * 0.5f); - Arm *arm = &arms[i]; + Arm &arm = arms[i]; quat rot; - if (!aim(target, joint, range, rot)) { - arm->target = -1; + if (!aim(target, joint, range, rot, &arm.rotAbs)) { + arm.target = -1; rot = quat(0, 0, 0, 1); - } else - arm->target = target; + } float t; - if (arm->anim == Weapon::Anim::FIRE) + if (arm.anim == Weapon::Anim::FIRE) t = 1.0f; - else if (arm->anim == Weapon::Anim::AIM) - t = arm->animTime / arm->animMaxTime; + else if (arm.anim == Weapon::Anim::AIM) + t = arm.animTime / arm.animMaxTime; else t = 0.0f; - arm->rot = arm->rot.slerp(rot, speed); - animOverrides[joint] = animOverrides[joint].slerp(arm->rot * animOverrides[joint], t); + arm.rot = arm.rot.slerp(rot, speed); + animOverrides[joint] = animOverrides[joint].slerp(arm.rot * animOverrides[joint], t); } } - bool aim(int target, int joint, const vec4 &angleRange, quat &rot) { - if (target == -1) return false; + bool aim(int target, int joint, const vec4 &angleRange, quat &rot, quat *rotAbs = NULL) { + if (target > -1) { + TR::Entity &e = level->entities[target]; + vec3 t(e.x, e.y, e.z); - TR::Entity &e = level->entities[target]; - vec3 t(e.x, e.y, e.z); + mat4 m = getJoint(joint); + vec3 delta = (m.inverse() * t).normal(); - mat4 m = getJoint(joint); - vec3 delta = (m.inverse() * t).normal(); + float angleY = clampAngle(atan2(delta.x, delta.z)); + float angleX = clampAngle(asinf(delta.y)); - float angleY = clampAngle(atan2(delta.x, delta.z)); - float angleX = clampAngle(asinf(delta.y)); + if (angleX > angleRange.x && angleX <= angleRange.y && + angleY > angleRange.z && angleY <= angleRange.w) { - if (angleX < angleRange.x || angleX > angleRange.y || - angleY < angleRange.z || angleY > angleRange.w) - return false; + quat ax(vec3(1, 0, 0), -angleX); + quat ay(vec3(0, 1, 0), angleY); - quat ax(vec3(1, 0, 0), -angleX); - quat ay(vec3(0, 1, 0), angleY); + rot = ay * ax; + if (rotAbs) + *rotAbs = m.getRot() * rot; + return true; + } + } - rot = ay * ax; - return true; + if (rotAbs) + *rotAbs = rotYXZ(angle); + return false; } void updateTargets() { @@ -771,24 +767,33 @@ struct Lara : Controller { return; } - if (!(mask & ACTION) || target == -1) { + if (!(mask & ACTION)) { target = getTarget(); - arms[0].target = target; - arms[1].target = target; - } + arms[0].target = arms[1].target = target; + } else + if (target > -1) { + TR::Entity &e = level->entities[target]; + vec3 to = vec3(e.x, e.y, e.z); + vec3 from = pos - vec3(0, 512, 0); + arms[0].target = arms[1].target = checkOcclusion(from, to, (to - from).length()) ? target : -1; + } } int getTarget() { - int dist = TARGET_MAX_DIST * 3.0f; - + vec3 dir = getDir().normal(); + int dist = TARGET_MAX_DIST;// * TARGET_MAX_DIST; + int index = -1; - TR::Entity &entity = getEntity(); for (int i = 0; i < level->entitiesCount; i++) { TR::Entity &e = level->entities[i]; - if (!e.isEnemy()) continue; + if (!e.flags.rendered || !e.isEnemy()) continue; - int d = abs(e.x - entity.x) + abs(e.y - entity.y) + abs(e.z - entity.z); - if (d < dist) { + vec3 p = vec3(e.x, e.y, e.z); + vec3 v = p - pos; + if (dir.dot(v.normal()) <= 0.5f) continue; // target is out of sigth -60..+60 degrees + + int d = v.length(); + if (d < dist && checkOcclusion(pos - vec3(0, 512, 0), p, d) ) { index = i; dist = d; } @@ -797,6 +802,25 @@ struct Lara : Controller { return index; } + bool checkOcclusion(const vec3 &from, const vec3 &to, float dist) { + int room; + vec3 d = trace(getRoomIndex(), from, to, room, false); // check occlusion + return ((d - from).length() > (dist - 512.0f)); + } + + bool checkHit(int target, const vec3 &from, const vec3 &to, vec3 &point) { + TR::Entity &e = level->entities[target]; + Box box = ((Controller*)e.controller)->getBoundingBox(); + float t; + vec3 v = (to - from); + vec3 dir = v.normal(); + if (box.intersect(from, dir, t) && v.length() > t) { + point = from + dir * t; + return true; + } else + return false; + } + bool waterOut(int &outState) { // TODO: playSound 36 vec3 dst = pos + getDir() * 32.0f; @@ -965,14 +989,14 @@ struct Lara : Controller { activateNext(); } - vec3 getViewOffset() { - vec3 offset = vec3(0.0f); + vec3 getViewPoint() { + vec3 offset = chestOffset; if (stand != STAND_UNDERWATER) offset.y -= 256.0f; if (wpnState != Weapon::IS_HIDDEN) offset.y -= 256.0f; - return chestOffset - pos + offset; + return offset; } virtual Stand getStand() { @@ -1722,14 +1746,11 @@ struct Lara : Controller { 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); + mat4 m(matrix); + m.rotateX(-PI * 0.5f); + m.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; + renderMesh(m, mesh, level->models[47].mStart); } virtual void render(Frustum *frustum, MeshBuilder *mesh) { diff --git a/src/level.h b/src/level.h index d10a685..cded603 100644 --- a/src/level.h +++ b/src/level.h @@ -371,12 +371,9 @@ struct Level { ASSERT(entity.controller); TR::Room &room = level.rooms[entity.room]; - if (!room.flags.rendered || entity.flags.invisible) // check for room visibility + if (!room.flags.rendered || entity.flags.invisible || entity.flags.rendered) return; - mat4 m = Core::mModel; - Core::mModel.translate(vec3(entity.x, entity.y, entity.z)); - float c = (entity.intensity > -1) ? (1.0f - entity.intensity / (float)0x1FFF) : 1.0f; float l = 1.0f; @@ -395,8 +392,6 @@ struct Level { Core::active.shader->setParam(uColor, Core::color); ((Controller*)entity.controller)->render(camera->frustum, mesh); - - Core::mModel = m; } void update() { @@ -446,6 +441,9 @@ struct Level { for (int j = 0; j < room.meshesCount; j++) room.meshes[j].flags.rendered = false; // clear visible flag for room static meshes } + + for (int i = 0; i < level.entitiesCount; i++) + level.entities[i].flags.rendered = false; } void renderRooms() { diff --git a/src/platform/web/index.html b/src/platform/web/index.html index d9f5488..f3bd5e5 100644 --- a/src/platform/web/index.html +++ b/src/platform/web/index.html @@ -6,7 +6,7 @@


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
- + + - + \ No newline at end of file diff --git a/src/utils.h b/src/utils.h index 3728077..9b0b1c4 100644 --- a/src/utils.h +++ b/src/utils.h @@ -26,12 +26,12 @@ #endif #endif - +#define EPS FLT_EPSILON +#define INF INFINITY #define PI 3.14159265358979323846f #define PI2 (PI * 2.0f) #define DEG2RAD (PI / 180.0f) #define RAD2DEG (180.0f / PI) -#define EPS FLT_EPSILON #define randf() ((float)rand()/RAND_MAX) typedef char int8; @@ -202,6 +202,11 @@ struct quat { w * q.w - x * q.x - y * q.y - z * q.z); } + vec3 operator * (const vec3 &v) const { + // return v + xyz.cross(xyz.cross(v) + v * w) * 2.0f; + return (*this * quat(v.x, v.y, v.z, 0) * inverse()).xyz; + } + float dot(const quat &q) const { return x * q.x + y * q.y + z * q.z + w * q.w; } @@ -541,6 +546,22 @@ struct Box { bool intersect(const Box &box) const { return !((max.x < box.min.x || min.x > box.max.x) || (max.y < box.min.y || min.y > box.max.y) || (max.z < box.min.z || min.z > box.max.z)); } + + bool intersect(const vec3 &rayPos, const vec3 &rayDir, float &t) const { + float t1 = INF, t0 = -t1; + + for (int i = 0; i < 3; i++) + if (rayDir[i] != 0) { + float lo = (min[i] - rayPos[i]) / rayDir[i]; + float hi = (max[i] - rayPos[i]) / rayDir[i]; + t0 = ::max(t0, ::min(lo, hi)); + t1 = ::min(t1, ::max(lo, hi)); + } else + if (rayPos[i] < min[i] || rayPos[i] > max[i]) + return false; + t = t0; + return (t0 <= t1) && (t1 > 0); + } }; struct Stream { From 0921c615e379025cce0627dcf5de3bc9ee112183 Mon Sep 17 00:00:00 2001 From: XProger Date: Wed, 16 Nov 2016 03:26:41 +0300 Subject: [PATCH 3/4] #12 flash lights --- bin/OpenLara.exe | Bin 107008 -> 109568 bytes src/controller.h | 1 + src/core.h | 9 +++- src/lara.h | 10 ++++ src/level.h | 57 +++++++++++++---------- src/mesh.h | 118 +++++++++++++++++++++++++++++++---------------- src/shader.glsl | 75 ++++++++++++++++-------------- src/utils.h | 1 + 8 files changed, 170 insertions(+), 101 deletions(-) diff --git a/bin/OpenLara.exe b/bin/OpenLara.exe index 54b450cc73fabe846bcfb59e8032f99054a22ef8..de8cf3bab416e931e3617451bacb3f36f1175ce9 100644 GIT binary patch delta 31554 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, m); Core::active.shader->setParam(uColor, vec4(0.0f, 0.0f, 0.0f, 0.5f)); + Core::active.shader->setParam(uAmbient, vec3(0.0f)); mesh->renderShadowSpot(); } diff --git a/src/core.h b/src/core.h index 260b53b..21c3592 100644 --- a/src/core.h +++ b/src/core.h @@ -83,6 +83,8 @@ #endif #endif +#define MAX_LIGHTS 3 + struct Shader; struct Texture; @@ -113,8 +115,8 @@ namespace Core { float deltaTime; mat4 mView, mProj, mViewProj, mViewInv, mModel; vec3 viewPos; - vec3 lightPos; - vec4 lightColor; + vec3 lightPos[MAX_LIGHTS]; + vec4 lightColor[MAX_LIGHTS]; vec3 ambient; vec4 color; @@ -185,6 +187,9 @@ namespace Core { support.VAO = (void*)glBindVertexArray != NULL; Sound::init(); + + for (int i = 0; i < MAX_LIGHTS; i++) + lightColor[i] = vec4(0, 0, 0, 1); } void free() { diff --git a/src/lara.h b/src/lara.h index 4df92e2..e080642 100644 --- a/src/lara.h +++ b/src/lara.h @@ -26,6 +26,7 @@ #define DESCENT_SPEED 2048.0f #define MUZZLE_FLASH_TIME 0.1f +#define FLASH_LIGHT_COLOR vec4(0.8f, 0.7f, 0.3f, 2048 * 2048) #define TARGET_MAX_DIST (8.0f * 1024.0f) struct Lara : Controller { @@ -490,12 +491,15 @@ struct Lara : Controller { for (int i = 0; i < count; i++) { Arm *arm; + int armIndex; if (wpnCurrent == Weapon::SHOTGUN) { if (!rightHand) continue; arm = &arms[0]; + armIndex = 0; } else { if (!(i ? leftHand : rightHand)) continue; arm = &arms[i]; + armIndex = i; } arm->shotTimer = 0.0f; @@ -523,6 +527,9 @@ struct Lara : Controller { nearDist = dist; } } + + Core::lightPos[1 + armIndex] = getJoint(armIndex == 0 ? 10 : 13, false).getPos(); + Core::lightColor[1 + armIndex] = FLASH_LIGHT_COLOR; } if (hasShot) { @@ -571,6 +578,9 @@ struct Lara : Controller { for (int i = 0; i < 2; i++){ arms[i].animTime += Core::deltaTime * arms[i].animDir; arms[i].shotTimer += Core::deltaTime; + + float intensity = clamp((0.1f - arms[i].shotTimer) * 20.0f, 0.0f, 1.0f); + Core::lightColor[1 + i] = FLASH_LIGHT_COLOR * vec4(intensity, intensity, intensity, sqrtf(intensity)); } if (isRifle) diff --git a/src/level.h b/src/level.h index cded603..bebcd31 100644 --- a/src/level.h +++ b/src/level.h @@ -172,11 +172,11 @@ struct Level { void initShaders() { char def[255], ext[255]; - sprintf(def, "#define MAX_RANGES %d\n#define MAX_OFFSETS %d\n", mesh->animTexRangesCount, mesh->animTexOffsetsCount); + sprintf(def, "#define MAX_LIGHTS %d\n#define MAX_RANGES %d\n#define MAX_OFFSETS %d\n", MAX_LIGHTS, mesh->animTexRangesCount, mesh->animTexOffsetsCount); shaders[shStatic] = new Shader(SHADER, def); - sprintf(ext, "%s#define CAUSTICS\n", def); + sprintf(ext, "#define MAX_LIGHTS %d\n%s#define CAUSTICS\n", MAX_LIGHTS, def); shaders[shCaustics] = new Shader(SHADER, ext); - sprintf(ext, "%s#define SPRITE\n", def); + sprintf(ext, "#define MAX_LIGHTS %d\n%s#define SPRITE\n", MAX_LIGHTS, def); shaders[shSprite] = new Shader(SHADER, ext); } @@ -238,11 +238,12 @@ struct Level { sh->bind(); sh->setParam(uColor, Core::color); + sh->setParam(uLightColor, Core::lightColor[0], MAX_LIGHTS); + sh->setParam(uLightPos, Core::lightPos[0], MAX_LIGHTS); + sh->setParam(uAmbient, vec3(0.0f));//Core::ambient); // room static meshes { - PROFILE_MARKER("R_MESH"); - for (int i = 0; i < room.meshesCount; i++) { TR::Room::Mesh &rMesh = room.meshes[i]; if (rMesh.flags.rendered) continue; // skip if already rendered @@ -261,11 +262,17 @@ struct Level { // set light parameters getLight(offset, roomIndex); + if (rMesh.intensity >= 0) { + Core::ambient = vec3(intensity(rMesh.intensity) / 255.0f); + Core::ambient = vec3(0.0); + sh->setParam(uAmbient, Core::ambient); + } + // render static mesh mat4 mTemp = Core::mModel; Core::mModel.translate(offset); Core::mModel.rotateY(rMesh.rotation); - Core::active.shader->setParam(uModel, Core::mModel); + sh->setParam(uModel, Core::mModel); mesh->renderMesh(mesh->meshMap[sMesh->mesh]); Core::mModel = mTemp; } @@ -275,30 +282,30 @@ struct Level { if (!room.flags.rendered) { // skip if already rendered mat4 mTemp = Core::mModel; - { - PROFILE_MARKER("R_GEOM"); + room.flags.rendered = true; - room.flags.rendered = true; + Core::lightColor[0] = vec4(0, 0, 0, 1); + Core::ambient = vec3(0.0); - Core::lightColor = vec4(0.0f, 0.0f, 0.0f, 1.0f); - Core::ambient = vec3(1.0f); - sh->setParam(uLightColor, Core::lightColor); - sh->setParam(uAmbient, Core::ambient); + sh->setParam(uLightColor, Core::lightColor[0], MAX_LIGHTS); + sh->setParam(uLightPos, Core::lightPos[0], MAX_LIGHTS); + sh->setParam(uAmbient, Core::ambient); - Core::mModel.translate(offset); + Core::mModel.translate(offset); - // render room geometry - sh->setParam(uModel, Core::mModel); - mesh->renderRoomGeometry(roomIndex); - } + // render room geometry + sh->setParam(uModel, Core::mModel); + mesh->renderRoomGeometry(roomIndex); // render room sprites if (mesh->hasRoomSprites(roomIndex)) { - PROFILE_MARKER("R_SPR"); sh = shaders[shSprite]; sh->bind(); sh->setParam(uModel, Core::mModel); sh->setParam(uColor, Core::color); + sh->setParam(uLightColor, Core::lightColor[0], MAX_LIGHTS); + sh->setParam(uLightPos, Core::lightPos[0], MAX_LIGHTS); + sh->setParam(uAmbient, vec3(0.0f));//Core::ambient); mesh->renderRoomSprites(roomIndex); } @@ -353,17 +360,17 @@ struct Level { if (idx > -1) { TR::Room::Light &light = level.rooms[room].lights[idx]; float c = level.rooms[room].lights[idx].intensity / 8191.0f; - Core::lightPos = vec3(light.x, light.y, light.z); - Core::lightColor = vec4(c, c, c, (float)light.attenuation * (float)light.attenuation); + Core::lightPos[0] = vec3(light.x, light.y, light.z); + Core::lightColor[0] = vec4(c, c, c, (float)light.attenuation * (float)light.attenuation); } else { - Core::lightPos = vec3(0); - Core::lightColor = vec4(0, 0, 0, 1); + Core::lightPos[0] = vec3(0); + Core::lightColor[0] = vec4(0, 0, 0, 1); } Core::ambient = vec3(1.0f - level.rooms[roomIndex].ambient / 8191.0f); Core::active.shader->setParam(uAmbient, Core::ambient); - Core::active.shader->setParam(uLightPos, Core::lightPos); - Core::active.shader->setParam(uLightColor, Core::lightColor); + Core::active.shader->setParam(uLightPos, Core::lightPos[0], MAX_LIGHTS); + Core::active.shader->setParam(uLightColor, Core::lightColor[0], MAX_LIGHTS); } void renderEntity(const TR::Entity &entity) { diff --git a/src/mesh.h b/src/mesh.h index e579e88..88335a9 100644 --- a/src/mesh.h +++ b/src/mesh.h @@ -106,6 +106,20 @@ struct Mesh { n.z = (int)o.z;\ }\ +#define CHECK_ROOM_NORMAL(n) \ + vec3 o(d.vertices[f.vertices[0]].vertex);\ + vec3 a = o - d.vertices[f.vertices[1]].vertex;\ + vec3 b = o - d.vertices[f.vertices[2]].vertex;\ + o = b.cross(a).normal() * 16300.0f;\ + n.x = (int)o.x;\ + n.y = (int)o.y;\ + n.z = (int)o.z; + + +uint8 intensity(int lighting) { + float a = 1.0f - (lighting >> 5) / 255.0f; + return int(255 * a * a); +} struct MeshBuilder { // rooms @@ -252,13 +266,14 @@ struct MeshBuilder { addQuad(indices, iCount, vCount, vStart, vertices, &t); + TR::Vertex n; + CHECK_ROOM_NORMAL(n); + for (int k = 0; k < 4; k++) { TR::Room::Data::Vertex &v = d.vertices[f.vertices[k]]; - uint8 a = 255 - (v.lighting >> 5); - - vertices[vCount].coord = { v.vertex.x, v.vertex.y, v.vertex.z, 0 }; - vertices[vCount].color = { a, a, a, 255 }; - vertices[vCount].normal = { 0, 0, 0, 1 }; + vertices[vCount].coord = { v.vertex.x, v.vertex.y, v.vertex.z, 0 }; + vertices[vCount].color = { 255, 255, 255, intensity(v.lighting) }; + vertices[vCount].normal = { n.x, n.y, n.z, 0 }; vCount++; } } @@ -269,13 +284,14 @@ struct MeshBuilder { addTriangle(indices, iCount, vCount, vStart, vertices, &t); + TR::Vertex n; + CHECK_ROOM_NORMAL(n); + for (int k = 0; k < 3; k++) { auto &v = d.vertices[f.vertices[k]]; - uint8 a = 255 - (v.lighting >> 5); - - vertices[vCount].coord = { v.vertex.x, v.vertex.y, v.vertex.z, 0 }; - vertices[vCount].color = { a, a, a, 255 }; - vertices[vCount].normal = { 0, 0, 0, 1 }; + vertices[vCount].coord = { v.vertex.x, v.vertex.y, v.vertex.z, 0 }; + vertices[vCount].color = { 255, 255, 255, intensity(v.lighting) }; + vertices[vCount].normal = { n.x, n.y, n.z, 0 }; vCount++; } } @@ -288,8 +304,7 @@ struct MeshBuilder { TR::Room::Data::Vertex &v = d.vertices[f.vertex]; TR::SpriteTexture &sprite = level.spriteTextures[f.texture]; - uint8 intensity = 255 - (v.lighting >> 5); - addSprite(indices, vertices, iCount, vCount, vStart, v.vertex.x, v.vertex.y, v.vertex.z, sprite, intensity); + addSprite(indices, vertices, iCount, vCount, vStart, v.vertex.x, v.vertex.y, v.vertex.z, sprite, intensity(v.lighting)); } } @@ -335,21 +350,26 @@ struct MeshBuilder { addQuad(indices, iCount, vCount, vStart, vertices, &t); + short4 normal; + if (!normals) { + TR::Vertex n = { 0, 0, 0 }; + CHECK_NORMAL(n); + normal = { n.x, n.y, n.z, 0 }; + } + for (int k = 0; k < 4; k++) { - uint16 idx = f.vertices[k]; - TR::Vertex &v = mVertices[idx]; + TR::Vertex &v = mVertices[f.vertices[k]]; vertices[vCount].coord = { v.x, v.y, v.z, 0 }; if (normals) { - TR::Vertex &n = normals[idx]; + TR::Vertex &n = normals[f.vertices[k]]; CHECK_NORMAL(n); vertices[vCount].normal = { n.x, n.y, n.z, 0 }; - vertices[vCount].color = { 255, 255, 255, 255 }; + vertices[vCount].color = { 255, 255, 255, 0 }; } else { - uint8 a = 255 - (lights[idx] >> 5); - vertices[vCount].normal = { 0, 0, 0, 1 }; - vertices[vCount].color = { a, a, a, 255 }; + vertices[vCount].normal = normal; + vertices[vCount].color = { 255, 255, 255, intensity(lights[f.vertices[k]]) }; } vCount++; } @@ -363,19 +383,25 @@ struct MeshBuilder { addTriangle(indices, iCount, vCount, vStart, vertices, &t); + short4 normal; + if (!normals) { + TR::Vertex n = { 0, 0, 0 }; + CHECK_NORMAL(n); + normal = { n.x, n.y, n.z, 0 }; + } + for (int k = 0; k < 3; k++) { auto &v = mVertices[f.vertices[k]]; - vertices[vCount].coord = { v.x, v.y, v.z, 0 }; + vertices[vCount].coord = { v.x, v.y, v.z, 0 }; - if (nCount > 0) { + if (normals) { TR::Vertex &n = normals[f.vertices[k]]; CHECK_NORMAL(n); vertices[vCount].normal = { n.x, n.y, n.z, 0 }; - vertices[vCount].color = { 255, 255, 255, 255 }; + vertices[vCount].color = { 255, 255, 255, 0 }; } else { - uint8 a = 255 - (lights[f.vertices[k]] >> 5); - vertices[vCount].normal = { 0, 0, 0, 1 }; - vertices[vCount].color = { a, a, a, 255 }; + vertices[vCount].normal = normal; + vertices[vCount].color = { 255, 255, 255, intensity(lights[f.vertices[k]]) }; } vCount++; } @@ -389,20 +415,26 @@ struct MeshBuilder { addQuad(indices, iCount, vCount, vStart, vertices, &whiteTileQuad); + short4 normal; + if (!normals) { + TR::Vertex n = { 0, 0, 0 }; + CHECK_NORMAL(n); + normal = { n.x, n.y, n.z, 0 }; + } + for (int k = 0; k < 4; k++) { auto &v = mVertices[f.vertices[k]]; - vertices[vCount].coord = { v.x, v.y, v.z, 0 }; + vertices[vCount].coord = { v.x, v.y, v.z, 0 }; - if (nCount > 0) { + if (normals) { TR::Vertex &n = normals[f.vertices[k]]; CHECK_NORMAL(n); vertices[vCount].normal = { n.x, n.y, n.z, 0 }; - vertices[vCount].color = { c.r, c.g, c.b, 255 }; + vertices[vCount].color = { c.r, c.g, c.b, 0 }; } else { - uint8 a = 255 - (lights[f.vertices[k]] >> 5); - vertices[vCount].normal = { 0, 0, 0, 1 }; - vertices[vCount].color = { a, a, a, 255 }; + vertices[vCount].normal = normal; + vertices[vCount].color = { c.r, c.g, c.b, intensity(lights[f.vertices[k]]) }; } vCount++; } @@ -419,17 +451,23 @@ struct MeshBuilder { for (int k = 0; k < 3; k++) { auto &v = mVertices[f.vertices[k]]; - vertices[vCount].coord = { v.x, v.y, v.z, 0 }; + vertices[vCount].coord = { v.x, v.y, v.z, 0 }; - if (nCount > 0) { + short4 normal; + if (!normals) { + TR::Vertex n = { 0, 0, 0 }; + CHECK_NORMAL(n); + normal = { n.x, n.y, n.z, 0 }; + } + + if (normals) { TR::Vertex &n = normals[f.vertices[k]]; CHECK_NORMAL(n); vertices[vCount].normal = { n.x, n.y, n.z, 0 }; - vertices[vCount].color = { c.r, c.g, c.b, 255 }; + vertices[vCount].color = { c.r, c.g, c.b, 0 }; } else { - uint8 a = 255 - (lights[f.vertices[k]] >> 5); - vertices[vCount].normal = { 0, 0, 0, 1 }; - vertices[vCount].color = { a, a, a, 255 }; + vertices[vCount].normal = normal; + vertices[vCount].color = { c.r, c.g, c.b, intensity(lights[f.vertices[k]]) }; } vCount++; } @@ -450,8 +488,8 @@ struct MeshBuilder { // build shadow spot for (int i = 0; i < 8; i++) { Vertex &v = vertices[vCount + i]; - v.normal = { 0, 0, 0, 1 }; - v.color = { 255, 255, 255, 255 }; + v.normal = { 0, -1, 0, 0 }; + v.color = { 255, 255, 255, 0 }; v.texCoord = { 32688, 32688, 0, 0 }; float a = i * (PI / 4.0f) + (PI / 8.0f); @@ -618,7 +656,7 @@ struct MeshBuilder { quad[0].coord = quad[1].coord = quad[2].coord = quad[3].coord = { x, y, z, 0 }; quad[0].normal = quad[1].normal = quad[2].normal = quad[3].normal = { 0, 0, 0, 0 }; - quad[0].color = quad[1].color = quad[2].color = quad[3].color = { intensity, intensity, intensity, 255 }; + quad[0].color = quad[1].color = quad[2].color = quad[3].color = { 255, 255, 255, intensity }; int tx = (sprite.tile % 4) * 256; int ty = (sprite.tile / 4) * 256; diff --git a/src/shader.glsl b/src/shader.glsl index 82e4d54..44a87aa 100644 --- a/src/shader.glsl +++ b/src/shader.glsl @@ -1,9 +1,7 @@ R"====( -#ifndef SPRITE - varying vec4 vNormal; - varying vec3 vLightVec; - varying vec3 vViewVec; -#endif +varying vec4 vNormal; +varying vec3 vLightVec[MAX_LIGHTS]; +varying vec3 vViewVec; varying vec2 vTexCoord; varying vec4 vColor; @@ -11,11 +9,10 @@ varying vec4 vColor; uniform mat4 uViewProj; uniform mat4 uModel; uniform mat4 uViewInv; - uniform vec4 uColor; + uniform vec3 uLightPos[MAX_LIGHTS]; + uniform vec3 uViewPos; - #ifndef SPRITE - uniform vec3 uViewPos; - uniform vec3 uLightPos; + #ifndef SPRITE uniform vec2 uAnimTexRanges[MAX_RANGES]; uniform vec2 uAnimTexOffsets[MAX_OFFSETS]; #endif @@ -31,7 +28,7 @@ varying vec4 vColor; void main() { vec4 coord = uModel * vec4(aCoord.xyz, 1.0); - vColor = aColor * uColor; + vColor = aColor; #ifdef CAUSTICS float sum = coord.x + coord.y + coord.z; @@ -46,49 +43,59 @@ varying vec4 vColor; vec2 offset = uAnimTexOffsets[int(range.x + f)]; // texCoord offset from first frame vTexCoord = (aTexCoord.xy + offset) * TEXCOORD_SCALE; // first frame + offset * isAnimated - - vViewVec = uViewPos - coord.xyz; - vLightVec = uLightPos - coord.xyz; vNormal = uModel * aNormal; #else vTexCoord = aTexCoord.xy * TEXCOORD_SCALE; coord.xyz -= uViewInv[0].xyz * aTexCoord.z + uViewInv[1].xyz * aTexCoord.w; + vNormal = vec4(uViewPos.xyz - coord.xyz, 0.0); #endif + vViewVec = uViewPos - coord.xyz; + for (int i = 0; i < MAX_LIGHTS; i++) + vLightVec[i] = uLightPos[i] - coord.xyz; + gl_Position = uViewProj * coord; } #else uniform sampler2D sDiffuse; - - #ifndef SPRITE - uniform vec3 uAmbient; - uniform vec4 uLightColor; - #endif + uniform vec4 uColor; + uniform vec3 uAmbient; + uniform vec4 uLightColor[MAX_LIGHTS]; void main() { vec4 color = texture2D(sDiffuse, vTexCoord); if (color.w < 0.6) discard; - color *= vColor; - #ifndef SPRITE - color.xyz = pow(abs(color.xyz), vec3(2.2)); - vec3 normal = normalize(vNormal.xyz); - vec3 lightVec = normalize(vLightVec); - vec3 viewVec = normalize(vViewVec); - float lum = dot(normal, lightVec); - float att = max(0.0, 1.0 - dot(vLightVec, vLightVec) / uLightColor.w); - vec3 light = uLightColor.xyz * max(vNormal.w, lum * att) + uAmbient; - // apply backlight - light *= max(vNormal.w, dot(normal, viewVec) * 0.5 + 0.5); - color.xyz *= light; - color.xyz = pow(abs(color.xyz), vec3(1.0/2.2)); - #endif + color *= uColor; + color.xyz *= vColor.xyz; - // fog + color.xyz = pow(abs(color.xyz), vec3(2.2)); // to linear space + + // calc point lights + vec3 normal = normalize(vNormal.xyz); + vec3 viewVec = normalize(vViewVec); + vec3 light = uAmbient; + for (int i = 0; i < MAX_LIGHTS; i++) { + vec3 lv = vLightVec[i]; + vec4 lc = uLightColor[i]; + float lum = max(0.0, dot(normal, normalize(lv))); + float att = max(0.0, 1.0 - dot(lv, lv) / lc.w); + light += lc.xyz * (lum * att); + } + // calc backlight + light *= dot(normal, viewVec) * 0.5 + 0.5; + + // apply lighting + color.xyz *= vColor.w + light; + + color.xyz = pow(abs(color.xyz), vec3(1.0/2.2)); // back to gamma space + + // apply fog float fog = clamp(1.0 / exp(gl_FragCoord.z / gl_FragCoord.w * 0.000025), 0.0, 1.0); + color = mix(vec4(0.0, 0.0, 0.0, 1.0), color, fog); - gl_FragColor = mix(vec4(0.0, 0.0, 0.0, 1.0), color, fog); + gl_FragColor = color; } #endif )====" \ No newline at end of file diff --git a/src/utils.h b/src/utils.h index 9b0b1c4..a8d318a 100644 --- a/src/utils.h +++ b/src/utils.h @@ -158,6 +158,7 @@ struct vec4 { vec4(float x, float y, float z, float w) : x(x), y(y), z(z), w(w) {} vec4(const vec3 &xyz, float w) : x(xyz.x), y(xyz.y), z(xyz.z), w(w) {} + vec4 operator * (const vec4 &v) const { return vec4(x*v.x, y*v.y, z*v.z, w*v.w); } vec4& operator *= (const vec4 &v) { x*=v.x; y*=v.y; z*=v.z; w*=v.w; return *this; } }; From ff6e3f8ece28f774d44ce50029676986377723be Mon Sep 17 00:00:00 2001 From: XProger Date: Wed, 16 Nov 2016 04:00:16 +0300 Subject: [PATCH 4/4] #15 Web platform: change window mode by Alt+Enter --- src/platform/web/index.html | 1 - src/platform/web/main.cpp | 57 ++++++++++++++++++++++++++----------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/platform/web/index.html b/src/platform/web/index.html index f3bd5e5..d8cee04 100644 --- a/src/platform/web/index.html +++ b/src/platform/web/index.html @@ -3,7 +3,6 @@ OpenLara Starting... -


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
diff --git a/src/platform/web/main.cpp b/src/platform/web/main.cpp index 8a839cd..12f95e0 100644 --- a/src/platform/web/main.cpp +++ b/src/platform/web/main.cpp @@ -96,10 +96,7 @@ void main_loop() { delta -= Core::deltaTime; } lastTime = time; - - int f; - emscripten_get_canvas_size(&Core::width, &Core::height, &f); - + Core::stats.dips = 0; Core::stats.tris = 0; Game::render(); @@ -146,6 +143,39 @@ void freeGL() { eglTerminate(display); } +EM_BOOL resize() { + int f; + emscripten_get_canvas_size(&Core::width, &Core::height, &f); + LOG("resize %d x %d\n", Core::width, Core::height); + return 1; +} + +EM_BOOL resizeCallback(int eventType, const EmscriptenUiEvent *e, void *userData) { + return resize(); +} + +EM_BOOL fullscreenCallback(int eventType, const void *reserved, void *userData) { + return resize(); +} + +bool isFullScreen() { + EmscriptenFullscreenChangeEvent status; + emscripten_get_fullscreen_status(&status); + return status.isFullscreen; +} + +void changeWindowMode() { + if (!isFullScreen()) { + EmscriptenFullscreenStrategy s; + s.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; + s.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_HIDEF; + s.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; + s.canvasResizedCallback = fullscreenCallback; + emscripten_request_fullscreen_strategy(NULL, 1, &s); + } else + emscripten_exit_fullscreen(); +} + InputKey keyToInputKey(int code) { static const int codes[] = { 0x25, 0x27, 0x26, 0x28, 0x20, 0x0D, 0x1B, 0x10, 0x11, 0x12, @@ -164,21 +194,16 @@ EM_BOOL keyCallback(int eventType, const EmscriptenKeyboardEvent *e, void *userD switch(eventType) { case EMSCRIPTEN_EVENT_KEYDOWN: case EMSCRIPTEN_EVENT_KEYUP: + if (eventType == EMSCRIPTEN_EVENT_KEYDOWN && e->altKey && e->keyCode == 0x0D) { // Alt + Enter + changeWindowMode(); + break; + } Input::setDown(keyToInputKey(e->keyCode), eventType == EMSCRIPTEN_EVENT_KEYDOWN); break; } return 1; } -EM_BOOL resizeCallback(int eventType, const EmscriptenUiEvent *e, void *userData) { -// Core::width = e->documentBodyClientWidth; -// Core::height = e->documentBodyClientHeight; - int f; - emscripten_get_canvas_size(&Core::width, &Core::height, &f); - LOG("resize %d x %d\n", Core::width, Core::height); - return 1; -} - EM_BOOL touchCallback(int eventType, const EmscriptenTouchEvent *e, void *userData) { bool flag = false; /* @@ -237,8 +262,8 @@ EM_BOOL mouseCallback(int eventType, const EmscriptenMouseEvent *e, void *userDa } int main() { - initGL(); - + initGL(); + emscripten_set_canvas_size(Core::width = 854, Core::height = 480); emscripten_set_keydown_callback(0, 0, 1, keyCallback); @@ -253,7 +278,7 @@ int main() { emscripten_set_mousedown_callback(0, 0, 1, mouseCallback); emscripten_set_mouseup_callback(0, 0, 1, mouseCallback); emscripten_set_mousemove_callback(0, 0, 1, mouseCallback); - + Game::init(); emscripten_run_script("snd_init()");