Unity Shader:深度和法線紋理

本文同時發布在我的個人博客上:https://dragon_boy.gitee.io

獲取深度和法線紋理

原理

深度紋理實際是一張渲染紋理,存儲的是深度值,范圍是[0,1],通常是非線性分布。

在頂點空間變換時,變換到裁剪空間的坐標為NDC,即范圍在[-1,1],所以需要映射一下到[0,1]。

在Unity中,會使用著色器替換技術選擇那些渲染類型為Opaque的物體,判斷它們使用的渲染隊列是否小于等于2500,如果滿足條件就把它渲染到深度和法線紋理中。

在Unity中,我們可以選擇讓一個攝像機生成一張深度紋理或是一張深度+法線紋理。選擇前者,Unity會直接獲取深度緩沖或上述的著色器替換技術,選擇需要的不透明物體,并對使用它投射陰影時使用的Pass來得到深度紋理。如果選擇后者,Unity會創建一張和屏幕分辨率相同、精度為32為的紋理,其中觀察空間下的發現信息在RG通道,深度信息在BA通道。法線信息在延遲測試中可以非常容易得到,Unity只需合并深度和發現緩存。在前向渲染中,默認情況下不會創建法線緩存,因此Unity底層使用一個單獨的Pass把整個場景再次渲染一遍來完成。

如何獲取

獲取深度紋理很簡單,只需在腳本中設置深度紋理模式,然后可以在Shader中使用_CameraDepthTexture訪問深度紋理:

camera.depthTextureMode = DepthTextureMode.Depth;

深度法線紋理同理:

camera.depthTextureMode = DepthTextureMode.DepthNormals;

在Shader中使用_CameraDepthNormalsTexture訪問。

Unity中提供了一個宏定義SAMPLE_DEPTH_TEXTURE來對深度紋理采樣,主要是為了處理平臺差異。

當通過紋理采樣獲得深度值后,這些深度值往往是非線性的,這種非線性來自于透視投影使用的裁剪矩陣。實際計算中我們需要線性的深度值,所以我們需要將深度值變換到線性空間下,例如視角空間下的深度值,推導過程如下:

當我們使用透視投影的裁剪矩陣對視角空間下的頂點變換后,裁剪空間下的頂點的z和w分量是:

z_{clip} = -z_{view}\frac{Far+Near}{Far-Near} - \frac{2\cdot Near \cdot Far}{Far- Near}

w_{clip} = -z_{view}

通過透視除法,就可以得到NDC下的z分量:

z_{ndc} = \frac{z_clip}{w_clip} = \frac{Far + Near}{Far - Near} + \frac{2\cdot Near \cdot Far}{(Far- Near)\cdot z_{view}}

深度紋理中的深度值是通過上面的NDC分量計算得到的:

d = 0.5 \cdot z_{ndc} + 0.5

根據上述推導的z_{view}表達式:

z_{view} = \frac{1}{\frac{Far - Near}{Near\cdot Far}d - \frac{1}{Near}}

由于Unity中視圖空間正對的z值為負數,將上式取反:

z'_{view} = \frac{1}{\frac{Near - Far}{Near\cdot Far}d + \frac{1}{Near}}

上式取值范圍是[Near,Far],為得到[0,1]之間的深度值,將上式結果除以Far,結果如下:

z_{01} = \frac{1}{\frac{Near - Far}{Near}d + \frac{Far}{Near}}

針對上述的推導過程,Unity提供了兩個函數。LinearEyeDepth將深度紋理的采樣結果轉換到視角空間下的深度值,即z'_{view}Linear01Depth將返回范圍在[0,1]的線性深度值,即z_{01}。這兩個函數使用Unity內置的_ZBuffferParams變量來得到遠近裁剪平面的距離。

我們可以使用tex2D直接對深度法線紋理采樣,然后使用Unity的DecodeDepthNormal函數來對采樣結果解碼。

運動模糊

這里我們使用速度映射圖來模擬運動模糊。我們利用深度紋理在片元著色器中為每個像素計算其在世界空間下的位置,這是通過使用當前視角投影矩陣的逆矩陣對NDC下的頂點坐標進行變換得到的。接著我們使用前一幀的視角投影矩陣對其進行變換,得到該位置在前一幀中的NDC坐標。然后,我們計算前一幀和當前幀的位置差,生成該像素的速度。

下面編寫腳本:

