Tutorials

PERSPECTIVE

This tutorial is to give you an introduction to using perspective to give a 3D effect to your graphics.

The principle of perpective is that horizontal lines, such as the tops and bottoms of the row of fence posts in the diagram below, appear to converge and meet at a vanishing point (B) at eye-level on the horizon.
perspec_2.PNG  © Barry Andrew 2016 Figure 1.

As you can see from the diagram, another feature of perspective distortion is that even though the fence posts as evenly spaced, they appear to get closer together as they recede into the distance. The tricky bit is that, unlike the vertical shortening, the reduction in horizontal spacing is not even. Thankfully the mathematics is very simple.

The X coordinates

Imagine you are standing in the middle of a road and there is a row of fence posts along the side of the road. In front of you is the screen on to which you want to project the image. (See figure 2)
perspec_1.PNG  © Barry Andrew 2016 Figure 2.

From where you are standing, at position I in the diagram, posts A, B and C will appear to be at positions E, F and G on the screen. If you move the position of I, closer to the screen or closer to the fence, say, then the positions of E, F and G are also going to change. Therefore, when you are creating the image, you need to define the position of I and the spacing between the posts. Given these values, how do we calculate E, F and G?

Consider post B. We use the properties of similar triangles which tells us that

    DF     HI
    --  =  --
    BD     BH

We know the post spacing (BD), the distance from the fence (HI) and the distance to the screen (DH) so it's a simple matter to calculate the x coordinate as

                BD
    DF  =  HI x --
                BH

The Y coordinates

Refering back to figure 1, we know our y coordinates for the post tops are going to lie on the straight line from A (top of first post) to B (vanishing point). The equation for a straight line is Y = MX + C, where M is the gradient of the line and C is the y value where x = 0. If (xa , ya) and (xb , yb) are the positions of A and B,

             ( yb - ya )
     Y   =   -----------  X    +   ya
             ( xb - xa )

Substituting our x values for each post into the above gives the y coordinates for the tops of the posts. A similar calculation for the line through the bottom of the posts will give the corresponding y values for the post bottoms.

Experimental Application

The code below is for a small PHP application that will let you play with the various parameters so you can see the how each affects the resulting perspective distortion. Try it

Screenshot application screenshot  © Barry Andrew 2016

<?php
session_start();

$defaults = [   
            'vpx' => 200,
            'vpy' => 60,
            'dist'=> 100,
            'space'=>100,
            'ptop' =>100,
            'num' =>15
            ];
$ht = 200;
$wid = 300;

foreach ($defaults as $dn => $dv) {
    if (isset($_GET[$dn]) && ctype_digit($_GET[$dn]) && $_GET[$dn]>0) {
        $_SESSION[$dn] = $_GET[$dn];
    }
    elseif (!isset($_SESSION[$dn])) {
        $_SESSION[$dn] = $dv;
    }
}

$image = "<svg width='$wid' height='400' viewBox='(0 0 $wid 400)'>\n";
    //
    // image background
    //
$h = 200 - $_SESSION['vpy'];
$image .= "<rect x='0' y='0' width='$wid' height='{$_SESSION['vpy']}' fill='#0080FF' />
        <rect x='0' y='{$_SESSION['vpy']}' width='$wid' height='$h' fill='#008000' />
        <path d='M 0 200 L {$_SESSION['vpx']} {$_SESSION['vpy']} L $wid 200 Z' fill='#AF2F00' />
        <rect x='0' y='0' width='10' height='200' fill='#ccc' fill-opacity='0.25' />
        <rect x='0' y='200' width='$wid' height='200' fill='#ccc' />";
    //
    // horizon and vanishing point
    //
$image .= sprintf("<path d='M 0 %2\$d h $wid M %1\$d %2\$d v -3 v 6' stroke='#fff' />",
                $_SESSION['vpx'],$_SESSION['vpy']);
    //
    // eye position
    //
