[OpenGL] Shadow Map 阴影
图:随着键盘控制点光源位置移动,阴影发生实时的变换
之前在 https://blog.csdn.net/ZJU_fish1996/article/details/51932954 一文中已经介绍了shadow map的基本原理,至今为止,它依旧是在游戏开发中运用较(最?)广的一种阴影技术。本文是自己花了周末放假两天的时间,做的一个单点光源版的低精度硬阴影纹理,且有些地方比较偷懒,比如第一次pass仅记录了需要生成阴影物体的深度(即图中的cube),而第二次pass中绘制阴影时仅对阴影投射的物体(即图中的平面) 进行深度比较,某种程度上避开了shadow map存在的浮点精度误差的缺陷。
基本原理
关于阴影,从理解上最直观的定义是灯光照不到的位置,以这一定义为基础,我们可以这样判断某一点是否落在阴影下:
在以灯光为中心的观察坐标系中,该点和灯光之间进行连线,如果线段中有其它遮挡物,那么该点在阴影中;如果没有遮挡物,那么该点被光照射。这和做z-buffer运算计算遮挡关系非常类似,从这个角度来看,如果该点的深度大于深度模板中记录的深度,那么该点在阴影中;如果该点的深度等于模板中的深度,那么该点被光照射。
最终,我们需要至少绘制两次:第一次把物体转换到灯光视图空间下,获取并写入深度到纹理。第二次将物体转换到相机视图空间下,并同时记录单个物体转换到灯光视图空间下的深度信息,用该深度信息与深度纹理进行比较,判断像素点是否在阴影中,如果在,按照正常的变换计算出颜色后,给颜色乘以一个阴影系数。
实现细节
(1) 深度计算最终取的值是投影空间的z值,该z值需要在片元着色器中除以远裁剪面保证归一化。不同的光源对应的投影矩阵有所差异,点光源对应透视矩阵,而平行光源对应正交矩阵。远近裁剪面差值较小时纹理精度会比较大,但是这将不利于表现大场景,且数值较大的裁剪面取得的深度能够更接近线性。也就是说,远近裁剪面的选取对算法的效果有一定影响。
(2) 在写入阴影深度纹理时,同样的,我们依然简单使用了整数帧缓冲区,对浮点深度进行编码存在rgba分量中,在使用的时候再进行解码。精度越高的纹理效果表现越好,锯齿效果越不明显,但也会耗费一定性能。
(3) 我们在片元着色器进行深度的比较。深度的比较需要在同一个坐标系下进行,才能保证比较的正确性,在这里,我们把物体都转换到灯光视角空间下进行对比。一个要点是,在第二次绘制,对某个像素点进行深度比较时,我们需要在阴影贴图中找到它对应的像素点。经过透视变换,顶点被转换到齐次裁剪空间坐标,此时x,y分布在[-1,1]之间,而纹理uv坐标的取值范围为[0,1],我们经过x' = 0.5 * x + 0.5的运算可将前者映射到后者范围中,再根据该值作为纹理索引去取对应位置的深度即可。
(4) 对于不在阴影中的物体,两者深度值是一样的,此时进行浮点数相等比较可能存在误差,会导致画面产生波纹,进行比较的时候需要尽可能排除这一影响,如两个深度差值到达了一个临界值才认为它们是不相等的。
代码部分
vShader0.glsl
记录物体在灯光视角空间下的深度。
uniform mat4 ProjectMatrix;
uniform mat4 LightMatrix;
uniform mat4 ModelMatrix;attribute vec4 a_position;varying float v_depth;void main()
{gl_Position = ModelMatrix * a_position;gl_Position = LightMatrix * gl_Position;gl_Position = ProjectMatrix * gl_Position;v_depth = gl_Position.z;
}
fShader0.glsl
归一化深度并编码(未做线性处理),存在256位RGBA通道的颜色缓冲区中。
补充:此处是2019年1月20日补充的发现的一处不规范的地方,这是我做的第一版demo,所以会有很多不完善的地方。这里深度取的是透视转换之后的z分量,再除以远裁剪面。正确的归一化操作实际上应该为:float fColor = position.z / position.w,也就是分母是透视空间齐次坐标的w分量,它的几何含义也就是到投影面的距离(此处取值范围为-1到1,所以还需要转到0,1);但是,如果灯光视锥体没有完全包裹住相机视锥体,此处的归一化计算会导致未被包裹的位置进行比较的时候,无法从shadowmap中读取到对应的结果。
在本次初版demo中,虽然计算是不准确的,但是结果不会受到影响,因为这里相当于把深度编码到(0,1)之间,再到比较的时候解码到原始大小的步骤,也就是一个编码解码的过程。取远裁剪面会导致精度受损,但是由于编解码规则是统一的,最终重建的结果也是可行的。
但是,这是不规范的,对于实际使用而言,应该求出合适灯光视锥体,使其恰好包含场景包围盒和相机视锥体相交构成的凸包,然后使用pos.z/pos.w来计算。此外,此处由于灯光视角使用的是透视投影,最好能把深度做线性处理(因为透视投影后z是非线性的)。正确的线性处理是视图空间z值除以远裁剪面。具体内容会在shadow map改进中给出。
varying float v_depth;
uniform float zFar;void main()
{float fColor = v_depth / zFar;float fR, fB, fG, fA;fColor = modf(fColor * 256, fR);fColor = modf(fColor * 256, fG);fColor = modf(fColor * 256, fB);fColor = modf(fColor * 256, fA);gl_FragColor = vec4(fR/256,fG/256,fB/256,fA/256);
}
vShader1.glsl
将物体变换到视点空间下,并且同时记录它在灯光视图空间下的位置信息。(该例子中,主要针对的是两个平面)
uniform mat4 ModelMatrix;
uniform mat4 ViewMatrix;
uniform mat4 ProjectMatrix;
uniform mat4 LightMatrix;attribute vec4 a_position;
varying vec4 lightPos;attribute vec3 a_normal;
varying vec3 v_normal;void main()
{v_normal = a_normal;gl_Position = ProjectMatrix * (ViewMatrix * (ModelMatrix * a_position));lightPos = ProjectMatrix * (LightMatrix * (ModelMatrix * a_position));
}
fShader1.glsl
解码纹理里深度,获取当前像素点的深度,以及对应纹理的深度,进行比较并判断是否在纹理中。对当前像素进行简单光照计算,最后乘以阴影系数。
uniform sampler2D ShadowMap;
uniform vec3 lightLocation;
varying vec4 lightPos;
varying vec3 v_normal;
uniform mat4 IT_ModelMatrix;
uniform float zFar;
varying vec3 worldPos;float GetShadow()
{float fShadow = 1.0;float fDistance = lightPos.z / zFar;vec2 uv = lightPos.xy / lightPos.w * 0.5 + vec2(0.5, 0.5);vec4 fFactor = vec4(1,65536.0/16777216.0,256.0/16777216.0,1.0/16777216.0);vec4 distance = texture2D(ShadowMap, uv);float fDistanceMap = dot(distance, fFactor);if(fDistance - 0.009 > fDistanceMap){fShadow = 0.4;}return fShadow;
}void main()
{float fShadow = GetShadow();vec3 ambient = vec3(0.3,0.3,0.3);vec3 worldLightLocation = normalize(lightLocation);vec3 worldNormal = normalize(mat3(IT_ModelMatrix) * v_normal);vec3 diffuseColor = vec3(0.4,0.4,0.4);vec3 diffuse = diffuseColor * clamp(dot(worldNormal,worldLightLocation), 0, 1);vec4 color = vec4(ambient + diffuse, 1);gl_FragColor = color * fShadow;
}
vShader1.glsl
为了偷懒添加的,对图中立方体做最简单的变换,不考虑阴影和光照,仅单独贴了一个纹理。
uniform mat4 ModelMatrix;
uniform mat4 ViewMatrix;
uniform mat4 ProjectMatrix;attribute vec2 a_texcoord;
attribute vec4 a_position;
varying vec2 v_texcoord;void main()
{gl_Position = ProjectMatrix * (ViewMatrix * (ModelMatrix * a_position));v_texcoord = a_texcoord;
}
fShader1.glsl
同上,仅应用于图中立方体。
uniform sampler2D texture;
varying vec2 v_texcoord;
void main()
{gl_FragColor = texture2D(texture, v_texcoord);
}
mainwidget.h
#ifndef MAINWIDGET_H
#define MAINWIDGET_H#include "geometryengine.h"#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QMatrix4x4>
#include <QVector2D>
#include <QOpenGLShaderProgram>
#include <QOpenGLTexture>
#include <QOpenGLShader>
#include <QBasicTimer>class GeometryEngine;class MainWidget : public QOpenGLWidget, protected QOpenGLFunctions
{Q_OBJECTpublic:explicit MainWidget(QWidget *parent = nullptr);~MainWidget() override;protected:void keyPressEvent(QKeyEvent* event) override;void initializeGL() override;void resizeGL(int w, int h) override;void paintGL() override;void mousePressEvent(QMouseEvent *e) override;void mouseReleaseEvent(QMouseEvent *e) override;void timerEvent(QTimerEvent *e) override;private:QQuaternion rotation;QBasicTimer timer;QVector2D mousePressPosition;QVector3D rotationAxis;qreal angularSpeed;GLuint shadowMap;GLuint fBO;float zFar = 100.0f;int screenX = 640;int screenY = 480;QMatrix4x4 lightMatrix;QMatrix4x4 viewMatrix;QMatrix4x4 projection;QVector3D lightPos = QVector3D(10, 20, 4);QVector3D eyeLocation = QVector3D(0, 0, 20);QVector3D lookAtLocation = QVector3D(0, 0, 0);GeometryEngine *geometries;QOpenGLTexture *texture;QOpenGLShaderProgram program0;QOpenGLShaderProgram program;QOpenGLShaderProgram program1;void CalculateViewMatrix();void CalculateLightMatrix();};#endif // MAINWIDGET_H
mainwidget.cpp
#include "mainwidget.h"
#include <QMouseEvent>
#include <math.h>MainWidget::MainWidget(QWidget *parent) :QOpenGLWidget(parent),angularSpeed(0),geometries(nullptr)
{}MainWidget::~MainWidget()
{makeCurrent();delete geometries;doneCurrent();
}void MainWidget::keyPressEvent(QKeyEvent* event)
{const float step = 0.3f;if(event->key() == Qt::Key_W){lightPos.setZ(lightPos.z() - step);CalculateLightMatrix();update();}else if(event->key() == Qt::Key_S){lightPos.setZ(lightPos.z() + step);CalculateLightMatrix();update();}else if(event->key() == Qt::Key_A){lightPos.setX(lightPos.x() - step);CalculateLightMatrix();update();}else if(event->key() == Qt::Key_D){lightPos.setX(lightPos.x() + step);CalculateLightMatrix();update();}else if(event->key() == Qt::Key_Q){lightPos.setY(lightPos.y() + step);CalculateLightMatrix();update();}else if(event->key() == Qt::Key_E){lightPos.setY(lightPos.y() - step);CalculateLightMatrix();update();}
}void MainWidget::initializeGL()
{initializeOpenGLFunctions();CalculateViewMatrix();CalculateLightMatrix();// 清屏颜色glClearColor(0, 0, 0, 0);// 开启剔除glEnable(GL_CULL_FACE);glEnable(GL_DEPTH_TEST);// add shader 0QOpenGLShader* vShader0 = new QOpenGLShader(QOpenGLShader::Vertex);QOpenGLShader* fShader0 = new QOpenGLShader(QOpenGLShader::Fragment);vShader0->compileSourceFile(":/vShader0.glsl");fShader0->compileSourceFile(":/fShader0.glsl");program0.addShader(vShader0);program0.addShader(fShader0);program0.link();// add shader 1QOpenGLShader* vShader = new QOpenGLShader(QOpenGLShader::Vertex);QOpenGLShader* fShader = new QOpenGLShader(QOpenGLShader::Fragment);vShader->compileSourceFile(":/vShader.glsl");fShader->compileSourceFile(":/fShader.glsl");program.addShader(vShader);program.addShader(fShader);program.link();// add shader 2QOpenGLShader* vShader1 = new QOpenGLShader(QOpenGLShader::Vertex);QOpenGLShader* fShader1 = new QOpenGLShader(QOpenGLShader::Fragment);vShader1->compileSourceFile(":/vShader1.glsl");fShader1->compileSourceFile(":/fShader1.glsl");program1.addShader(vShader1);program1.addShader(fShader1);program1.link();geometries = new GeometryEngine;// 加载立方体的纹理texture = new QOpenGLTexture(QImage(":/cube.png").mirrored());texture->setMinificationFilter(QOpenGLTexture::Nearest);texture->setMagnificationFilter(QOpenGLTexture::Linear);texture->setWrapMode(QOpenGLTexture::Repeat);// 创建一个帧缓冲对象glGenFramebuffers(1, &fBO);glBindFramebuffer(GL_FRAMEBUFFER, fBO);// 生成纹理图像,附加到帧缓冲glGenTextures(1, &shadowMap);glBindTexture(GL_TEXTURE_2D, shadowMap);glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, screenX, screenY, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);glBindTexture(GL_TEXTURE_2D, 0);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, shadowMap, 0);timer.start(12, this);
}// 计算view矩阵
void MainWidget::CalculateViewMatrix()
{QVector3D upDir(0, 1, 0);QVector3D N = eyeLocation - lookAtLocation; // 这里是和OpenGL的z轴方向保持一致QVector3D U = QVector3D::crossProduct(upDir, N);QVector3D V = QVector3D::crossProduct(N, U);N.normalize();U.normalize();V.normalize();viewMatrix.setRow(0, {U.x(), U.y(), U.z(), -QVector3D::dotProduct(U, eyeLocation)}); // xviewMatrix.setRow(1, {V.x(), V.y(), V.z(), -QVector3D::dotProduct(V, eyeLocation)}); // yviewMatrix.setRow(2, {N.x(), N.y(), N.z(), -QVector3D::dotProduct(N, eyeLocation)}); // zviewMatrix.setRow(3, {0, 0, 0, 1});
}void MainWidget::mousePressEvent(QMouseEvent *e)
{// Save mouse press positionmousePressPosition = QVector2D(e->localPos());
}void MainWidget::mouseReleaseEvent(QMouseEvent *e)
{// Mouse release position - mouse press positionQVector2D diff = QVector2D(e->localPos()) - mousePressPosition;// Rotation axis is perpendicular to the mouse position difference// vectorQVector3D n = QVector3D(diff.y(), diff.x(), 0.0).normalized();// Accelerate angular speed relative to the length of the mouse sweepqreal acc = diff.length() / 100.0;// Calculate new rotation axis as weighted sumrotationAxis = (rotationAxis * angularSpeed + n * acc).normalized();// Increase angular speedangularSpeed += acc;
}void MainWidget::timerEvent(QTimerEvent *)
{// Decrease angular speed (friction)angularSpeed *= 0.99;// Stop rotation when speed goes below thresholdif (angularSpeed < 0.01) {angularSpeed = 0.0;} else {// Update rotationrotation = QQuaternion::fromAxisAndAngle(rotationAxis, angularSpeed) * rotation;// Request an updateupdate();}
}void MainWidget::CalculateLightMatrix()
{QVector3D lookAtLocation = QVector3D(0, 0, 0);QVector3D upDir(0, 1, 0);QVector3D N = lightPos - lookAtLocation;QVector3D U = QVector3D::crossProduct(upDir, N);QVector3D V = QVector3D::crossProduct(N, U);N.normalize();U.normalize();V.normalize();lightMatrix.setRow(0, {U.x(), U.y(), U.z(), -QVector3D::dotProduct(U, lightPos)}); // xlightMatrix.setRow(1, {V.x(), V.y(), V.z(), -QVector3D::dotProduct(V, lightPos)}); // ylightMatrix.setRow(2, {N.x(), N.y(), N.z(), -QVector3D::dotProduct(N, lightPos)}); // zlightMatrix.setRow(3, {0, 0, 0, 1});}void MainWidget::resizeGL(int w, int h)
{screenX = w;screenY = h;float aspect = float(w) / float(h ? h : 1);const qreal zNear = 2.0, fov = 60.0;projection.setToIdentity();projection.perspective(fov, aspect, zNear, zFar);
}void MainWidget::paintGL()
{QMatrix4x4 plane1ModelMatrix;plane1ModelMatrix.translate(0, -5, 5);plane1ModelMatrix.scale(8.0f, 1.0f, 5.0f);QMatrix4x4 plane2ModelMatrix;plane2ModelMatrix.rotate(90, QVector3D(1,0,0));plane2ModelMatrix.scale(8.0f, 1.0f, 5.0f);QMatrix4x4 cubeModelMatrix;cubeModelMatrix.translate(0,-3,4);cubeModelMatrix.rotate(rotation);cubeModelMatrix.scale(2,2,2);glBindFramebuffer(GL_FRAMEBUFFER, fBO);glClearColor(1,1,1,1);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);program0.bind();program0.setUniformValue("LightMatrix", lightMatrix);program0.setUniformValue("ProjectMatrix", projection);program0.setUniformValue("zFar", zFar);program0.setUniformValue("ModelMatrix", cubeModelMatrix);geometries->drawCubeGeometry(&program0);glBindFramebuffer(GL_FRAMEBUFFER, 0);glClearColor(0,0,0,1);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);program.bind();glBindTexture(GL_TEXTURE_2D, shadowMap);program.setUniformValue("ShadowMap",0);program.setUniformValue("LightMatrix", lightMatrix);program.setUniformValue("ProjectMatrix", projection);program.setUniformValue("ViewMatrix", viewMatrix);program.setUniformValue("lightLocation",lightPos);program.setUniformValue("zFar", zFar);QMatrix4x4 IT_Matrix;IT_Matrix = plane1ModelMatrix.inverted();IT_Matrix = IT_Matrix.transposed();program.setUniformValue("IT_ModelMatrix", IT_Matrix);program.setUniformValue("ModelMatrix", plane1ModelMatrix);geometries->drawPlane(&program);QMatrix4x4 IT_Matrix2;IT_Matrix2 = plane2ModelMatrix.inverted();IT_Matrix2 = IT_Matrix2.transposed();program.setUniformValue("IT_ModelMatrix", IT_Matrix2);program.setUniformValue("ModelMatrix", plane2ModelMatrix);geometries->drawPlane(&program);program1.bind();texture->bind();program1.setUniformValue("ProjectMatrix", projection);program1.setUniformValue("ViewMatrix", viewMatrix);program1.setUniformValue("ModelMatrix", cubeModelMatrix);program1.setUniformValue("texture",0);geometries->drawCubeGeometry(&program1);
}
geometryengine.h
#ifndef GEOMETRYENGINE_H
#define GEOMETRYENGINE_H#include <QOpenGLFunctions>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>class GeometryEngine : protected QOpenGLFunctions
{
public:GeometryEngine();virtual ~GeometryEngine();void drawCubeGeometry(QOpenGLShaderProgram *program);void drawPlane(QOpenGLShaderProgram *program);private:void initCubeGeometry();QOpenGLBuffer screenArrayBuf;QOpenGLBuffer screenIndexBuf;QOpenGLBuffer arrayBuf;QOpenGLBuffer indexBuf;
};#endif // GEOMETRYENGINE_H
geometryengine.cpp
#include "geometryengine.h"#include <QVector2D>
#include <QVector3D>struct VertexData
{QVector3D position;QVector2D texture;
};struct VertexData1
{QVector3D position;QVector3D normal;
};GeometryEngine::GeometryEngine(): screenIndexBuf(QOpenGLBuffer::IndexBuffer),indexBuf(QOpenGLBuffer::IndexBuffer)
{initializeOpenGLFunctions();arrayBuf.create();indexBuf.create();screenArrayBuf.create();screenIndexBuf.create();initCubeGeometry();
}GeometryEngine::~GeometryEngine()
{arrayBuf.destroy();indexBuf.destroy();screenArrayBuf.destroy();screenIndexBuf.destroy();
}void GeometryEngine::initCubeGeometry()
{VertexData vertices[] = {// Vertex data for face 0{QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(0.0f, 0.0f)}, // v0{QVector3D( 1.0f, -1.0f, 1.0f), QVector2D(0.33f, 0.0f)}, // v1{QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(0.0f, 0.5f)}, // v2{QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v3// Vertex data for face 1{QVector3D( 1.0f, -1.0f, 1.0f), QVector2D( 0.0f, 0.5f)}, // v4{QVector3D( 1.0f, -1.0f, -1.0f), QVector2D(0.33f, 0.5f)}, // v5{QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.0f, 1.0f)}, // v6{QVector3D( 1.0f, 1.0f, -1.0f), QVector2D(0.33f, 1.0f)}, // v7// Vertex data for face 2{QVector3D( 1.0f, -1.0f, -1.0f), QVector2D(0.66f, 0.5f)}, // v8{QVector3D(-1.0f, -1.0f, -1.0f), QVector2D(1.0f, 0.5f)}, // v9{QVector3D( 1.0f, 1.0f, -1.0f), QVector2D(0.66f, 1.0f)}, // v10{QVector3D(-1.0f, 1.0f, -1.0f), QVector2D(1.0f, 1.0f)}, // v11// Vertex data for face 3{QVector3D(-1.0f, -1.0f, -1.0f), QVector2D(0.66f, 0.0f)}, // v12{QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(1.0f, 0.0f)}, // v13{QVector3D(-1.0f, 1.0f, -1.0f), QVector2D(0.66f, 0.5f)}, // v14{QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(1.0f, 0.5f)}, // v15// Vertex data for face 4{QVector3D(-1.0f, -1.0f, -1.0f), QVector2D(0.33f, 0.0f)}, // v16{QVector3D( 1.0f, -1.0f, -1.0f), QVector2D(0.66f, 0.0f)}, // v17{QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v18{QVector3D( 1.0f, -1.0f, 1.0f), QVector2D(0.66f, 0.5f)}, // v19// Vertex data for face 5{QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v20{QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.66f, 0.5f)}, // v21{QVector3D(-1.0f, 1.0f, -1.0f), QVector2D(0.33f, 1.0f)}, // v22{QVector3D( 1.0f, 1.0f, -1.0f), QVector2D(0.66f, 1.0f)}, // v23};GLushort indices[] = {0, 1, 2, 3, 3, // Face 0 - triangle strip ( v0, v1, v2, v3)4, 4, 5, 6, 7, 7, // Face 1 - triangle strip ( v4, v5, v6, v7)8, 8, 9, 10, 11, 11, // Face 2 - triangle strip ( v8, v9, v10, v11)12, 12, 13, 14, 15, 15, // Face 3 - triangle strip (v12, v13, v14, v15)16, 16, 17, 18, 19, 19, // Face 4 - triangle strip (v16, v17, v18, v19)20, 20, 21, 22, 23 // Face 5 - triangle strip (v20, v21, v22, v23)};// Transfer vertex data to VBO 0arrayBuf.bind();arrayBuf.allocate(vertices, 24 * sizeof(VertexData));// Transfer index data to VBO 1indexBuf.bind();indexBuf.allocate(indices, 34 * sizeof(GLushort));VertexData1 screenVertices[] ={{QVector3D(-1.0f, 0.0f, -1.0f), QVector3D(0,1,0) },{QVector3D(-1.0f, 0.0f, 1.0f), QVector3D(0,1,0) },{QVector3D(1.0f, 0.0f, 1.0f), QVector3D(0,1,0) },{QVector3D(1.0f, 0.0f, -1.0f), QVector3D(0,1,0)},};GLushort screenIndices[] = {0, 1, 2, 2, 3, 0};screenArrayBuf.bind();screenArrayBuf.allocate(screenVertices, 4 * sizeof(VertexData1));screenIndexBuf.bind();screenIndexBuf.allocate(screenIndices, 6 * sizeof(GLushort));
}void GeometryEngine::drawCubeGeometry(QOpenGLShaderProgram *program)
{arrayBuf.bind();indexBuf.bind();int offset = 0;int vertexLocation = program->attributeLocation("a_position");program->enableAttributeArray(vertexLocation);program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 3, sizeof(VertexData));offset += sizeof(QVector3D);int texcoordLocation = program->attributeLocation("a_texcoord");program->enableAttributeArray(texcoordLocation);program->setAttributeBuffer(texcoordLocation, GL_FLOAT, offset, 2, sizeof(VertexData));glDrawElements(GL_TRIANGLE_STRIP, 34, GL_UNSIGNED_SHORT, nullptr);
}void GeometryEngine::drawPlane(QOpenGLShaderProgram *program)
{screenArrayBuf.bind();screenIndexBuf.bind();int offset = 0;int vertexLocation = program->attributeLocation("a_position");program->enableAttributeArray(vertexLocation);program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 3, sizeof(VertexData1));offset += sizeof(QVector3D);int normalLocation = program->attributeLocation("a_normal");program->enableAttributeArray(normalLocation);program->setAttributeBuffer(normalLocation, GL_FLOAT, offset, 3, sizeof(VertexData1));glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, nullptr);
}
main.cpp
#include <QApplication>
#include <QLabel>
#include <QSurfaceFormat>#ifndef QT_NO_OPENGL
#include "mainwidget.h"
#endifint main(int argc, char *argv[])
{QApplication app(argc, argv);QSurfaceFormat format;format.setDepthBufferSize(24);QSurfaceFormat::setDefaultFormat(format);app.setApplicationName("cube");app.setApplicationVersion("0.1");
#ifndef QT_NO_OPENGLMainWidget widget;widget.show();
#elseQLabel note("OpenGL Support required");note.show();
#endifreturn app.exec();
}
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
相关文章
- 工作积累(九)——前后台传递类Map型参数
最近在工作中整合友盟消息推送服务时,遇到了用 Ajax 向 Java 后台传递自定义参数的需求,当时想要采取 java.util.Map ,但发现 Ajax 无法传递 java.util.Map 类型的参数,后来无奈采取的方式的是采用了这样的 Vo 对象: p…...
2024/5/9 4:45:40 - 割双眼皮价位SOU奇致治病
...
2024/5/9 5:01:35 - 双眼皮和填脂肪可以一起做吗
...
2024/5/9 15:33:59 - 双眼皮缝线位置可以填脂肪吗
...
2024/5/9 6:49:19 - 做完双眼皮怎么能维持的更久
...
2024/5/3 5:30:51 - 双眼皮割过维持多久
...
2024/5/3 9:37:02 - 割一个永久的双眼皮多少钱
...
2024/5/6 5:47:32 - 南通丽人整形美容医院做怎么样自然做出割了双眼皮能管多久
...
2024/5/9 6:27:29 - 做双眼皮价格VIP只搜奇致
...
2024/4/20 16:03:41 - AngularJs和ajax动态修改url地址而不刷新页面的方法
2019独角兽企业重金招聘Python工程师标准>>> 1、Angularjs: 原始地址为:http://xxx/#/a 添加或修改参数: $location.url(?a1&aa23); 执行后浏览器的url地址变为:http://xxx/#/a?a1&aa23,而页面也没有刷新。 …...
2024/4/20 16:03:39 - 割双眼皮整形to奇致勤奋
...
2024/4/20 16:03:38 - 双眼皮填脂肪太多了
...
2024/4/28 8:39:28 - 双眼皮 张春光
...
2024/4/21 13:14:49 - 全切双眼皮怎样避免疤痕增生
...
2024/5/9 5:55:55 - 双眼皮手术怎么做多少钱啊
...
2024/4/21 13:14:48 - Echarts 通过时间轴timeline改变xAxis.data数据进行不合并处理
写在前面: 使用时间轴 timeline 绘制图形的时候会有一种动态的效果,让图形看起来更加生动,也达到了交互式数据的展现。但是在使用 timeline 的时候我遇到了几个问题,其中最头疼的还是对 xAxis.data 数据进行不合并处理。本文章就是通过使用 TIMELINE_CHANGED 方法和…...
2024/5/6 6:02:48 - 双眼皮多少钱伍约伊思整形ok
...
2024/4/21 13:14:46 - 多点埋切双眼皮明眸大眼术做法
...
2024/4/21 13:14:45 - 额头填充和双眼皮多久能洗头
...
2024/4/26 13:19:30 - 贝塞尔双眼皮哪里好
...
2024/5/9 6:17:59
最新文章
- LeetCode hot100-33-Y
148. 排序链表 给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 这题能通过但是投机取巧了,一般应该不能这样做,直接把节点里的值拿出来,排序后再更新每个节点的值。 /*** Definition for singly-linked list.* p…...
2024/5/10 8:49:51 - 梯度消失和梯度爆炸的一些处理方法
在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言,在此感激不尽。 权重和梯度的更新公式如下: w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...
2024/5/9 21:23:04 - 自我介绍的HTML 页面(入门)
一.前情提要 1.主要是代码示例,具体内容需自己填充 2.代码后是详解 二.代码实例和解析 代码 <!DOCTYPE html> <html lang"zh-CN"> <head> <meta charset"UTF-8"> <title>自我介绍页面</title>…...
2024/5/10 6:46:31 - CAJViewer7.3 下载地址及安装教程
CAJViewer是中国学术期刊(CAJ)全文数据库的专用阅读软件。CAJViewer是中国知识资源总库(CNKI)开发的一款软件,旨在方便用户在线阅读和下载CAJ数据库中的学术论文、期刊和会议论文等文献资源。 CAJViewer具有直观的界面…...
2024/5/9 10:34:33 - 【外汇早评】美通胀数据走低,美元调整
原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...
2024/5/8 6:01:22 - 【原油贵金属周评】原油多头拥挤,价格调整
原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...
2024/5/9 15:10:32 - 【外汇周评】靓丽非农不及疲软通胀影响
原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...
2024/5/4 23:54:56 - 【原油贵金属早评】库存继续增加,油价收跌
原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...
2024/5/9 4:20:59 - 【外汇早评】日本央行会议纪要不改日元强势
原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...
2024/5/4 23:54:56 - 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响
原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...
2024/5/4 23:55:05 - 【外汇早评】美欲与伊朗重谈协议
原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...
2024/5/4 23:54:56 - 【原油贵金属早评】波动率飙升,市场情绪动荡
原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...
2024/5/7 11:36:39 - 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试
原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...
2024/5/4 23:54:56 - 【原油贵金属早评】市场情绪继续恶化,黄金上破
原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...
2024/5/6 1:40:42 - 【外汇早评】美伊僵持,风险情绪继续升温
原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...
2024/5/4 23:54:56 - 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势
原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...
2024/5/8 20:48:49 - 氧生福地 玩美北湖(上)——为时光守候两千年
原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...
2024/5/7 9:26:26 - 氧生福地 玩美北湖(中)——永春梯田里的美与鲜
原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...
2024/5/4 23:54:56 - 氧生福地 玩美北湖(下)——奔跑吧骚年!
原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...
2024/5/8 19:33:07 - 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!
原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...
2024/5/5 8:13:33 - 「发现」铁皮石斛仙草之神奇功效用于医用面膜
原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...
2024/5/8 20:38:49 - 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者
原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...
2024/5/4 23:54:58 - 广州械字号面膜生产厂家OEM/ODM4项须知!
原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...
2024/5/9 7:32:17 - 械字号医用眼膜缓解用眼过度到底有无作用?
原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...
2024/5/9 17:11:10 - 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...
解析如下:1、长按电脑电源键直至关机,然后再按一次电源健重启电脑,按F8健进入安全模式2、安全模式下进入Windows系统桌面后,按住“winR”打开运行窗口,输入“services.msc”打开服务设置3、在服务界面,选中…...
2022/11/19 21:17:18 - 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。
%读入6幅图像(每一幅图像的大小是564*564) f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...
2022/11/19 21:17:16 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...
win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面,在等待界面中我们需要等待操作结束才能关机,虽然这比较麻烦,但是对系统进行配置和升级…...
2022/11/19 21:17:15 - 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...
有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows,请勿关闭计算机”的提示,要过很久才能进入系统,有的用户甚至几个小时也无法进入,下面就教大家这个问题的解决方法。第一种方法:我们首先在左下角的“开始…...
2022/11/19 21:17:14 - win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...
置信有很多用户都跟小编一样遇到过这样的问题,电脑时发现开机屏幕显现“正在配置Windows Update,请勿关机”(如下图所示),而且还需求等大约5分钟才干进入系统。这是怎样回事呢?一切都是正常操作的,为什么开时机呈现“正…...
2022/11/19 21:17:13 - 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...
Win7系统开机启动时总是出现“配置Windows请勿关机”的提示,没过几秒后电脑自动重启,每次开机都这样无法进入系统,此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一:开机按下F8,在出现的Windows高级启动选…...
2022/11/19 21:17:12 - 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...
有不少windows10系统用户反映说碰到这样一个情况,就是电脑提示正在准备windows请勿关闭计算机,碰到这样的问题该怎么解决呢,现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法:1、2、依次…...
2022/11/19 21:17:11 - 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...
今天和大家分享一下win7系统重装了Win7旗舰版系统后,每次关机的时候桌面上都会显示一个“配置Windows Update的界面,提示请勿关闭计算机”,每次停留好几分钟才能正常关机,导致什么情况引起的呢?出现配置Windows Update…...
2022/11/19 21:17:10 - 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...
只能是等着,别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚,只能是考虑备份数据后重装系统了。解决来方案一:管理员运行cmd:net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...
2022/11/19 21:17:09 - 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?
原标题:电脑提示“配置Windows Update请勿关闭计算机”怎么办?win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢?一般的方…...
2022/11/19 21:17:08 - 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...
关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!关机提示 windows7 正在配…...
2022/11/19 21:17:05 - 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...
钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...
2022/11/19 21:17:05 - 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...
前几天班里有位学生电脑(windows 7系统)出问题了,具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面,长时间没反应,无法进入系统。这个问题原来帮其他同学也解决过,网上搜了不少资料&#x…...
2022/11/19 21:17:04 - 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...
本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法,并在最后教给你1种保护系统安全的好方法,一起来看看!电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中,添加了1个新功能在“磁…...
2022/11/19 21:17:03 - 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...
许多用户在长期不使用电脑的时候,开启电脑发现电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机。。.这要怎么办呢?下面小编就带着大家一起看看吧!如果能够正常进入系统,建议您暂时移…...
2022/11/19 21:17:02 - 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...
配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!配置windows update失败 还原更改 请勿关闭计算机&#x…...
2022/11/19 21:17:01 - 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...
不知道大家有没有遇到过这样的一个问题,就是我们的win7系统在关机的时候,总是喜欢显示“准备配置windows,请勿关机”这样的一个页面,没有什么大碍,但是如果一直等着的话就要两个小时甚至更久都关不了机,非常…...
2022/11/19 21:17:00 - 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...
当电脑出现正在准备配置windows请勿关闭计算机时,一般是您正对windows进行升级,但是这个要是长时间没有反应,我们不能再傻等下去了。可能是电脑出了别的问题了,来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...
2022/11/19 21:16:59 - 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...
我们使用电脑的过程中有时会遇到这种情况,当我们打开电脑之后,发现一直停留在一个界面:“配置Windows Update失败,还原更改请勿关闭计算机”,等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢࿰…...
2022/11/19 21:16:58 - 如何在iPhone上关闭“请勿打扰”
Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...
2022/11/19 21:16:57