【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 4148|回复: 0

[基础技术] Unity3D研究院之处理摄像机跟随避免相机穿墙拉近或透明的方法

[复制链接]
发表于 2013-4-12 15:41:56 | 显示全部楼层 |阅读模式

当摄像机处于跟随主角状态时,那么主角移动后很有可能摄像机被别的模型挡住。这样用户体验就非常不好,一般3D游戏在处理视角的时候有两种方法,第一种是让被挡住的模型变成透明,第二种是拉近摄像机。前者时候固定视角游戏使用,后者适合变化视角游戏使用。两个我们都学一学蛤蛤。

          如下图所示,MOMO写了一个简单的例子,通过鼠标或触摸来牵引主角向不同的角度移动,旁边有一面墙当主角被这面墙挡住,此时摄像机将拉近。  


首先,为了方面鼠标与移动平台上的触摸同时相应我写了一个通用的方法。

包括 开始触摸  触摸中 结束触摸 ,电脑手机都可以用这个方法相应到。

JFConst.cs


[代码]:using UnityEngine;using System.Collections;public class JFConst{        public static  bool TouchBegin()        {                if(Input.GetMouseButtonDown(0))                {                        return true;                }                if(Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began)                {                        return true;                }                return false;        }        public static bool TouchEnd()        {                if(Input.GetMouseButtonUp(0))                {                        return true;                }                if(Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Ended)                {                        return true;                }                return false;        }        public static bool TouchIng()        {                if(Input.GetMouseButton(0))                {                        return true;                }else if(Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Moved)                {                        return true;                }                return false;        }}
以前写过一篇文章关于移动主角的,如果对移动主角还有不懂的请看这篇 Unity3D研究院之鼠标控制角色移动与奔跑示例(二十四)  。 这里我就不解释移动主角这部分的代码了。 下面是角色控制器ThirdPersonController.cs 绑定在主角身上。下面代码中需要详细看的就是161行到174行 ,这部分就是根据触摸屏幕的2D坐标计算主角在3D世界中移动的坐标。[代码]:using UnityEngine;using System.Collections;/** *  @Author : www.xuanyusong.com */[RequireComponent(typeof(CharacterController))]public class ThirdPersonController : MonoBehaviour {public AnimationClip idleAnimation ;public AnimationClip walkAnimation ;public AnimationClip runAnimation ;public AnimationClip jumpPoseAnimation;public float walkMaxAnimationSpeed  = 0.75f;public float trotMaxAnimationSpeed  = 1.0f;public float runMaxAnimationSpeed  = 1.0f;public float jumpAnimationSpeed  = 1.15f;public float landAnimationSpeed  = 1.0f;private Animation _animation;enum CharacterState{        Idle = 0,        Walking = 1,        Trotting = 2,        Running = 3,        Jumping = 4,}private CharacterState _characterState;// The speed when walkingpublic float walkSpeed = 2.0f;// after trotAfterSeconds of walking we trot with trotSpeedpublic float trotSpeed = 4.0f;// when pressing "Fire3" button (cmd) we start runningpublic float runSpeed = 6.0f;public float inAirControlAcceleration = 3.0f;// How high do we jump when pressing jump and letting go immediatelypublic float jumpHeight = 0.5f;// The gravity for the characterpublic float gravity = 20.0f;// The gravity in controlled descent modepublic float speedSmoothing = 10.0f;public float rotateSpeed = 500.0f;public float trotAfterSeconds = 3.0f;public bool canJump = true;private float jumpRepeatTime = 0.05f;private float jumpTimeout = 0.15f;private float groundedTimeout = 0.25f;// The camera doesnt start following the target immediately but waits for a split second to avoid too much waving around.private float lockCameraTimer = 0.0f;// The current move direction in x-zprivate Vector3 moveDirection = Vector3.zero;// The current vertical speedprivate float verticalSpeed = 0.0f;// The current x-z move speedprivate float moveSpeed = 0.0f;// The last collision flags returned from controller.Moveprivate CollisionFlags collisionFlags; // Are we jumping? (Initiated with jump button and not grounded yet)private bool jumping = false;private bool jumpingReachedApex = false;// Are we moving backwards (This locks the camera to not do a 180 degree spin)private bool movingBack = false;// Is the user pressing any keys?private bool isMoving = false;// When did the user start walking (Used for going into trot after a while)private float walkTimeStart = 0.0f;// Last time the jump button was clicked downprivate float lastJumpButtonTime = -10.0f;// Last time we performed a jumpprivate float lastJumpTime = -1.0f;// the height we jumped from (Used to determine for how long to apply extra jump power after jumping.)private float lastJumpStartHeight = 0.0f;private Vector3 inAirVelocity = Vector3.zero;private float lastGroundedTime = 0.0f;private bool isControllable = true;void Awake (){        moveDirection = transform.TransformDirection(Vector3.forward);        _animation = GetComponent<Animation>();        if(!_animation)                Debug.Log("The character you would like to control doesn't have animations. Moving her might look weird.");        /*public var idleAnimation : AnimationClip;public var walkAnimation : AnimationClip;public var runAnimation : AnimationClip;public var jumpPoseAnimation : AnimationClip;        */        if(!idleAnimation) {                _animation = null;                Debug.Log("No idle animation found. Turning off animations.");        }        if(!walkAnimation) {                _animation = null;                Debug.Log("No walk animation found. Turning off animations.");        }        if(!runAnimation) {                _animation = null;                Debug.Log("No run animation found. Turning off animations.");        }        if(!jumpPoseAnimation && canJump) {                _animation = null;                Debug.Log("No jump animation found and the character has canJump enabled. Turning off animations.");        }}void UpdateSmoothedMovementDirection (){        Transform cameraTransform = Camera.main.transform;        bool grounded = IsGrounded();        // Forward vector relative to the camera along the x-z plane        Vector3 forward = cameraTransform.TransformDirection(Vector3.forward);        forward.y = 0;        forward = forward.normalized;        // Right vector relative to the camera        // Always orthogonal to the forward vector        Vector3 right = new Vector3(forward.z, 0, -forward.x);        float v = Input.GetAxisRaw("Vertical");        float h = Input.GetAxisRaw("Horizontal");        // Are we moving backwards or looking backwards        if (v < -0.2f)                movingBack = true;        else                movingBack = false;        bool wasMoving = isMoving;        isMoving = Mathf.Abs (h) > 0.1f || Mathf.Abs (v) > 0.1f;        // Target direction relative to the camera        Vector3 targetDirection = h * right + v * forward;        if(JFConst.TouchIng())        {                        Vector3 vpos3 = Camera.main.WorldToScreenPoint(transform.position);                        Vector2 vpos2 = new Vector2 (vpos3.x,vpos3.y);                        Vector2 input = new Vector2 (Input.mousePosition.x,Input.mousePosition.y);                        if(Vector2.Distance(vpos2,input) > 10.0f)                        {                                Vector2 normalied =         ((vpos2 - input)).normalized;                                targetDirection = new Vector3(normalied.x,0.0f,normalied.y) ;                                float y = Camera.main.transform.rotation.eulerAngles.y;                                targetDirection = Quaternion.Euler(0f,y - 180,0f) * targetDirection;                                }        }                // Grounded controls        if (grounded)        {                // Lock camera for short period when transitioning moving & standing still                lockCameraTimer += Time.deltaTime;                if (isMoving != wasMoving)                        lockCameraTimer = 0.0f;                // We store speed and direction seperately,                // so that when the character stands still we still have a valid forward direction                // moveDirection is always normalized, and we only update it if there is user input.                if (targetDirection != Vector3.zero)                {                        // If we are really slow, just snap to the target direction                        if (moveSpeed < walkSpeed * 0.9f && grounded)                        {                                moveDirection = targetDirection.normalized;                        }                        // Otherwise smoothly turn towards it                        else                        {                                moveDirection = Vector3.RotateTowards(moveDirection, targetDirection, rotateSpeed * Mathf.Deg2Rad * Time.deltaTime, 1000);                                moveDirection = moveDirection.normalized;                        }                }                // Smooth the speed based on the current target direction                float curSmooth = speedSmoothing * Time.deltaTime;                // Choose target speed                //* We want to support analog input but make sure you cant walk faster diagonally than just forward or sideways                float targetSpeed = Mathf.Min(targetDirection.magnitude, 1.0f);                _characterState = CharacterState.Idle;                // Pick speed modifier                if (Input.GetKey (KeyCode.LeftShift) | Input.GetKey (KeyCode.RightShift))                {                        targetSpeed *= runSpeed;                        _characterState = CharacterState.Running;                }                else if (Time.time - trotAfterSeconds > walkTimeStart)                {                        targetSpeed *= trotSpeed;                        _characterState = CharacterState.Trotting;                }                else                {                        targetSpeed *= walkSpeed;                        _characterState = CharacterState.Walking;                }                moveSpeed = Mathf.Lerp(moveSpeed, targetSpeed, curSmooth);                // Reset walk time start when we slow down                if (moveSpeed < walkSpeed * 0.3f)                        walkTimeStart = Time.time;        }        // In air controls        else        {                // Lock camera while in air                if (jumping)                        lockCameraTimer = 0.0f;                if (isMoving)                        inAirVelocity += targetDirection.normalized * Time.deltaTime * inAirControlAcceleration;        }}void ApplyJumping (){        // Prevent jumping too fast after each other        if (lastJumpTime + jumpRepeatTime > Time.time)                return;        if (IsGrounded()) {                // Jump                // - Only when pressing the button down                // - With a timeout so you can press the button slightly before landing                if (canJump && Time.time < lastJumpButtonTime + jumpTimeout) {                        verticalSpeed = CalculateJumpVerticalSpeed (jumpHeight);                        SendMessage("DidJump", SendMessageOptions.DontRequireReceiver);                }        }}void ApplyGravity (){        if (isControllable)        // don't move player at all if not controllable.        {                // Apply gravity                bool jumpButton = Input.GetButton("Jump");                // When we reach the apex of the jump we send out a message                if (jumping && !jumpingReachedApex && verticalSpeed <= 0.0f)                {                        jumpingReachedApex = true;                        SendMessage("DidJumpReachApex", SendMessageOptions.DontRequireReceiver);                }                if (IsGrounded ())                        verticalSpeed = 0.0f;                else                        verticalSpeed -= gravity * Time.deltaTime;        }}float CalculateJumpVerticalSpeed (float targetJumpHeight){        // From the jump height and gravity we deduce the upwards speed        // for the character to reach at the apex.        return Mathf.Sqrt(2 * targetJumpHeight * gravity);}void  DidJump (){        jumping = true;        jumpingReachedApex = false;        lastJumpTime = Time.time;        lastJumpStartHeight = transform.position.y;        lastJumpButtonTime = -10;        _characterState = CharacterState.Jumping;}void  Update() {        if (!isControllable)        {                // kill all inputs if not controllable.                Input.ResetInputAxes();        }        if (Input.GetButtonDown ("Jump"))        {                lastJumpButtonTime = Time.time;        }        UpdateSmoothedMovementDirection();        // Apply gravity        // - extra power jump modifies gravity        // - controlledDescent mode modifies gravity        ApplyGravity ();        // Apply jumping logic        ApplyJumping ();        // Calculate actual motion        Vector3 movement = moveDirection * moveSpeed + new Vector3 (0, verticalSpeed, 0) + inAirVelocity;        movement *= Time.deltaTime;        // Move the controller        CharacterController controller = GetComponent<CharacterController>();        collisionFlags = controller.Move(movement);        // ANIMATION sector        if(_animation) {                if(_characterState == CharacterState.Jumping)                {                        if(!jumpingReachedApex) {                                _animation[jumpPoseAnimation.name].speed = jumpAnimationSpeed;                                _animation[jumpPoseAnimation.name].wrapMode = WrapMode.ClampForever;                                _animation.CrossFade(jumpPoseAnimation.name);                        } else {                                _animation[jumpPoseAnimation.name].speed = -landAnimationSpeed;                                _animation[jumpPoseAnimation.name].wrapMode = WrapMode.ClampForever;                                _animation.CrossFade(jumpPoseAnimation.name);                        }                }                else                {                        if(controller.velocity.sqrMagnitude < 0.1f) {                                _animation.CrossFade(idleAnimation.name);                        }                        else                        {                                if(_characterState == CharacterState.Running) {                                        _animation[runAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0f, runMaxAnimationSpeed);                                        _animation.CrossFade(runAnimation.name);                                }                                else if(_characterState == CharacterState.Trotting) {                                        _animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0f, trotMaxAnimationSpeed);                                        _animation.CrossFade(walkAnimation.name);                                }                                else if(_characterState == CharacterState.Walking) {                                        _animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0f, walkMaxAnimationSpeed);                                        _animation.CrossFade(walkAnimation.name);                                }                        }                }        }        // ANIMATION sector        // Set rotation to the move direction        if (IsGrounded())        {                transform.rotation = Quaternion.LookRotation(moveDirection);        }        else        {                Vector3 xzMove = movement;                xzMove.y = 0;                if (xzMove.sqrMagnitude > 0.001f)                {                        transform.rotation = Quaternion.LookRotation(xzMove);                }        }                // We are in jump mode but just became grounded        if (IsGrounded())        {                lastGroundedTime = Time.time;                inAirVelocity = Vector3.zero;                if (jumping)                {                        jumping = false;                        SendMessage("DidLand", SendMessageOptions.DontRequireReceiver);                }        }}void  OnControllerColliderHit (ControllerColliderHit hit ){//        Debug.DrawRay(hit.point, hit.normal);        if (hit.moveDirection.y > 0.01f)                return;}float GetSpeed () {        return moveSpeed;}public bool IsJumping () {        return jumping;}bool IsGrounded () {        return (collisionFlags & CollisionFlags.CollidedBelow) != 0;}Vector3 GetDirection () {        return moveDirection;}public bool IsMovingBackwards () {        return movingBack;}public float GetLockCameraTimer (){        return lockCameraTimer;}bool IsMoving (){        return Mathf.Abs(Input.GetAxisRaw("Vertical")) + Mathf.Abs(Input.GetAxisRaw("Horizontal")) > 0.5f;}bool HasJumpReachedApex (){        return jumpingReachedApex;}bool IsGroundedWithTimeout (){        return lastGroundedTime + groundedTimeout > Time.time;}void Reset (){        gameObject.tag = "Player";}}
如此我们就可以通过鼠标与触摸牵引控制主角移动了,别急这紧紧是开始,下面是本章的要点。
这里我们分析一下摄像机到底什么时候该拉近,什么时候该不拉近。 我的作法是这样的,当角色移动的时候我会从主角身上向摄像机方向发射一条射线,如果射线碰撞到的第一个对象是“摄像机” 那么就标示主角和摄像机之间没有别的模型挡住,此时摄像机就不应该拉近。如果射线碰撞到的第一个对象不是摄像机,那么就标示主角和摄像机之间有别的模型所挡住,此时取得射线与别的模型碰撞的3D坐标接着将“摄像机”的坐标移动到这个坐标上即可。
如下图所示,(红色表示由主角发射到摄像机的射线)主角身后的射线没有被别的模型挡住,射线机不会拉近。





如下图所示,MOMO改变了一下主角的位置,此时主角身后的射向已经被墙挡住,这时把摄像机的坐标移动到被墙挡住点的坐标,这样摄像机就拉近了。




原理大概就是这样,接着我们学习一下这段代码该如何来写。
MyCamera.cs 挂在射线机上

[代码]:using UnityEngine;using System.Collections;public class MyCamera : MonoBehaviour{    //摄像机朝向的目标模型        public Transform target;        //摄像机与模型保持的距离        public float distance = 10.0f;        //射线机与模型保持的高度        public float height = 5.0f;        //高度阻尼        public float heightDamping = 2.0f;        //旋转阻尼        public float rotationDamping = 3.0f;        //主角对象        private GameObject controller;         void Start()        {                //得到主角对象                controller = GameObject.FindGameObjectWithTag("Player");        }        void Update()        {        }        void LateUpdate ()        {                // Early out if we don't have a target                if (!target)                        return;                //当鼠标或者手指在触摸中时                if(JFConst.TouchIng())                {                        bool follow = true;                //计算相机与主角Y轴旋转角度的差。                        float abs = Mathf.Abs(transform.rotation.eulerAngles.y - controller.transform.rotation.eulerAngles.y);                        //abs等于180的时候标示摄像机完全面对这主角, 》130 《 230 表示让面对的角度左右偏移50度                        //这样做是不希望摄像机跟随主角,具体效果大家把代码下载下来看看,这样的摄像机效果很好。                        if(abs > 130 && abs < 230)                        {                                follow = false;                        }else                        {                                follow = true;                        }                        float wantedRotationAngle = target.eulerAngles.y;                        float wantedHeight = target.position.y + height;                        float currentRotationAngle = transform.eulerAngles.y;                        float currentHeight = transform.position.y;                        //主角面朝射线机 和背对射线机 计算正确的位置                        if(follow)                        {                                currentRotationAngle = Mathf.LerpAngle (currentRotationAngle, wantedRotationAngle, rotationDamping * Time.deltaTime);                                currentHeight = Mathf.Lerp (currentHeight, wantedHeight, heightDamping * Time.deltaTime);                                Quaternion currentRotation = Quaternion.Euler (0, currentRotationAngle, 0);                                Vector3 positon = target.position;                                positon -= currentRotation * Vector3.forward * distance;                                positon = new Vector3(positon.x,currentHeight,positon.z);                                transform.position = Vector3.Lerp(transform.position,positon,Time.time);                        }else                        {                                Vector3 positon = target.position;                                Quaternion cr = Quaternion.Euler (0, currentRotationAngle, 0);                                        positon += cr * Vector3.back * distance;                                positon = new Vector3(positon.x,target.position.y + height,positon.z);                                transform.position = Vector3.Lerp(transform.position,positon,Time.time);                        }                }                    //这里是计算射线的方向,从主角发射方向是射线机方向                        Vector3 aim = target.position;                        //得到方向                        Vector3 ve = (target.position - transform.position).normalized;                        float an = transform.eulerAngles.y;                        aim -= an * ve ;                    //在场景视图中可以看到这条射线                        Debug.DrawLine(target.position,aim,Color.red);                    //主角朝着这个方向发射射线                        RaycastHit hit;                        if(Physics.Linecast(target.position,aim,out hit))                        {                                string name =  hit.collider.gameObject.tag;                                if(name != "MainCamera" && name !="terrain")                                {                                        //当碰撞的不是摄像机也不是地形 那么直接移动摄像机的坐标                                        transform.position = hit.point;                                }                }                // 让射线机永远看着主角                transform.LookAt (target);                }}
牛头人被墙挡住了,摄像机直接拉近了,小牛头人是不是很帅气?哈哈哈!!





最后雨松MOMO把源码放出来,希望大家学习愉快,哇咔咔。。。

补充 :摄像机遮挡物体透明。
遮挡透明最简单的办法就是修改材质的透明度,前提需要把材质设置成支持透明通道的 shader Transparent 。 如下图所示,摄像机挡住的那面墙已经成透明状态了。





看看代码是如何写的,把上述代码MyCamera.cs 简单的改一下就可以。 主要是92行到101行。
代码核心就是通过射线得到碰撞模型的材质,然后修改材质的透明度。color.a就是透明度 1是完全不透明,0是完全透明。注意一定是支持透明通道的模型才可以。lastobj是记录上次透明的对象,用于模型透明还原使用。

[代码]:if(Physics.Linecast(target.position,aim,out hit)){        GameObject obj = hit.collider.gameObject;        string name =  obj.tag;        if(name != "MainCamera" && name !="terrain")        {                Color color = obj.renderer.material.color;                color.a = 0.5f;                obj.renderer.material.SetColor("_Color", color);                lastobj = obj;        }else        {                if(lastobj != null)                {                        Color color = lastobj.renderer.material.color;                        color.a = 1f;                        lastobj.renderer.material.SetColor("_Color", color);                }        }}
最后,假设你的模型做的非常大,贴图材质也非常的多,你像透明其中一小部分那么上述方法就不合适了,除非把他们都拆出来。我觉得也可以自己写shader实现。我们的项目采用了摄像机拉近的方式。欢迎讨论!!!

回答楼下问题:上帝视角代码,看到楼下有朋友问我,那么我就贴出来,其实很简单。下面代码挂在摄像机上, target就是角色对象,distance 是摄像机与角色的距离 height是摄像机与角色的高度。在编辑器中手动调节即可。

[代码]:using UnityEngine;using System.Collections;public class ICamera : MonoBehaviour {public Transform target;// The distance in the x-z plane to the targetpublic float distance = 10.0f;// the height we want the camera to be above the targetpublic float height = 5.0f;void LateUpdate () {        // Early out if we don't have a target        if (!target)                return;        // Set the position of the camera on the x-z plane to:        // distance meters behind the target        transform.position = target.position;        transform.position -=  Vector3.forward * distance;        transform.position = new Vector3(transform.position.x,transform.position.y + height,transform.position.z);        // Always look at the target        transform.LookAt (target);        }}
回答问题 :摄像机抖动如何修改
今天微薄上有个朋友问我这样的问题,我在这里解答一下。
来福12345:我又厚颜无耻的来了,您的《Unity3D的研究院之处理摄像机跟随避免相机穿墙拉近或透明的方法(四十四)》对于不规则的物体该怎么弄,我是直接在unity上弄了几个山脉,由于地面凹凸不平,沿着山走的时候发现摄像机会一直透视,而且抖动的很厉害,请问该怎么解决啊

1.从设计上避免射线遇到一些复杂的面,如下图所示,红颜色是主角身后的射线,当主角身后的射线碰撞到这样的面,如果此时移动速快快一些摄像机肯定会抖动。 这里最好直接删除Mesh Collider 换成BoxCollider 效率还可以得到提升。产经设计这块我觉得可以参考一下魔兽世界,我在项目中遇到这样问题的地方就是 比较小的门(棱角那种) 或者这样的树。






2.但是还有一些比较特殊的地方,如果你的项目必需使用MeshColider 的话 我建议你写触发脚本来修改摄像机,假设 当摄像机被挡住后,变成跟随视角,只到摄像机没被挡住的时候在回到原来视角。
3.我检查了一下上面的代码中我们在加一个判断,应该就可以解决大部分问题、在LateUpdate () 中最后加入代码

[代码]:if(!closer){        transform.position = positon;}else{        float dis = Vector3.Distance(transform.position,controller.transform.position);        if(dis < 8)        {                distance = dis;        }else        {                closer = false;        }}
bool closer 是表示是否拉近。 当摄像机被挡住的时候这个数值等于true;

[代码]:if(Physics.Linecast(tirggerTaget,aim,out hit)){        string name =  hit.collider.gameObject.tag;        if(name != "MainCamera")        {                closer = true;                transform.position = hit.point;                distance = Vector3.Distance(transform.position,controller.transform.position) - 2f;                }}
当closer等于假的时候,此时直接修更新摄像机的位置,因为没被挡住。 当closer等于真的时候,摄像机已经被挡住了,我们不修改摄像机的位置,计算摄像机保证不被挡住时,摄像机此时和主角的距离是多少,然后修改distance全局变量即可。当摄像机不被遮挡的时候在恢复之前的距离。 如有问题请留言,我会即时解答

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2024-5-10 23:23

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表