1. 程式人生 > >TPS相機及相機遮擋的一些處理方法

TPS相機及相機遮擋的一些處理方法

提要

第三人稱相機有非常多種,今天要實現的一個第三人稱射擊遊戲的相機。


如果對相機控制不是很瞭解,建議看一下上一篇博文 FPS相機

控制思路

滑鼠控制yaw和pitch,新增一個distance變數來記錄角色和相機之間的距離。通過yaw和pitch來得到相機的position。

最後新增一個向右的位移和向上的位移量,將角色放在螢幕偏左邊的位置。

transform.localEulerAngles = new Vector3(-rotationY, rotationX, 0);

characterModel.transform.forward = new Vector3(transform.forward.x, characterModel.transform.forward.y, transform.forward.z);
target.forward = new Vector3(transform.forward.x, 0, transform.forward.z);

float yaw = rotationX;
float pitch = rotationY;
float yawRed = Mathf.Deg2Rad * (yaw - 90f);
float pitchRed = Mathf.Deg2Rad * pitch;
Vector3 direction = new Vector3(-Mathf.Cos(yawRed) * Mathf.Cos(pitchRed), -Mathf.Sin(pitchRed), Mathf.Sin(yawRed) * Mathf.Cos(pitchRed));

transform.position = target.transform.position + distance * direction;
transform.position += transform.right + transform.up;

在這裡,相機只控制了model的rotation。

direction是通過yaw和pitch計算出的角色到相機的Ray的方向。

一些問題的處理

角色往斜方向跑的動畫處理

通常在TPS遊戲中,角色的背面是始終對著攝像機的。當玩家希望角色往斜方向走的時候,不能直接播放角色往前走的動畫,這時候就需要給角色Model一個額外的角度偏移量,這個偏移量由玩家的輸入決定。

                                                                                                       

程式碼如下

	characterModel.transform.forward = new Vector3(transform.forward.x, characterModel.transform.forward.y, transform.forward.z);
				if (characterModel.transform.parent.GetComponent<Character>().characterFPAnimation.extraRotation == 0)
				{
					extraRot = Mathf.Lerp(extraRot, 0f, 10 * Time.deltaTime);
				}else
				{
					extraRot = Mathf.Lerp(extraRot, characterModel.transform.parent.GetComponent<Character>().characterFPAnimation.extraRotation, 10 * Time.deltaTime);
				}
				Quaternion targetRotation = characterModel.transform.rotation * Quaternion.AngleAxis(extraRot, Vector3.up);

				characterModel.transform.rotation = targetRotation;

添加了Lerp,讓轉身更加順滑。

牆體遮擋

環境遮擋是第三人稱攝像機一個經常遇到問題,下面是幾個常見的方法。

解法一  射線檢測,將相機移動到不被遮擋的位置。

在Unity官網的一個Tutorial裡面,處理的方法是將相機慢慢上移,直到看到角色(遊戲的場景是沒有天花板的)

 bool ViewingPosCheck (Vector3 checkPos)
    {
        RaycastHit hit;
        
        // If a raycast from the check position to the player hits something...
        if(Physics.Raycast(checkPos, player.position - checkPos, out hit, relCameraPosMag))
            // ... if it is not the player...
            if(hit.transform != player)
                // This position isn't appropriate.
                return false;
        
        // If we haven't hit anything or we've hit the player, this is an appropriate position.
        newPos = checkPos;
        return true;
    }
    
    
    void SmoothLookAt ()
    {
        // Create a vector from the camera towards the player.
        Vector3 relPlayerPosition = player.position - transform.position;
        
        // Create a rotation based on the relative position of the player being the forward vector.
        Quaternion lookAtRotation = Quaternion.LookRotation(relPlayerPosition, Vector3.up);
        
        // Lerp the camera's rotation between it's current rotation and the rotation that looks at the player.
        transform.rotation = Quaternion.Lerp(transform.rotation, lookAtRotation, smooth * Time.deltaTime);
    }

在Update裡面的處理是這樣的
  void FixedUpdate ()
    {
        // The standard position of the camera is the relative position of the camera from the player.
        Vector3 standardPos = player.position + relCameraPos;
        
        // The abovePos is directly above the player at the same distance as the standard position.
        Vector3 abovePos = player.position + Vector3.up * relCameraPosMag;
        
        // An array of 5 points to check if the camera can see the player.
        Vector3[] checkPoints = new Vector3[5];
        
        // The first is the standard position of the camera.
        checkPoints[0] = standardPos;
        
        // The next three are 25%, 50% and 75% of the distance between the standard position and abovePos.
        checkPoints[1] = Vector3.Lerp(standardPos, abovePos, 0.25f);
        checkPoints[2] = Vector3.Lerp(standardPos, abovePos, 0.5f);
        checkPoints[3] = Vector3.Lerp(standardPos, abovePos, 0.75f);
        
        // The last is the abovePos.
        checkPoints[4] = abovePos;
        
        // Run through the check points...
        for(int i = 0; i < checkPoints.Length; i++)
        {
            // ... if the camera can see the player...
            if(ViewingPosCheck(checkPoints[i]))
                // ... break from the loop.
                break;
        }
        
        // Lerp the camera's position between it's current position and it's new position.
        transform.position = Vector3.Lerp(transform.position, newPos, smooth * Time.deltaTime);
        
        // Make sure the camera is looking at the player.
        SmoothLookAt();
    }

從角色的腳到頭,分四個地方都進行了射線檢測,最後的結果是這樣的

 

類似的還可以將相機拉到被遮擋的牆前面。

檢測的程式碼如下