using UnityEngine;
using System.Collections;

public class MotionBlurWithDepthTexture : PostEffectsBase
{

    public Shader motionBlurShader;
    private Material motionBlurMaterial = null;

    public Material material
    {
        get
        {
            motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
            return motionBlurMaterial;
        }
    }

    private Camera myCamera;
    public Camera camera
    {
        get
        {
            if (myCamera == null)
            {
                myCamera = GetComponent<Camera>();
            }
            return myCamera;
        }
    }

    [Range(0.0f, 1.0f)]
    public float blurSize = 0.5f;

    private Matrix4x4 previousViewProjectionMatrix;

    void OnEnable()
    {
        camera.depthTextureMode |= DepthTextureMode.Depth;

        previousViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
    }

    void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (material != null)
        {
            material.SetFloat("_BlurSize", blurSize);

            material.SetMatrix("_PreviousViewProjectionMatrix", previousViewProjectionMatrix);
            Matrix4x4 currentViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
            Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse;
            material.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix);
            previousViewProjectionMatrix = currentViewProjectionMatrix;

            Graphics.Blit(src, dest, material);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }
}

Shader代碼如下:

Shader "Unlit/MotionBlurWIthDepthTexture"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _BlurSize ("Blur Size", Float) = 1.0
    }
        SubShader{
            CGINCLUDE

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            half4 _MainTex_TexelSize;
            sampler2D _CameraDepthTexture;
            float4x4 _CurrentViewProjectionInverseMatrix;
            float4x4 _PreviousViewProjectionMatrix;
            half _BlurSize;

            struct v2f {
                float4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
                half2 uv_depth : TEXCOORD1;
            };

            v2f vert(appdata_img v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                o.uv = v.texcoord;
                o.uv_depth = v.texcoord;

                #if UNITY_UV_STARTS_AT_TOP
                if (_MainTex_TexelSize.y < 0)
                    o.uv_depth.y = 1 - o.uv_depth.y;
                #endif

                return o;
            }

            fixed4 frag(v2f i) : SV_Target {
                // Get the depth buffer value at this pixel.
                float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
                // H is the viewport position at this pixel in the range -1 to 1.
                float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1);
                // Transform by the view-projection inverse.
                float4 D = mul(_CurrentViewProjectionInverseMatrix, H);
                // Divide by w to get the world position. 
                float4 worldPos = D / D.w;

                // Current viewport position 
                float4 currentPos = H;
                // Use the world position, and transform by the previous view-projection matrix.  
                float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos);
                // Convert to nonhomogeneous points [-1,1] by dividing by w.
                previousPos /= previousPos.w;

                // Use this frame's position and last frame's to compute the pixel velocity.
                float2 velocity = (currentPos.xy - previousPos.xy) / 2.0f;

                float2 uv = i.uv;
                float4 c = tex2D(_MainTex, uv);
                uv += velocity * _BlurSize;
                for (int it = 1; it < 3; it++, uv += velocity * _BlurSize) {
                    float4 currentColor = tex2D(_MainTex, uv);
                    c += currentColor;
                }
                c /= 3;

                return fixed4(c.rgb, 1.0);
            }

            ENDCG

        Pass {
            ZTest Always Cull Off ZWrite Off

            CGPROGRAM

            #pragma vertex vert  
            #pragma fragment frag  

            ENDCG
        }
    }
}

當得到像素速度后,我們就根據這個速度來對它的鄰域像素進行采樣,接著平均。

全局霧效

這里介紹一種快速從深度紋理重建世界坐標的方法。這種方法首先對圖像空間下的視錐體射線(從攝像機出發,指向圖像上的某點的射線)進行插值,這條射線存儲了該像素在世界空間下到攝像機的方向信息。然后,我們把該射線和線性化后的視角空間下的深度值相乘,再加上攝像機的世界位置,就可以得到該像素再世界空間下的位置。

重建世界坐標

重建世界坐標的代碼如下:

float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;

其中,_WorldSpaceCameraPos是攝像機在世界空間下的位置,這可以右Unity的內置變量直接訪問得到。而linearDepth * interpolatedRay則可以計算得到該像素相對于攝像機的偏移量,linearDepth是由深度紋理得到的線性深度值,interpolatedRay是由頂點著色器輸出并插值后得到的射線,它不僅包含該像素到攝像機的方向,也包含了距離信息。

