做有态度的前端团队

网易FEG前端团队

ThreeJS学习笔记(三)——三维空间用户交互与动画

拾取器raycaster

ThreeJS提供了一个 raycaster的API用于返回用户光标所在位置的所有3维元素,它的实现原理是在屏幕上某个二维坐标点与相机位置和视角形成的向量方向上投射一条射线,返回与射线相交的所有三维物体的集合,集合的第一个物体为距离相机最近的物体,最后一个则为离相机最远的。
当使用拾取器去获取用户点击的物体时,需要事先将所有可参与用户交互的三维物体放到一个集合里。在创建拾取器后获取两个集合的交集,即当前用户在屏幕点击的位置上所有被设置为可被选择的物体,第一个即可视为用户直接点击的物体。

拾取器示例

以下代码段实现当用户鼠标移动到object1和object2上时鼠标指针形状变为pointer;点击时将相机旋转到物体正面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var _raycaster = new THREE.Raycaster();//拾取器
var raycAsix=new THREE.Vector2();//屏幕点击点二维坐标
var _curObj=null;//当前点击物体
function onDocumentMouseMove( event ) {
    event.preventDefault();
    raycAsix.x = ( (event.pageX-$(container).offset().left) / container.offsetWidth ) * 2 - 1;
    raycAsix.y = - ( (event.pageY-$(container).offset().top) /container.offsetHeight ) * 2 + 1;
    _raycaster.setFromCamera(raycAsix, camera );
    var intersects = _raycaster.intersectObjects( clickObjects );//获取投射线上与用户预设的可被点击物体的集合的交集
    if ( intersects.length > 0 ) {
        document.body.style.cursor = 'pointer';
        console.log(intersects[0].object.name);
    }else{
        document.body.style.cursor = 'default';
    }
}
     
function onDocumentClick( event ) {
     
    event.preventDefault();
    _raycaster.setFromCamera( raycAsix, camera );
    var intersects = _raycaster.intersectObjects( clickObjects );
    if(intersects.length== 0){
        return;
         
    }
    if ( intersects.length > 0 &&intersects[ 0 ].object!=_curObj) {
        if(_userView.curObj ==intersects[ 0 ].object){
            return;
        }
        _curObj =intersects[ 0 ].object;
        rotateTo(intersects[ 0 ]);//点击时旋转到物体的位置
    }
}
        

关于动画

动画一般是在render()函数里处理,实时修改元素的位置大小等。
上面的rotateTo()函数里旋转动画是使用一个 tween.js实现缓动,并在render()中根据缓动计算的数值去修改相机的位置。大部分交互动画需要使用运动曲线的都可以使用此插件完成。

GITHUP地址:https://github.com/tweenjs/tween.js

  • 利用TweenJS的缓动曲线改变相机的theta(水平夹角) phi(竖立夹角) 和离中心点坐标的距离R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var _userView={};//用于存储相机theta(水平夹角) phi(竖立夹角) 和离中心点坐标的距离R,使用这三个变量去修改相机的位置
