【问题标题】:Threejs: compute projected coordinate in fragment shaderThreejs:在片段着色器中计算投影坐标
【发布时间】:2023-04-10 16:11:01
【问题描述】:

我正在努力处理片段着色器中的坐标。 简而言之,我只想使用世界空间的(x,y,z) 用片段着色器绘制圆圈。但由于相机位置和圆中心位置的z,我无法获得实际正确投影的xy 坐标。

假设我的相机放置在(0, 0, 1000) 并以

  • 视野:45度
  • 方面与screen_width/screen_height
  • Z 附近:1
  • 远:10000

相机看(0,0)。在这种情况下使用three.js,我可以获得相机的projectionMatrixModelViewMatrix(例如PerspectiveCamera.projectionMatrix),并且默认情况下我可以在ShaderMaterial 的fragmentShader 中使用viewMatrix three.js

所以在 fragmentShader 中,为了计算放置(300, 300, -1000) 的圆的投影坐标,我写了我的VertexShaderFragmentShader,如下所示。

我的顶点着色器仅用于将projectionMatrixmodelViewMatrix 设置为PMV

// vertexShader
varying mat4 P;
varying mat4 MV;
void main(){
    P = projectionMatrix;
    MV = modelViewMatrix;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

然后,我只需使用 PMV 计算 x 和 y,如下所示。

// fragmentShader
varying mat4 P;
varying mat4 MV;
uniform float x;
uniform float y;
uniform float z;
uniform float r;
uniform vec2 u_resolution;

float circle(vec2 _st, vec2 _center, float _radius){
    vec2 dist = _st - _center + u_resolution;
    return 1.-smoothstep(_radius-(_radius*0.01),
                     _radius+(_radius*0.01),
                     length(dist));
}

void main(){
    vec2 coord = (P * MV * vec4(x, y, z, 1.0)).xy;
    float point = circle(gl_FragCoord.xy, coord, r); // ignore r scaling.
    gl_FragColor = vec4(vec4(point), point);
}

但结果与我的预期不符。并且还发现了一些奇怪的行为。

  • 不管z的制服是什么,都没有任何变化。
  • 像素比可能是某种原因(例如视网膜显示器的像素比为 2),但从我的实验来看,这与此无关。

我犯了什么错误?还是有什么误导? (不知何故,circle 函数可能有错误,但我认为这不会造成严重问题..)

【问题讨论】:

  • 哇是u_resolution,你对_st - _center + u_resolution有什么期望?不应该是(_st - _center) / u_resolution 之类的东西吗?请注意,gl_FragCoord.xy 是窗口(像素)坐标。
  • 嗨。我希望圆圈放置在(x,y) 并且因为gl_FragCoord 从左下角的(0,0)开始,我添加了u_resolution(这也是因为_center 是来自(0,0)的坐标,表示屏幕中心。
  • 为什么你的投影和模型视图矩阵作为varyings 传递?
  • 嗯,很可能,您的矩阵一开始是uniforms,因此它们已经可用于程序中的所有着色器阶段,将它们作为varyings 传递只是一种巨大的浪费资源。
  • 但这里的主要问题是您的计算没有任何意义。您尝试使用原点的剪辑空间位置和窗口空间片段坐标来定义一个圆。我不清楚的是您实际想要实现的目标:实际的 2D、不失真的圆,或某个圆的正确透视图像。

标签: three.js glsl shader fragment-shader perspectivecamera


【解决方案1】:

假设xyz 定义了世界空间中的圆心。您想在与屏幕空间通道中的视口平行的平面上绘制一个圆,在整个视口上绘制一个四边形。

您必须将圆心从世界空间坐标转换为标准化的设备坐标。最好的解决方案是在 CPU 上执行此操作并设置统一的结果。

根据您问题的代码,这也可以在顶点着色器中完成。但是你必须做一个Perspective divide,经过模型视图矩阵和投影矩阵的变换,将点从裁剪空间变换到视图归一化的设备空间:

uniform mat4 P;
uniform mat4 MV;
uniform float x;
uniform float y;
uniform float z;

varying vec3 cpt; 

void main(){
    vec4 cpt_h  =  projectionMatrix * modelViewMatrix * vec4(x, y, z, 1.0);
    vec3 cpt    =  cpt_h.xyz / cpt_h.w;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

如果u_resolution,是视口的宽度和高度,那么片段在标准化设备空间中的x和y坐标可以通过以下方式计算:

vec2 coord = gl_FragCoord.xy / u_resolution.xy * 2.0 - 1.0;

但我建议将圆的中心点转换为窗口(像素)坐标,那么半径也可以设置为像素:

vec2 cpt_p = (cpt.xy * 0.5 + 0.5) * u_resolution.xy;

要计算向量的长度,您可以使用 GLSL 函数length

最终的片段着色器可能如下所示:

varying vec3 cpt; 

uniform vec2 u_resolution;

uniform float u_pixel_ratio; // device pixel ratio

uniform float r; // e.g. 100.0 means a radius of 100 pixel

float circle( vec2 _st, vec2 _center, float _radius )
{
    // thickness of the circle in pixel
    const float thickness = 20.0;

    // distance to the center  point in pixel
    float dist = length(_st - _center);

    return 1.0 - smoothstep(0.0, thickness/2.0, abs(_radius-dist));
}

void main(){
    vec2  cpt_p  = (cpt.xy * 0.5 + 0.5) * u_resolution.xy * u_pixel_ratio;
    float point  = circle(gl_FragCoord.xy, cpt_p, r);
    gl_FragColor = vec4(point);
}  

例如一个半径为 50.0,厚度为 20.0 的圆:


如果你想对圆应用透视变形,这意味着圆的大小会随着距离而减小,那么你必须在世界坐标中设置半径r。 计算圆上的一个点,并在归一化设备空间的顶点着色器中计算该点到圆中心点的距离。 这是除了圆的中心点之外,您必须从顶点着色器传递到片段着色器的半径。

uniform mat4 P;
uniform mat4 MV;
uniform float x;
uniform float y;
uniform float z;
uniform float r; // e.g. radius in world space

varying vec3  cpt;
varying float radius;

void main(){
    vec4 cpt_v  = modelViewMatrix * vec4(x, y, z, 1.0);
    vec4 rpt_v  = vec4(cpt_v.x, cpt_v.y + r, cpt_v.zw);

    vec4 cpt_h  = projectionMatrix * cpt_v;
    vec4 rpt_h  = projectionMatrix * rpt_v;

    cpt         =  cpt_h.xyz / cpt_h.w;
    vec3 rpt    =  rpt_v.xyz / rpt_v.w;
    radius      =  length(rpt-cpt);

    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

varying vec3  cpt;
varying float radius;

uniform vec2 u_resolution;
uniform float u_pixel_ratio; // device pixel ratio

uniform float r; // e.g. 100.0 means a radius of 100 pixel

float circle( vec2 _st, vec2 _center, float _radius )
{
    const float thickness = 20.0;
    float dist = length(_st - _center);
    return 1.0 - smoothstep(0.0, thickness/2.0, abs(_radius-dist));
}

void main()
{
    vec2  cpt_p    = (cpt.xy * 0.5 + 0.5) * u_resolution.xy * u_pixel_ratio;
    float radius_p = radius * 0.5 * u_resolution.y * u_pixel_ratio.y;

    float point  = circle(gl_FragCoord.xy, cpt_p, radius_p);
    gl_FragColor = vec4(point);
} 

【讨论】:

  • 你的答案绝了!真的感谢您的贡献!我了解到我不知道“透视划分”对于这个问题可能是完全关键的。我还在你的代码中编辑了一些错字。如果不介意的话,能否推荐一些与这个问题相关的资源或者关于gl的基本概念知识?
  • 很抱歉打扰你,但我很好奇如果我想像@derhass 提到的那样画扭曲的圆圈怎么办?
  • @Hyuntak.Cha 你的意思是圆的半径必须减少距离吗?
  • 是的,从您的代码中,我想知道为什么我们只从“y”计算 radius_p。是不是也可以看到x方向的畸变? (另外我发现代码中有错误:u_pixel_ratio.y。它的类型是float。
猜你喜欢
  • 2018-10-03
  • 1970-01-01
  • 1970-01-01
  • 2022-07-21
  • 1970-01-01
  • 2014-05-17
  • 1970-01-01
  • 2020-03-03
  • 2015-08-30
相关资源
最近更新 更多