Android ARCore demo 简析

1. 简介

ARCore 是 google 官方出的一款 AR SDK,其基本原理为:

ARCore 使用手机摄像头来辨识特征点,并跟踪这些特征点的移动轨迹。结合特征点的移动轨迹和手机的惯性传感器,ARCore 就可以在手机移动时判定它的位置、角度等信息。识别出特征点,就能在特征点的基础上,侦测平面,如地板、桌子等。另外能 ARCore 也支持估测周围的平均光照强度。有了手机自身的位置角度信息和周围的光照强度信息,ARCore 就可以构建周边世界的模型。

  1. 运动跟踪

    它利用 IMU 传感器和设备的相机来发现空间的特征点,由此确定 Android 设备的位置和方向。此外,使用 VPS,可以让 AR 物体每次看起来似乎都在同一位置。

  2. 环境感知

    虚拟物体一般都是放置于平坦平面上的,用 ARCore 可以检测物体的水平表面,建立环境认知感,以保证虚拟的对象可以准确放置,然后让您看到放置在这些表面上的 AR 物体。

  3. 光线预测

    ARCore 根据环境的光强度,使开发人员可以与周围环境相匹配的方式点亮虚拟对象。此外,最近的一个实验发现,虚拟阴影在真实环境光照下的调整功能也是如此,这样就可以使 AR 物体的外观更为逼真。

取自 ARCore ——移动AR的浪潮

2. Android Studio 工程配置

  1. 安装 Android Studio 2.3 及以上,使用 Android SDK Platform 版本 7.0
  2. 一台支持的 Android 设备 (暂时仅支持 Google Pixel 和 Samsung Galaxy S8 的2款设备)
  3. 获取 ARCore SDK

鉴于支持 ARCore 的 Android 设备太少,为此可通过修改 ARCore 的设备支持接口,修改方式如下:

  1. Open a command line interface
  2. Unzip the AAR to a temporary directory: unzip arcore_client-original.aar -d aar-tmp
  3. Enter the temporary aar directory: cd aar-tmp
  4. Unzip classes.jar to a temporary directory: unzip classes.jar -d classes-tmp
  5. Enter the temporary classes directory: cd classes-tmp
  6. Enter the directory containing the SupportedDevices class: cd com/google/atap/tangoservice
  7. Decompile the SupportedDevices class: java -jar /path/to/cfr.jar SupportedDevices.class > SupportedDevices.java
  8. Open a text editor and delete return false from the end of isSupported()
  9. Compile the modified SupportedDevice class: javac -cp /path/to/sdk/platform/android.jar -source 1.7 -target 1.7 SupportedDevices.java
  10. Delete the Java source: rm SupportedDevices.java
  11. Change directory back to aar-tmp: cd ../../../../../
  12. Create a JAR from the modified classes directory: jar cvf classes.jar -C classes-tmp .
  13. Change directory back to repo root: cd ..
  14. Create an AAR from the modified aar directory: jar cvf arcore_client.aar -C aar-tmp .

取自 arcore-for-all

强行修改 ARCore 的支持函数判断后,各机型的支持程度如下:

Manufacturer Device Model GPU 64-bit? Official Support? Functional?
Google Pixel All Adreno 530
Google Pixel XL All Adreno 530
Samsung Galaxy S6 G920 Mali-T760MP8 × ××
Samsung Galaxy S7 G930F Mali-T880 MP12 × ×
Samsung Galaxy S7 Edge G9350 (Hong Kong) Adreno 530 × ?
Samsung Galaxy S7 Edge G935FD, G935F, G935W8 Mali-T880 MP12 × ×
Samsung Galaxy S8 USA & China Adreno 540
Samsung Galaxy S8 EMEA Mali-G71 MP20 ?
Samsung Galaxy S8+ USA & China Adreno 540 ×
Samsung Galaxy S8+ G955F (EMEA) Mali-G71 MP20 ×
HTC HTC 10 All Adreno 530 × ×
Huawei Nexus 6P All Adreno 430 ×
Huawei P9 Lite All Mali-T830MP2 × ×
Huawei P10 All Mali-G71 MP8 × ×
LG G2 All Adreno 330 × × ×
LG V20 US996 Adreno 530 × ×
LG Nexus 5 All Adreno 330 × ×
LG Nexus 5X All Adreno 418 × ×
OnePlus 3 All Adreno 530 × ×
OnePlus 3T All Adreno 530 × ×
OnePlus X All Adreno 330 × × ×
OnePlus 5 All Adreno 540 ×
Nvidia Shield K1 All ULP GeForce Kepler × ×
Xiaomi Redmi Note 4 All Adreno 506 × ×
Xiaomi Mi 5s capricorn Adreno 530 × ×
Xiaomi Mi Mix All Adreno 530 × ×
Motorola Moto G4 All Adreno 405 × ×
Motorola Nexus 6 All Adreno 420 × × ×
ZTE Axon 7 A2017 Adreno 530 × ×
Sony Xperia XZs All Adreno 530 × ×

取自 arcore-for-all Device Research

实际使用 Nexus 6P 运行 arcore-for-all sample.apk 效果并不如意,平面监测效果较差,较难形成平面

3. demo 简析

3.0 demo 效果

gif

3.1 配置工程

  1. 配置 sdk 版本信息

    compileSdkVersion 25
    buildToolsVersion "25.0.0"
    
    defaultConfig {
        applicationId "com.google.ar.core.examples.java.helloar"
        minSdkVersion 19
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
    }
    

    其中配置的 minSdkVersion 最小为 19

  2. 引入需要的第三方库

    dependencies {
        compile (name: 'arcore_client', ext: 'aar')
        compile (name: 'obj-0.2.1', ext: 'jar')
        ...
    }
    

    其中 obj-0.2.1.jar 包用于加载解析 obj 文件为模型数据

3.2 显示 Activity 布局和准备对象

  1. 布局

    <android.opengl.GLSurfaceView
        android:id="@+id/surfaceview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="top"/>
    
  2. 渲染封装对象

    • ObjectRenderer mVirtualObject

      google 机器人模型

    • ObjectRenderer mVirtualObjectShadow

      google 机器人阴影模型

    • PointCloudRenderer mPointCloud

      平面监测特征点

    • PlaneRenderer mPlaneRenderer

      平面识别成功之后的网格模型

    • BackgroundRenderer mBackgroundRenderer

      相机视频流数据显示至纹理

3.3 onCreate 初始化

setContentView(R.layout.activity_main);
mSurfaceView = (GLSurfaceView) findViewById(R.id.surfaceview);

// 1. 
mSession = new Session(/*context=*/this);

// 2. 
// Create default config, check is supported, create session from that config.
mDefaultConfig = Config.createDefaultConfig();
if (!mSession.isSupported(mDefaultConfig)) {
    Toast.makeText(this, "This device does not support AR", Toast.LENGTH_LONG).show();
    finish();
    return;
}

// 3. 
// 创建并设置 SurfaceView Tap 事件
...

// 4.
// Set up renderer.
mSurfaceView.setPreserveEGLContextOnPause(true);
mSurfaceView.setEGLContextClientVersion(2);
mSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); // Alpha used for plane blending.
mSurfaceView.setRenderer(this);
mSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
  1. 创建 Session 对象

    Session 用于处理 ARCore 状态,处理当前 AR 的生命周期(resume,pause),绑定背景相机图像纹理,设置视口显示大小,从视图中获取 frame 数据(可得到估计光照强度、投影矩阵、模型变换矩阵、图像特征点数据和模型矩阵、监测平面数据等)

  2. 创建 mDefaultConfig 并判断当前机型是否支持 ARCore

    暂时支持的机型,见 Supported Devices

  3. 创建并设置 SurfaceView Tap 事件

    记录用户在 Surface Tap 的位置信息,用于创建 Google 机器人

  4. SurfaceView 相关设置

    设置在 Pause 时保留 GL 上下文环境,设置 EGL 版本为 2.0,设置各个通道的大小,设置渲染监听实现,设置为主动渲染

