2
\$\begingroup\$

everyone. I am working on a game that relies pretty substantially on "faking" 3D by using different parallaxing and polygonal transform techniques. In a current example, I am working on a fan with tilted blades, that spins along the x-axis. I first establish a base set of 3D points (Vector3) for a blade (basically the UV, but translated to where the blade should be at the bottom of the spin, and with Z values of zero):

#Establish base polygon

var bladePoints = [
    Vector3(-BladeHalfSize.x,CoreRadius,0),
    Vector3(BladeHalfSize.x,CoreRadius,0),
    Vector3(BladeHalfSize.x, (CoreRadius + (BladeHalfSize.y * 2)),0 ),
    Vector3(-BladeHalfSize.x,(CoreRadius + (BladeHalfSize.y * 2)),0 )
]

(CoreRadius is the distance from the axis of spin to the inside of each blade.) This fan has four blades, so I iterate through them with a "for" loop. I clone the base set of points once for each blade, as each has its own unique angle about the fan's axis of rotation. I then iterate through the four points for each set, rotating each along the blade's lengthwise axis to "tilt" it, then again along the fan's axis of rotation to "spin" it:

for f in 4:
    
    var cloneBladePoints = bladePoints.duplicate()
    
    #Rotate the base polygon
    
    for i in cloneBladePoints.size():
        
        #Tilt Blade
        
        cloneBladePoints[i] = cloneBladePoints[i].rotated(Vector3.DOWN,-BladeTilt)
        
        #Rotate Blade about Core
        
        cloneBladePoints[i] = cloneBladePoints[i].rotated(Vector3.RIGHT,curPropVec.angle())
        
        #Apply Parallax
        
        #???
        
        #Flatten to XY plane
        
        cloneBladePoints[i] = Vector2(cloneBladePoints[i].x,cloneBladePoints[i].y)

Each blade is a Polygon2D node,the polygon of which accepts an array of 2D points, so I flatten each of the points along the XY axis. I then apply each blade's set of points to its corresponding blade polygon:

    #Apply Stuff to blade
    
    var blade = $NuSprite/Blades.get_child(f)
    
    blade.polygon = PoolVector2Array(cloneBladePoints)
    
    blade.z_index = 4.0 * curPropVec.y
    
    blade.modulate.v = 0.75 + (curPropVec.y / 4.0)

(rinse/repeat with the other 3 blades)

This all coalesces to do this:

enter image description here

(animated) https://imgur.com/a/V1iJ9yf