function rotateTo(obj){//
    _isRotateing=true;
    controls.enabled = false;
    var point=obj.point;
    var pointAngle=Math3D.get3DAngle(point.x,point.y,point.z);//点击点的角度和球半径
    var toAngle={//需要旋转到的用户视角的角度和半径
        theta:pointAngle.theta,
        phi:30/180*Math.PI,
        r:1000
    }      
    _userView.cameraPosTo=Math3D.get3DAxis(toAngle.theta,toAngle.phi,toAngle.r);//旋转用户视角停止时摄像机位置
    _userView.dmy={};
    _userView.dmy.theta=Math3D.getAngleByAxis2d({x:camera.position.x,y:camera.position.z});//当前摄像机与Z轴的水平夹角
    _userView.dmy.r=Math.sqrt(camera.position.x * camera.position.x + camera.position.z * camera.position.z);//当前摄像机离坐标轴原点的水平距离
    _userView.dmy.y=camera.position.y;//当前摄像机的Y点坐标
     
    var dmyStop={};//相机将到移动到的最终位置
    dmyStop.theta=Math3D.getAngleByAxis2d({x:point.x,y:point.z});//旋转到用户点击点所在位置时摄像机与Z轴的水平夹角
    dmyStop.r=1000;//旋转到用户点击点所在位置时摄像机与坐标原点的水平距离
    dmyStop.y=300;//旋转到用户点击点所在位置时摄像机Y点坐标
    var tween = new TWEEN.Tween(_userView.dmy).to(dmyStop, 1000).easing(TWEEN.Easing.Quadratic.InOut)
    .onComplete(function(){
        _isRotateing=false;
        controls.enabled = true;
    })
    .start();//设置缓动动画
}
  • 在render函数里根据缓动计算出来的相机的theta(水平夹角) phi(竖立夹角) 和离中心点坐标的距离R去计算相机的position并设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function render() {
 
    if(_isRotateing){//用户点击行为执行旋转动画
        var newCameraPos=Math3D.getAxis2dByAngle(_userView.dmy.theta,_userView.dmy.r);
        camera.position.x=newCameraPos.x;
        camera.position.y=_userView.dmy.y;
        camera.position.z=newCameraPos.y;
    }else {//自动旋转
        var newCameraPos=Math3D.getRotateAxis2d({
            x:camera.position.x,
            y:camera.position.z
        },-0.001,0);
        camera.position.x=newCameraPos.x;
        camera.position.z=newCameraPos.y;
    }
    camera.lookAt( scene.position );
    renderer.render( scene, camera );
 
}
  • 注意要在animate函数中执行TWEEN.update();才会更新_userView变量
1
2
3
4
5
6
function animate() {
        requestAnimationFrame( animate );
        controls.update();
        TWEEN.update();
        render();
}
  • 如果不需要做二次计算也可以直接使用TweenJS去设置动画元素的属性如:
1
var tween = new TWEEN.Tween(camera.position).to({x:100,y:100,z:100}, 1000).easing(TWEEN.Easing.Quadratic.InOut).start();

