three.js 筆記七 Matrix

一、行主序、列主序

概念參考行主序 列主序

以線性代數中描述的矩陣為標準,行主序就是依次按行存儲,而列主序就是依次按列存儲。在threeJS中:

var A = new THREE.Matrix4();
A.set(1, 2, 3, 4,
    5, 6, 7, 8,
    9, 10, 11, 12,
    13, 14, 15, 16);
console.log(A);

var B = new THREE.Matrix4();
B.set(16, 15, 14, 13,
    12, 11, 10, 9,
    8, 7, 6, 5,
    4, 3, 2, 1);
console.log(B);

var C = new THREE.Matrix4();
C.multiplyMatrices (A, B);    
console.log(C);

其運行結果為:

image.png

在網上找一個在線矩陣計算器,比如http://www.yunsuan.info/matrixcomputations/solvematrixmultiplication.html
相對應的計算結果如下:
image.png

因此可以認為,threejs矩陣內部儲存形式為列主序,表達和描述的仍然是線性代數中行主序,set()函數就是以行主序接受矩陣參數的。

二、如何根據變換設計自己的矩陣

概念性的東西,可以參考
線性代數筆記三 線性變換和矩陣乘法
圖形學筆記一 仿射變換和齊次坐標

1.向量或點的縮放平移等操作

這部分比較好處理,例子可以參考
three.js 之 Matrix

2.坐標系的轉化

馮樂樂講MVP的例子也很好,可以參考
UnityShader精要筆記二 數學基礎

核心思路就是以世界坐標為中轉,應用坐標的變換等價于基變換。

比如模型坐標系轉世界坐標系,就是模型空間任意一點計算出其在世界坐標系的位置。即模型每個點動了,整個模型也動了。

而世界坐標系轉觀察坐標系,則是先用觀察坐標系轉世界坐標系之后求逆,這樣快速運算。所以說,世界坐標系是用來中轉的,世界中心點不會動。

三、THREEJS封裝的矩陣API
1.平移
var vector = new THREE.Vector3(20, 20, 0);
var matrix = new THREE.Matrix4();
matrix.makeTranslation(10, 40, 0);
vector.applyMatrix4(matrix);
2.旋轉
matrix.makeRotationX(angle);
matrix.makeRotationY(angle);
matrix.makeRotationZ(angle);
matrix.makeRotationAxis(axis, angle);
matrix.makeRotationFromEuler(euler);
matrix.makeRotationFromQuaternion(quaternion);

前三個方法分別代表的是繞X、Y、Z三個軸旋轉,無需贅述。
第四個方法是前三個方法的整合版,第一個參數表示的是代表xyz的THREE.Vector3,第二個參數是旋轉的弧度。下面兩行代碼是等價的:

matrix.makeRotationX(Math.PI);
matrix.makeRotationAxis(new THREE.Vector3(1, 0, 0), Math.PI);

第五個方法表示圍繞x、y和z軸的旋轉,這是表示旋轉最常用的方式;第六個方法是一種基于軸和角度表示旋轉的替代方法。

關于旋轉,可以參考
Cocos 3.x 四元數 rotateAroundLocal
Three.js歐拉對象Euler和四元數Quaternion

構造函數:Euler(x,y,z,order)
參數xyz分別表示繞xyz軸旋轉的角度值,角度單位是弧度。參數order表示旋轉順序,默認值XYZ,也可以設置為YXZ、YZX等值

// 創建一個歐拉對象,表示繞著xyz軸分別旋轉45度,0度,90度
var Euler = new THREE.Euler( Math.PI/4,0, Math.PI/2);

四元數的方法.setFromAxisAngle(axis, angle)通過旋轉軸axis和旋轉角度angle設置四元數數據,也就是x、y、z和w四個分量。

var quaternion = new THREE.Quaternion();
// 旋轉軸new THREE.Vector3(0,1,0)
// 旋轉角度Math.PI/2
quaternion.setFromAxisAngle(new THREE.Vector3(0,1,0),Math.PI/2)
console.log('查看四元數結構',quaternion);

四元數乘法.multiply()
對象的一個旋轉可以用一個四元數表示,兩次連續旋轉可以理解為兩次旋轉對應的四元數對象進行乘法運算。

// 四元數q1、q2分別表示一個旋轉,兩個四元數進行乘法運算,相乘結果保存在q2中
// 在q1表示的旋轉基礎在進行q2表示的旋轉操作
q1.quaternion.multiply( q2 );

