Project the shadow of a disk in 3D space onto a 2D plane

I want to project a disk and its shadow in a 3D space onto the 2D viewer plane. I’m working with Javascript and PIXI, but my problem mostly concerns the general geometrics of the problem. The projection of the disk itself works fine, but I have troubles with displaying the shadow correctly.

My approach for the disk is that I define the center c, two radii r1 and r2, and a normal vector to the disk plane. Then I find two vectors that are in the disk plane, viz. orthogonal to the defined normal vector, and orthogonal to each other. I move r1 steps from the center c in the direction of the first of these vectors to get a first point on the edge of the disk; then I move r2 steps in the direction of the second vector for a second point. Next, I project these two edge points and the center c onto the 2D viewer plane and compute the distance between the projected center and the projected edge point. Finally, my projected disk can be plotted as an ellipse around the projected center and radii according to the computed distances.

Now, my approach for the shadow which I cannot get to work is similar. Again, I compute two edge points of the original disk the same way as before, but now I protect these edge points and the center point onto the shadow plane. In the graphic below, you can see the center point projected on the shadow plane in green; the projected edge points in blue. Next, I again compute the distances between the edge points and center points of the shadow to get two radii for the shadow. Finally, for the projection of the shadow onto the viewer plane, I again use the same function as for the original disk.

In the figure below, the white lines are parallel lines orthogonal to the viewer. The two lines on the bottom are in the same plane that the shadows is projected on.

Example with light source, disk, points of shadow and wrong shadow

I’ve also added my code below. I excluded the function for projecting specifc points to the projection or shadow plane since nothing indicates a problem there.

// define position of light source and object
let light_vec = [768, 400, 600]
let obj_vec = [468, 472, 1000]

plotDisk(graphics, obj_vec, 50, 50, [0, 0, 1], color_str, 1.0)
plotShadow(graphics, obj_vec, 50, [0, 0, 1], light_vec, [0.0, 1.0 ,0.0], [0, 838, 0], color_str, 0.5)

// plot a 3d disk projected to the camera plane
function plotDisk(disk_obj, center_vec, rad1_fl, rad2_fl, norm1_vec, color_str, trans_fl) {

    // get non-colinear vector
    norm1_vec = normalize(norm1_vec)

    let nonCol_vec = [0.0, 0.0, 0.0]
    let eqZ_boo = true;
    let neqZ_boo = true;

    let i = 0;
    while (i < norm1_vec.length) {
        if (norm1_vec[i] !== 0 && eqZ_boo) {
            nonCol_vec[i] = - norm1_vec[i]  
            eqZ_boo = false
        } else if (norm1_vec[i] == 0 && neqZ_boo) {
            nonCol_vec[i] = 1.0 
            neqZ_boo = false;
        }
        i++;
    }

    // get second and third normal vector
    let norm2_vec = normalize(crossProduct(norm1_vec, normalize(nonCol_vec)))
    let norm3_vec = normalize(crossProduct(norm1_vec, norm2_vec))

    // construct points on disk line and project to plane
    let radPoint1_vec = projToPlane(addVec(center_vec, scalarMulti(norm2_vec, rad1_fl)), view_proj);
    let radPoint2_vec = projToPlane(addVec(center_vec, scalarMulti(norm3_vec, rad2_fl)), view_proj);
    let centerPro_vec = projToPlane(center_vec, view_proj) 

    // get radii of ellipse
    let radPro1_fl = Math.sqrt(Math.pow(centerPro_vec[0] - radPoint1_vec[0], 2) +  Math.pow(centerPro_vec[1] - radPoint1_vec[1], 2));
    let radPro2_fl = Math.sqrt(Math.pow(centerPro_vec[0] - radPoint2_vec[0], 2) +  Math.pow(centerPro_vec[1] - radPoint2_vec[1], 2));

    // plot actual ellipse
    disk_obj.beginFill(color_str, trans_fl);
    disk_obj.drawEllipse(centerPro_vec[0], centerPro_vec[1], radPro1_fl, radPro2_fl);
    disk_obj.endFill();
}

// plot the shadow of a disk
function plotShadow(disk_obj, center_vec, rad_fl, norm1_vec, light_vec, plane_normal, plane_point, shadow_color_str, trans_fl) {

    // get two orthonal vectors on the disk as before
    norm1_vec = normalize(norm1_vec);

    let nonCol_vec = [0.0, 0.0, 0.0];
    let eqZ_boo = true;
    let neqZ_boo = true;
    let i = 0;
    while (i < norm1_vec.length) {
        if (norm1_vec[i] !== 0 && eqZ_boo) {
            nonCol_vec[i] = -norm1_vec[i];
            eqZ_boo = false;
        } else if (norm1_vec[i] == 0 && neqZ_boo) {
            nonCol_vec[i] = 1.0;
            neqZ_boo = false;
        }
        i++;
    }

    let norm2_vec = normalize(crossProduct(norm1_vec, normalize(nonCol_vec)));
    let norm3_vec = normalize(crossProduct(norm1_vec, norm2_vec));

    // project points on the disk to the shadow plane
    let diskEdge1_vec = addVec(center_vec, scalarMulti(norm2_vec, rad_fl));
    let diskEdge2_vec = addVec(center_vec, scalarMulti(norm3_vec, rad_fl));

    // get shadow points on the plane
    let shadowEdge1_vec = projectToShadowPlane(diskEdge1_vec, light_vec, plane_normal, plane_point);
    let shadowEdge2_vec = projectToShadowPlane(diskEdge2_vec, light_vec, plane_normal, plane_point);
    let shadowCenter_vec = projectToShadowPlane(center_vec, light_vec, plane_normal, plane_point);

    // compute radius on shadow plane
    let planeRad1_fl = Math.sqrt(Math.pow(shadowCenter_vec[0] - shadowEdge1_vec[0], 2) + Math.pow(shadowCenter_vec[1] - shadowEdge1_vec[1], 2) + Math.pow(shadowCenter_vec[2] - shadowEdge1_vec[2], 2));
    let planeRad2_fl = Math.sqrt(Math.pow(shadowCenter_vec[0] - shadowEdge2_vec[0], 2) + Math.pow(shadowCenter_vec[1] - shadowEdge2_vec[1], 2) + Math.pow(shadowCenter_vec[2] - shadowEdge2_vec[2], 2));

    plotDisk(disk_obj, shadowCenter_vec, planeRad1_fl, planeRad2_fl, plane_normal, shadow_color_str, trans_fl)

    // plot the points of the shadow for debugging
    let shadowPoint1_proj = projToPlane(shadowEdge1_vec, view_proj);
    let shadowPoint2_proj = projToPlane(shadowEdge2_vec, view_proj);
    let shadowCenter_proj = projToPlane(shadowCenter_vec, view_proj);

    graphics.beginFill("blue");
    graphics.drawCircle(shadowPoint1_proj[0], shadowPoint1_proj[1], 5);
    graphics.drawCircle(shadowPoint2_proj[0], shadowPoint2_proj[1], 5);
    graphics.endFill();

    graphics.beginFill("green");
    graphics.drawCircle(shadowCenter_proj[0], shadowCenter_proj[1], 5);
    graphics.endFill();   
}

As you can see in the figure, the blue edge points of the shadow are not actually on the edge and the circle is too big and the proportions are not right. I looked into the computation of the radii for the shadow and tested that the the plotDisk function really works for an ellipse and not just for circles.