模型渲染到屏幕上之后,鼠标点击屏幕,我们怎么知道是否点击了模型,点击了模型的哪个位置呢? 这些需求都需要坐标转换,常规来说就是从世界坐标系转换到屏幕坐标系,或者从屏幕坐标系转换到世界坐标系。
博主在渲染这块也是个新手,大家一起学习。
本文使用的是google的filament渲染引擎。
提起坐标转换,就不得不提经典的openGL了,可以参考以下文章:
ModelMatrix、ModelViewMatrix、ProjectionMatrix、NormalMatrix模型矩阵、模型视图矩阵、投影矩阵、正规矩阵详解_妙为的博客-CSDN博客
经典渲染流程:
坐标转换流程:
假定读者已经了解了基本的渲染知识,我们这里不做过多解释,毕竟不是重点。以下概念来源:OpenGL矩阵变换的数学推导-腾讯云开发者社区-腾讯云
还可以参考以下文章加深理解:
ModelMatrix、ModelViewMatrix、ProjectionMatrix、NormalMatrix模型矩阵、模型视图矩阵、投影矩阵、正规矩阵详解_妙为的博客-CSDN博客
[OpenGL]OpenGL坐标系及坐标转换-腾讯云开发者社区-腾讯云
上面的图让我们知道了渲染的矩阵变换,从屏幕坐标到世界坐标其实就是反步骤来计算。例如顺序矩阵变化是viewMatrix * projectionMatrix,那么求逆就是inverse(viewMatrix * projectionMatrix) 了。
可以参考stack overflow的回答:https://stackoverflow.com/questions/46749675/opengl-mouse-coordinates-to-space-coordinates/46752492#46752492
通过逆矩阵的方式
mat4 inversePrjMat = inverse( prjMat );
vec4 viewPosH = inversePrjMat * vec4(ndc_x, ndc_y, 2.0*depth - 1.0, 1.0)
vec3 viewPos = viewPos.xyz / viewPos.w;
https://github.com/google/filament/discussions/5998?sort=new
这种是不使用官方的pick()函数,自己手写计算的射线拾取。这个issue主要存在的问题就是没有设置裁剪空间的w,并且也没有除以w。
射线拾取参考:屏幕坐标转世界坐标与射线生成
/**
* screen space coordinates in GL convention, this can be used to compute the view or
* world space position of the picking hit. For e.g.:
* clip_space_position = (fragCoords.xy / viewport.wh, fragCoords.z) * 2.0 - 1.0
* view_space_position = inverse(projection) * clip_space_position
* world_space_position = model * view_space_position
*
* The viewport, projection and model matrices can be obtained from Camera. Because
* pick() has some latency, it might be more accurate to obtain these values at the
* time the View::pick() call is made.
*/
具体代码
// 1、获取clip_space_position
const Viewport& vp = view->getViewport();
float clip_space_x = (result.fragCoords.x / vp.width - 0.5f) * 2.0f;
float clip_space_y = (result.fragCoords.y / vp.height - 0.5f) * 2.0f;
float clip_space_z = result.fragCoords.z * 2.0f - 1.0f;
float4 clip_space_position {clip_space_x, clip_space_y, clip_space_z, 1.0f};
// 2、获取视图矩阵
mat4 projection = camera.getProjectionMatrix();
mat4 model = camera.getModelMatrix();
mat4 view_space_position = model * inverse(projection);
// 3、获取世界坐标系
float4 world_space_position = clip_space_position * view_space_position;
View::pick()会有延迟,需要在pick()函数中获取模型矩阵,投影矩阵进行计算。在pick()函数外获取投影矩阵是不准确的,我踩的坑就是在pick()函数外获取的投影矩阵,在使用的时候发现矩阵变化了,导致inverse出来了inf和-nan等值。
inf 表示一个数超过了浮点类型所能表示的最大范围,通常为正无穷或负无穷。
nan 表示一个数不是一个合法的数字,通常出现在无法进行有效运算时。
剪辑空间是齐次坐标系,转换成笛卡尔坐标系需要除以w,而我们设置的w是1.0,当缩放坐标的W为1时,坐标不会增大或缩小,保持原有的大小。所以,当W=1,不会影响到X,Y,Z分量的值。因此不影响最终结果。
什么是齐次坐标系?为什么要用齐次坐标系?
在filament中,viewMatrix = inverse(getModelMatrix()) ,所以:
inverse(viewMatrix * projectionMatrix) = inverse(viewMatrix) * inverse(projectionMatrix)
= getModelMatrix() * inverse(projectionMatrix)
参考:https://stackoverflow.com/questions/66160973/finding-world-position-of-element-in-screen-space
https://stackoverflow.com/questions/68870053/how-to-get-world-coordinates-from-the-screen-coordinates
filament中是通过pick()函数来实现的,获取到点击位置对应的实体,如果是模型的话,就可以获取到模型对应实体的name。底层是调用了driver.readPixels()方法来从渲染目标缓冲区中读取相应的像素信息,然后进行点击判断。
这部分的转换就是按照上面的渲染流程计算即可。
// 1、获取模型空间位置并将其转换为剪辑空间
vec4 clipSpacePos = projectionMatrix * (viewMatrix * vec4(point3D, 1.0));
// 2、从剪辑空间转换到标准化设备坐标空间(NDC 空间)
vec3 ndcSpacePos = clipSpacePos.xyz / clipSpacePos.w;
// 3、获取窗口位置
vec2 windowSpacePos = ((ndcSpacePos.xy + 1.0) / 2.0) * viewSize + viewOffset;
参考:https://stackoverflow.com/questions/8491247/c-opengl-convert-world-coords-to-screen2d-coords
坐标转换离不开矩阵的运算,建议是再回头看看线性代数。。没时间系统的看的话,也可以先找一些网上的文章看看。以下几篇讲解的比较通俗,可以看看。
线性代数的秘密:矩阵相乘的本质是什么?
讲一点点数学:什么是矩阵?
线性代数的秘密:逆矩阵的意义是什么?(上)
模型矩阵,投影矩阵,视图矩阵的推导
OpenGL矩阵变换的数学推导-腾讯云开发者社区-腾讯云
屏幕坐标转世界坐标与射线生成
end