歐拉、四元數和矩陣轉化
歐拉對象、四元數對象和旋轉矩陣可以相關轉化,都可以表示旋轉變換。

//通過矩陣對象Matrix4的.makeRotationFromQuaternion(q)方法可以把四元數轉化對應的矩陣對象。
Matrix4.makeRotationFromQuaternion(q)

//通過歐拉對象設置四元數對象
quaternion.setFromEuler(Euler)

//四元數轉化為歐拉對象
Euler.setFromQuaternion(quaternion)

Object3D對象角度屬性.rotation的值是歐拉對象Euler,四元數屬性.quaternion的值是四元數對象Quaternion。

執行Object3D對象旋轉方法,會同時改變對象的角度屬性和四元數屬性。四元數屬性和位置.position、縮放屬性.scale一樣會轉化為對象的本地矩陣屬性.matrix,本地矩陣屬性值包含了旋轉矩陣、縮放矩陣、平移矩陣。

Object3D對象角度屬性.rotation和四元數屬性.quaternion是相互關聯的一個改變會同時改變另一個。

// 一個網格模型對象,基類是Object3D
var mesh = new THREE.Mesh()
// 繞z軸旋轉
mesh.rotateZ(Math.PI)

console.log('查看角度屬性rotation',mesh.rotation);
console.log('查看四元數屬性quaternion',mesh.quaternion);
3.compose

參考Three.js 克隆其他模型的矩陣 Matrix4

image.png

//使用make系列的方法操作
Object3D.applyMatrix(new THREE.Matrix4().makeScale(2,1,1));
Object3D.applyMatrix(new THREE.Matrix4().makeTranslation(0,4,0));
Object3D.applyMatrix(new THREE.Matrix4().makeRotationZ(Math.PI/6));
//使用compose方法操作
var matrix = new THREE.Matrix4();
var trans = new THREE.Vector3(0,4,0);
var rotat = new THREE.Quaternion().setFromEuler(new THREE.Euler(0,0,Math.PI/6));
var scale = new THREE.Vector3(2,1,1);
Object3D.applyMatrix4(matrix.compose(trans, rotat, scale)); //效果同上
image.png

就是compose的逆過程。隨便舉個例子。

var matrix = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
var trans = new THREE.Vector3();
var rotat = new THREE.Quaternion();
var scale = new THREE.Vector3();
matrix.decompose(trans, rotat, scale);

//返回Vector3 {x: 4, y: 5, z: 6} 因為是隨便寫的,所以只有平移變量不需計算就可以看出來的
console.log(trans); 

//返回Quaternion {_x: 0.05565363763555474, _y: -0.11863820054057297
//, _z: 0.051265314875937947, _w: 0.7955271896092125}
console.log(rotat); 

//返回Vector3 {x: 3.7416573867739413, y: 5.385164807134504, z: 7.0710678118654755}
console.log(scale); 

如何通過矩陣設置Object3D對象位置呢,參考108 THREE.JS 使用矩陣對3D對象進行位置設置

//最后先將模型移動到中心位置
var inverseM = new THREE.Matrix4();
inverseM.getInverse(centerM);
matrix.multiply(inverseM);

//將矩陣賦值給模型
cube.matrix = matrix;

//使用矩陣更新模型的信息
cube.matrix.decompose(cube.position, cube.quaternion, cube.scale);

還有個注意點,就是threejs的decompose是X軸方向的負縮放,有可能與其它3D庫的matrix.decompose不同(比如是Z軸方向的負縮放)


image.png

測試的matrix數據如下:
rawData
(16) [5.053215498074303e-16, -1, 6.123233995736772e-17, 0, -1, -5.053215498074303e-16, -1.2246467991473532e-16, 0, -1.2246467991473535e-16, 6.123233995736766e-17, 1, 0, 1282.7820737078252, 3307.4240662137445, 3.128811373451107e-13, 1]

企業微信截圖_17144707166307.png

企業微信截圖_17144707508493.png
4.相乘

之前用過的matrix.multiplyMatrices(matrixA, matrixB),表示 將矩陣設置為matrixA * matrixB的結果。

threejs矩陣還有前乘和后乘的區別,也很容易混淆。

在threeJS中矩陣的后乘方法為multiply():

var A = new THREE.Matrix4();
A.set(1, 2, 3, 4,
    5, 6, 7, 8,
    9, 10, 11, 12,
    13, 14, 15, 16);