interpolatedRay來源于對近裁剪平面的4個角的某個特定向量的插值,這4個向量包含了它們到攝像機的方向和距離信息。下面進行推導:

首先計算兩個向量,toTop,toRight,它們是起點位于近裁剪平面中心、分別指向攝像機正上方和正右方的向量,計算公式如下:

halfHeight = Near\times tan(\frac{FOV}{2})

toTop = camera.up \times halfHeight

toRight = camera.right \times halfHeight \cdot aspect

得到這兩個矢量后,就可以計算近裁剪平面的四個角相對于攝像機的方向。以左上角TL為例:

TL = camera.forward\cdot Near + toTop - toRight

同理,其它三個角:

TR = camera.forward\cdot Near + toTop + toRight

BL = camera.forward\cdot Near - toTop - toRight

BR = camera.forward\cdot Near - toTop + toRight

上面求得的四個向量不僅包含方向信息,它們的模對應了4個點到攝像機的空間距離。由于我們得到的線性深度值并非是顛倒攝像機的歐式距離,而是z方向的距離,因此,不能直接使用深度值和4個角的單位方向的乘積來計算它們到攝像機的偏移量。下面進行線性深度值到歐式距離的轉化。

TL所在的射線上,像素的深度值和它到攝像機的實際距離的比等于近裁剪平面的距離和TL向量的模的比,即:

\frac{depth}{dist} = \frac{Near}{|TL|}

那么TL點距離攝像機的歐式距離dist:

dist = \frac{|TL|}{Near}\times depth

由于其它三個向量的模和TL相等,那么我們可以提取一個縮放因子:

scale = \frac{|TL|}{|Near|}

我們可以使用這個縮放因子和單位向量相乘來得到對應的向量值,如:

Ray_{TL} = \frac{TL}{|TL|}\times scale

屏幕后處理的原理就是使用特定的材質去渲染一個剛好填充整個屏幕的四邊形面片。這個四邊形面片的4個頂點對應了近裁剪平面的4個角。我們將上述的計算結果傳遞給頂點著色器,頂點著色器根據當前的位置選擇他所對應的相應向量,然后將其輸出,經插值后傳遞給片元著色器得到interpolatedRay

霧的計算

在簡單的霧效實現中,我們需要計算一個霧效系數f,作為混合原始顏色和霧的顏色的混合系數:

float3 afterFog = f * fogColor + (1 - f) * origColor;

f的計算方法很多,Unity內置的霧效實現支持三種:線性、指數和指數的平方。當給定距離z后,f的計算公式如下:

  • Linear:
    f = \frac{d_{max} - |z|}{d_{max} - d_{min}}d_{min}d_{max}分別是受霧影響的最小距離和最大距離。
  • Exponential:
    f = e^{-d\cdot |z|}d是控制霧的濃度的參數。
  • Exponential Squared:
    f = e^{-(d-|z|)^2}d是控制霧的濃度的參數。

在這里使用類似線性霧的計算方式,計算基于高度的霧效,具體方法是,當給定一點在世界空間下的高度y后,f的計算公式為:
f = \frac{H_{end} - y}{H_{end} - H_{start}}H_{strat}H_{end}分別表示受霧影響的起始高度和終止高度。

首先實現腳本:

using UnityEngine;
using System.Collections;

public class FogWithDepthTexture : PostEffectsBase
{

    public Shader fogShader;
    private Material fogMaterial = null;

