/* Copyright (c) 2013 yvt This file is part of OpenSpades. OpenSpades is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. OpenSpades is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenSpades. If not, see . */ #include #include #include #include #include "CTFGameMode.h" #include "Client.h" #include "ClientPlayer.h" #include "GameMap.h" #include "GunCasing.h" #include "IAudioChunk.h" #include "IAudioDevice.h" #include "IImage.h" #include "IModel.h" #include "IRenderer.h" #include "Player.h" #include "Weapon.h" #include "World.h" #include #include #include #include #include #include #include #include #include #include SPADES_SETTING(cg_ragdoll); SPADES_SETTING(cg_ejectBrass); DEFINE_SPADES_SETTING(cg_animations, "1"); SPADES_SETTING(cg_shake); SPADES_SETTING(r_hdr); DEFINE_SPADES_SETTING(cg_environmentalAudio, "1"); DEFINE_SPADES_SETTING(cg_viewWeaponX, "0"); DEFINE_SPADES_SETTING(cg_viewWeaponY, "0"); DEFINE_SPADES_SETTING(cg_viewWeaponZ, "0"); DEFINE_SPADES_SETTING(cg_debugToolSkinAnchors, "0"); namespace spades { namespace client { class SandboxedRenderer : public IRenderer { Handle base; AABB3 clipBox; bool allowDepthHack; void OnProhibitedAction() {} bool CheckVisibility(const AABB3 &box) { if (!clipBox.Contains(box) || !std::isfinite(box.min.x) || !std::isfinite(box.min.y) || !std::isfinite(box.min.z) || !std::isfinite(box.max.x) || !std::isfinite(box.max.y) || !std::isfinite(box.max.z)) { OnProhibitedAction(); return false; } return true; } protected: ~SandboxedRenderer() {} public: SandboxedRenderer(Handle base) : base(std::move(base)) {} void SetClipBox(const AABB3 &b) { clipBox = b; } void SetAllowDepthHack(bool h) { allowDepthHack = h; } void Init() { OnProhibitedAction(); } void Shutdown() { OnProhibitedAction(); } Handle RegisterImage(const char *filename) { return base->RegisterImage(filename); } Handle RegisterModel(const char *filename) { return base->RegisterModel(filename); } Handle CreateImage(Bitmap &bmp) { return base->CreateImage(bmp); } Handle CreateModel(VoxelModel &m) { return base->CreateModel(m); } void SetGameMap(stmp::optional) { OnProhibitedAction(); } void SetFogDistance(float) { OnProhibitedAction(); } void SetFogColor(Vector3) { OnProhibitedAction(); } void StartScene(const SceneDefinition &) { OnProhibitedAction(); } void AddLight(const client::DynamicLightParam &light) { AABB3 aabb{light.origin, light.origin}; if (light.type == DynamicLightTypeLinear) { aabb += light.point2; } if (CheckVisibility(aabb.Inflate(light.radius))) { base->AddLight(light); } } void RenderModel(IModel &model, const ModelRenderParam &_p) { ModelRenderParam p = _p; if (p.depthHack && !allowDepthHack) { OnProhibitedAction(); return; } // Disable depth hack when `cg_debugToolSkinAnchors` is set // so that the drawn debug lines intersect with the weapon model if (cg_debugToolSkinAnchors) { p.depthHack = false; } // Overbright surfaces bypass the fog p.customColor.x = std::max(std::min(p.customColor.x, 1.0f), 0.0f); p.customColor.y = std::max(std::min(p.customColor.y, 1.0f), 0.0f); p.customColor.z = std::max(std::min(p.customColor.z, 1.0f), 0.0f); // NaN values bypass the fog if (std::isnan(p.customColor.x) || std::isnan(p.customColor.y) || std::isnan(p.customColor.z)) { OnProhibitedAction(); return; } auto bounds = (p.matrix * OBB3(model.GetBoundingBox())).GetBoundingAABB(); if (CheckVisibility(bounds)) { base->RenderModel(model, p); } } void AddDebugLine(Vector3 a, Vector3 b, Vector4 color) { OnProhibitedAction(); } void AddSprite(IImage &image, Vector3 center, float radius, float rotation) { Vector3 rad(radius * 1.5f, radius * 1.5f, radius * 1.5f); if (CheckVisibility(AABB3(center - rad, center + rad))) { base->AddSprite(image, center, radius, rotation); } } void AddLongSprite(IImage &image, Vector3 p1, Vector3 p2, float radius) { Vector3 rad(radius * 1.5f, radius * 1.5f, radius * 1.5f); AABB3 bounds1(p1 - rad, p1 + rad); AABB3 bounds2(p2 - rad, p2 + rad); bounds1 += bounds2; if (CheckVisibility(bounds1)) { base->AddLongSprite(image, p1, p2, radius); } } void EndScene() { OnProhibitedAction(); } void MultiplyScreenColor(Vector3) { OnProhibitedAction(); } /** Sets color for image drawing. Deprecated because * some methods treats this as an alpha premultiplied, while * others treats this as an alpha non-premultiplied. * @deprecated */ void SetColor(Vector4 col) { base->SetColor(col); } /** Sets color for image drawing. Always alpha premultiplied. */ void SetColorAlphaPremultiplied(Vector4 col) { base->SetColorAlphaPremultiplied(col); } void DrawImage(stmp::optional img, const Vector2 &outTopLeft) { if (allowDepthHack) base->DrawImage(img, outTopLeft); else OnProhibitedAction(); } void DrawImage(stmp::optional img, const AABB2 &outRect) { if (allowDepthHack) base->DrawImage(img, outRect); else OnProhibitedAction(); } void DrawImage(stmp::optional img, const Vector2 &outTopLeft, const AABB2 &inRect) { if (allowDepthHack) base->DrawImage(img, outTopLeft, inRect); else OnProhibitedAction(); } void DrawImage(stmp::optional img, const AABB2 &outRect, const AABB2 &inRect) { if (allowDepthHack) base->DrawImage(img, outRect, inRect); else OnProhibitedAction(); } void DrawImage(stmp::optional img, const Vector2 &outTopLeft, const Vector2 &outTopRight, const Vector2 &outBottomLeft, const AABB2 &inRect) { if (allowDepthHack) base->DrawImage(img, outTopLeft, outTopRight, outBottomLeft, inRect); else OnProhibitedAction(); } void DrawFlatGameMap(const AABB2 &outRect, const AABB2 &inRect) { OnProhibitedAction(); } void FrameDone() { OnProhibitedAction(); } void Flip() { OnProhibitedAction(); } Handle ReadBitmap() { OnProhibitedAction(); return {}; } float ScreenWidth() { return base->ScreenWidth(); } float ScreenHeight() { return base->ScreenHeight(); } }; ClientPlayer::ClientPlayer(Player &p, Client &c) : client(c), player(p), hasValidOriginMatrix(false) { SPADES_MARK_FUNCTION(); sprintState = 0.f; aimDownState = 0.f; toolRaiseState = 0.f; currentTool = p.GetTool(); localFireVibrationTime = -100.f; time = 0.f; viewWeaponOffset = MakeVector3(0, 0, 0); lastFront = MakeVector3(0, 0, 0); flashlightOrientation = p.GetFront(); ScriptContextHandle ctx; IAudioDevice &audio = client.GetAudioDevice(); sandboxedRenderer = Handle::New(client.GetRenderer()); IRenderer &renderer = *sandboxedRenderer; static ScriptFunction spadeFactory( "ISpadeSkin@ CreateThirdPersonSpadeSkin(Renderer@, AudioDevice@)"); spadeSkin = initScriptFactory(spadeFactory, renderer, audio); static ScriptFunction spadeViewFactory( "ISpadeSkin@ CreateViewSpadeSkin(Renderer@, AudioDevice@)"); spadeViewSkin = initScriptFactory(spadeViewFactory, renderer, audio); static ScriptFunction blockFactory( "IBlockSkin@ CreateThirdPersonBlockSkin(Renderer@, AudioDevice@)"); blockSkin = initScriptFactory(blockFactory, renderer, audio); static ScriptFunction blockViewFactory( "IBlockSkin@ CreateViewBlockSkin(Renderer@, AudioDevice@)"); blockViewSkin = initScriptFactory(blockViewFactory, renderer, audio); static ScriptFunction grenadeFactory( "IGrenadeSkin@ CreateThirdPersonGrenadeSkin(Renderer@, AudioDevice@)"); grenadeSkin = initScriptFactory(grenadeFactory, renderer, audio); static ScriptFunction grenadeViewFactory( "IGrenadeSkin@ CreateViewGrenadeSkin(Renderer@, AudioDevice@)"); grenadeViewSkin = initScriptFactory(grenadeViewFactory, renderer, audio); static ScriptFunction rifleFactory( "IWeaponSkin@ CreateThirdPersonRifleSkin(Renderer@, AudioDevice@)"); static ScriptFunction smgFactory( "IWeaponSkin@ CreateThirdPersonSMGSkin(Renderer@, AudioDevice@)"); static ScriptFunction shotgunFactory( "IWeaponSkin@ CreateThirdPersonShotgunSkin(Renderer@, AudioDevice@)"); static ScriptFunction rifleViewFactory( "IWeaponSkin@ CreateViewRifleSkin(Renderer@, AudioDevice@)"); static ScriptFunction smgViewFactory( "IWeaponSkin@ CreateViewSMGSkin(Renderer@, AudioDevice@)"); static ScriptFunction shotgunViewFactory( "IWeaponSkin@ CreateViewShotgunSkin(Renderer@, AudioDevice@)"); switch (p.GetWeapon().GetWeaponType()) { case RIFLE_WEAPON: weaponSkin = initScriptFactory(rifleFactory, renderer, audio); weaponViewSkin = initScriptFactory(rifleViewFactory, renderer, audio); break; case SMG_WEAPON: weaponSkin = initScriptFactory(smgFactory, renderer, audio); weaponViewSkin = initScriptFactory(smgViewFactory, renderer, audio); break; case SHOTGUN_WEAPON: weaponSkin = initScriptFactory(shotgunFactory, renderer, audio); weaponViewSkin = initScriptFactory(shotgunViewFactory, renderer, audio); break; default: SPAssert(false); } } ClientPlayer::~ClientPlayer() { spadeSkin->Release(); blockSkin->Release(); weaponSkin->Release(); grenadeSkin->Release(); spadeViewSkin->Release(); blockViewSkin->Release(); weaponViewSkin->Release(); grenadeViewSkin->Release(); } asIScriptObject *ClientPlayer::initScriptFactory(ScriptFunction &creator, IRenderer &renderer, IAudioDevice &audio) { ScriptContextHandle ctx = creator.Prepare(); ctx->SetArgObject(0, reinterpret_cast(&renderer)); ctx->SetArgObject(1, reinterpret_cast(&audio)); ctx.ExecuteChecked(); asIScriptObject *result = reinterpret_cast(ctx->GetReturnObject()); result->AddRef(); return result; } bool ClientPlayer::IsChangingTool() { return currentTool != player.GetTool() || toolRaiseState < .999f; } float ClientPlayer::GetLocalFireVibration() { float localFireVibration = 0.f; localFireVibration = time - localFireVibrationTime; localFireVibration = 1.f - localFireVibration / 0.1f; if (localFireVibration < 0.f) localFireVibration = 0.f; return localFireVibration; } void ClientPlayer::Update(float dt) { time += dt; PlayerInput actualInput = player.GetInput(); WeaponInput actualWeapInput = player.GetWeaponInput(); if (actualInput.sprint && player.IsAlive()) { sprintState += dt * 4.f; if (sprintState > 1.f) sprintState = 1.f; } else { sprintState -= dt * 3.f; if (sprintState < 0.f) sprintState = 0.f; } if (actualWeapInput.secondary && player.IsToolWeapon() && player.IsAlive()) { // This is the only animation that can be turned off // here; others affect the gameplay directly and // turning them off would be considered cheating if (cg_animations) { aimDownState += dt * 8.f; if (aimDownState > 1.f) aimDownState = 1.f; } else { aimDownState = 1.f; } } else { if (cg_animations) { aimDownState -= dt * 3.f; if (aimDownState < 0.f) aimDownState = 0.f; } else { aimDownState = 0.f; } } if (currentTool == player.GetTool()) { toolRaiseState += dt * 4.f; if (toolRaiseState > 1.f) toolRaiseState = 1.f; if (toolRaiseState < 0.f) toolRaiseState = 0.f; } else { toolRaiseState -= dt * 4.f; if (toolRaiseState < 0.f) { toolRaiseState = 0.f; currentTool = player.GetTool(); // play tool change sound if (player.IsLocalPlayer()) { IAudioDevice &audioDevice = client.GetAudioDevice(); Handle c; switch (player.GetTool()) { case Player::ToolSpade: c = audioDevice.RegisterSound("Sounds/Weapons/Spade/RaiseLocal.opus"); break; case Player::ToolBlock: c = audioDevice.RegisterSound("Sounds/Weapons/Block/RaiseLocal.opus"); break; case Player::ToolWeapon: switch (player.GetWeapon().GetWeaponType()) { case RIFLE_WEAPON: c = audioDevice.RegisterSound( "Sounds/Weapons/Rifle/RaiseLocal.opus"); break; case SMG_WEAPON: c = audioDevice.RegisterSound( "Sounds/Weapons/SMG/RaiseLocal.opus"); break; case SHOTGUN_WEAPON: c = audioDevice.RegisterSound( "Sounds/Weapons/Shotgun/RaiseLocal.opus"); break; } break; case Player::ToolGrenade: c = audioDevice.RegisterSound( "Sounds/Weapons/Grenade/RaiseLocal.opus"); break; } audioDevice.PlayLocal(c.GetPointerOrNull(), MakeVector3(.4f, -.3f, .5f), AudioParam()); } } else if (toolRaiseState > 1.f) { toolRaiseState = 1.f; } } { float scale = dt; Vector3 vel = player.GetVelocity(); Vector3 front = player.GetFront(); Vector3 right = player.GetRight(); Vector3 up = player.GetUp(); // Offset the view weapon according to the player movement viewWeaponOffset.x += Vector3::Dot(vel, right) * scale; viewWeaponOffset.y -= Vector3::Dot(vel, front) * scale; viewWeaponOffset.z += Vector3::Dot(vel, up) * scale; // Offset the view weapon according to the camera movement Vector3 diff = front - lastFront; viewWeaponOffset.x += Vector3::Dot(diff, right) * 0.05f; viewWeaponOffset.z += Vector3::Dot(diff, up) * 0.05f; lastFront = front; if (dt > 0.f) viewWeaponOffset *= powf(.02f, dt); // Limit the movement auto softLimitFunc = [&](float &v, float minLimit, float maxLimit) { float transition = (maxLimit - minLimit) * 0.5f; if (v < minLimit) { float strength = std::min(1.f, (minLimit - v) / transition); v = Mix(v, minLimit, strength); } if (v > maxLimit) { float strength = std::min(1.f, (v - maxLimit) / transition); v = Mix(v, maxLimit, strength); } }; softLimitFunc(viewWeaponOffset.x, -0.06f, 0.06f); softLimitFunc(viewWeaponOffset.y, -0.06f, 0.06f); softLimitFunc(viewWeaponOffset.z, -0.06f, 0.06f); // When the player is aiming down the sight, the weapon's movement // must be restricted so that other parts of the weapon don't // cover the ironsight. if (currentTool == Player::ToolWeapon && player.GetWeaponInput().secondary) { if (dt > 0.f) viewWeaponOffset *= powf(.01f, dt); const float limitX = .003f; const float limitY = .003f; softLimitFunc(viewWeaponOffset.x, -limitX, limitX); softLimitFunc(viewWeaponOffset.z, 0, limitY); } } { // Smooth the flashlight's movement Vector3 diff = player.GetFront() - flashlightOrientation; float sq = diff.GetLength(); if (sq > 0.1) { flashlightOrientation += diff.Normalize() * (sq - 0.1); } flashlightOrientation = Mix(flashlightOrientation, player.GetFront(), 1.0f - powf(1.0e-6, dt)) .Normalize(); } // FIXME: should do for non-active skins? asIScriptObject *skin; if (ShouldRenderInThirdPersonView()) { if (currentTool == Player::ToolSpade) { skin = spadeSkin; } else if (currentTool == Player::ToolBlock) { skin = blockSkin; } else if (currentTool == Player::ToolGrenade) { skin = grenadeSkin; } else if (currentTool == Player::ToolWeapon) { skin = weaponSkin; } else { SPInvalidEnum("currentTool", currentTool); } } else { if (currentTool == Player::ToolSpade) { skin = spadeViewSkin; } else if (currentTool == Player::ToolBlock) { skin = blockViewSkin; } else if (currentTool == Player::ToolGrenade) { skin = grenadeViewSkin; } else if (currentTool == Player::ToolWeapon) { skin = weaponViewSkin; } else { SPInvalidEnum("currentTool", currentTool); } } { ScriptIToolSkin interface(skin); interface.Update(dt); } } Matrix4 ClientPlayer::GetEyeMatrix() { Vector3 eye = player.GetEye(); if ((int)cg_shake >= 2) { float sp = SmoothStep(GetSprintState()); float p = cosf(player.GetWalkAnimationProgress() * static_cast(M_PI) * 2.f - 0.8f); p = p * p; p *= p; p *= p; p *= p; eye.z -= p * 0.06f * sp; } return Matrix4::FromAxis(-player.GetRight(), player.GetFront(), -player.GetUp(), eye); } void ClientPlayer::SetSkinParameterForTool(Player::ToolType type, asIScriptObject *skin) { Player &p = player; if (currentTool == Player::ToolSpade) { ScriptISpadeSkin interface(skin); WeaponInput inp = p.GetWeaponInput(); if (p.GetTool() != Player::ToolSpade) { interface.SetActionType(SpadeActionTypeIdle); interface.SetActionProgress(0.f); } else if (inp.primary) { interface.SetActionType(SpadeActionTypeBash); interface.SetActionProgress(p.GetSpadeAnimationProgress()); } else if (inp.secondary) { interface.SetActionType(p.IsFirstDig() ? SpadeActionTypeDigStart : SpadeActionTypeDig); interface.SetActionProgress(p.GetDigAnimationProgress()); } else { interface.SetActionType(SpadeActionTypeIdle); interface.SetActionProgress(0.f); } } else if (currentTool == Player::ToolBlock) { // TODO: smooth ready state ScriptIBlockSkin interface(skin); if (p.GetTool() != Player::ToolBlock) { // FIXME: use block's IsReadyToUseTool // for smoother transition interface.SetReadyState(0.f); } else if (p.IsReadyToUseTool()) { interface.SetReadyState(1.f); } else { interface.SetReadyState(0.f); } interface.SetBlockColor(MakeVector3(p.GetBlockColor()) / 255.f); } else if (currentTool == Player::ToolGrenade) { ScriptIGrenadeSkin interface(skin); interface.SetReadyState(1.f - p.GetTimeToNextGrenade() / 0.5f); WeaponInput inp = p.GetWeaponInput(); if (inp.primary) { interface.SetCookTime(p.GetGrenadeCookTime()); } else { interface.SetCookTime(0.f); } } else if (currentTool == Player::ToolWeapon) { Weapon &w = p.GetWeapon(); ScriptIWeaponSkin interface(skin); interface.SetReadyState(1.f - w.TimeToNextFire() / w.GetDelay()); interface.SetAimDownSightState(aimDownState); interface.SetAmmo(w.GetAmmo()); interface.SetClipSize(w.GetClipSize()); interface.SetReloading(w.IsReloading()); interface.SetReloadProgress(w.GetReloadProgress()); } else { SPInvalidEnum("currentTool", currentTool); } } void ClientPlayer::SetCommonSkinParameter(asIScriptObject *skin) { asIScriptObject *curSkin; if (ShouldRenderInThirdPersonView()) { if (currentTool == Player::ToolSpade) { curSkin = spadeSkin; } else if (currentTool == Player::ToolBlock) { curSkin = blockSkin; } else if (currentTool == Player::ToolGrenade) { curSkin = grenadeSkin; } else if (currentTool == Player::ToolWeapon) { curSkin = weaponSkin; } else { SPInvalidEnum("currentTool", currentTool); } } else { if (currentTool == Player::ToolSpade) { curSkin = spadeViewSkin; } else if (currentTool == Player::ToolBlock) { curSkin = blockViewSkin; } else if (currentTool == Player::ToolGrenade) { curSkin = grenadeViewSkin; } else if (currentTool == Player::ToolWeapon) { curSkin = weaponViewSkin; } else { SPInvalidEnum("currentTool", currentTool); } } float sprint = SmoothStep(sprintState); float putdown = 1.f - toolRaiseState; putdown *= putdown; putdown = std::min(1.f, putdown * 1.5f); { ScriptIToolSkin interface(skin); interface.SetRaiseState((skin == curSkin) ? (1.f - putdown) : 0.f); interface.SetSprintState(sprint); interface.SetMuted(client.IsMuted()); } } std::array ClientPlayer::GetFlashlightAxes() { std::array axes; axes[2] = flashlightOrientation; axes[0] = Vector3::Cross(flashlightOrientation, player.GetUp()).Normalize(); axes[1] = Vector3::Cross(axes[0], axes[2]); return axes; } void ClientPlayer::AddToSceneFirstPersonView() { Player &p = player; IRenderer &renderer = client.GetRenderer(); World *world = client.GetWorld(); Matrix4 eyeMatrix = GetEyeMatrix(); sandboxedRenderer->SetClipBox(AABB3(eyeMatrix.GetOrigin() - Vector3(20.f, 20.f, 20.f), eyeMatrix.GetOrigin() + Vector3(20.f, 20.f, 20.f))); sandboxedRenderer->SetAllowDepthHack(true); // no flashlight if spectating other players while dead if (client.flashlightOn && world->GetLocalPlayer()->IsAlive()) { float brightness = client.time - client.flashlightOnTime; brightness = 1.f - expf(-brightness * 5.f); brightness *= r_hdr ? 3.0f : 1.5f; // add flash light DynamicLightParam light; Handle image = renderer.RegisterImage("Gfx/Spotlight.jpg"); light.origin = (eyeMatrix * MakeVector3(0, -0.05f, -0.1f)).GetXYZ(); light.color = MakeVector3(1.0f, 0.7f, 0.5f) * brightness; light.radius = 60.f; light.type = DynamicLightTypeSpotlight; light.spotAngle = 90.f * M_PI / 180.f; light.spotAxis = GetFlashlightAxes(); light.image = image.GetPointerOrNull(); renderer.AddLight(light); light.color *= .3f; light.radius = 10.f; light.type = DynamicLightTypePoint; light.image = NULL; renderer.AddLight(light); } Vector3 leftHand, rightHand; leftHand = MakeVector3(0, 0, 0); rightHand = MakeVector3(0, 0, 0); // view weapon Vector3 viewWeaponOffset = this->viewWeaponOffset; // bobbing { float sp = 1.f - aimDownState; sp *= .3f; sp *= std::min(1.f, p.GetVelocity().GetLength() * 5.f); viewWeaponOffset.x += sinf(p.GetWalkAnimationProgress() * M_PI * 2.f) * 0.013f * sp; float vl = cosf(p.GetWalkAnimationProgress() * M_PI * 2.f); vl *= vl; viewWeaponOffset.z += vl * 0.018f * sp; } // slow pulse { float sp = 1.f - aimDownState; float vl = sinf(world->GetTime() * 1.f); viewWeaponOffset.x += vl * 0.001f * sp; viewWeaponOffset.y += vl * 0.0007f * sp; viewWeaponOffset.z += vl * 0.003f * sp; } // manual adjustment viewWeaponOffset += Vector3{cg_viewWeaponX, cg_viewWeaponY, cg_viewWeaponZ} * (1.f - aimDownState); asIScriptObject *skin; if (currentTool == Player::ToolSpade) { skin = spadeViewSkin; } else if (currentTool == Player::ToolBlock) { skin = blockViewSkin; } else if (currentTool == Player::ToolGrenade) { skin = grenadeViewSkin; } else if (currentTool == Player::ToolWeapon) { skin = weaponViewSkin; } else { SPInvalidEnum("currentTool", currentTool); } SetSkinParameterForTool(currentTool, skin); SetCommonSkinParameter(skin); // common process { ScriptIViewToolSkin interface(skin); interface.SetEyeMatrix(GetEyeMatrix()); interface.SetSwing(viewWeaponOffset); } { ScriptIToolSkin interface(skin); interface.AddToScene(); } { ScriptIViewToolSkin interface(skin); leftHand = interface.GetLeftHandPosition(); rightHand = interface.GetRightHandPosition(); } // view hands if (leftHand.GetPoweredLength() > 0.001f && rightHand.GetPoweredLength() > 0.001f) { ModelRenderParam param; param.depthHack = true; Handle model = renderer.RegisterModel("Models/Player/Arm.kv6"); Handle model2 = renderer.RegisterModel("Models/Player/UpperArm.kv6"); IntVector3 col = p.GetColor(); param.customColor = MakeVector3(col.x / 255.f, col.y / 255.f, col.z / 255.f); const float armlen = 0.5f; Vector3 shoulders[] = {{0.4f, 0.0f, 0.25f}, {-0.4f, 0.0f, 0.25f}}; Vector3 hands[] = {leftHand, rightHand}; Vector3 benddirs[] = {{0.5f, 0.2f, 0.f}, {-0.5f, 0.2f, 0.f}}; for (int i = 0; i < 2; i++) { Vector3 shoulder = shoulders[i]; Vector3 hand = hands[i]; Vector3 benddir = benddirs[i]; float len2 = (hand - shoulder).GetPoweredLength(); // len2/4 + x^2 = armlen^2 float bendlen = sqrtf(std::max(armlen * armlen - len2 * .25f, 0.f)); Vector3 bend = Vector3::Cross(benddir, hand - shoulder); bend = bend.Normalize(); if (bend.z < 0.f) bend.z = -bend.z; Vector3 elbow = (hand + shoulder) * .5f; elbow += bend * bendlen; { Vector3 axises[3]; axises[2] = (hand - elbow).Normalize(); axises[0] = MakeVector3(0, 0, 1); axises[1] = Vector3::Cross(axises[2], axises[0]).Normalize(); axises[0] = Vector3::Cross(axises[1], axises[2]).Normalize(); Matrix4 mat = Matrix4::Scale(.05f); mat = Matrix4::FromAxis(axises[0], axises[1], axises[2], elbow) * mat; mat = eyeMatrix * mat; param.matrix = mat; renderer.RenderModel(*model, param); } { Vector3 axises[3]; axises[2] = (elbow - shoulder).Normalize(); axises[0] = MakeVector3(0, 0, 1); axises[1] = Vector3::Cross(axises[2], axises[0]).Normalize(); axises[0] = Vector3::Cross(axises[1], axises[2]).Normalize(); Matrix4 mat = Matrix4::Scale(.05f); mat = Matrix4::FromAxis(axises[0], axises[1], axises[2], shoulder) * mat; mat = eyeMatrix * mat; param.matrix = mat; renderer.RenderModel(*model2, param); } } } // --- local view ends } void ClientPlayer::AddToSceneThirdPersonView() { Player &p = player; IRenderer &renderer = client.GetRenderer(); World *world = client.GetWorld(); if (!p.IsAlive()) { if (!cg_ragdoll) { ModelRenderParam param; param.matrix = Matrix4::Translate(p.GetOrigin() + MakeVector3(0, 0, 1)); param.matrix = param.matrix * Matrix4::Scale(.1f); IntVector3 col = p.GetColor(); param.customColor = MakeVector3(col.x / 255.f, col.y / 255.f, col.z / 255.f); Handle model = renderer.RegisterModel("Models/Player/Dead.kv6"); renderer.RenderModel(*model, param); } return; } auto origin = p.GetOrigin(); sandboxedRenderer->SetClipBox( AABB3(origin - Vector3(2.f, 2.f, 4.f), origin + Vector3(2.f, 2.f, 2.f))); sandboxedRenderer->SetAllowDepthHack(false); // ready for tool rendering asIScriptObject *skin; if (currentTool == Player::ToolSpade) { skin = spadeSkin; } else if (currentTool == Player::ToolBlock) { skin = blockSkin; } else if (currentTool == Player::ToolGrenade) { skin = grenadeSkin; } else if (currentTool == Player::ToolWeapon) { skin = weaponSkin; } else { SPInvalidEnum("currentTool", currentTool); } SetSkinParameterForTool(currentTool, skin); SetCommonSkinParameter(skin); float pitchBias; { ScriptIThirdPersonToolSkin interface(skin); pitchBias = interface.GetPitchBias(); } ModelRenderParam param; Handle model; Vector3 front = p.GetFront(); IntVector3 col = p.GetColor(); param.customColor = MakeVector3(col.x / 255.f, col.y / 255.f, col.z / 255.f); float yaw = atan2(front.y, front.x) + M_PI * .5f; float pitch = -atan2(front.z, sqrt(front.x * front.x + front.y * front.y)); // lower axis Matrix4 lower = Matrix4::Translate(p.GetOrigin()); lower = lower * Matrix4::Rotate(MakeVector3(0, 0, 1), yaw); Matrix4 scaler = Matrix4::Scale(0.1f); scaler = scaler * Matrix4::Scale(-1, -1, 1); PlayerInput inp = p.GetInput(); // lower Matrix4 torso, head, arms; if (inp.crouch) { Matrix4 leg1 = Matrix4::Translate(-0.25f, 0.2f, -0.1f); Matrix4 leg2 = Matrix4::Translate(0.25f, 0.2f, -0.1f); float ang = sinf(p.GetWalkAnimationProgress() * M_PI * 2.f) * 0.6f; float walkVel = Vector3::Dot(p.GetVelocity(), p.GetFront2D()) * 4.f; leg1 = leg1 * Matrix4::Rotate(MakeVector3(1, 0, 0), ang * walkVel); leg2 = leg2 * Matrix4::Rotate(MakeVector3(1, 0, 0), -ang * walkVel); walkVel = Vector3::Dot(p.GetVelocity(), p.GetRight()) * 3.f; leg1 = leg1 * Matrix4::Rotate(MakeVector3(0, 1, 0), ang * walkVel); leg2 = leg2 * Matrix4::Rotate(MakeVector3(0, 1, 0), -ang * walkVel); leg1 = lower * leg1; leg2 = lower * leg2; model = renderer.RegisterModel("Models/Player/LegCrouch.kv6"); param.matrix = leg1 * scaler; renderer.RenderModel(*model, param); param.matrix = leg2 * scaler; renderer.RenderModel(*model, param); torso = Matrix4::Translate(0.f, 0.f, -0.55f); torso = lower * torso; model = renderer.RegisterModel("Models/Player/TorsoCrouch.kv6"); param.matrix = torso * scaler; renderer.RenderModel(*model, param); head = Matrix4::Translate(0.f, 0.f, -0.0f); head = torso * head; arms = Matrix4::Translate(0.f, 0.f, -0.0f); arms = torso * arms; } else { Matrix4 leg1 = Matrix4::Translate(-0.25f, 0.f, -0.1f); Matrix4 leg2 = Matrix4::Translate(0.25f, 0.f, -0.1f); float ang = sinf(p.GetWalkAnimationProgress() * M_PI * 2.f) * 0.6f; float walkVel = Vector3::Dot(p.GetVelocity(), p.GetFront2D()) * 4.f; leg1 = leg1 * Matrix4::Rotate(MakeVector3(1, 0, 0), ang * walkVel); leg2 = leg2 * Matrix4::Rotate(MakeVector3(1, 0, 0), -ang * walkVel); walkVel = Vector3::Dot(p.GetVelocity(), p.GetRight()) * 3.f; leg1 = leg1 * Matrix4::Rotate(MakeVector3(0, 1, 0), ang * walkVel); leg2 = leg2 * Matrix4::Rotate(MakeVector3(0, 1, 0), -ang * walkVel); leg1 = lower * leg1; leg2 = lower * leg2; model = renderer.RegisterModel("Models/Player/Leg.kv6"); param.matrix = leg1 * scaler; renderer.RenderModel(*model, param); param.matrix = leg2 * scaler; renderer.RenderModel(*model, param); torso = Matrix4::Translate(0.f, 0.f, -1.0f); torso = lower * torso; model = renderer.RegisterModel("Models/Player/Torso.kv6"); param.matrix = torso * scaler; renderer.RenderModel(*model, param); head = Matrix4::Translate(0.f, 0.f, -0.0f); head = torso * head; arms = Matrix4::Translate(0.f, 0.f, 0.1f); arms = torso * arms; } float armPitch = pitch; if (inp.sprint) { armPitch -= .5f; } armPitch += pitchBias; if (armPitch < 0.f) { armPitch = std::max(armPitch, -(float)M_PI * .5f); armPitch *= .9f; } arms = arms * Matrix4::Rotate(MakeVector3(1, 0, 0), armPitch); model = renderer.RegisterModel("Models/Player/Arms.kv6"); param.matrix = arms * scaler; renderer.RenderModel(*model, param); head = head * Matrix4::Rotate(MakeVector3(1, 0, 0), pitch); model = renderer.RegisterModel("Models/Player/Head.kv6"); param.matrix = head * scaler; renderer.RenderModel(*model, param); // draw tool { ScriptIThirdPersonToolSkin interface(skin); interface.SetOriginMatrix(arms); } { ScriptIToolSkin interface(skin); interface.AddToScene(); } hasValidOriginMatrix = true; // draw intel in ctf stmp::optional mode = world->GetMode(); if (mode && IGameMode::m_CTF == mode->ModeType()) { CTFGameMode &ctfMode = dynamic_cast(*mode); int tId = p.GetTeamId(); if (tId < 3) { CTFGameMode::Team &team = ctfMode.GetTeam(p.GetTeamId()); if (team.hasIntel && team.carrier == p.GetId()) { IntVector3 col2 = world->GetTeam(1 - p.GetTeamId()).color; param.customColor = MakeVector3(col2.x / 255.f, col2.y / 255.f, col2.z / 255.f); Matrix4 mIntel = torso * Matrix4::Translate(0, 0.6f, 0.5f); model = renderer.RegisterModel("Models/MapObjects/Intel.kv6"); param.matrix = mIntel * scaler; renderer.RenderModel(*model, param); param.customColor = MakeVector3(col.x / 255.f, col.y / 255.f, col.z / 255.f); } } } // third person player rendering, done } void ClientPlayer::AddToScene() { SPADES_MARK_FUNCTION(); Player &p = player; const SceneDefinition &lastSceneDef = client.GetLastSceneDef(); hasValidOriginMatrix = false; if (p.GetTeamId() >= 2) { // spectator, or dummy player return; } float distancePowered = (p.GetOrigin() - lastSceneDef.viewOrigin).GetPoweredLength(); if (distancePowered > 140.f * 140.f) { return; } if (!ShouldRenderInThirdPersonView()) { AddToSceneFirstPersonView(); } else { AddToSceneThirdPersonView(); } if (cg_debugToolSkinAnchors && currentTool == Player::ToolWeapon && player.IsLocalPlayer()) { IRenderer &renderer = client.GetRenderer(); auto drawAxes = [&renderer](Vector3 p) { renderer.AddDebugLine(Vector3{p.x - 0.2f, p.y, p.z}, Vector3{p.x + 0.2f, p.y, p.z}, Vector4{1.0f, 0.0f, 0.0f, 1.0f}); renderer.AddDebugLine(Vector3{p.x, p.y - 0.2f, p.z}, Vector3{p.x, p.y + 0.2f, p.z}, Vector4{0.0f, 0.6f, 0.0f, 1.0f}); renderer.AddDebugLine(Vector3{p.x, p.y, p.z - 0.2f}, Vector3{p.x, p.y, p.z + 0.2f}, Vector4{0.0f, 0.0f, 1.0f, 1.0f}); }; drawAxes(ShouldRenderInThirdPersonView() ? GetMuzzlePosition() : GetMuzzlePositionInFirstPersonView()); drawAxes(ShouldRenderInThirdPersonView() ? GetCaseEjectPosition() : GetCaseEjectPositionInFirstPersonView()); } } void ClientPlayer::Draw2D() { if (!ShouldRenderInThirdPersonView() && player.IsAlive()) { asIScriptObject *skin; if (currentTool == Player::ToolSpade) { skin = spadeViewSkin; } else if (currentTool == Player::ToolBlock) { skin = blockViewSkin; } else if (currentTool == Player::ToolGrenade) { skin = grenadeViewSkin; } else if (currentTool == Player::ToolWeapon) { skin = weaponViewSkin; } else { SPInvalidEnum("currentTool", currentTool); } SetSkinParameterForTool(currentTool, skin); SetCommonSkinParameter(skin); // common process { ScriptIViewToolSkin interface(skin); interface.SetEyeMatrix(GetEyeMatrix()); interface.SetSwing(viewWeaponOffset); interface.Draw2D(); } } } bool ClientPlayer::ShouldRenderInThirdPersonView() { // The player from whom's perspective the game is return !IsFirstPerson(client.GetCameraMode()) || &player != &client.GetCameraTargetPlayer(); } Vector3 ClientPlayer::GetMuzzlePosition() { ScriptIWeaponSkin3 interface(weaponSkin); if (interface.ImplementsInterface()) { Vector3 muzzle = interface.GetMuzzlePosition(); // The skin should return a legit position. Return the default // position if it didn't. Vector3 origin = player.GetOrigin(); AABB3 clip = AABB3(origin - Vector3(2.f, 2.f, 4.f), origin + Vector3(2.f, 2.f, 2.f)); if (clip.Contains(muzzle)) { return muzzle; } } return (GetEyeMatrix() * MakeVector3(-0.13f, 1.5f, 0.2f)).GetXYZ(); } Vector3 ClientPlayer::GetMuzzlePositionInFirstPersonView() { ScriptIWeaponSkin3 interface(weaponViewSkin); if (interface.ImplementsInterface()) { return interface.GetMuzzlePosition(); } return (GetEyeMatrix() * MakeVector3(-0.13f, 1.5f, 0.2f)).GetXYZ(); } Vector3 ClientPlayer::GetCaseEjectPosition() { ScriptIWeaponSkin3 interface(weaponSkin); if (interface.ImplementsInterface()) { Vector3 CaseEject = interface.GetCaseEjectPosition(); // The skin should return a legit position. Return the default // position if it didn't. Vector3 origin = player.GetOrigin(); AABB3 clip = AABB3(origin - Vector3(2.f, 2.f, 4.f), origin + Vector3(2.f, 2.f, 2.f)); if (clip.Contains(CaseEject)) { return CaseEject; } } return (GetEyeMatrix() * MakeVector3(-0.13f, .5f, 0.2f)).GetXYZ(); } Vector3 ClientPlayer::GetCaseEjectPositionInFirstPersonView() { ScriptIWeaponSkin3 interface(weaponViewSkin); if (interface.ImplementsInterface()) { return interface.GetCaseEjectPosition(); } return (GetEyeMatrix() * MakeVector3(-0.13f, .5f, 0.2f)).GetXYZ(); } struct ClientPlayer::AmbienceInfo { float room; float size; float distance; }; ClientPlayer::AmbienceInfo ClientPlayer::ComputeAmbience() { const SceneDefinition &lastSceneDef = client.GetLastSceneDef(); if (!cg_environmentalAudio) { AmbienceInfo result; result.room = 0.0f; result.distance = (lastSceneDef.viewOrigin - player.GetEye()).GetLength(); result.size = 0.0f; return result; } float maxDistance = 40.f; GameMap &map = *client.map; Vector3 rayFrom = player.GetEye(); // uniformly distributed random unit vectors const Vector3 directions[24] = { {-0.4806003057749437f, -0.42909622618705534f, 0.7647874049440525f}, {-0.32231294555647927f, 0.6282069816346844f, 0.7081457147735524f}, {0.048740582496498826f, -0.6670915238644523f, 0.7433796166200044f}, {0.4507022412112344f, 0.2196054264547812f, 0.8652403980621708f}, {-0.42721511627413183f, -0.587164590982542f, -0.6875499891085622f}, {-0.5570464880797501f, 0.3832470400156089f, -0.7367638131974799f}, {0.4379032819319448f, -0.5217172826725083f, -0.732155579528044f}, {0.5505793235065188f, 0.5884516130938041f, -0.5921039668625805f}, {0.681714179159347f, -0.6289005125058891f, -0.3738314102679548f}, {0.882424317058847f, 0.4680895178240496f, -0.047111866514457174f}, {0.8175844570742612f, -0.5123280060684333f, 0.26282250616819125f}, {0.7326555076593512f, 0.16938649523355995f, 0.6591844372623717f}, {-0.8833847855718798f, -0.46859333747646814f, -0.007183640636104698f}, {-0.6478926243769724f, 0.5325399055055595f, -0.5446433661783178f}, {-0.7011236289377749f, -0.4179353735633245f, 0.5777159167528706f}, {-0.8834742898471629f, 0.3226030059694268f, 0.3397064611080296f}, {-0.701272268659947f, 0.7126868112640804f, -0.017167243773185584f}, {-0.4048459451282839f, 0.8049148135357349f, 0.4338339586338529f}, {0.10511344475950758f, 0.7400485819463978f, -0.664288536774432f}, {0.4228172536676786f, 0.7759558485735245f, 0.46810051384874957f}, {-0.641642302739998f, -0.7293326298605313f, -0.23742171416118207f}, {-0.269582155924164f, -0.957885171758109f, 0.09890125850168793f}, {0.09274966874325204f, -0.9126579244190587f, -0.39806156803076687f}, {0.49359438685568013f, -0.721891173178783f, 0.48501310843226225f}}; std::array distances; std::array feedbacknesses; std::fill(feedbacknesses.begin(), feedbacknesses.end(), 0.0f); for (std::size_t i = 0; i < distances.size(); ++i) { float &distance = distances[i]; float &feedbackness = feedbacknesses[i]; const Vector3 &rayTo = directions[i]; IntVector3 hitPos; bool hit = map.CastRay(rayFrom, rayTo, maxDistance, hitPos); if (hit) { Vector3 hitPosf = {(float)hitPos.x, (float)hitPos.y, (float)hitPos.z}; distance = (hitPosf - rayFrom).GetLength(); } else { distance = maxDistance * 2.f; } if (hit) { bool hit2 = map.CastRay(rayFrom, -rayTo, maxDistance, hitPos); if (hit2) feedbackness = 1.f; else feedbackness = 0.f; } } // monte-carlo integration unsigned int rayHitCount = 0; float roomSize = 0.f; float feedbackness = 0.f; for (float dist : distances) { if (dist < maxDistance) { rayHitCount++; roomSize += dist; } } for (float fb : feedbacknesses) { feedbackness += fb; } float reflections; if (rayHitCount > distances.size() / 4) { roomSize /= (float)rayHitCount; reflections = (float)rayHitCount / (float)distances.size(); } else { reflections = 0.1f; roomSize = 100.f; } feedbackness /= (float)distances.size(); feedbackness = std::min(std::max(0.0f, feedbackness - 0.35f) / 0.5f, 1.0f); AmbienceInfo result; result.room = reflections * feedbackness; result.distance = (lastSceneDef.viewOrigin - player.GetEye()).GetLength(); result.size = std::max(std::min(roomSize / 15.0f, 1.0f), 0.0f); result.room *= std::max(0.0f, std::min((result.size - 0.1f) * 4.0f, 1.0f)); result.room *= 1.0f - result.size * 0.3f; return result; } void ClientPlayer::FiredWeapon() { World &world = player.GetWorld(); const SceneDefinition &lastSceneDef = client.GetLastSceneDef(); IRenderer &renderer = client.GetRenderer(); IAudioDevice &audioDevice = client.GetAudioDevice(); Player &p = player; Vector3 muzzle = ShouldRenderInThirdPersonView() ? GetMuzzlePosition() : GetMuzzlePositionInFirstPersonView(); // make dlight client.MuzzleFire(muzzle, player.GetFront(), &player == world.GetLocalPlayer()); if (cg_ejectBrass) { float dist = (player.GetOrigin() - lastSceneDef.viewOrigin).GetPoweredLength(); if (dist < 130.f * 130.f) { Handle model; Handle snd = NULL; Handle snd2 = NULL; switch (player.GetWeapon().GetWeaponType()) { case RIFLE_WEAPON: model = renderer.RegisterModel("Models/Weapons/Rifle/Casing.kv6"); snd = SampleRandomBool() ? audioDevice.RegisterSound("Sounds/Weapons/Rifle/ShellDrop1.opus") : audioDevice.RegisterSound("Sounds/Weapons/Rifle/ShellDrop2.opus"); snd2 = audioDevice.RegisterSound("Sounds/Weapons/Rifle/ShellWater.opus"); break; case SHOTGUN_WEAPON: // FIXME: don't want to show shotgun't casing // because it isn't ejected when firing // model = renderer.RegisterModel("Models/Weapons/Shotgun/Casing.kv6"); break; case SMG_WEAPON: model = renderer.RegisterModel("Models/Weapons/SMG/Casing.kv6"); snd = SampleRandomBool() ? audioDevice.RegisterSound("Sounds/Weapons/SMG/ShellDrop1.opus") : audioDevice.RegisterSound("Sounds/Weapons/SMG/ShellDrop2.opus"); snd2 = audioDevice.RegisterSound("Sounds/Weapons/SMG/ShellWater.opus"); break; } if (model) { Vector3 origin = ShouldRenderInThirdPersonView() ? GetCaseEjectPosition() : GetCaseEjectPositionInFirstPersonView(); Vector3 vel; vel = p.GetFront() * 0.5f + p.GetRight() + p.GetUp() * 0.2f; switch (p.GetWeapon().GetWeaponType()) { case SMG_WEAPON: vel -= p.GetFront() * (0.6f + SampleRandomFloat()/5); vel += p.GetRight() * (SampleRandomFloat()/5 - 0.1f); vel += p.GetUp() * (SampleRandomFloat()/5 - 0.1f); break; case SHOTGUN_WEAPON: vel *= .5f; break; default: break; } auto ent = stmp::make_unique( &client, model.GetPointerOrNull(), snd.GetPointerOrNull(), snd2.GetPointerOrNull(), origin, p.GetFront(), vel); client.AddLocalEntity(std::move(ent)); } } } // sound ambience estimation auto ambience = ComputeAmbience(); asIScriptObject *skin; // FIXME: what if current tool isn't weapon? if (ShouldRenderInThirdPersonView()) { skin = weaponSkin; } else { skin = weaponViewSkin; } { ScriptIWeaponSkin2 interface(skin); if (interface.ImplementsInterface()) { interface.SetSoundEnvironment(ambience.room, ambience.size, ambience.distance); interface.SetSoundOrigin(player.GetEye()); } else if (ShouldRenderInThirdPersonView() && !hasValidOriginMatrix) { // Legacy skin scripts rely on OriginMatrix which is only updated when // the player's location is within the fog range. return; } } { ScriptIWeaponSkin interface(skin); interface.WeaponFired(); } } void ClientPlayer::ReloadingWeapon() { asIScriptObject *skin; // FIXME: what if current tool isn't weapon? if (ShouldRenderInThirdPersonView()) { skin = weaponSkin; } else { skin = weaponViewSkin; } // sound ambience estimation auto ambience = ComputeAmbience(); { ScriptIWeaponSkin2 interface(skin); if (interface.ImplementsInterface()) { interface.SetSoundEnvironment(ambience.room, ambience.size, ambience.distance); interface.SetSoundOrigin(player.GetEye()); } else if (ShouldRenderInThirdPersonView() && !hasValidOriginMatrix) { // Legacy skin scripts rely on OriginMatrix which is only updated when // the player's location is within the fog range. return; } } { ScriptIWeaponSkin interface(skin); interface.ReloadingWeapon(); } } void ClientPlayer::ReloadedWeapon() { asIScriptObject *skin; // FIXME: what if current tool isn't weapon? if (ShouldRenderInThirdPersonView()) { skin = weaponSkin; } else { skin = weaponViewSkin; } { ScriptIWeaponSkin interface(skin); interface.ReloadedWeapon(); } } } // namespace client } // namespace spades