zoukankan      html  css  js  c++  java
  • 【OpenGL(SharpGL)】支持任意相机可平移缩放的轨迹球实现

    【OpenGL(SharpGL)】支持任意相机可平移缩放的轨迹球

    (本文PDF版在这里。)

    在3D程序中,轨迹球(ArcBall)可以让你只用鼠标来控制模型(旋转),便于观察。在这里(http://www.yakergong.net/nehe/ )有nehe的轨迹球教程。

    本文提供一个本人编写的轨迹球类(ArcBall.cs),它可以直接应用到任何camera下,还可以同时实现缩放平移。工程源代码在文末。

    2016-07-08

    再次更新了轨迹球代码,重命名为ArcBallManipulater。

      1     /// <summary>
      2     /// Rotate model using arc-ball method.
      3     /// </summary>
      4     public class ArcBallManipulater : Manipulater, IMouseHandler
      5     {
      6 
      7         private ICamera camera;
      8         private GLCanvas canvas;
      9 
     10         private MouseEventHandler mouseDownEvent;
     11         private MouseEventHandler mouseMoveEvent;
     12         private MouseEventHandler mouseUpEvent;
     13         private MouseEventHandler mouseWheelEvent;
     14 
     15         private vec3 _vectorRight;
     16         private vec3 _vectorUp;
     17         private vec3 _vectorBack;
     18         private float _length, _radiusRadius;
     19         private CameraState cameraState = new CameraState();
     20         private mat4 totalRotation = mat4.identity();
     21         private vec3 _startPosition, _endPosition, _normalVector = new vec3(0, 1, 0);
     22         private int _width;
     23         private int _height;
     24         private bool mouseDownFlag;
     25 
     26         public float MouseSensitivity { get; set; }
     27 
     28         public MouseButtons BindingMouseButtons { get; set; }
     29         private MouseButtons lastBindingMouseButtons;
     30 
     31         /// <summary>
     32         /// Rotate model using arc-ball method.
     33         /// </summary>
     34         /// <param name="bindingMouseButtons"></param>
     35         public ArcBallManipulater(MouseButtons bindingMouseButtons = MouseButtons.Left)
     36         {
     37             this.MouseSensitivity = 0.1f;
     38             this.BindingMouseButtons = bindingMouseButtons;
     39 
     40             this.mouseDownEvent = new MouseEventHandler(((IMouseHandler)this).canvas_MouseDown);
     41             this.mouseMoveEvent = new MouseEventHandler(((IMouseHandler)this).canvas_MouseMove);
     42             this.mouseUpEvent = new MouseEventHandler(((IMouseHandler)this).canvas_MouseUp);
     43             this.mouseWheelEvent = new MouseEventHandler(((IMouseHandler)this).canvas_MouseWheel);
     44         }
     45 
     46         private void SetCamera(vec3 position, vec3 target, vec3 up)
     47         {
     48             _vectorBack = (position - target).normalize();
     49             _vectorRight = up.cross(_vectorBack).normalize();
     50             _vectorUp = _vectorBack.cross(_vectorRight).normalize();
     51 
     52             this.cameraState.position = position;
     53             this.cameraState.target = target;
     54             this.cameraState.up = up;
     55         }
     56 
     57         class CameraState
     58         {
     59             public vec3 position;
     60             public vec3 target;
     61             public vec3 up;
     62 
     63             public bool IsSameState(ICamera camera)
     64             {
     65                 if (camera.Position != this.position) { return false; }
     66                 if (camera.Target != this.target) { return false; }
     67                 if (camera.UpVector != this.up) { return false; }
     68 
     69                 return true;
     70             }
     71         }
     72 
     73         public mat4 GetRotationMatrix()
     74         {
     75             return totalRotation;
     76         }
     77 
     78         public override void Bind(ICamera camera, GLCanvas canvas)
     79         {
     80             if (camera == null || canvas == null) { throw new ArgumentNullException(); }
     81 
     82             this.camera = camera;
     83             this.canvas = canvas;
     84 
     85             canvas.MouseDown += this.mouseDownEvent;
     86             canvas.MouseMove += this.mouseMoveEvent;
     87             canvas.MouseUp += this.mouseUpEvent;
     88             canvas.MouseWheel += this.mouseWheelEvent;
     89 
     90             SetCamera(camera.Position, camera.Target, camera.UpVector);
     91         }
     92 
     93         public override void Unbind()
     94         {
     95             if (this.canvas != null && (!this.canvas.IsDisposed))
     96             {
     97                 this.canvas.MouseDown -= this.mouseDownEvent;
     98                 this.canvas.MouseMove -= this.mouseMoveEvent;
     99                 this.canvas.MouseUp -= this.mouseUpEvent;
    100                 this.canvas.MouseWheel -= this.mouseWheelEvent;
    101                 this.canvas = null;
    102                 this.camera = null;
    103             }
    104         }
    105 
    106         void IMouseHandler.canvas_MouseWheel(object sender, MouseEventArgs e)
    107         {
    108         }
    109 
    110         void IMouseHandler.canvas_MouseDown(object sender, MouseEventArgs e)
    111         {
    112             this.lastBindingMouseButtons = this.BindingMouseButtons;
    113             if ((e.Button & this.lastBindingMouseButtons) != MouseButtons.None)
    114             {
    115                 var control = sender as Control;
    116                 this.SetBounds(control.Width, control.Height);
    117 
    118                 if (!cameraState.IsSameState(this.camera))
    119                 {
    120                     SetCamera(this.camera.Position, this.camera.Target, this.camera.UpVector);
    121                 }
    122 
    123                 this._startPosition = GetArcBallPosition(e.X, e.Y);
    124 
    125                 mouseDownFlag = true;
    126             }
    127         }
    128 
    129         private void SetBounds(int width, int height)
    130         {
    131             this._width = width; this._height = height;
    132             _length = width > height ? width : height;
    133             var rx = (width / 2) / _length;
    134             var ry = (height / 2) / _length;
    135             _radiusRadius = (float)(rx * rx + ry * ry);
    136         }
    137 
    138         void IMouseHandler.canvas_MouseMove(object sender, MouseEventArgs e)
    139         {
    140             if (mouseDownFlag && ((e.Button & this.lastBindingMouseButtons) != MouseButtons.None))
    141             {
    142                 if (!cameraState.IsSameState(this.camera))
    143                 {
    144                     SetCamera(this.camera.Position, this.camera.Target, this.camera.UpVector);
    145                 }
    146 
    147                 this._endPosition = GetArcBallPosition(e.X, e.Y);
    148                 var cosAngle = _startPosition.dot(_endPosition) / (_startPosition.length() * _endPosition.length());
    149                 if (cosAngle > 1.0f) { cosAngle = 1.0f; }
    150                 else if (cosAngle < -1) { cosAngle = -1.0f; }
    151                 var angle = MouseSensitivity * (float)(Math.Acos(cosAngle) / Math.PI * 180);
    152                 _normalVector = _startPosition.cross(_endPosition).normalize();
    153                 if (!
    154                     ((_normalVector.x == 0 && _normalVector.y == 0 && _normalVector.z == 0)
    155                     || float.IsNaN(_normalVector.x) || float.IsNaN(_normalVector.y) || float.IsNaN(_normalVector.z)))
    156                 {
    157                     _startPosition = _endPosition;
    158 
    159                     mat4 newRotation = glm.rotate(angle, _normalVector);
    160                     this.totalRotation = newRotation * totalRotation;
    161                 }
    162             }
    163         }
    164 
    165         private vec3 GetArcBallPosition(int x, int y)
    166         {
    167             float rx = (x - _width / 2) / _length;
    168             float ry = (_height / 2 - y) / _length;
    169             float zz = _radiusRadius - rx * rx - ry * ry;
    170             float rz = (zz > 0 ? (float)Math.Sqrt(zz) : 0.0f);
    171             var result = new vec3(
    172                 rx * _vectorRight.x + ry * _vectorUp.x + rz * _vectorBack.x,
    173                 rx * _vectorRight.y + ry * _vectorUp.y + rz * _vectorBack.y,
    174                 rx * _vectorRight.z + ry * _vectorUp.z + rz * _vectorBack.z
    175                 );
    176             //var position = new vec3(rx, ry, rz);
    177             //var matrix = new mat3(_vectorRight, _vectorUp, _vectorBack);
    178             //result = matrix * position;
    179 
    180             return result;
    181         }
    182 
    183         void IMouseHandler.canvas_MouseUp(object sender, MouseEventArgs e)
    184         {
    185             if ((e.Button & this.lastBindingMouseButtons) != MouseButtons.None)
    186             {
    187                 mouseDownFlag = false;
    188             }
    189         }
    190 
    191     }
    ArcBallManipulater

    注意,在GetArcBallPosition(int x, int y);中,获取位置实际上是一个坐标变换的过程,所以可以用矩阵*向量实现。详见被注释掉的代码。

     1         private vec3 GetArcBallPosition(int x, int y)
     2         {
     3             float rx = (x - _width / 2) / _length;
     4             float ry = (_height / 2 - y) / _length;
     5             float zz = _radiusRadius - rx * rx - ry * ry;
     6             float rz = (zz > 0 ? (float)Math.Sqrt(zz) : 0.0f);
     7             var result = new vec3(
     8                 rx * _vectorRight.x + ry * _vectorUp.x + rz * _vectorBack.x,
     9                 rx * _vectorRight.y + ry * _vectorUp.y + rz * _vectorBack.y,
    10                 rx * _vectorRight.z + ry * _vectorUp.z + rz * _vectorBack.z
    11                 );
    12             // Get position using matrix * vector.
    13             //var position = new vec3(rx, ry, rz);
    14             //var matrix = new mat3(_vectorRight, _vectorUp, _vectorBack);
    15             //result = matrix * position;
    16 
    17             return result;
    18         }

    2016-02-10

    我已在CSharpGL中集成了最新的轨迹球代码。轨迹球只负责旋转。

      1 using GLM;
      2 using System;
      3 using System.Collections.Generic;
      4 using System.Diagnostics;
      5 using System.Drawing;
      6 using System.IO;
      7 using System.Linq;
      8 using System.Text;
      9 using System.Threading.Tasks;
     10 
     11 namespace CSharpGL.Objects.Cameras
     12 {
     13     /// <summary>
     14     /// 用鼠标旋转模型。
     15     /// </summary>
     16     public class ArcBallRotator
     17     {
     18         vec3 _vectorCenterEye;
     19         vec3 _vectorUp;
     20         vec3 _vectorRight;
     21         float _length, _radiusRadius;
     22         CameraState cameraState = new CameraState();
     23         mat4 totalRotation = mat4.identity();
     24         vec3 _startPosition, _endPosition, _normalVector = new vec3(0, 1, 0);
     25         int _width;
     26         int _height;
     27 
     28         float mouseSensitivity = 0.1f;
     29 
     30         public float MouseSensitivity
     31         {
     32             get { return mouseSensitivity; }
     33             set { mouseSensitivity = value; }
     34         }
     35 
     36         /// <summary>
     37         /// 标识鼠标是否按下
     38         /// </summary>
     39         public bool MouseDownFlag { get; private set; }
     40 
     41         /// <summary>
     42         /// 
     43         /// </summary>
     44         public ICamera Camera { get; set; }
     45 
     46 
     47         const string listenerName = "ArcBallRotator";
     48 
     49         /// <summary>
     50         /// 用鼠标旋转模型。
     51         /// </summary>
     52         /// <param name="camera">当前场景所用的摄像机。</param>
     53         public ArcBallRotator(ICamera camera)
     54         {
     55             this.Camera = camera;
     56 
     57             SetCamera(camera.Position, camera.Target, camera.UpVector);
     58 #if DEBUG
     59             const string filename = "ArcBallRotator.log";
     60             if (File.Exists(filename)) { File.Delete(filename); }
     61             Debug.Listeners.Add(new TextWriterTraceListener(filename, listenerName));
     62             Debug.WriteLine(DateTime.Now, listenerName);
     63             Debug.Flush();
     64 #endif
     65         }
     66 
     67         private void SetCamera(vec3 position, vec3 target, vec3 up)
     68         {
     69             _vectorCenterEye = position - target;
     70             _vectorCenterEye.Normalize();
     71             _vectorUp = up;
     72             _vectorRight = _vectorUp.cross(_vectorCenterEye);
     73             _vectorRight.Normalize();
     74             _vectorUp = _vectorCenterEye.cross(_vectorRight);
     75             _vectorUp.Normalize();
     76 
     77             this.cameraState.position = position;
     78             this.cameraState.target = target;
     79             this.cameraState.up = up;
     80         }
     81 
     82         class CameraState
     83         {
     84             public vec3 position;
     85             public vec3 target;
     86             public vec3 up;
     87 
     88             public bool IsSameState(ICamera camera)
     89             {
     90                 if (camera.Position != this.position) { return false; }
     91                 if (camera.Target != this.target) { return false; }
     92                 if (camera.UpVector != this.up) { return false; }
     93 
     94                 return true;
     95             }
     96         }
     97 
     98         public void SetBounds(int width, int height)
     99         {
    100             this._width = width; this._height = height;
    101             _length = width > height ? width : height;
    102             var rx = (width / 2) / _length;
    103             var ry = (height / 2) / _length;
    104             _radiusRadius = (float)(rx * rx + ry * ry);
    105         }
    106 
    107         /// <summary>
    108         /// 必须先调用<see cref="SetBounds"/>()方法。
    109         /// </summary>
    110         /// <param name="x"></param>
    111         /// <param name="y"></param>
    112         public void MouseDown(int x, int y)
    113         {
    114             Debug.WriteLine("");
    115             Debug.WriteLine("=================>MouseDown:", listenerName);
    116             if (!cameraState.IsSameState(this.Camera))
    117             {
    118                 SetCamera(this.Camera.Position, this.Camera.Target, this.Camera.UpVector);
    119                 Debug.WriteLine(string.Format(
    120                     "update camera state: {0}, {1}, {2}",
    121                     this.cameraState.position, this.cameraState.target, this.cameraState.up), listenerName);
    122             }
    123 
    124             this._startPosition = GetArcBallPosition(x, y);
    125             Debug.WriteLine(string.Format("Start position: {0}", this._startPosition), listenerName);
    126 
    127             MouseDownFlag = true;
    128 
    129             Debug.WriteLine("-------------------MouseDown end.", listenerName);
    130         }
    131 
    132         private vec3 GetArcBallPosition(int x, int y)
    133         {
    134             var rx = (x - _width / 2) / _length;
    135             var ry = (_height / 2 - y) / _length;
    136             var zz = _radiusRadius - rx * rx - ry * ry;
    137             var rz = (zz > 0 ? Math.Sqrt(zz) : 0);
    138             var result = new vec3(
    139                 (float)(rx * _vectorRight.x + ry * _vectorUp.x + rz * _vectorCenterEye.x),
    140                 (float)(rx * _vectorRight.y + ry * _vectorUp.y + rz * _vectorCenterEye.y),
    141                 (float)(rx * _vectorRight.z + ry * _vectorUp.z + rz * _vectorCenterEye.z)
    142                 );
    143             return result;
    144         }
    145 
    146 
    147         public void MouseMove(int x, int y)
    148         {
    149             if (MouseDownFlag)
    150             {
    151                 Debug.WriteLine("    =================>MouseMove:", listenerName);
    152                 if (!cameraState.IsSameState(this.Camera))
    153                 {
    154                     SetCamera(this.Camera.Position, this.Camera.Target, this.Camera.UpVector);
    155                     Debug.WriteLine(string.Format(
    156                         "    update camera state: {0}, {1}, {2}",
    157                         this.cameraState.position, this.cameraState.target, this.cameraState.up), listenerName);
    158                 }
    159 
    160                 this._endPosition = GetArcBallPosition(x, y);
    161                 Debug.WriteLine(string.Format(
    162                     "    End position: {0}", this._endPosition), listenerName);
    163                 var cosAngle = _startPosition.dot(_endPosition) / (_startPosition.Magnitude() * _endPosition.Magnitude());
    164                 if (cosAngle > 1) { cosAngle = 1; }
    165                 else if (cosAngle < -1) { cosAngle = -1; }
    166                 Debug.Write(string.Format("    cos angle: {0}", cosAngle), listenerName);
    167                 var angle = mouseSensitivity * (float)(Math.Acos(cosAngle) / Math.PI * 180);
    168                 Debug.WriteLine(string.Format(
    169                     ", angle: {0}", angle), listenerName);
    170                 _normalVector = _startPosition.cross(_endPosition);
    171                 _normalVector.Normalize();
    172                 if ((_normalVector.x == 0 && _normalVector.y == 0 && _normalVector.z == 0)
    173                     || float.IsNaN(_normalVector.x) || float.IsNaN(_normalVector.y) || float.IsNaN(_normalVector.z))
    174                 {
    175                     Debug.WriteLine("    no movement recorded.", listenerName);
    176                 }
    177                 else
    178                 {
    179                     Debug.WriteLine(string.Format(
    180                         "    normal vector: {0}", _normalVector), listenerName);
    181                     _startPosition = _endPosition;
    182 
    183                     mat4 newRotation = glm.rotate(angle, _normalVector);
    184                     Debug.WriteLine(string.Format(
    185                         "    new rotation matrix:   {0}", newRotation), listenerName);
    186                     this.totalRotation = newRotation * totalRotation;
    187                     Debug.WriteLine(string.Format(
    188                         "    total rotation matrix: {0}", totalRotation), listenerName);
    189                 }
    190                 Debug.WriteLine("    -------------------MouseMove end.", listenerName);
    191             }
    192         }
    193 
    194         public void MouseUp(int x, int y)
    195         {
    196             Debug.WriteLine("=================>MouseUp:", listenerName);
    197             MouseDownFlag = false;
    198             Debug.WriteLine("-------------------MouseUp end.", listenerName);
    199             Debug.WriteLine("");
    200             Debug.Flush();
    201         }
    202 
    203         public mat4 GetRotationMatrix()
    204         {
    205             return totalRotation;
    206         }
    207     }
    208 }
    ArcBallRotator

    1. 轨迹球原理

    clip_image003[4]clip_image004[4]

    上面是我黑来的两张图,拿来说明轨迹球的原理。

    看左边这个,网格代表绘制3D模型的窗口,上面放了个半球,这个球就是轨迹球。假设鼠标在网格上的某点A,过A点作网格所在平面的垂线,与半球相交于点P,P就是A在轨迹球上的投影。鼠标从A1点沿直线移动到A2点,对应着轨迹球上的点P1沿球面移动到了P2。那么,从球心O到P1和P2分别有两个向量OP1和OP2。OP1旋转到了OP2,我们就认为是模型也按照这个方式作同样的旋转。这就是轨迹球的旋转思路。

    右边这个图没用上…

    2. 轨迹球实现

    实现轨迹球,首先要求出鼠标点A1、A2投影到轨迹球上的点P1、P2的坐标,然后计算两个向量A1P1和A2P2之间的夹角以及旋转轴,最后让模型按照求出的夹角和旋转轴,调用glRotate就可以了。

    1) 计算投影点

    在摄像机上应用轨迹球,才能实现适应任意位置摄像机的ArcBall类。

    在相机上应用轨迹球

    如图所示,红绿蓝三色箭头的交点是摄像机eye的位置,红色箭头指向center的位置,绿色箭头指向up的位置,蓝色箭头指向右侧。

    说明:1.Up是可能在蓝色Right箭头的垂面内的任意方向的,这里我们要把它调整为与红色视线垂直的Up,即上图所示的Up。2.绿色和蓝色箭头组成的平面即为程序窗口所在位置,因为Eye就在这里嘛。而且Up指的就是屏幕正上方,Right指的就是屏幕正右方。3.显然轨迹球的半球在图中矩形所在的这一侧,球心就是Eye。

    鼠标在Up和Right所在的平面移动,当它位于A点时,投影到轨迹球的点P。现在已知的是Eye、Center、原始Up、A点在屏幕上的坐标、向量Eye-P的长度、向量AP的长度。现在要求P点的坐标,只不过是一个数学问题了。

    当然,开始的时候要设置相机位置。

     1         public void SetCamera(float eyex, float eyey, float eyez,
     2             float centerx, float centery, float centerz,
     3             float upx, float upy, float upz)
     4         {
     5             _vectorCenterEye = new Vertex(eyex - centerx, eyey - centery, eyez - centerz);
     6             _vectorCenterEye.Normalize();
     7             _vectorUp = new Vertex(upx, upy, upz);
     8             _vectorRight = _vectorUp.VectorProduct(_vectorCenterEye);
     9             _vectorRight.Normalize();
    10             _vectorUp = _vectorCenterEye.VectorProduct(_vectorRight);
    11             _vectorUp.Normalize();
    12         }

      

    根据鼠标在屏幕上的位置投影点的计算方法如下。

     1         private Vertex GetArcBallPosition(int x, int y)
     2         {
     3             var rx = (x - _width / 2) / _length;
     4             var ry = (_height / 2 - y) / _length;
     5             var zz = _radiusRadius - rx * rx - ry * ry;
     6             var rz = (zz > 0 ? Math.Sqrt(zz) : 0);
     7             var result = new Vertex(
     8                 (float)(rx * _vectorRight.X + ry * _vectorUp.X + rz * _vectorCenterEye.X),
     9                 (float)(rx * _vectorRight.Y + ry * _vectorUp.Y + rz * _vectorCenterEye.Y),
    10                 (float)(rx * _vectorRight.Z + ry * _vectorUp.Z + rz * _vectorCenterEye.Z)
    11                 );
    12             return result;
    13         }

     这里主要应用了向量的思想,向量(Eye-P) = 向量(Eye-A) + 向量(A-P)。而向量(Eye-A)和向量(A-P)都是可以通过单位长度的Up、Center-Eye和Right向量求得的。

    2) 计算夹角和旋转轴

    首先,设置鼠标按下事件

    1         public void MouseDown(int x, int y)
    2         {
    3             this._startPosition = GetArcBallPosition(x, y);
    4 
    5             mouseDownFlag = true;
    6         }

    然后,设置鼠标移动事件。此时P1P2两个点都有了,旋转轴和夹角就都可以计算了。

     1         public void MouseMove(int x, int y)
     2         {
     3             if (mouseDownFlag)
     4             {
     5                 this._endPosition = GetArcBallPosition(x, y);
     6                 var cosAngle = _startPosition.ScalarProduct(_endPosition) / (_startPosition.Magnitude() * _endPosition.Magnitude());
     7                 if (cosAngle > 1) { cosAngle = 1; }
     8                 else if (cosAngle < -1) { cosAngle = -1; }
     9                 var angle = 10 * (float)(Math.Acos(cosAngle) / Math.PI * 180);
    10                 System.Threading.Interlocked.Exchange(ref _angle, angle);
    11                 _normalVector = _startPosition.VectorProduct(_endPosition);
    12                 _startPosition = _endPosition;
    13             }
    14         }

      

    然后,设置鼠标弹起的事件。

    1         public void MouseUp(int x, int y)
    2         {
    3             mouseDownFlag = false;
    4         }

    在使用opengl(sharpgl)绘制的时候,调用

     1         public void TransformMatrix(OpenGL gl)
     2         {
     3             gl.PushMatrix();
     4             gl.LoadIdentity();
     5             gl.Rotate(2 * _angle, _normalVector.X, _normalVector.Y, _normalVector.Z);
     6             System.Threading.Interlocked.Exchange(ref _angle, 0);
     7             gl.MultMatrix(_lastTransform);
     8             gl.GetDouble(Enumerations.GetTarget.ModelviewMatix, _lastTransform);
     9             gl.PopMatrix();
    10             gl.Translate(_translateX, _translateY, _translateZ);
    11             gl.MultMatrix(_lastTransform);
    12             gl.Scale(Scale, Scale, Scale);
    13         }

    3. 额外功能实现

    缩放很容易实现,直接设置Scale属性即可。

    沿着屏幕上下左右前后地移动,则需要参照着camera的方向动了。

    1         public void GoUp(float interval)
    2         {
    3             this._translateX += this._vectorUp.X * interval;
    4             this._translateY += this._vectorUp.Y * interval;
    5             this._translateZ += this._vectorUp.Z * interval;
    6         }

    其余方向与此类似,不再浪费篇幅。

    工程源代码在此。(http://files.cnblogs.com/bitzhuwei/Arcball6662014-02-07_20-07-00.rar

  • 相关阅读:
    shell 编程 如何实现 比较两个整数的大小
    从Mysql某一表中随机读取n条数据的SQL查询语句
    AS3中UTF8、GB2312、BIG5、GBK编码转换类
    Google Map API V3 离线版
    linux下解压命令大全
    PHP 5.3无法安装Memcached解决方案
    根据淘宝商品 num_iid 批量生成淘宝客链接的 PHP 函数
    Linux curl使用简单介绍
    TCP/IP UDP用户数据报协议 运输层
    TCP/IP 应用层
  • 原文地址:https://www.cnblogs.com/bitzhuwei/p/arcball_4_all_camera.html
Copyright © 2011-2022 走看看