    public Material material
    {
        get
        {
            fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial);
            return fogMaterial;
        }
    }

    private Camera myCamera;
    public Camera camera
    {
        get
        {
            if (myCamera == null)
            {
                myCamera = GetComponent<Camera>();
            }
            return myCamera;
        }
    }

    private Transform myCameraTransform;
    public Transform cameraTransform
    {
        get
        {
            if (myCameraTransform == null)
            {
                myCameraTransform = camera.transform;
            }

            return myCameraTransform;
        }
    }

    [Range(0.0f, 3.0f)]
    public float fogDensity = 1.0f;

    public Color fogColor = Color.white;

    public float fogStart = 0.0f;
    public float fogEnd = 2.0f;

    void OnEnable()
    {
        camera.depthTextureMode |= DepthTextureMode.Depth;
    }

    void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (material != null)
        {
            Matrix4x4 frustumCorners = Matrix4x4.identity;

            float fov = camera.fieldOfView;
            float near = camera.nearClipPlane;
            float aspect = camera.aspect;

            float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
            Vector3 toRight = cameraTransform.right * halfHeight * aspect;
            Vector3 toTop = cameraTransform.up * halfHeight;

            Vector3 topLeft = cameraTransform.forward * near + toTop - toRight;
            float scale = topLeft.magnitude / near;

            topLeft.Normalize();
            topLeft *= scale;

            Vector3 topRight = cameraTransform.forward * near + toRight + toTop;
            topRight.Normalize();
            topRight *= scale;

            Vector3 bottomLeft = cameraTransform.forward * near - toTop - toRight;
            bottomLeft.Normalize();
            bottomLeft *= scale;

            Vector3 bottomRight = cameraTransform.forward * near + toRight - toTop;
            bottomRight.Normalize();
            bottomRight *= scale;

            frustumCorners.SetRow(0, bottomLeft);
            frustumCorners.SetRow(1, bottomRight);
            frustumCorners.SetRow(2, topRight);
            frustumCorners.SetRow(3, topLeft);

            material.SetMatrix("_FrustumCornersRay", frustumCorners);

            material.SetFloat("_FogDensity", fogDensity);
            material.SetColor("_FogColor", fogColor);
            material.SetFloat("_FogStart", fogStart);
            material.SetFloat("_FogEnd", fogEnd);

            Graphics.Blit(src, dest, material);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }
}

按照之前的理論計算四個射線向量,然后按照以左下角為原點,逆時針按行構建矩陣。這個順序非常重要,因為這決定了我們在頂點著色器中使用哪一行作為該點的待插值向量。

Shader代碼如下:

Shader "Unlit/Fog"
{
    Properties{
         _MainTex("Base (RGB)", 2D) = "white" {}
         _FogDensity("Fog Density", Float) = 1.0
         _FogColor("Fog Color", Color) = (1, 1, 1, 1)
         _FogStart("Fog Start", Float) = 0.0
         _FogEnd("Fog End", Float) = 1.0
    }
        SubShader{
            CGINCLUDE

            #include "UnityCG.cginc"

            float4x4 _FrustumCornersRay;

            sampler2D _MainTex;
            half4 _MainTex_TexelSize;
            sampler2D _CameraDepthTexture;
            half _FogDensity;
            fixed4 _FogColor;
            float _FogStart;
            float _FogEnd;

            struct v2f {
                float4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
                half2 uv_depth : TEXCOORD1;
                float4 interpolatedRay : TEXCOORD2;
            };

            v2f vert(appdata_img v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                o.uv = v.texcoord;
                o.uv_depth = v.texcoord;

                #if UNITY_UV_STARTS_AT_TOP
                if (_MainTex_TexelSize.y < 0)
                    o.uv_depth.y = 1 - o.uv_depth.y;
                #endif

                int index = 0;
                if (v.texcoord.x < 0.5 && v.texcoord.y < 0.5) {
                    index = 0;
                }
                else if (v.texcoord.x > 0.5 && v.texcoord.y < 0.5) {
                     index = 1;
                }
                else if (v.texcoord.x > 0.5 && v.texcoord.y > 0.5) {
                 index = 2;
                }
                else {
                 index = 3;
                }

                #if UNITY_UV_STARTS_AT_TOP
                if (_MainTex_TexelSize.y < 0)
                    index = 3 - index;
                #endif

                o.interpolatedRay = _FrustumCornersRay[index];

                return o;
                }

                fixed4 frag(v2f i) : SV_Target {
                    float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
                    float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;

                    float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
                    fogDensity = saturate(fogDensity * _FogDensity);

                    fixed4 finalColor = tex2D(_MainTex, i.uv);
                    finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);

                    return finalColor;
                }

                ENDCG

                Pass {
                    ZTest Always Cull Off ZWrite Off

                    CGPROGRAM

                    #pragma vertex vert  
                    #pragma fragment frag  

                    ENDCG
                }
         }
}

邊緣檢測

這里使用深度和法線紋理進行邊緣檢測。

腳本實現如下:

using UnityEngine;
using System.Collections;

public class EdgeDetectNormalsAndDepth : PostEffectsBase
{

    public Shader edgeDetectShader;
    private Material edgeDetectMaterial = null;
    public Material material
    {
        get
        {
            edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
            return edgeDetectMaterial;
        }
    }