3.4 处理生命周期

  1. onResume

    @Override
    protected void onResume() {
        super.onResume();
    
        if (CameraPermissionHelper.hasCameraPermission(this)) {
            showLoadingMessage();
            // Note that order matters - see the note in onPause(), the reverse applies here.
            mSession.resume(mDefaultConfig);
            mSurfaceView.onResume();
        } else {
            CameraPermissionHelper.requestCameraPermission(this);
        }
    }
    

    在页面 onResume 时调用 mSession.resume(mDefaultConfig)

  2. onPause

    @Override
    public void onPause() {
        super.onPause();
        mSurfaceView.onPause();
        mSession.pause();
    }
    

    在页面 onPause 时调用 mSession.pause(),停止页面查询 Session

    注意:mSession.pause() 必须在 mSurfaceView.onPause() 后面执行,否则 可能发生在 mSession.pause() 之后继续调用 mSession.update(),进而发生 SessionPausedException 异常

3.5 SurfaceView 显示准备

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    // 1.
    GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);

    // 2.
    mBackgroundRenderer.createOnGlThread(/*context=*/this);
    mSession.setCameraTextureName(mBackgroundRenderer.getTextureId());

    try {
        // 3.
        mVirtualObject.createOnGlThread(/*context=*/this, "andy.obj", "andy.png");
        mVirtualObject.setMaterialProperties(0.0f, 3.5f, 1.0f, 6.0f);

        // 4.
        mVirtualObjectShadow.createOnGlThread(/*context=*/this,
                "andy_shadow.obj", "andy_shadow.png");
        mVirtualObjectShadow.setBlendMode(BlendMode.Shadow);
        mVirtualObjectShadow.setMaterialProperties(1.0f, 0.0f, 0.0f, 1.0f);
    } catch (IOException e) {
        Log.e(TAG, "Failed to read obj file");
    }
    try {
        // 5.
        mPlaneRenderer.createOnGlThread(/*context=*/this, "trigrid.png");
    } catch (IOException e) {
        Log.e(TAG, "Failed to read plane texture");
    }
    
    // 6.
    mPointCloud.createOnGlThread(/*context=*/this);
}
  1. 设置 opengl 帧缓存清空颜色为灰白色
  2. 初始化背景纹理对象(后续详细介绍),并将创建的纹理 id 绑定给 mSession 的相机纹理对象,用于显示相机产生的视频内容
  3. 初始化 Android 机器人显示对象,并设置材料反射光照(环境光无, 漫反射光 3.5, 镜面光 1.0, 光照聚焦 6.0)
  4. 初始化 Android 机器人阴影显示对象,设置显示混合模式(开启透明度,GLES20.GL_ONE_MINUS_SRC_ALPHA),设置显示材料反射光照信息(1.0f, 0.0f, 0.0f, 1.0f)
  5. 初始化平面监测结果显示对象
  6. 初始化特征点云显示对象

3.6 SurfaceView 大小改变

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    // 1. 设置 opengl 的视口大小
    GLES20.glViewport(0, 0, width, height);
    // 2. 设置 mSession 中的显示视口大小(和后续计算 frame 有关)
    mSession.setDisplayGeometry(width, height);
}

3.7 SurfaceView 绘制

