13.0
23 Jun 2024 00:19
//raycast rendering for jazz jackrabbit 2 + (https://jj2.plus)
//by stijn, 2023 (https://www.jazz2online.com)
#pragma name "Raycasting renderer"
// field of view
float fov = TWO_PI / 6; // 60 degrees
// initial direction of the player
float angle = 0;
// speed at which the player can rotate
float turnSpeed = 0.01;
// speed at which the player can move
float moveSpeed = 2.0;
// ray precision (1.0 = pixel perfect, higher=worse)
float stepPrecision = 1.0;
// max distance for a ray (bigger is slower in large levels)
float maxDistance = 1250;
float maxWallHeight = 100.0;
// minimap scale, e.g. 16 = 1/16 scale
int minimapScale = 16;
// these are just constants and assorted things we'll use later, don't change
int wMap = 0;
int hMap = 0;
float planeDistance;
bool playerInit = false;
bool levelInit = false;
float turnX = 0.0;
float turnY = 0.0;
float xMap = 0.0;
float yMap = 0.0;
const int KEY_W = 87;
const int KEY_A = 65;
const int KEY_S = 83;
const int KEY_D = 68;
const int KEY_UP = 38;
const int KEY_DOWN = 40;
const int KEY_LEFT = 37;
const int KEY_RIGHT = 39;
const int KEY_M = 77;
const float PI = 3.14159;
const float HALF_PI = PI / 2;
const float TWO_PI = 6.28318;
bool showMinimap = true;
jjPIXELMAP@ minimap;
int keyDecay = 0;
float max(float one, float two) { return (one > two) ? one : two; }
float min(float one, float two) { return (one < two) ? one : two; }
// we're not controlling the rabbit this time, so keep it fixed in place
void onPlayer(jjPLAYER@ player) {
if(!playerInit) {
xMap = player.xPos;
yMap = player.yPos;
playerInit = true;
}
player.xSpeed = 0;
player.ySpeed = 0;
player.xPos = 0;
player.yPos = 0;
player.frozen = 1;
}
// in onMain, we handle set-up and movement controls
void onMain() {
if(keyDecay > 0) {
keyDecay -= 1;
}
if(!levelInit) {
// determine map size in pixels
hMap = jjLayers[4].height * 32;
wMap = jjLayers[4].width * 32;
// how far is the player from the 'projection screen'?
planeDistance = float(jjResolutionWidth) / 2.0 / tan(fov / 2.0);
// initialise minimap as top-down view of the map, scaled
@minimap = jjPIXELMAP(wMap / minimapScale, hMap / minimapScale);
int mmX = 0;
int mmY = 0;
for(int y = 0; y < hMap; y += minimapScale) {
for(int x = 0; x < wMap; x += minimapScale) {
if(jjMaskedPixel(x, y)) {
minimap[mmX, mmY] = 16;
} else {
minimap[mmX, mmY] = 22;
}
mmX += 1;
}
mmY += 1;
mmX = 0;
}
minimap.save(jjAnimFrames[jjAnimations[jjAnimSets[ANIM::CUSTOM[0]].firstAnim].firstFrame]);
jjConsole("Welcome to the third dimension");
jjConsole("Control the camera with the WASD keys");
jjConsole("Press M to toggle the mini-map (will give you some extra FPS)");
levelInit = true;
}
// WASD controls; W and S move, A and D turn (no strafing)
// mouselook and/or independent movement and rotation could be implemented
// here
if(!(jjKey[KEY_A] && jjKey[KEY_D])) {
if(jjKey[KEY_A]) {
if(angle > 0) {
angle -= turnSpeed;
} else {
angle = TWO_PI;
}
}
if(jjKey[KEY_D]) {
if(angle < TWO_PI) {
angle += turnSpeed;
} else {
angle = 0;
}
}
}
// move forward and backwards
if(jjKey[KEY_W] || jjKey[KEY_S]) {
float xNew = xMap;
float yNew = yMap;
float stepX;
float stepY;
if(angle > (TWO_PI * 0.875) || angle <= (TWO_PI * 0.125)) { // 'up'
stepY = -moveSpeed;
stepX = abs(stepY) * tan(angle);
} else if(angle > (TWO_PI * 0.125) && angle <= (TWO_PI * 0.375)) { // 'right'
stepX = moveSpeed;
stepY = abs(stepX) * tan(angle - HALF_PI);
} else if(angle > (TWO_PI * 0.375) && angle <= (TWO_PI * 0.625)) { // 'down'
stepY = moveSpeed;
stepX = abs(stepY) * tan(-angle - PI);
} else if(angle > (TWO_PI * 0.625) && angle <= (TWO_PI * 0.875)) { // 'left'
stepX = -moveSpeed;
stepY = abs(stepX) * tan(-angle - HALF_PI - PI);
}
if(jjKey[KEY_W]) {
// forward
xNew += stepX;
yNew += stepY;
}
if(jjKey[KEY_S]) {
// backward
xNew -= stepX;
yNew -= stepY;
}
// only allow movement if it doesn't put the player inside a wall
if(!jjMaskedPixel(int(xNew), int(yNew))) {
xMap = xNew;
yMap = yNew;
}
}
// toggle minimap
if(jjKey[KEY_M] && keyDecay == 0) {
showMinimap = !showMinimap;
keyDecay = 25;
}
}
// in onDrawAmmo, we do the rendering
// this is pretty arbitrary, we just need a jjCANVAS handle
bool onDrawAmmo(jjPLAYER@ player, jjCANVAS@ screen) {
if(!levelInit) {
// wait for set-up in onMain to complete
return false;
}
// ceiling
screen.drawRectangle(0, 0, jjResolutionWidth, jjResolutionHeight / 2, 32);
// floor
screen.drawRectangle(0, jjResolutionHeight / 2, jjResolutionWidth, jjResolutionHeight / 2, 64);
// get 'empty' minimap to draw current vision cone on
jjPIXELMAP@ rayMap = jjPIXELMAP(jjAnimFrames[jjAnimations[jjAnimSets[ANIM::CUSTOM[0]].firstAnim].firstFrame]);
// cast some rays
// the ray will be at a different angle for each 'slice' of the wall - we
// use the configured field of view to calculate by how much that angle
// increases for each subsequent slice (i.e. vertical line of pixels)
float fovStep = fov / float(jjResolutionWidth);
float localAngle = angle - (fov / 2) - fovStep; // start on the left
// for each pixel of horizontal resolution, determine if a wall slice
// should be drawn and if yes of what height
for(int x = 0; x < jjResolutionWidth; x += 1) {
localAngle += fovStep;
// normalize angle (does AS have fmod?)
if(localAngle < 0) {
localAngle += TWO_PI;
}
if(localAngle > TWO_PI) {
localAngle -= TWO_PI;
}
float stepX, stepY;
float calcAngle = localAngle + (HALF_PI / 2);
// we cast the ray by determining a horizontal and vertical delta/step,
// and then checking pixels along a path by starting at the player
// position and adjusting the position by the calculated stepX and
// stepY. if performance becomes an issue, increasing the step sizes
// is one way to speed things up - as it is we never increment by more
// than a single pixel for pixel-perfect mapping, but that is probably
// overkill
if(localAngle > (TWO_PI * 0.875) || localAngle <= (TWO_PI * 0.125)) {
// facing 'up'
stepY = -stepPrecision;
stepX = abs(stepY) * tan(localAngle);
} else if(localAngle > (TWO_PI * 0.125) && localAngle <= (TWO_PI * 0.375)) {
// facing 'right'
stepX = stepPrecision;
stepY = abs(stepX) * tan(localAngle - HALF_PI);
} else if(localAngle > (TWO_PI * 0.375) && localAngle <= (TWO_PI * 0.625)) {
// facing 'down'
stepY = stepPrecision;
stepX = abs(stepY) * tan(-localAngle - PI);
} else if(localAngle > (TWO_PI * 0.625) && localAngle <= (TWO_PI * 0.875)) {
// facing 'left'
stepX = -stepPrecision;
stepY = abs(stepX) * tan(-localAngle - HALF_PI - PI);
}
// traverse the ray until we hit the edges of the map or a masked pixel
// traditional raycasters often don't check every pixel but instead use
// an underlying lower-resolution map of 'blocks' to check against - this
// would work well with JJ2's tile system but we have the computing power
// to just check every pixel and it allows using e.g. sloped tiles for
// different wall shapes
float xRay = xMap;
float yRay = yMap;
float distance = 0.0;
// what we want to find out by casting the ray is the distance to the wall
// the distance for each step can thus be determined through pythagoras'
// theorem, since the x and y step sizes form two sides of a triangle and
// the traversed distance is the third side
float stepLength = sqrt(pow(abs(stepX), 2) + pow(abs(stepY), 2));
// default colour
int colour = 24;
while(distance < maxDistance) {
if(xRay < 0 || xRay > wMap || yRay < 0 || yRay > hMap) {
// out of bounds
break;
}
if(jjMaskedPixel(int(xRay), int(yRay))) {
// masked pixel, draw wall
// assume colors are from the first half of the palette - use
// the first color of the relevant gradient so we can use the
// others for hue later
// texture mapping could be implemented here
jjPIXELMAP tile(jjLayers[4].tileGet(int(xRay / 32), int(yRay / 32)));
colour = int(tile[xRay % 32, yRay % 32] / 8) * 8;
break;
} else if(showMinimap) {
// draw vision cone on minimap
rayMap[int(xRay / minimapScale), int(yRay / minimapScale)] = 66;
}
distance += stepLength;
xRay += stepX;
yRay += stepY;
}
if(distance >= maxDistance) {
// nothing within the scanning distance, assume no wall
continue;
}
// correct fisheye effect and determine rendered wall height
// https://www.permadi.com/tutorial/raycast/rayc8.html
distance *= cos(localAngle - angle);
distance = max(0.1, distance); // avoid divide by zero
int wallHeight = int(maxWallHeight / distance * planeDistance);
if(wallHeight > 0) {
// draw the wall slice! actually just a rectangle of the calculated
// height and colour - the 750 is arbitrary here and determines how
// far away something needs to be to be the 'darkest' hue possible
int hue = int(float(min(distance, 750.0) / 750) * 7.0);
screen.drawRectangle(x, (jjResolutionHeight / 2) - (wallHeight / 2), 1, wallHeight, colour + hue);
}
}
// minimap
if(showMinimap) {
// 3x3 rectangle to mark player position
int mmX = int(xMap / minimapScale);
int mmY = int(yMap / minimapScale);
rayMap[mmX - 1, mmY - 1] = 64;
rayMap[mmX - 1, mmY] = 64;
rayMap[mmX - 1, mmY + 1] = 64;
rayMap[mmX, mmY - 1] = 64;
rayMap[mmX, mmY] = 64;
rayMap[mmX, mmY + 1] = 64;
rayMap[mmX + 1, mmY - 1] = 64;
rayMap[mmX + 1, mmY] = 64;
rayMap[mmX + 1, mmY + 1] = 64;
// display as sprite
rayMap.save(jjAnimFrames[jjAnimations[jjAnimSets[ANIM::CUSTOM[0]].firstAnim].firstFrame + 1]);
screen.drawSprite(25, 25, ANIM::CUSTOM[0], 1, 0);
}
return false;
}
Jazz2Online © 1999-INFINITY (Site Credits). We have a Privacy Policy. Jazz Jackrabbit, Jazz Jackrabbit 2, Jazz Jackrabbit Advance and all related trademarks and media are ™ and © Epic Games. Lori Jackrabbit is © Dean Dodrill. J2O development powered by Loops of Fury and Chemical Beats.
Eat your lima beans, Johnny.