void ShelterTest()
	{
		RaycastResult result = new RaycastResult();
		float characterHeight = GameManager.GetInstance().character.height * 0.4f;
		Vector3 targetHeadPos = new Vector3(target.position.x, target.position.y + characterHeight, target.position.z);

		 Ray[] testRays = new Ray[5];
		 testRays[0] = new Ray(targetHeadPos, transform.position + 0.8f * transform.right + 0.5f * transform.up - targetHeadPos);
		 testRays[1] = new Ray(targetHeadPos, transform.position + 0.8f * transform.right - 0.5f * transform.up - targetHeadPos);
		 testRays[2] = new Ray(targetHeadPos, transform.position - 0.8f * transform.right + 0.5f * transform.up - targetHeadPos);
		 testRays[3] = new Ray(targetHeadPos, transform.position - 0.8f * transform.right - 0.5f * transform.up - targetHeadPos);
		 
		 testRays[4] = new Ray(transform.position, transform.position - targetHeadPos);

		float castDist = (transform.position - targetHeadPos).magnitude;
		float[] dists = new float[5]; 
		for (int i = 0; i < 5; i++)
		{
			if (RaycastHelper.RaycastAll(testRays[i], castDist, true, GameManager.GetInstance().character.floorMask, out result))
			{
				Debug.DrawLine(targetHeadPos, result.point, Color.red);
				dists[i] = Vector3.Distance(result.point, targetHeadPos);
			}else
			{
				Debug.DrawLine(targetHeadPos, targetHeadPos + castDist * testRays[i].direction, Color.blue);
				dists[i] = castDist;
			}
		}

		float minDist0 = Mathf.Min(dists[0], dists[1]);
		float minDist1 = Mathf.Min(dists[2], dists[3]);
		float minDist2 = Mathf.Min(minDist0, minDist1);
		float minDist = Mathf.Min(minDist2, dists[4]);

		transform.position = targetHeadPos + minDist * testRays[4].direction.normalized;

	}

用了5根射線來檢測,為了避免fov穿牆的問題。注意是從角色射向攝像機。

解法二  半透明掉中間遮擋的物體


用raycast進行檢測,然後動態替換掉材質就可以了。

解法三 利用Stencil對角色進行重繪

對Stencil Buffer 不瞭解的請參考這一篇 : Stencil buffer

通過Ztest將角色被遮擋部分的Stencial標記出來,然後就可以對這部分的畫素進行處理。要麼用一種單色繪製出來,要麼繪製成透明,要麼繪製一個發光的描邊,都可以。

簡單的效果如下:


這裡分三個pass處理,第一遍繪製利用ZTest寫Stencil

Shader "Custom/Player" {

	Properties {
		_MaskValue("Mask Value", int) = 2
		_MainTex ("Base (RGB)", 2D) = "white" {}
	}

	SubShader {
	Tags { "RenderType"="Opaque" }
	LOD 200

	Stencil {
		Ref [_MaskValue]
		Comp always
		Pass replace
		ZFail keep
    }


		
		CGPROGRAM
		#pragma surface surf Lambert

		sampler2D _MainTex;

		struct Input {
			float2 uv_MainTex;
		};

		void surf (Input IN, inout SurfaceOutput o) {
			half4 c = tex2D (_MainTex, IN.uv_MainTex);
			o.Albedo = c.rgb;
			o.Alpha = c.a;
		}
		ENDCG
	} 
	FallBack "Diffuse"
}

再加一個Shader來清掉ZTest
Shader "Custom/ClearZbuffer" {
	Properties {
		_MainTex ("Base (RGB) Gloss (A)", 2D) = "white" {}
	}
	SubShader {
		Tags { "RenderType"="Transparent"  "Queue"="Transparent+100"}
		LOD 80
		ColorMask 0
		ZTest Greater
		ZWrite On

		CGPROGRAM
		#pragma surface surf Lambert
		
		sampler2D _MainTex;

		struct Input {
			float2 uv_MainTex;
		};

		void surf (Input IN, inout SurfaceOutput o) {  
			half4 c = tex2D (_MainTex, IN.uv_MainTex);
			o.Albedo = half4(1,0,0,1);
			o.Alpha = 0.3;
		}
		ENDCG
	} 
	FallBack "Diffuse"
}

最後用一個Shader對被Stencil標記出來的畫素進行處理
Shader "Custom/StencilTransparent" {
	Properties {
	    _MaskValue("Mask Value", int) = 2
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_TransVal ("Transparency Value", Range(0,1)) = 1.0  
		_ColorAdd ("Color (Add, RGB)", Color) = (0.5,0,0)
	}
	SubShader {
		Tags { "RenderType"="Opaque" "Queue"="Transparent+100"}
		LOD 80

		Stencil {
		Ref [_MaskValue]
		Comp notequal
		Pass keep
		}

		ZTest LEqual
		ZWrite On
		Blend SrcAlpha OneMinusSrcAlpha
		BlendOp Add

		CGPROGRAM
		#pragma surface surf Lambert

		sampler2D _MainTex;
		fixed _TransVal;  
		half4 _ColorAdd;

		struct Input {  
			float2 uv_MainTex;
		};

		void surf (Input IN, inout SurfaceOutput o) {  
			half4 c = tex2D (_MainTex, IN.uv_MainTex);
			//o.Albedo = c.rgb * half4(1,0,0,1);
			//o.Alpha = 1;
			o.Albedo = c.rgb * _ColorAdd;
			o.Alpha = _TransVal;
		}
		ENDCG
	} 
	FallBack "Diffuse"
}
遮擋處理的方法並不是說哪一種最好,可以進行混合使用達到最好的效果。


參考

Real-Time Cameras:A Guide for Game Designers and Developers

Unity tutorial stealth - http://unity3d.com/learn/tutorials/projects/stealth-tutorial-4x-only