本章示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
    <div id="space"></div
    <script   src="https://code.jquery.com/jquery-1.12.4.min.js"   integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="   crossorigin="anonymous"></script>
    <script src="../js/lib/threejs/three.js"></script>
    <script src="../js/lib/threejs/MTLLoader.js"></script>
    <script src="../js/lib/threejs/OBJLoader.js"></script>
    <script src="../js/lib/threejs/OrbitControls.js"></script>
    <script src="../js/lib/Tween.js"></script>
    <script>
        var Math3D=function(window,document){
    function _createRandomCoord(maxR,minR){
        var r=Math.round(Math.random()*(maxR-minR))+minR;
        var theta=Math.random()*Math.PI*2;
        //console.log(theta+"="+theta/Math.PI*180);
        var phi=Math.random()*Math.PI*2;
        //console.log(phi+"="+phi/Math.PI*180);
         
        return get3DAxis(theta,phi,r);
    }
    function get3DAxis(theta,phi,r){
        //X=rsinθcosφ y=rsinθsinφ z=rcosθ
        return{
            x:r*Math.sin(theta)*Math.cos(phi),
            y:r*Math.sin(theta)*Math.sin(phi),
            z:r*Math.cos(theta)
        }
    }
    function get3DAngle(x,y,z){
        //r=sqrt(x*2 + y*2 + z*2); θ= arccos(z/r); φ=arctan(y/x);
        var r=Math.sqrt(x*x + y*y + z*z);
        return{
            theta:Math.acos(z/r),
            phi:Math.atan(y/x),
            r:r
        }
    }
    function getAngle(point){
                return Math.atan2(point.y,point.x)//atan2自带坐标系识别, 注意X,Y的顺序
            }
    function Rotate(source,angle,rudius)//Angle为正时逆时针转动, 单位为弧度
    {
        var A,R;
        A = getAngle(source);
        A += angle;//旋转
        R = Math.sqrt(source.x * source.x + source.y * source.y)//半径
        if(rudius){
            R-=rudius
        }
        return {
            x : Math.cos(A) * R,
            y : Math.sin(A) * R
        }
    }
    function getpositionFromAngel(A,R)//Angle为正时逆时针转动, 单位为弧度
    {
         
        return {
            x : Math.cos(A) * R,
            y : Math.sin(A) * R
        }
    }
     
    return{
        createRandomCoord:_createRandomCoord,
        getAngleByAxis2d:getAngle,
        getRotateAxis2d:Rotate,
        getAxis2dByAngle:getpositionFromAngel,
        get3DAxis:get3DAxis,
        get3DAngle:get3DAngle
    }
}(window,document,undefined);
 
 
        var container, stats;
 
        var camera, scene, renderer,controls;
 
        var mouseX = 0, mouseY = 0;
 
        var windowHalfX = window.innerWidth / 2;
        var windowHalfY = window.innerHeight / 2;
 
        var clickObjects=[];
        var _raycaster = new THREE.Raycaster();
        var raycAsix=new THREE.Vector2();
        var _curObj=null,_isRotateing=false;
        var _userView={};
        init();
        animate();
        var mesh;
 
        function init() {
 
            container = document.getElementById("space")
            camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 8000 );
            camera.position.set(0, 0, 1500);
             
            scene = new THREE.Scene();
 
            var ambient = new THREE.AmbientLight( 0xffffff );
            scene.add( ambient );
             
             
            var directionalLight = new THREE.DirectionalLight( 0xffffff );
            directionalLight.position.set( -5, 5, 5).normalize();
            scene.add( directionalLight );
 
            var pointlight = new THREE.PointLight(0x63d5ff, 1, 200);
            pointlight.position.set(0, 0, 200);
            scene.add( pointlight );               
            var pointlight2 = new THREE.PointLight(0xffffff, 1, 200);
            pointlight2.position.set(-200, 200, 200);
            scene.add( pointlight2 );
            var pointlight3 = new THREE.PointLight(0xffffff, 1.5, 200);
            pointlight3.position.set(-200, 200, 0);
            scene.add( pointlight3 );
            scene.add( new THREE.PointLightHelper( pointlight3 ) );
            scene.add( new THREE.PointLightHelper( pointlight2 ) );
            scene.add( new THREE.PointLightHelper( pointlight ) );
         
             
            var path = "../resource/sky/";
            var format = '.jpg';
            var urls = [
                    path + 'px' + format, path + 'nx' + format,
                    path + 'py' + format, path + 'ny' + format,
                    path + 'pz' + format, path + 'nz' + format
                ];
            var skyMaterials = [];
            for (var i = 0; i < urls.length; ++i) {
                var loader = new THREE.TextureLoader();
                loader.setCrossOrigin( this.crossOrigin );
                var texture = loader.load( urls[i], function(){}, undefined, function(){} );
                 
                skyMaterials.push(new THREE.MeshBasicMaterial({
                    //map: THREE.ImageUtils.loadTexture(urls[i], {},function() { }),
                    map: texture,
                    overdraw: true,
                    side: THREE.BackSide,
                    //transparent: true,
                    //needsUpdate:true,
                    premultipliedAlpha: true
                    //depthWrite:true,
                     
    //              wireframe:false,
                })
                );
                 
            }
             
            var cube = new THREE.Mesh(new THREE.CubeGeometry(4000, 4000,4000), new THREE.MeshFaceMaterial(skyMaterials));
            cube.name="sky";
            scene.add(cube);
             
            createMtlObj({
                mtlBaseUrl:"../resource/haven/",
                mtlPath: "../resource/haven/",
                mtlFileName:"threejs.mtl",
                objPath:"../resource/haven/",
                objFileName:"threejs.obj",
                completeCallback:function(object){
                    object.traverse(function(child) {
                        if (child instanceof THREE.Mesh) {
                            child.material.side = THREE.DoubleSide;
                            child.material.emissive.r=0;
                            child.material.emissive.g=0.01;
                            child.material.emissive.b=0.05;
                            child.material.transparent=true;
                            //child.material.opacity=0;                    
                            //child.material.shading=THREE.SmoothShading;
                            clickObjects.push(child);
                        }
                    });
 
                    object.emissive=0x00ffff;
                    object.ambient=0x00ffff;
                    //object.rotation.x= 10/180*Math.PI;
                    object.position.y = 0;
                    object.position.z = 0;
                    object.scale.x=1;
                    object.scale.y=1;
                    object.scale.z=1;
                    object.name="haven";
                    object.rotation.y=-Math.PI;
                    scene.add(object);
                },
                progress:function(persent){
                     
                    $("#havenloading .progress").css("width",persent+"%");
                }
            })
            controls = new THREE.OrbitControls(camera,container);
            controls.maxPolarAngle=1.5;
            controls.minPolarAngle=1;
            controls.enableDamping=true;
            controls.enableKeys=false;
            controls.enablePan=false;
            controls.dampingFactor = 0.1;
            controls.rotateSpeed=0.1;
    //      controls.enabled = false;
            controls.minDistance=1000;
            controls.maxDistance=3000;
             
            renderer = new THREE.WebGLRenderer();
            renderer.setPixelRatio( window.devicePixelRatio );
            renderer.setSize( window.innerWidth, window.innerHeight );
            container.appendChild( renderer.domElement );
             
            window.addEventListener( 'resize', onWindowResize, false );
            window.addEventListener( 'mousemove', onDocumentMouseMove, false );
            window.addEventListener( 'click', onDocumentClick, false );    
        }
         
         
        function createMtlObj(options){
        //      options={
        //          mtlBaseUrl:"",
        //          mtlPath:"",
        //          mtlFileName:"",
        //          objPath:"",
        //          objFileName:"",
        //          completeCallback:function(object){ 
        //          }
        //          progress:function(persent){
        //             
        //          }
        //      }
                //THREE.Loader.Handlers.add( /\.dds$/i, new THREE.DDSLoader() );
            var mtlLoader = new THREE.MTLLoader();
            mtlLoader.setBaseUrl( options.mtlBaseUrl );
            mtlLoader.setPath( options.mtlPath );
            mtlLoader.load( options.mtlFileName, function( materials ) {
                materials.preload();
                var objLoader = new THREE.OBJLoader();
                objLoader.setMaterials( materials );
                objLoader.setPath( options.objPath );
                objLoader.load( options.objFileName, function ( object ) {
                    if(typeof options.completeCallback=="function"){
                        options.completeCallback(object);
                    }
                }, function ( xhr ) {
                    if ( xhr.lengthComputable ) {
                        var percentComplete = xhr.loaded / xhr.total * 100;
                        if(typeof options.progress =="function"){
                            options.progress( Math.round(percentComplete, 2));
                        }
                        //console.log( Math.round(percentComplete, 2) + '% downloaded' );
                    }
                }, function(error){
                     
                } );
         
            });
        }
        function onWindowResize() {
            windowHalfX = window.innerWidth / 2;
            windowHalfY = window.innerHeight / 2;
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize( window.innerWidth, window.innerHeight );
        }
         
        function onDocumentMouseMove( event ) {
            event.preventDefault();
             
            raycAsix.x = ( (event.pageX-$(container).offset().left) / container.offsetWidth ) * 2 - 1;
            raycAsix.y = - ( (event.pageY-$(container).offset().top) /container.offsetHeight ) * 2 + 1;
            _raycaster.setFromCamera(raycAsix, camera );
            var intersects = _raycaster.intersectObjects( clickObjects );
            if ( intersects.length > 0 ) {
                document.body.style.cursor = 'pointer';
                console.log(intersects[0].object.name);
            }else{
                document.body.style.cursor = 'default';
            }
        }
             
        function onDocumentClick( event ) {
             
            event.preventDefault();
            _raycaster.setFromCamera( raycAsix, camera );
            var intersects = _raycaster.intersectObjects( clickObjects );
            if(intersects.length== 0){
                return;
                resetRotate();
            }
            if ( intersects.length > 0 &&intersects[ 0 ].object!=_curObj) {
                if(_userView.curObj ==intersects[ 0 ].object){
                    return;
                }
                _curObj =intersects[ 0 ].object;
                rotateTo(intersects[ 0 ]);
            }
        }
         
        function rotateTo(obj){
            _isRotateing=true;
            controls.enabled = false;
            var point=obj.point;
            var pointAngle=Math3D.get3DAngle(point.x,point.y,point.z);//点击点的角度和球半径
            var toAngle={//需要旋转到的用户视角的角度和半径
                theta:pointAngle.theta,
                phi:30/180*Math.PI,
                r:1000
            }
             
            _userView.cameraPosTo=Math3D.get3DAxis(toAngle.theta,toAngle.phi,toAngle.r);//旋转用户视角停止时摄像机位置
            _userView.dmy={};
            _userView.dmy.theta=Math3D.getAngleByAxis2d({x:camera.position.x,y:camera.position.z});//当前摄像机与Z轴的水平夹角
            _userView.dmy.r=Math.sqrt(camera.position.x * camera.position.x + camera.position.z * camera.position.z);//当前摄像机离坐标轴原点的水平距离
            _userView.dmy.y=camera.position.y;//当前摄像机的Y点坐标
             
            var dmyStop={};
            dmyStop.theta=Math3D.getAngleByAxis2d({x:point.x,y:point.z});//旋转到用户点击点所在位置时摄像机与Z轴的水平夹角
            dmyStop.r=1000;//用户视角模式时摄像机与坐标原点的水平距离
            dmyStop.y=300;//用户视角模式时摄像机Y点坐标
            var tween = new TWEEN.Tween(_userView.dmy).to(dmyStop, 1000).easing(TWEEN.Easing.Quadratic.InOut)
            .onComplete(function(){
                _isRotateing=false;
                controls.enabled = true;
            })
            .start();//设置缓动动画
        }
        function animate() {
            requestAnimationFrame( animate );
            controls.update();
            TWEEN.update();
            render();
        }
        function render() {
//              camera.position.x += ( mouseX - camera.position.x ) ;
//              camera.position.y += ( mouseY - camera.position.y ) ;
            if(_isRotateing){
                var newCameraPos=Math3D.getAxis2dByAngle(_userView.dmy.theta,_userView.dmy.r);
                camera.position.x=newCameraPos.x;
                camera.position.y=_userView.dmy.y;
                camera.position.z=newCameraPos.y;
            }else {
                var newCameraPos=Math3D.getRotateAxis2d({
                    x:camera.position.x,
                    y:camera.position.z
                },-0.001,0);
                camera.position.x=newCameraPos.x;
                camera.position.z=newCameraPos.y;
         
                 
            }
             
            camera.lookAt( scene.position );
            renderer.render( scene, camera );
 
        }
         
    </script>
    </body>