@Override
public void onDrawFrame(GL10 gl) {
    // 1.
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

    try {
        // 2.
        Frame frame = mSession.update();

        // 3. 
        MotionEvent tap = mQueuedSingleTaps.poll();
        if (tap != null && frame.getTrackingState() == TrackingState.TRACKING) {
            for (HitResult hit : frame.hitTest(tap)) {
                // Check if any plane was hit, and if it was hit inside the plane polygon.
                if (hit instanceof PlaneHitResult && ((PlaneHitResult) hit).isHitInPolygon()) {
                    // Cap the number of objects created. This avoids overloading both the
                    // rendering system and ARCore.
                    if (mTouches.size() >= 16) {
                        mSession.removeAnchors(Arrays.asList(mTouches.get(0).getAnchor()));
                        mTouches.remove(0);
                    }
                    // Adding an Anchor tells ARCore that it should track this position in
                    // space. This anchor will be used in PlaneAttachment to place the 3d model
                    // in the correct position relative both to the world and to the plane.
                    mTouches.add(new PlaneAttachment(
                            ((PlaneHitResult) hit).getPlane(),
                            mSession.addAnchor(hit.getHitPose())));

                    // Hits are sorted by depth. Consider only closest hit on a plane.
                    break;
                }
            }
        }

        // 4.
        mBackgroundRenderer.draw(frame);

        // 5.
        if (frame.getTrackingState() == TrackingState.NOT_TRACKING) {
            return;
        }

        // 6.
        float[] projmtx = new float[16];
        mSession.getProjectionMatrix(projmtx, 0, 0.1f, 100.0f);

        // 7.
        float[] viewmtx = new float[16];
        frame.getViewMatrix(viewmtx, 0);

        // 8.
        // Compute lighting from average intensity of the image.
        final float lightIntensity = frame.getLightEstimate().getPixelIntensity();

        // 9.
        // Visualize tracked points.
        mPointCloud.update(frame.getPointCloud());
        mPointCloud.draw(frame.getPointCloudPose(), viewmtx, projmtx);

        // 10. 
        // Check if we detected at least one plane. If so, hide the loading message.
        ...

        // 11. Visualize planes.
        mPlaneRenderer.drawPlanes(mSession.getAllPlanes(), frame.getPose(), projmtx);

        // 12. Visualize anchors created by touch.
        float scaleFactor = 1.0f;
        for (PlaneAttachment planeAttachment : mTouches) {
            if (!planeAttachment.isTracking()) {
                continue;
            }
            // Get the current combined pose of an Anchor and Plane in world space. The Anchor
            // and Plane poses are updated during calls to session.update() as ARCore refines
            // its estimate of the world.
            planeAttachment.getPose().toMatrix(mAnchorMatrix, 0);

            // Update and draw the model and its shadow.
            mVirtualObject.updateModelMatrix(mAnchorMatrix, scaleFactor);
            mVirtualObjectShadow.updateModelMatrix(mAnchorMatrix, scaleFactor);
            mVirtualObject.draw(viewmtx, projmtx, lightIntensity);
            mVirtualObjectShadow.draw(viewmtx, projmtx, lightIntensity);
        }

    } catch (Throwable t) {
        // Avoid crashing the application due to unhandled exceptions.
        Log.e(TAG, "Exception on the OpenGL thread", t);
    }
}
  1. 清除颜色和深度缓冲区
  2. 从 mSession 得到最新的 frame
  3. 遍历前面 tap 操作得到的点列表(可以理解以此为起点,垂直向下的一条射线),计算射线是否和 frame 中的平面有交点,且交点是否处理平面监测得到的区域里面。若是,则保存平面信息和 pose 信息(可得到模型的位置和转向信息)
  4. 绘制相机背景视图
  5. 判断是否已经检测到平面,没有的话,不在显示后续内容
  6. 从 frame 中得到投影矩阵
  7. 从 frame 中得到模型变换矩阵
  8. 得到预计的环境光照强度
  9. 更新检测的特征点云的坐标并绘制
  10. 取消显示正在检测中的 toast(和 AR 无关)
  11. 绘制检测到的平面(显示为网格状)
  12. 遍历第 3 步记录的 PlaneAttachment 列表,得到投影矩阵和模型变换矩阵,并结合第 8 步得到的光照强度,绘制 Android 机器人和阴影

3.8 显示相机捕捉的视频内容

OpenGL 相关,和 AR 无关