var B = new THREE.Matrix4();
B.set(16, 15, 14, 13,
    12, 11, 10, 9,
    8, 7, 6, 5,
    4, 3, 2, 1);

A.multiply(B);
console.log(A);
console.log(B);

其運行結果為:


image.png

表明A.multiply(B)不會改變B的值,會改變A的值,相當于A=A*B,即后乘方法multiply()的結果就是把傳入的參數放自己后面去乘。
反過來,使用前乘方法A.premultiply(B);,結果就是B?A

5.逆矩陣
let m4 = new THREE.Matrix4();
m4.elements = [10, 8, 3, 0, 15, 7, 2, 0, 10, 6, 1, 0, 0, 0, 0, 1];
        
let m3 = new THREE.Matrix4();
//執行這行,會將m4的逆矩陣設置給m3
m3.getInverse(m4);
6.例子

image.png

參考Three.js中的矩陣,做出如圖的旋轉效果:

var box_geometry = new THREE.BoxGeometry();
var sphere_geometry = new THREE.SphereGeometry(0.5, 32, 32);
var cylinder_geometry = new THREE.CylinderGeometry(0.1, 0.1, 0.5);

var material = new THREE.MeshLambertMaterial({color: new THREE.Color(0.9, 0.55, 0.4)});

var box = new THREE.Mesh(box_geometry, material);
var sphere = new THREE.Mesh(sphere_geometry, material);
var cylinder = new THREE.Mesh(cylinder_geometry, material);

scene.add(box);
scene.add(sphere);
scene.add(cylinder);

box.matrixAutoUpdate = false;
sphere.matrixAutoUpdate = false;
cylinder.matrixAutoUpdate = false;

var sphere_matrix = new THREE.Matrix4().makeTranslation(0.0, 1.0, 0.0); 
sphere_matrix.multiply(new THREE.Matrix4().makeRotationZ(-Math.PI * 0.25));
sphere.applyMatrix(sphere_matrix);

var cylinder_matrix = sphere_matrix.clone(); 
cylinder_matrix.multiply(new THREE.Matrix4().makeTranslation(0.0, 0.75, 0.0)); 

cylinder.applyMatrix(cylinder_matrix);


注意這個例子中只給了部分代碼,由于使用的是MeshLambertMaterial,需要添加光照才能看到幾何體,當然也可以換其它material:

// 環境光
const ambientLight = new THREE.AmbientLight(0xffffff, 1); // 創建環境光
this.scene.add(ambientLight); // 將環境光添加到場景

如果因為threejs版本問題,applyMatrix報undefined,改成applyMatrix4即可。

也可以參考這個例子,確認一下multiply的用法,上面有提到結論:

表明A.multiply(B)不會改變B的值,會改變A的值,相當于A=A*B

var sphere_matrix = new THREE.Matrix4().makeTranslation(0.0, 1.0, 0.0); 
sphere.applyMatrix(sphere_matrix);
var cylinder_matrix = sphere_matrix.clone(); 
cylinder_matrix.multiply(new THREE.Matrix4().makeTranslation(0.0, 0.75, 0.0)); 
cylinder.applyMatrix(cylinder_matrix);

這段代碼和之前的功能是一樣的:

var box = new THREE.Mesh(box_geometry, material);
var sphere = new THREE.Mesh(sphere_geometry, material);
sphere.position.y += 1;
var cylinder = new THREE.Mesh(cylinder_geometry, material);
cylinder.position.y += 1.75;
scene.add(box);
scene.add(sphere);
scene.add(cylinder);

可以看出,cylinder_matrix使用multiply,在之前的平移矩陣上,再移動了0.75

但是,如果矩陣中還有其他值,使用乘法,都會改變:

        let ttt = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
        ttt.multiply(new THREE.Matrix4().makeTranslation(10,20,30));
        console.log("ttt:",ttt.elements);

        let ttt2 = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
        ttt2.premultiply(new THREE.Matrix4().makeTranslation(10,20,30));
        console.log("ttt2:",ttt2.elements);

結果:

ttt: (16) [1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6, 144, 205, 266, 327]
ttt2: (16) [41, 82, 123, 4, 52, 103, 154, 5, 63, 124, 185, 6, 74, 145, 216, 7]
spher: (16) [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1] cylinder: (16) [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1.75, 0, 1]
7.Object3D.matrix和matrixWorld