</html>

手机阅读请扫描下方二维码:

分享到:

    已有 57 条评论

    1. 776

      ali-45.gif ali-52.gif

    2. ali-49.gif ali-49.gif ali-50.gif ali-50.gif ali-51.gif ali-52.gif ali-53.gif ali-54.gif ali-55.gif ali-60.gif ali-61.gif ali-60.gif ali-51.gif

    3. 1

      1 ali-40.gif ali-40.gif ali-41.gif ali-41.gif ali-42.gif ali-42.gif ali-43.gif ali-43.gif ali-44.gif ali-44.gif ali-45.gif ali-45.gif ali-46.gif ali-46.gif ali-47.gif ali-47.gif ali-48.gif ali-48.gif ali-49.gif ali-49.gif ali-50.gif ali-50.gif ali-51.gif ali-51.gif ali-52.gif ali-52.gif ali-53.gif ali-53.gif ali-54.gif ali-54.gif ali-55.gif ali-55.gif ali-56.gif ali-56.gif ali-57.gif ali-57.gif ali-58.gif ali-58.gif ali-59.gif ali-59.gif ali-60.gif ali-60.gif ali-61.gif ali-61.gif

    4. 1

      1 ali-40.gif ali-40.gif ali-41.gif ali-41.gif ali-42.gif ali-42.gif ali-43.gif ali-43.gif ali-44.gif ali-44.gif ali-45.gif ali-45.gif ali-46.gif ali-46.gif ali-47.gif ali-47.gif ali-48.gif ali-48.gif ali-49.gif ali-49.gif ali-50.gif ali-50.gif ali-51.gif ali-51.gif ali-52.gif ali-52.gif ali-53.gif ali-53.gif ali-54.gif ali-54.gif ali-55.gif ali-55.gif ali-56.gif ali-56.gif ali-57.gif ali-57.gif ali-58.gif ali-58.gif ali-59.gif ali-59.gif ali-60.gif ali-60.gif ali-61.gif ali-61.gif

    5. 1

      1 ali-40.gif ali-40.gif ali-41.gif ali-41.gif ali-42.gif ali-42.gif ali-43.gif ali-43.gif ali-44.gif ali-44.gif ali-45.gif ali-45.gif ali-46.gif ali-46.gif ali-47.gif ali-47.gif ali-48.gif ali-48.gif ali-49.gif ali-49.gif ali-50.gif ali-50.gif ali-51.gif ali-51.gif ali-52.gif ali-52.gif ali-53.gif ali-53.gif ali-54.gif ali-54.gif ali-55.gif ali-55.gif ali-56.gif ali-56.gif ali-57.gif ali-57.gif ali-58.gif ali-58.gif ali-59.gif ali-59.gif ali-60.gif ali-60.gif ali-61.gif ali-61.gif

    6. 1

      1 ali-40.gif ali-40.gif ali-41.gif ali-41.gif ali-42.gif ali-42.gif ali-43.gif ali-43.gif ali-44.gif ali-44.gif ali-45.gif ali-45.gif ali-46.gif ali-46.gif ali-47.gif ali-47.gif ali-48.gif ali-48.gif ali-49.gif ali-49.gif ali-50.gif ali-50.gif ali-51.gif ali-51.gif ali-52.gif ali-52.gif ali-53.gif ali-53.gif ali-54.gif ali-54.gif ali-55.gif ali-55.gif ali-56.gif ali-56.gif ali-57.gif ali-57.gif ali-58.gif ali-58.gif ali-59.gif ali-59.gif ali-60.gif ali-60.gif ali-61.gif ali-61.gif

    7. 1

      1

    8. 1

      1

    9. 1

      1

    10. 1

      1

    11. 1

      1

    12. 1

      1

    13. 1

      1

    14. 1

      1

    15. 1

      1

    16. 1

      1

    17. 1

      1

    18. 1

      1

    19. 1

      1

    20. 1

      1

    21. 1

      1

    22. 1

      1

    23. 1

      1

    24. 1

      1

    25. 1

      1

    26. 1

      1

    27. 1

      1

    28. 1

      1

    29. 1

      1

    30. 1

      1

    31. 1

      1

    添加新评论

    ali-40.gifali-41.gifali-42.gifali-43.gifali-44.gifali-45.gifali-46.gifali-47.gifali-48.gifali-49.gifali-50.gifali-51.gifali-52.gifali-53.gifali-54.gifali-55.gifali-56.gifali-57.gifali-58.gifali-59.gifali-60.gifali-61.gif