    [Range(0.0f, 1.0f)]
    public float edgesOnly = 0.0f;

    public Color edgeColor = Color.black;

    public Color backgroundColor = Color.white;

    public float sampleDistance = 1.0f;

    public float sensitivityDepth = 1.0f;

    public float sensitivityNormals = 1.0f;

    void OnEnable()
    {
        GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
    }

    [ImageEffectOpaque]
    void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (material != null)
        {
            material.SetFloat("_EdgeOnly", edgesOnly);
            material.SetColor("_EdgeColor", edgeColor);
            material.SetColor("_BackgroundColor", backgroundColor);
            material.SetFloat("_SampleDistance", sampleDistance);
            material.SetVector("_Sensitivity", new Vector4(sensitivityNormals, sensitivityDepth, 0.0f, 0.0f));

            Graphics.Blit(src, dest, material);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }
}

注意我們為OnRenderImage函數添加了[ImageEffectOpaque]屬性,不對透明物體產生影響。

這里使用Roberts算子進行邊緣檢測,Shader代碼如下:

Shader "Unlit/EdgeDetect"
{
    Properties{
        _MainTex("Base (RGB)", 2D) = "white" {}
        _EdgeOnly("Edge Only", Float) = 1.0
        _EdgeColor("Edge Color", Color) = (0, 0, 0, 1)
        _BackgroundColor("Background Color", Color) = (1, 1, 1, 1)
        _SampleDistance("Sample Distance", Float) = 1.0
        _Sensitivity("Sensitivity", Vector) = (1, 1, 1, 1)
    }
        SubShader{
            CGINCLUDE

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            half4 _MainTex_TexelSize;
            fixed _EdgeOnly;
            fixed4 _EdgeColor;
            fixed4 _BackgroundColor;
            float _SampleDistance;
            half4 _Sensitivity;

            sampler2D _CameraDepthNormalsTexture;

            struct v2f {
                float4 pos : SV_POSITION;
                half2 uv[5]: TEXCOORD0;
            };

            v2f vert(appdata_img v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);

                half2 uv = v.texcoord;
                o.uv[0] = uv;

                #if UNITY_UV_STARTS_AT_TOP
                if (_MainTex_TexelSize.y < 0)
                    uv.y = 1 - uv.y;
                #endif

                o.uv[1] = uv + _MainTex_TexelSize.xy * half2(1,1) * _SampleDistance;
                o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1,-1) * _SampleDistance;
                o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1,1) * _SampleDistance;
                o.uv[4] = uv + _MainTex_TexelSize.xy * half2(1,-1) * _SampleDistance;

                return o;
            }

            half CheckSame(half4 center, half4 sample) {
                half2 centerNormal = center.xy;
                float centerDepth = DecodeFloatRG(center.zw);
                half2 sampleNormal = sample.xy;
                float sampleDepth = DecodeFloatRG(sample.zw);

                // difference in normals
                // do not bother decoding normals - there's no need here
                half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
                int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
                // difference in depth
                float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
                // scale the required threshold by the distance
                int isSameDepth = diffDepth < 0.1 * centerDepth;

                // return:
                // 1 - if normals and depth are similar enough
                // 0 - otherwise
                return isSameNormal * isSameDepth ? 1.0 : 0.0;
            }

            fixed4 fragRobertsCrossDepthAndNormal(v2f i) : SV_Target {
                half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv[1]);
                half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv[2]);
                half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv[3]);
                half4 sample4 = tex2D(_CameraDepthNormalsTexture, i.uv[4]);

                half edge = 1.0;

                edge *= CheckSame(sample1, sample2);
                edge *= CheckSame(sample3, sample4);

                fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]), edge);
                fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);

                return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
            }

            ENDCG

            Pass {
                ZTest Always Cull Off ZWrite Off

                CGPROGRAM

                #pragma vertex vert  
                #pragma fragment fragRobertsCrossDepthAndNormal

                ENDCG
            }
        }
}

我們調用CheckSame來計算算子的對角線上的兩個紋理值的插值,返回0表明存在邊界。

CheckSame函數中,我們首先得到兩個采樣點法線和深度值,我們計算兩個采樣點的法線和深度值的插值,并稱一對應的敏感系數,將差異值得每個分量相加再與閾值比較,如果小于閾值則表明不存在邊界,反之存在邊界。最后將法線和深度得檢查結果相乘,作為組合值返回。

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