仍然以UnityShader精要筆記二 數學基礎中MVP的例子:

就是有個奶牛叫妞妞,她有自己的坐標空間即模型空間,在這個空間里,她的鼻子坐標是(0,2,4),最后如何顯示在屏幕上呢?首先,轉化為齊次坐標(0,2,4,1)。頂點變換的第一步就是將頂點坐標從模型空間變換到世界空間,這個變換通常叫做模型變換(model transform)。根據Transform的信息,妞妞進行了(2,2,2)的縮放,(0,150,0)的旋轉以及(5,0,25)的平移。根據之前的知識,要先縮放再旋轉再平移:

image.png

我們使用代碼來驗證一下:

var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
var cube = new THREE.Mesh(geometry, material);
cube.scale.set(2, 2, 2);
cube.rotateY(150 * Math.PI / 180);
cube.position.set(5, 0, 25);
var vec = new THREE.Vector4(0, 2, 4, 1);
vec.applyMatrix4(cube.matrix);
console.log("vec:", vec);

現在打印出來的是0,2,4,1 這是因為matrix并沒有立即生效,可以手動調用cube.updateMatrix(),關于更新的問題后面再說,現在先換一個打印方式:

function animate() {
    requestAnimationFrame(animate);
    // cube.rotation.x += 0.01;
    // cube.rotation.y += 0.01;
    renderer.render(scene, camera);
    var vec = new THREE.Vector4(0, 2, 4, 1);
    vec.applyMatrix4(cube.matrix);
    console.log("vec:", vec);
}
//animate();

打印結果與例子中計算結果一致:vec: Vector4 {x: 9, y: 4, z: 18.07179676972449, w: 1}

這也說明,cube.matrix是由模型坐標系轉向其父容器世界坐標系的。現在繼續做測試,把cube再添加一個父容器:

var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
var cube = new THREE.Mesh(geometry, material);
cube.scale.set(2, 2, 2);
cube.rotateY(150 * Math.PI / 180);
cube.position.set(5, 0, 25);
var vec = new THREE.Vector4(0, 2, 4, 1);
vec.applyMatrix4(cube.matrix);

var cubeParent = new THREE.Object3D();
cubeParent.position.set(3, 0, 0);
cubeParent.add(cube);
scene.add(cubeParent);

然后打印的地方,把cube.matrix和cube.matrixWorld都打印:

function animate() {
    requestAnimationFrame(animate);
    // cube.rotation.x += 0.01;
    // cube.rotation.y += 0.01;
    renderer.render(scene, camera);
    var vec = new THREE.Vector4(0, 2, 4, 1);
    vec.applyMatrix4(cube.matrix);
    console.log("vec:", vec);
    console.log("matrix:", cube.matrix);
    console.log("matrixWorld:", cube.matrixWorld);
}
image.png

image.png

顯然,能看出cube.matrixWorld是把嵌套的父容器也考慮進去,一步到位,直接轉到世界坐標系。

cube.modelViewMatrix
表示對象相對于相機坐標系的變換。也就是matrixWorld左乘相機的matrixWorldInverse。
但是,我打印一下,發現這個值不對:

image.png

那沒辦法,我們自己用矩陣乘法計算:

camera.rotateX(30 * Math.PI / 180);
camera.position.set(0, 10, -10);