My question is this: how do I properly apply parallax to this fan? (fan blades appear bigger when "closer" to the viewer, and smaller when "further away, moving camera to right of fan makes you see its front plane, etc." The system is effectively a one-point perspective, with a vanishing point at the center of the camera. Apologies if this is a duplicate question, I honestly don't even know what I would search for in looking for this solution. Also, my code definitely isn't optimal, I plan on cleaning stuff up once I've gotten it working.

Here's the full code, for reference:

BladeTiltVec = Vector2.RIGHT.rotated(BladeTilt)

var curPropVec = PropAngleVec

#Establish base polygon

var bladePoints = [
    Vector3(-BladeHalfSize.x,CoreRadius,0),
    Vector3(BladeHalfSize.x,CoreRadius,0),
    Vector3(BladeHalfSize.x, (CoreRadius + (BladeHalfSize.y * 2)),0 ),
    Vector3(-BladeHalfSize.x,(CoreRadius + (BladeHalfSize.y * 2)),0 )
]

for f in 4:
    
    var cloneBladePoints = bladePoints.duplicate()
    
    #Rotate the base polygon
    
    for i in cloneBladePoints.size():
        
        #Tilt Blade
        
        cloneBladePoints[i] = cloneBladePoints[i].rotated(Vector3.DOWN,-BladeTilt)
        
        #Rotate Blade about Core
        
        cloneBladePoints[i] = cloneBladePoints[i].rotated(Vector3.RIGHT,curPropVec.angle())
        
        #Apply Parallax
        
        #???
        
        #Flatten to XY plane
        
        cloneBladePoints[i] = Vector2(cloneBladePoints[i].x,cloneBladePoints[i].y)
    
    #Apply Stuff to blade
    
    var blade = $NuSprite/Blades.get_child(f)
    
    blade.polygon = PoolVector2Array(cloneBladePoints)
    
    blade.z_index = 4.0 * curPropVec.y
    
    blade.modulate.v = 0.75 + (curPropVec.y / 4.0)
    
    #Rotate to next blade
    
    curPropVec = curPropVec.rotated(PI/2)

Thank you!

\$\endgroup\$

2 Answers 2

0
\$\begingroup\$

In a "true" 3D game the view frustum is mapped/transformed to screen coordinates. If the camera is positioned at the world origin looking directly along the positive Z axis the calculation simplifies to:

sx = (x / z) * c
sy = (y / z) * c

Where sx & sy are screen coordinates and c is just a constant to determine the FOV/size of the image on screen.

In 3D as the fan moves horizontally across the screen you would see the front or back of the fan respectively as the fan approached the left or right side of the screen.

how do I properly apply parallax to this fan.

Since you are faking (2.5D) I am not sure there is a definitive "proper" answer to this question.

Instead I would try creating an origin (X,Y & Z) somewhere between the center of the fan and the screen (i.e. moving the origin along the Z axis) such that all Z coordinates for the fan are always positive (> 0) with distances further away from the screen having larger Z values.

You can then use the formulas above to shrink the fan blade (points) as they move away from the camera. You will likely need to tinker with the origin position and the value of c to see what looks right.

However all fans will always have a straight on view (since you are doing a 2.5D render) - you will not see the front and back of the fan.

\$\endgroup\$
0
\$\begingroup\$

Principles

The system is effectively a one-point perspective, with a vanishing point at the center of the camera

This is referred to as a pinhole camera; google this and you should get an AI explanation and the formula.

scalingConstant = 1.0; //or your desired value
xScreen = scalingConstant * xWorld / zCamera
yScreen = scalingConstant * yWorld / zCamera

The size of objects is affected by their distance into the world from the eye / camera z or linearDepth. This is how (voxel) raycasters work, as in the Doom / IDTech 1 and the Novalogic's VoxelSpace engines.

The basic idea is that:

  • Any object at a linearDepth of 1.0 from the camera, is rendered at its regular, unscaled "sprite" size (n divided by 1 is n).
  • If it is further away from the camera than 1.0, it is divided by a value larger than 1, making it appear smaller
  • If it is closer to the camera than 1.0, it will be divided by a fraction, therefore making it appear larger (n divided by 0.5 is 2n, for instance).

This scaling should be applied to each pixel individually, unless a more complex method of rendering is specified, such as rendering scaled corners and then using e.g. an OpenGL texture to render what lies between them.

Changing camera heading i.e. "looking around"

If you intend to allow looking around (changing camera heading), you first need to rotate each object around the camera in by -cameraHeading, then (if there are many objects) render what is in the camera's viewport, which can be determined by view-arc culling rather than view-frustum culling, as we are operating primarily in 2D.

Z-buffer or Depth buffer

Another thing to do is create and use a depth buffer. This is a simple floating point depth array, with one element for every pixel in your viewport in x and y. When rendering multiple objects (your multiple propellers), you will want only to render the pixels of those closest to you, ignoring the rest (i.e. pixels from objects behind do not contribute to the final pixel color). So what we do with the depth buffer is:

//do this every frame when rendering
clearDepthBuffer(depthBuffer); //sets all values to float maximum value i.e. very far away

//...get value of 
for each sprite
    for each pixel of sprite
        if depthBuffer[xScreen, yScreen] > sprite[xSprite, ySprite]
           depthBuffer[xScreen, yScreen] = sprite[xSprite, ySprite]
           colorBuffer[xScreen, yScreen] = sprite[xSprite, ySprite]
        //...where xScreen and xSprite are analogous to one another, just that xSprite takes into account the sprite's offset from the screen origin (0,0).

Therefore, the final pixel rendered will be that of the nearest object to the screen.

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.