hwloom
hwloom

Reputation: 25

Threejs: compute projected coordinate in fragment shader

I'm struggling with handling Coord in fragment Shader. In brief, I just want to draw circle with fragment shader using (x,y,z) of world space. But because of camera position and the z of circle's center position, I cannot get actual right projected x and y coords.

Let's suppose that my camera placed at (0, 0, 1000) and perspective with

Camera look at (0,0). In this case with three.js, I can get projectionMatrix and ModelViewMatrix of camera(e.g.PerspectiveCamera.projectionMatrix) and also in default I can use viewMatrix in fragmentShader of ShaderMaterial in three.js.

So in fragmentShader, for calculating projected coordinate of circle placed (300, 300, -1000), I write my VertexShader and FragmentShader like below.

My Vertex Shader is only for get projectionMatrix and modelViewMatrix as P and MV.

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

And then, I just calculate x and y using P and MV like below.

// 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);
}

But the result doesn't match what I expected. And also some weird behaviors were found.

Any mistake that I made? Or any misleading? (somehow there can be mistake in circle function but I think it doesn't make critical problem..)

Upvotes: 2

Views: 1912

Answers (1)

Rabbid76
Rabbid76

Reputation: 210946

Lets assume that x, y and z, define the center of a circle in world space. You want to draw a circle in a plane which is parallel to the view port in a screen space pass, where you draw a quad over the entire viewport.

You have to transform the center of the circle from world space coordinates to normalized device coordinates. The best solution would be to do this on the CPU and to set uniform with the result.

According to the code of your question, this can be done in the vertex shader, too. But you have to do a Perspective divide, after the transformation by the model view matrix and the projection matrix, to transform the point form clip space to view normalized device space:

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);
}

If u_resolution, is the width and the height of the viewport, then the x and y coordinate of the fragment in normalized device space can be calculated by:

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

But I recommend to transform the center point of the circle to window (pixel) coordinates, then the radius can be set in pixel, too:

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

To calculate the length of a vector you can use the GLSL function length.

The final fragment shader may look like this:

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);
}  

e.g. a circle with a radius of 50.0 and a thickness of 20.0:


If you want to apply a perspective distortion to the circle, this means the size of the circle decreases by distance, then you have to set the radius r in world coordinates. Calculate a point on the circle and calculate the distance of the point to the center point of the circle in the vertex shader in normalized device space. This is the radius which you have to pass from the vertex shader to the fragment shader additional to the center point of the circle.

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);
} 

Upvotes: 2

Related Questions