function animate() {
    requestAnimationFrame(animate);
    // cube.rotation.x += 0.01;
    // cube.rotation.y += 0.01;
    renderer.render(scene, camera);
    var vec = new THREE.Vector4(0, 2, 4, 1);
    vec.applyMatrix4(cube.matrix);
    console.log("vec:", vec);
    console.log("matrix:", cube.matrix);
    console.log("matrixWorld:", cube.matrixWorld);

    var vec2 = new THREE.Vector4(0, 2, 4, 1);
    let m = cube.matrixWorld.clone();
    m.premultiply(camera.matrixWorldInverse);
    vec2.applyMatrix4(m);
    console.log("vec2:", vec2);

回到我們的農場游戲。現在我們需要把妞妞的鼻子從世界空間變換到觀察空間中。為此我們需要知道世界坐標系下攝像機的變換信息。這同樣可以通過攝像機面板中的Transform組件得到:(1,1,1)的縮放,(30,0,0)的旋轉,(0,10,-10)的平移。

image.png

image.png

可以看到結果與例子中的Z值是相反的,這是因為unity用的左手坐標系,而threejs是右手坐標系。

注,這里用的是premultiply,可以參考之前的結論:

表明A.multiply(B)不會改變B的值,會改變A的值,相當于A=A*B

8.camera相關的matrix

攝像機Cameras 有兩個額外的四維矩陣:

  • Camera.matrixWorldInverse: 視圖矩陣 - 攝像機世界坐標變換的逆矩陣。
  • Camera.projectionMatrix: 投影矩陣 - 表示將場景中的信息投影到裁剪空間。
9.makeScale會清理其它數據
let ttt = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
ttt.makeScale(2,2,2);
console.log("ttt:",ttt.elements);

let ttt2 = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
ttt2.makeTranslation(1,2,3);
console.log("ttt2:",ttt2.elements);

let ttt3 = new THREE.Matrix4().set(1,2,3,4,2,3,4,5,3,4,5,6,4,5,6,7);
ttt3.compose(new THREE.Vector3(2,2,2),new THREE.Quaternion(),new THREE.Vector3(1,2,3));
console.log("ttt3:",ttt3.elements);

結果如下:

ttt: (16) [2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1]

ttt2: (16) [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 2, 3, 1]

ttt3: (16) [1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, 2, 2, 2, 1]

也就是說,如果不想清理之前的數據,可以使用compose進行一次性轉換。

10.更多的API

參考
three.js 數學方法之Matrix4

四、THREEJS來更新對象的變換

參考
three.js 之 Matrix
學習ThreeJS 04 更新機制

1.更改對象的位置,四元數,和伸縮屬性,three.js 會根據這些屬性重新計算對象的矩陣:
object.position.copy(start_position);
object.quaternion.copy(quaternion);

默認情況下,matrixAutoUpdate 屬性是設置為 true 的,矩陣會自動重新計算(如果它們已添加到場景中,或者是已添加到場景中的另一個對象的子節點)。

var object1 = new THREE.Object3D();
var object2 = new THREE.Object3D();
object1.add( object2 );

//object1 和 object2 會自動更新它們的矩陣
scene.add( object1 ); 

如果對象是靜態的,或者你希望自己手動控制什么時候重新計算,可以通過將屬性設置為 false 來獲取更好的性能。

object.matrixAutoUpdate = false

同時在改變任何屬性之后,手動更新矩陣:

object.updateMatrix();
2.直接修改對象的矩陣
object.matrix.setRotationFromQuaternion(quaternion);
object.matrix.setPosition(start_position);
object.matrixAutoUpdate = false;

注意在這種情況下 matrixAutoUpdate 必須設置成 false。并且你要確定不要調用 updateMatrix 方法。調用 updateMatrix 會阻斷對矩陣的手動更改,會根據位置、伸縮等屬性重新計算矩陣。

3.matrixWorldNeedsUpdate

參考https://sogrey.top/Three.js-start/cores/#Object3D
matrixWorldNeedsUpdate : Boolean
當這個屬性設置了之后,它將計算在那一幀中的matrixWorld,并將這個值重置為false。默認值為false。

五、窗口 resize事件更新

參考Three.js自適應窗口變化渲染

  • 發生場景:當窗口大小發生變化時,會出現局部空白區域。
  • 解決方法:重新獲取瀏覽器窗口新的寬高尺寸,然后通過新的寬高尺寸更新相機Camera和渲染器WebGLRenderer的參數。
  • 要注意一下,Three.js自適應渲染不一定就是窗口變化,本質上還是你要渲染的區域寬高尺寸變化了;更進一步變化是視圖矩陣.matrixWorldInverse和投影矩陣.projectionMatrix的變化。
// onresize 事件會在窗口被調整大小時發生
window.οnresize=function(){
  // 重置渲染器輸出畫布canvas尺寸
  renderer.setSize(window.innerWidth,window.innerHeight);
  // 全屏情況下:設置觀察范圍長寬比aspect為窗口寬高比
  camera.aspect = window.innerWidth/window.innerHeight;
  // 渲染器執行render方法的時候會讀取相機對象的投影矩陣屬性projectionMatrix
  // 但是不會每渲染一幀,就通過相機的屬性計算投影矩陣(節約計算資源)
  // 如果相機的一些屬性發生了變化,需要執行updateProjectionMatrix ()方法更新相機的投影矩陣
  camera.updateProjectionMatrix ();
};

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容