$image .= sprintf("<path d='M %1\$d 200 v 200' stroke='#000' />
                <path d='M 0 %2\$d h $wid' stroke='#000' />
                <circle cx='%1\$d' cy='%2\$d' r='3' stroke='#000' fill='none' />\n",
                $_SESSION['vpx'], $_SESSION['dist']+200);
    //
    // draw the fence posts
    //
for ($i=0; $i<= $_SESSION['num']; $i++) {
    $image .= post($i, $ht);
}
    //                
    // add clickable areas
    //                
$tx = $wid/2;
$ty = 300;
$image .= "<text x='$tx' y='$ty' text-anchor='middle' style='font-size:20pt; fill:#888;'>Eye position</text>\n";
$image .= "<rect x='0' y='0' width='$wid' height='200' id='vp' fill='#fff' opacity='0'/>
           <rect x='0' y='200' width='$wid' height='200' id='eyepos' fill='#fff' fill-opacity='0' />\n";
// close the image                 
$image .= "</svg>\n" ;

function post($n, $iht)
{
    // get the x coordinate of the fence post
    $d1 = $n*$_SESSION['space']+$_SESSION['dist'];
    $d2 = $_SESSION['vpx'];
    $x = $n*$_SESSION['space'] * $d2/$d1;
    
    // get y coord of top of the post
    $m1 = ($_SESSION['vpy']-$_SESSION['ptop'])/$d2;
    $y1 = $m1*$x + $_SESSION['ptop'];
    
    // get y coord of bottom of the post
    $m2 = ($_SESSION['vpy']-$iht)/$d2;
    $y2 = $m2*$x + $iht;
    $y2 -= $y1;
    return "<path d='M $x $y1 v $y2' stroke='#000' stroke-width='1' />\n";
}

?>
<!DOCTYPE html>
<html>
<head>
<title>Perspective</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script type='text/javascript'>
    $().ready(function() {
        
        $("#vp").click(function(e) {
            var x = e.offsetX;
            var y = e.offsetY;
            if (x < 10) {
                location.href = "?ptop="+y;
            }
            else {
                location.href = "?vpy="+y;
            }
        })
        
        $("#eyepos").click(function(e) {
            var x = e.offsetX;
            var y = (e.offsetY);
            location.href = "?dist="+y+"&vpx="+x;
        })
    })
</script>
<style type='text/css'>
body {
    font-family: sans-serif;
    font-size: 9pt;
}
div#im {
    float: left;
}
div#frm {
    float: left;
    margin: 30px;
}
input {
    text-align: right;
}
p {
    width: 300px;
}
</style>
</head>
<body>
<div id='im'>
    <?=$image?>
</div>
<div id='frm'>
    <form action=''>
    <h3>Perspective Parameters</h3>
        <label>Number of fence posts<br>
            <input type='number' name='num' value='<?=$_SESSION['num']?>' size='3'>
        </label><br>
        <label>Fence post spacing<br>
            <input type='number' name='space' value='<?=$_SESSION['space']?>' size='3'>
        </label> <input type='submit' name='btnsub' value='Change'>
        <hr/>
        <p>
        Click on the artistic masterpiece to set the position of the horizon. (Clicking in the narrow zone on the far left sets the height of the fence posts).</p><p> Click in the grey area below the image to set the eye position relative to the fence and the screen.
        </p>
        <br/>
        <label>Fence post top 
            <input type='text' name='ptop' value='<?=$_SESSION['ptop']?>' size='3' readonly='readonly'>
        </label><br>
        <label>Position of horizon 
            <input type='text' name='vpy' value='<?=$_SESSION['vpy']?>' size='3' readonly='readonly'>
        </label><br>
        <label>Eye distance from fence 
            <input type='text' name='vpx' value='<?=$_SESSION['vpx']?>' size='3' readonly='readonly'>
        </label><br>
        <label>Eye distance from screen 
            <input type='text' name='dist' value='<?=$_SESSION['dist']?>' size='3' readonly='readonly'>
        </label>
        <hr>
    </form>
</div>
</body>
</html>