3.8.1 初始化背景纹理对象
public void createOnGlThread(Context context) {
    // 1.
    int textures[] = new int[1];
    GLES20.glGenTextures(1, textures, 0);
    mTextureId = textures[0];
    GLES20.glBindTexture(mTextureTarget, mTextureId);
    GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
    GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
    GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
    GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);

    int numVertices = 4;
    if (numVertices != QUAD_COORDS.length / COORDS_PER_VERTEX) {
      throw new RuntimeException("Unexpected number of vertices in BackgroundRenderer.");
    }

    // 2.
    ByteBuffer bbVertices = ByteBuffer.allocateDirect(QUAD_COORDS.length * FLOAT_SIZE);
    bbVertices.order(ByteOrder.nativeOrder());
    mQuadVertices = bbVertices.asFloatBuffer();
    mQuadVertices.put(QUAD_COORDS);
    mQuadVertices.position(0);

    // 3.
    ByteBuffer bbTexCoords = ByteBuffer.allocateDirect(
            numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE);
    bbTexCoords.order(ByteOrder.nativeOrder());
    mQuadTexCoord = bbTexCoords.asFloatBuffer();
    mQuadTexCoord.put(QUAD_TEXCOORDS);
    mQuadTexCoord.position(0);

    // 4.
    ByteBuffer bbTexCoordsTransformed = ByteBuffer.allocateDirect(
        numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE);
    bbTexCoordsTransformed.order(ByteOrder.nativeOrder());
    mQuadTexCoordTransformed = bbTexCoordsTransformed.asFloatBuffer();

    // 5.
    int vertexShader = ShaderUtil.loadGLShader(TAG, context,
            GLES20.GL_VERTEX_SHADER, R.raw.screenquad_vertex);
    // 6.
    int fragmentShader = ShaderUtil.loadGLShader(TAG, context,
            GLES20.GL_FRAGMENT_SHADER, R.raw.screenquad_fragment_oes);

    // 7.
    mQuadProgram = GLES20.glCreateProgram();
    GLES20.glAttachShader(mQuadProgram, vertexShader);
    GLES20.glAttachShader(mQuadProgram, fragmentShader);
    GLES20.glLinkProgram(mQuadProgram);
    GLES20.glUseProgram(mQuadProgram);

    // 8.
    ShaderUtil.checkGLError(TAG, "Program creation");

    // 9.
    mQuadPositionParam = GLES20.glGetAttribLocation(mQuadProgram, "a_Position");
    mQuadTexCoordParam = GLES20.glGetAttribLocation(mQuadProgram, "a_TexCoord");

    ShaderUtil.checkGLError(TAG, "Program parameters");
}
  1. 创建纹理对象id,并绑定到 GL_TEXTURE_EXTERNAL_OES,设置纹理贴图的效果和缩放效果

    绑定的纹理不是 GL_TEXTURE_2D,而是 GL_TEXTURE_EXTERNAL_OES,是因为 Camera 使用的输出 texture 是一种特殊的格式。同样的,在 shader 中也必须使用 SamperExternalOES 的变量类型来访问该纹理

    #extension GL_OES_EGL_image_external : require
    
    precision mediump float;
    varying vec2 v_TexCoord;
    uniform samplerExternalOES sTexture;
    
    void main() {
        gl_FragColor = texture2D(sTexture, v_TexCoord);
    }
    

    片元显示器

  2. 设置纹理几何的顶点坐标

  3. 设置纹理的初始贴图坐标

  4. 设置纹理最终的贴图坐标,可从 Frame 中计算得到

  5. 加载顶点显示器

    attribute vec4 a_Position;
    attribute vec2 a_TexCoord;
    
    varying vec2 v_TexCoord;
    
    void main() {
       gl_Position = a_Position;
       v_TexCoord = a_TexCoord;
    }
    
  6. 加载片元显示器

  7. opengl 程序连接编译

  8. 检查 opengl 错误

  9. 初始化背景矩形的顶点坐标对象,和纹理坐标对象

3.8.2 绘制相机捕获的视频帧至纹理

3.7 SurfaceView 绘制 的第 4 步调用了 mBackgroundRenderer.draw(frame) 其内容如下:

