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阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345