public class BackgroundRenderer {

    ...

    public void draw(Frame frame) {
        // 1.
        if (frame.isDisplayRotationChanged()) {
            frame.transformDisplayUvCoords(mQuadTexCoord, mQuadTexCoordTransformed);
        }

        // 2.
        GLES20.glDisable(GLES20.GL_DEPTH_TEST);
        GLES20.glDepthMask(false);

        // 3.
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId);

        // 4.
        GLES20.glUseProgram(mQuadProgram);

        // 5.
        GLES20.glVertexAttribPointer(
            mQuadPositionParam, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, mQuadVertices);
        GLES20.glVertexAttribPointer(mQuadTexCoordParam, TEXCOORDS_PER_VERTEX,
                GLES20.GL_FLOAT, false, 0, mQuadTexCoordTransformed);
        GLES20.glEnableVertexAttribArray(mQuadPositionParam);
        GLES20.glEnableVertexAttribArray(mQuadTexCoordParam);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
        GLES20.glDisableVertexAttribArray(mQuadPositionParam);
        GLES20.glDisableVertexAttribArray(mQuadTexCoordParam);

        // 6.
        GLES20.glDepthMask(true);
        GLES20.glEnable(GLES20.GL_DEPTH_TEST);

        // 7.
        ShaderUtil.checkGLError(TAG, "Draw");
    }
}
  1. 当显示角度发生变化或者 SurfaceView 大小发生变化,重新计算背景的纹理 uv 坐标

    是否发生变化,如何计算,均有 frame 接口提供

  2. 关闭深度测试和深度缓冲区的可读性为不可读

    相机视频帧数据是在所有模型的后面

  3. 绑定 mTextureId 至 GLES11Ext.GL_TEXTURE_EXTERNAL_OES

  4. 设置使用绘制背景的 shader 程序

  5. 将背景四边形顶点数据和纹理贴图的 uv 数据设置给绑定的 shader 变量,设置数据在着色器端可见,绘制矩形内容,关闭数据在着色器端可见

  6. 重新打开深度测试和深度缓冲区的可读性

  7. 检查错误

其他内容显示类似,不再赘述

4. 总结

由上其实可以看到场景的显示等,其实并不是 ARCore 关心的,ARCore 提供给我们的数据或帮我们完成的功能有:

  1. 判断当前机型是否支持 ARCore
  2. 提供接口判断当前角度是否发生变化,转换相机帧纹理的 uv 坐标
  3. 提供接口提取可用的 frame,得到投影矩阵和模型变换矩阵,得到特征点云,监测出来的平面,用于后续业务开发显示场景
  4. 提供接口获取估计光照强度
  5. 提供相机帧中的特征点和检测得到的平面数据
  6. 提供接口计算点击操作和场景是否相交,和交点信息(包括交点垂直平面的位置和方向)
  7. 暂时未见如果识别特定图片,显示模型的 demo

相比其他第三方库,如 EasyAR 收费版本,支持估计周围环境光照强度,slam 算法更加稳定强大,效果更好;相比 Vuforia 支持平面监测,但当前可支持机型还比较少。而非支持机型,通过改 ARR 代码强制支持,效果也不太理想,如 Nexus 6P。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • http://blog.csdn.net/wangdingqiaoit/article/details/51457...
    jerryhigh阅读 5,156评论 0 8
  • 在Android系统中,有一种特殊的视图,称为SurfaceView,它拥有独立的绘图表面,即它不与其宿主窗口共享...
    一个不掉头发的开发阅读 11,080评论 12 74
  • 题目没别的意思 只是正好听这首歌而已 晚秋的天已经热不起来 寒意也越来越重。入夜 , 我点了一根烟 , 披了大衣出...
    巫小楼阅读 156评论 1 2
  • 我刚才坐在马桶上面,百无聊赖,开始打开手机看新闻。看到一个消息就是阿里巴巴上市居然用1%的承销费找了六大投行来IP...
    生如如花阅读 158评论 0 0