#ifndef FDUNGEON_CPP
#define FDUNGEON_CPP
#ifndef FDUNGEON_SEPARATE_COMPILATION
// Perlin noise.
#define STB_PERLIN_IMPLEMENTATION
#include <stb_perlin.h>
// glTF loader.
#define FGLTF_IMPLEMENTATION
// Avatar engine.
#define AVATAR_IMPLEMENTATION
// Terrain engine.
#define TERRA_IMPLEMENTATION
// Particle engine.
#define PARTICLE_IMPLEMENTATION
// Bullet physics.
#define BTRAPVERTEXBUFFER_IMPLEMENTATION
#endif
// Use doubles to match the version of Bullet produced by
// build_cmake_pybullet_double.sh
#ifndef BT_USE_DOUBLE_PRECISION
#define BT_USE_DOUBLE_PRECISION
#endif
#include <btBulletDynamicsCommon.h>
#include <BulletCollision/Gimpact/btGImpactCollisionAlgorithm.h>
#include <btrapCopypasta.h>
#include <btrapVertexBuffer.h>
// Implementations in libs.cpp.
#include <stb_image.h>
#include <tm_json.h>
#include <fgltf.h>
#include <avatar.h>
#include <terra.h>
#include <particle.h>
#include <limits.h>
#include <stdint.h>
#include <cmath>
#include <functional>
#include <iostream>
#include <map>
#include <string>
#include <utility>
#include <vector>
namespace fdungeon {
// Agent type flags.
static const int
TYPE_AGENT = 1,
TYPE_VEHICLE = 2,
TYPE_DYNAMIC = 4,
TYPE_STATIC = 8;
// An inventory instance of an item, such as a weapon or a trinket.
struct InventoryInstance {
std::string weapon; // A string to look up the weapon if applicable.
std::string trinket; // A string to look up the trinket if applicable.
int64_t count; // How many of the item the agent owns.
double clip; // The amount of "energy" contained within the instance.
};
// A global weapon definition.
struct Weapon {
std::map<std::string,std::string> names;
std::map<std::string,std::string> descriptions;
ParticleTemplate particleFire;
std::string ammo;
std::string soundFire;
std::string soundReload;
std::string soundHit;
std::string soundRicochet;
fgl::Texture *icon;
fgltf::Scene *model;
linalg::vec<double,3> translation;
linalg::vec<double,3> zoomTranslation;
linalg::vec<float,2> animationFire;
linalg::vec<float,2> animationIdle;
linalg::vec<float,2> animationReload;
linalg::vec<float,2> fireSpotCone;
fgl::Color fireSpotColor;
GLfloat fireSpotRange;
double fireSpotDuration;
double blockDamage;
double damage;
double impact;
double speed;
double range;
double clipMax;
double zoom;
double recoil;
bool recoilCompensator;
bool thirdPersonPitch;
int64_t price;
};
// A global trinket definition.
struct Trinket {
std::map<std::string,std::string> names;
std::map<std::string,std::string> descriptions;
fgl::Texture *icon;
fgltf::Scene *model;
double clipMax;
int64_t price;
};
// An instance of an agent, such as a player, character, vehicle, or item.
struct Agent {
std::string nodeName; // The name of the node that spawned the agent. First position to dereference as a userPointer. (NOT SERIALIZED)
std::map<std::string,std::string> names; // The human-readable localized names of the agent.
int type; // The category of the agent's physics.
int team; // The category of the agent's objectives.
std::string dialogue; // The script that triggers when a player interacts with the agent.
std::string onDeath; // The script that triggers when the agent dies.
std::string playlist; // The playlist that will play when a player enters the vehicle.
std::string weapon;
std::string quickWeapon;
std::string soundExplode;
std::string soundCollisionDamage;
std::string soundJump;
std::string soundHeal;
std::vector<InventoryInstance> inventory; // The agent's inventory.
std::map<std::string,double> ammo; // Ammo counts (to be displayed as properties of weapons and avoid inventory clutter).
std::map<std::string,double> skills; // "Skills" affect combat. Can also be used to store quest progress.
std::string dungeonFile; // The last dungeon the agent was seen in.
std::string avatarFile;
Avatar avatar;
std::string modelFile;
fgltf::Scene *model;
std::string modelDestroyedFile;
fgltf::Scene *modelDestroyed;
int modelLoop; // Defaults to -1 (loop infinite). Set to 0 to disable looping.
btCollisionShape *colShape;
btRigidBody *rigidBody;
btSphereShape *footSphere; // Used for foot spherecasts.
btVehicleRaycaster *vehicleRaycaster;
btRaycastVehicle *raycastVehicle;
linalg::mat<double,4,4> spawnMat; // Spawn-time transformation of the node that spawned the agent. (NOT SERIALIZED)
linalg::vec<double,3> angle; // View angle. Y component affects movement.
linalg::vec<double,3> scale; // Size of capsule or box collider.
linalg::vec<double,3> driverEyeTranslation; // Driver eye translation for vehicles. (Serialized as current pos for non-vehicles.)
linalg::vec<double,3> lastVel; // Last linear velocity of the agent. (NOT SERIALIZED)
linalg::vec<float,2> lightCone; // The agent's toggleable headlamp.
fgl::Color lightColor;
GLfloat lightRange;
double offsetYaw; // The rotational offset applied to upright agents.
double health; // Health of agent. When health <= 0, agent is destroyed or transformed in some way.
double maxHealth; // Health cannot exceed this amount.
double waterHealthRate; // Health added per second when submerged in water.
double mass; // Mass of agent.
double restitution; // Restitution of agent.
double passiveBrakeForce; // Brake force when not accelerating.
double brakeForce; // Brake force when actively braking.
double thrustForce; // Directional force per second for vehicles.
double suspensionStiffness; // Wheel suspension stiffness. Defaults to 30.
double suspensionDamping; // Wheel suspension damping. Defaults to 2.3.
double suspensionCompression; // Wheel suspension compression. Defaults to 4.4.
double wheelFriction; // Wheel friction.
double wheelFloor; // Vertical offset of bottom of wheels.
double turn; // Front wheel angle.
double turnAcceleration; // Turn acceleration in radians/s^2.
double maxTurn; // Maximum turning speed in radians/s.
double maxLean; // Horizontal leaning in radians at max turn.
double acceleration; // Acceleration in m/s^2.
double topSpeed; // Top speed in m/s.
double walkSpeed; // The speed of a walking agent.
double eyeHeight; // View and weapon raycast offset from bottom of capsule.
double footLength; // Length of foot ray from bottom of capsule.
double jumpVel; // Vertical velocity applied to the agent when it jumps.
double terminalVel; // Vertical terminal velocity. TODO: Don't ignore this value!
double collisionDamageThreshold; // The minimum velocity to cause collision damage.
double collisionDamageFactor; // The number multiplied by velocity to produce collision damage.
double sittingCollisionDamageFactor; // The collision damage factor when sitting in/on an agent (such as a vehicle).
double standing; // Timer indicating whether or not the foot sphere was recently intersecting an object. (NOT SERIALIZED)
double weaponBob; // Vertical offset of weapon based on walking. (NOT SERIALIZED)
double weaponCooldown; // Firing is possible when weaponCooldown is 0. (NOT SERIALIZED)
double weaponFireSpotCooldown; // Fire spot is on when this is > 0. (NOT SERIALIZED)
float weaponTime; // The animation timestamp for the weapon. (NOT SERIALIZED)
bool lightOn; // Whether or not the toggleable headlamp is on. (NOT SERIALIZED)
bool visible; // Whether or not the agent will be drawn.
Agent *parentAgent, *driverAgent, *prev, *next;
};
// A map partition.
struct Partition {
std::string mapFile;
fgltf::Scene mapScene;
terra::Terrain terrain;
linalg::vec<double,3> translation;
double friction;
double restitution;
};
// A node as defined in JSON and by the in-game editor.
struct WorldNode {
std::string name;
linalg::vec<double,4> rotation;
linalg::vec<double,3> translation;
btRigidBody *rigidBody; // A world node's rigidbody must be null except when shared with an agent.
};
// A cubemap.
struct Cube {
std::string paths;
fgl::Texture texture;
};
class Dungeon {
public:
/********** READY FLAG **********/
bool ready = false;
/********** PATHS **********/
// The path to load dungeon files from.
std::string dungeonPath = ".";
// The path to load agent files from.
std::string agentPath = ".";
// The path to load avatar files from.
std::string avatarPath = ".";
// The path to load particle files from.
std::string particlePath = ".";
// The current dungeon JSON file name. Initially empty.
std::string dungeonFile = "";
/********** MISC. STRINGS **********/
std::string onLoad = "";
/********** MESHES **********/
// A mesh that is used for animated water.
fgl::Mesh waterMesh = {};
// An instance buffer used to draw water tiles.
fgl::InstanceBuffer waterBuffer = {};
/********** WEAPONS AND TRINKETS **********/
// A table of weapons for lookup by name.
std::map<std::string,Weapon> weapons;
// A table of trinkets for lookup by name.
std::map<std::string,Trinket> trinkets;
/********** AGENTS **********/
// A doubly linked list of agents.
Agent *agents = nullptr;
// A pointer to the player agent.
Agent *playerAgent = nullptr;
// A pointer to the agent to highlight.
Agent *targetAgent = nullptr;
// An empty agent.
Agent defaultAgent = Agent();
// The JSON file containing the player agent definition.
std::string playerAgentFile = "player_default.json";
// If playerJSON is non-empty, the player will be spawned with
// the properties herein rather than those of playerAgentFile.
// To avoid bugs, do not set this directly. Use the parameter
// to loadWorld instead.
std::string playerJSON = "";
// The amount the player's camera is zoomed.
// AI agents do nothing like this.
double playerZoom = 1.0;
// The amount the weapon behaves as if the camera is zoomed.
// This controls animations and works in VR.
double playerWeaponZoom = 1.0;
// The Euler angles to offset the player's on-screen weapon by.
linalg::vec<double,3> playerOffsetAngle = { 0.0, 0.0, 0.0 };
// Determines whether or not to asjust the player's weapon lighting.
// This is set automatically on a frame-by-frame basis.
bool playerFireSpotOn = false;
/********** PARTITIONS **********/
std::vector<Partition> partitions = {};
// Indices and distances of the nearest partitions to the camera.
std::vector<std::pair<size_t,double>> nearestPartitions = {};
// Determines the maximum distance for partitions to be drawn.
double maxPartitionDistance = 500.0;
/********** WORLD NODES **********/
std::vector<WorldNode> worldNodes = {};
// The world node the player is currently interacting with.
size_t currentWorldNode = SIZE_MAX;
/********** WAYPOINTS **********/
// Waypoints and their distances from the current agent.
std::vector<linalg::vec<double,4>> waypoints = {};
/********** PARTICLES **********/
std::map<std::string,ParticleServer> particleServers = {};
/********** BULLET **********/
// Variables used for the Bullet physics simulation.
btDefaultCollisionConfiguration* collisionConfiguration = nullptr;
btCollisionDispatcher* dispatcher = nullptr;
btBroadphaseInterface* overlappingPairCache = nullptr;
btSequentialImpulseConstraintSolver* solver = nullptr;
btDiscreteDynamicsWorld* dynamicsWorld = nullptr;
/********** GLOBAL NUMERIC PROPERTIES **********/
// The positional offset of the third-person camera, when enabled.
linalg::vec<double,3> thirdPersonCameraTranslation{0.0, 0.0, 2.0};
// The minimum and maximum pitch for the camera in third-person mode.
double thirdPersonCameraMinPitch = -1.570796;
double thirdPersonCameraMaxPitch = 1.570796;
// The yaw of the camera about the player in third-person mode.
double thirdPersonYawOffset = 0.0;
// A good default. Let's call it 5/10 mouse sensitivity.
// Slider factor would be 0.0003.
double mouseSpeed = 0.0015;
// Vertical FoV. Default is 70 degrees.
double verticalField = 70.0 * 0.01745;
// Vertical orthographic visible range. Default is +/- 15 meters.
double orthoRadius = 15.0;
// The amount of time the simulation has been running.
// Used to control animations.
double age = 0.0;
// The cloud scroll position.
double cloudScroll = 0.5;
// Distance between the top of the tallest wave and waterLevel.
// Automatically set and animated at runtime.
float waterWaveHeight = 0.0f;
// Pen thickness relative to the screen.
float outlineThickness = 0.0015f;
/********** SHADOW PROPERTIES **********/
// The width and height of the directional shadow map.
unsigned int shadowResolution = 1750;
// The width and height of the "low" directional shadow map.
// This is used for scenes that don't benefit from high-res shadows.
unsigned int shadowResolutionLow = 512;
// The radius of the shadow frustum in meters.
double shadowScale = 40.0;
// The near and far clip of the shadow camera.
double shadowNearClip = 1.0;
double shadowFarClip = 400.0;
// The offset from the player to the shadow camera (light source).
linalg::vec<double,3> shadowOffset = { 0.0, 200.0, 0.0 };
/********** NUMERIC MAP CONFIGURATION PROPERTIES **********/
// The maximum distance at which aiFrameCallback is called.
// TODO: Why does the max AI distance often end up as 75m?
double maxAIDistance = 500.0;
// Gravity in m/s^2.
double gravity = 9.8;
// The maximum distance at which punctual lights are drawn.
double lightRange = 300.0;
// Scale the brightness of cubemap illumination on lit objects.
// This is combined with ambient_light and can be used to
// compensate for the low dynamic range of cubemaps.
double lightBrightness = 1.0;
// Fog brightness. This is generally lower than illumination
// intensity.
double fogBrightness = 0.6;
// Fog density from 0.0 to 1.0. Mathematically, this kind of
// exponential fog never converges to 100% density.
double fogDensity = 0.01;
// Near and far clipping planes.
double nearClip = 0.06;
double farClip = 400.0;
// Global water level on the Y axis.
double waterLevel = 0.0;
// Scale of wave amplitude.
double waterAmplitude = 1.0;
// Water animation speed. 1.0 is fast.
double waterSpeed = 1.0;
// The Y value below which everything is in shadow and nothing may pass.
// Disabled by default until worldFloor is set in JSON.
double worldFloor = 0.0;
// The speed at which the cloud texture scrolls.
double cloudSpeed = 0.011;
/********** BOOLEANS **********/
// Third-person view.
bool thirdPerson = false;
// Top-down view.
bool topDown = false;
// High-quality rendering. TODO: Non-PBR is currently non-functional.
bool pbr = true;
// High-resolution shadowing.
bool shadowHighRes = true;
// Outline rendering.
bool outline = true;
// Map punctual lights always on.
bool lights_always_on = false;
// Draw the sky cube.
bool draw_cube = false;
// Draw clouds.
bool draw_clouds = false;
/********** VR VARIABLES **********/
// The center matrix before it is transformed by other functions.
// The copy is made by Dungeon::updateStereoCamera().
linalg::mat<double,4,4> viewMatCenter = linalg::identity;
// Stereoscopic matrices generated by Dungeon::updateStereoCamera().
linalg::mat<double,4,4> viewMatL = linalg::identity;
linalg::mat<double,4,4> viewMatR = linalg::identity;
// Inter-pupilary distance in meters. 63mm is Monado's default.
double ipd = 0.063;
// Eye convergence angle in radians. For VR this should be 0.
// For oldschool 3D glasses, 0.02 is about 2 meters and looks good.
double stereoAngle = 0.0;
/********** MATRICES **********/
// viewMat and projMat are generated by Dungeon::simulate.
linalg::mat<double,4,4> viewMat = linalg::identity;
linalg::mat<double,4,4> projMat = linalg::identity;
// Initial light matrix used for shadow texel snapping.
linalg::mat<double,4,4> lightOriginMat = linalg::identity;
/********** EPHEMERAL PUNCTUAL LIGHT PROPERTIES **********/
std::vector<GLfloat> pointLights;
std::vector<GLfloat> spotLights;
/********** FGL PROPERTIES **********/
// Pointer to a Framebuffer struct. MUST be set from your code
// at runtime.
fgl::Framebuffer *frameBuf;
// Amount to scale the framebuffer viewport for Dynamic
// Resolution Scaling.
float frameBufScale = 1.0f;
// The framebuffers for variance shadow mapping. Reused to avoid
// needless allocations.
fgl::Framebuffer shadowBufMultisample = fgl::newFramebuffer;
fgl::Framebuffer shadowBuf = fgl::newFramebuffer;
fgl::Framebuffer shadowBufTemp = fgl::newFramebuffer;
fgl::Framebuffer shadowBufLowMultisample = fgl::newFramebuffer;
fgl::Framebuffer shadowBufLow = fgl::newFramebuffer;
fgl::Framebuffer shadowBufLowTemp = fgl::newFramebuffer;
// The world ambient light color.
fgl::Color ambient_light = fgl::newColor;
// The outline colors.
fgl::Color outline_regular = { 0.0f, 0.0f, 0.0f, 1.0f };
fgl::Color outline_highlight = { 1.0f, 1.0f, 1.0f, 1.0f };
// Amount to scale [clamped] emissiveFactor by. Workaround for a
// glTF format bug, like the elusive emissiveStrength extension:
// https://github.com/KhronosGroup/glTF/pull/1994
GLfloat emissive_scale = 1.0f;
// Underwater color with alpha. Defaults to match standard
// shadow color (dependent on choice of shaders).
fgl::Color underwater_color = { 0.0f, 0.0f, 0.0f, 0.43f };
// Water surface texture file.
std::string waterTextureName;
// Water surface texture.
fgl::Texture waterTexture = fgl::newTexture;
// Clouds texture file.
std::string cloudsTextureName;
// Clouds texture.
fgl::Texture cloudsTexture = fgl::newTexture;
// Default impact particle file.
std::string defaultImpactName;
// Default impact particle template.
ParticleTemplate defaultImpact = ParticleTemplate();
// Default blood particle file.
std::string defaultBloodName;
// Default impact particle template.
ParticleTemplate defaultBlood = ParticleTemplate();
// The radiance cubemap.
fgl::Texture radiance = fgl::newTexture;
// The diffuse irradiance cubemap generated from the radiance
// cubemap.
fgl::Texture irradiance = fgl::newTexture;
// The framebuffer for the diffuse irradiance cubemap. Reused to
// avoid needless allocations.
fgl::Framebuffer irradianceBuf = fgl::newFramebuffer;
/********** FOPEN OVERRIDE **********/
FILE *(*ModFopen)(const char*,const char*) = fopen;
/********** CALLBACK FUNCTIONS **********/
// Called when playerAgent->rigidBody intersects a portal.
void (*portalCallback)(std::string);
// Called when an agent takes falling damage.
void (*collisionDamageCallback)(Agent*,double);
// Called every simulation frame when an agent is active.
bool (*aiFrameCallback)(Agent*,double);
/********** MEMBER FUNCTIONS **********/
unsigned char *stbiLoadModdable( std::string filePath, int *w, int *h, int *channels );
fgl::Texture loadSprite( const std::string &filePath );
fgl::Pipeline loadShaders( std::string vertFile, std::string fragFile, std::vector<std::string> samplers );
void setCube( std::vector<std::string> imageFiles );
ParticleTemplate loadParticles( std::string filePath );
void unloadWorld();
void initGraphics( std::string shaderPath );
void initPhysics();
void loadWorld(
std::string dungeonPath,
std::string agentPath,
std::string avatarPath,
std::string particlePath,
std::string filePath,
const std::string &playerJSON = "",
bool append = false );
std::string serializePartition( Partition &p );
std::string serializeWorld();
void appendWeapons( std::string weaponPath, std::string filePath );
void appendTrinkets( std::string trinketPath, std::string filePath );
fgltf::Scene *getModel( const std::string &filePath );
void setAgentModel( Agent *a, const std::string &modelFile );
Agent *addAgent( btVector3 position, btScalar mass, btScalar restitution, linalg::vec<double,3> scale, linalg::vec<double,3> boxTranslation = {0,0,0}, bool box = false );
Agent *parseAgent( const std::string &text, std::string filePath, linalg::mat<double,4,4> spawnMat );
Agent *loadAgent( std::string filePath, linalg::mat<double,4,4> spawnMat );
void loadSavedPlayerAgent( std::string filePath );
std::string serializeAgent( Agent *a );
double getRotationDelta( double a, double b, double speed );
void rotateAgentConstrained( Agent *a, linalg::vec<double,3> rotation );
void rotateAgentUnconstrained( Agent *a, linalg::vec<double,3> rotation );
bool moveAgent( Agent *a, linalg::vec<double,3> translation, double standingTime, double d );
void positionAgent( Agent *a, linalg::vec<double,3> position );
void accelerateVehicle( Agent *a, double force );
void brakeVehicle( Agent *a, double brakeForce );
void rightVehicle( Agent *a );
bool isVehicleUpright( Agent *a );
double getVehicleLean( Agent *a );
double getVehiclePitch( Agent *a );
double getVehicleYaw( Agent *a );
void setAgentCollisions( Agent *a, bool enable );
void setAgentCrouch( Agent *a, bool enable, double d );
void unsitAgent( Agent *a );
linalg::vec<double,3> getNodeLookAt( linalg::mat<double,4,4> m );
btTransform getAgentPhysicsTransform( Agent *a );
linalg::mat<double,4,4> getAgentPoseMat( Agent *a );
std::pair<linalg::vec<double,3>,linalg::vec<double,3>> getAgentGazeRay( Agent *a, double range, bool pitch = true );
std::pair<void*,double> getAgentGazeHit( Agent *a, double range );
std::string getAgentLocalizedName( Agent *a, const std::string &languageCode );
std::string getWeaponLocalizedName( Weapon *w, const std::string &languageCode );
void removeAgent( Agent *a, bool conservative = false );
Agent *rayImpact( Agent *a, std::pair<linalg::vec<double,3>,linalg::vec<double,3>> ray, double impact, double damage, std::string defenseSkill = "def" );
std::pair<bool,Agent*> agentFireWeapon( Agent *a, std::string weapon = "", std::string attackSkill = "atk", std::string defenseSkill = "def" );
void agentAddWeapon( Agent *a, const std::string &weapon );
void agentReloadWeapon( Agent *a, const std::string &weapon );
bool agentCanReloadCurrentWeapon( Agent *a );
void moveMouse( double x, double y );
void prepareLightBuffers( double d );
void simulateWater( float wscale, double d );
void simulate( double maxDraw, double d, std::string defenseSkill = "def" );
void updateMonoCamera();
void updateStereoCamera();
fgl::Pipeline useDungeonPipeline( bool skinPass, bool shadowPass );
void drawSolid( double maxDraw, bool shadowPass = false, int agent_types = 0xFFFFFFFF );
void drawWater( bool shadowPass = false );
void drawTransparent();
void draw( double maxDraw, bool allowTransparent = true, fgl::Mesh *nodeMesh = nullptr, int agent_types = 0xFFFFFFFF );
private:
void addAgentGazeSpot( Agent *a, fgl::Color color, GLfloat range = 0.0f, GLfloat innerConeAngle = 0.0f, GLfloat outerConeAngle = 0.785f );
void sceneNodeHandler( fgltf::SceneNode &node, linalg::mat<double,4,4> &m );
void worldNodeHandler( WorldNode &node, linalg::mat<double,4,4> &m );
void updateCallback( double d );
void steerVehicle( Agent *a );
};
// Implementation begins here.
#ifndef FDUNGEON_SEPARATE_COMPILATION
fgl::Pipeline skyPipeline = fgl::newPipeline;
std::vector<std::string> cloudsSamplers = { "u_texture" };
fgl::Pipeline cloudsPipeline = fgl::newPipeline;
fgl::Pipeline outlinePipeline = fgl::newPipeline;
fgl::Pipeline particleShadowPipeline = fgl::newPipeline;
fgl::Pipeline shadowPipeline = fgl::newPipeline;
std::vector<std::string> shadowBlurSamplers = { "u_shadow" };
fgl::Pipeline shadowBlurPipeline = fgl::newPipeline;
std::vector<std::string> dungeonSamplers = { "u_albedo", "u_metallicRoughness", "u_emissive", "u_shadow", "u_irradiance" };
fgl::Pipeline dungeonPipeline = fgl::newPipeline;
std::vector<std::string> pbrDungeonSamplers = { "u_albedo", "u_metallicRoughness", "u_emissive", "u_shadow", "u_irradiance" };
fgl::Pipeline pbrDungeonPipeline = fgl::newPipeline;
std::vector<std::string> skinSamplers = { "u_albedo", "u_metallicRoughness", "u_emissive", "u_shadow", "u_irradiance" };
fgl::Pipeline skinPipeline = fgl::newPipeline;
std::vector<std::string> waterSamplers = { "u_albedo", "u_shadow", "u_radiance" };
fgl::Pipeline waterPipeline = fgl::newPipeline;
std::vector<std::string> waterInstanceSamplers = { "u_albedo", "u_shadow" };
fgl::Pipeline waterInstancePipeline = fgl::newPipeline;
std::map<std::string,fgl::Texture> sprites = {};
std::map<std::string,fgltf::Scene> models = {};
std::map<std::string,terra::Heightmap> heightmaps = {};
std::vector<Cube> cubes;
unsigned char *Dungeon::stbiLoadModdable( std::string filePath, int *w, int *h, int *channels ){
unsigned char *imgData = nullptr;
FILE *file = ModFopen( filePath.c_str(), "rb" );
if( file ){
imgData = stbi_load_from_file( file, w, h, channels, 0 );
fclose( file );
}
return imgData;
}
fgl::Texture Dungeon::loadSprite( const std::string &filePath ){
fgl::Texture result = {};
int width, height, channels;
void *img =
stbiLoadModdable( filePath, &width, &height, &channels );
if( img )
result = fgl::loadTexture( img, width, height, channels );
else
std::cerr << "Failed to load " << filePath << std::endl;
if( result.success ){
// Disable texture wrapping.
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
}
stbi_image_free( img );
return result;
}
fgl::Pipeline Dungeon::loadShaders( std::string vertFile, std::string fragFile, std::vector<std::string> samplers ){
std::string
shader_files[2] = { vertFile, fragFile },
shader_text[2] = {};
for( size_t i = 0; i < 2; i++ ){
FILE* file = ModFopen( shader_files[i].c_str(), "rb" );
if( !file ){
fprintf( stderr, "Failed to open %s\n", shader_files[i].c_str() );
return fgl::newPipeline;
}
// TODO: Profile this against ftell/fseek.
char buf[4096];
while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
shader_text[i] += std::string( buf, len );
}
fclose( file );
}
return fgl::loadPipeline( shader_text[0].c_str(), shader_text[1].c_str(), samplers );
}
// Load and set a Cube, never loading the same Cube twice in the same
// process.
void Dungeon::setCube( std::vector<std::string> imageFiles ){
if( imageFiles.size() != 6 ){
return;
}
std::string paths = "";
for( size_t i = 0; i < imageFiles.size(); i++ ){
// Store image names as a concatenated string.
paths += imageFiles[i] + ";";
// Convert image names to full paths.
imageFiles[i] = dungeonPath + "/" + imageFiles[i];
}
for( Cube &cube : cubes ){
if( cube.paths == paths ){
radiance = cube.texture;
irradianceBuf =
fgl::getIrradianceFramebuffer( radiance, irradianceBuf );
irradiance = fgl::getFramebufferTexture( irradianceBuf );
return;
}
}
// No Cube found, so load the named files.
int width, height, channels;
std::vector<GLvoid*> faces = {};
bool err = false;
for( size_t i = 0; i < 6; i++ ){
faces.push_back( stbiLoadModdable( imageFiles[i], &width, &height, &channels ) );
if( !faces.back() ){
std::cerr << "Failed to load " << imageFiles[i] << std::endl;
err = true;
break;
}
}
if( !err ){
cubes.push_back( {
paths,
fgl::loadCubemap( faces, width, height, channels )
} );
radiance = cubes.back().texture;
irradianceBuf =
fgl::getIrradianceFramebuffer( radiance, irradianceBuf );
irradiance = fgl::getFramebufferTexture( irradianceBuf );
}
for( GLvoid* f : faces ){
stbi_image_free( f );
}
}
ParticleTemplate Dungeon::loadParticles( std::string filePath ){
std::string text = "";
// Load the particle definition.
filePath = particlePath + "/" + filePath;
FILE *file = ModFopen( filePath.c_str(), "rb" );
if( file ){
char buf[4096];
while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
text += std::string( buf, len );
}
fclose( file );
}else{
std::cerr << "Failed to open " << filePath << std::endl;
}
return ParticleTemplate( text );
}
//
void Dungeon::unloadWorld(){
// Safety hatch to stop an already-unloaded world from being
// unloaded twice.
if( !ready )
return;
ready = false;
// Clear the onLoad string.
onLoad = "";
// Unload the partitions and associated data.
for( auto &p : partitions ){
fgltf::UnloadScene( p.mapScene );
terra::FreeTerrain( p.terrain, dynamicsWorld );
}
partitions.clear();
// Unload world nodes.
worldNodes.clear();
// Unload waypoints.
waypoints.clear();
// Unload the water texture.
waterTextureName = "";
if( waterTexture.success )
fgl::freeTexture( waterTexture );
// Unload the clouds texture.
cloudsTextureName = "";
if( cloudsTexture.success )
fgl::freeTexture( cloudsTexture );
// Remove the agents.
while( agents ){
Agent *a = agents->next;
if( agents )
removeAgent( agents );
agents = a;
}
playerAgent = nullptr;
// Remove the world floor.
worldFloor = 0.0;
// Clean up any remaining physics data.
if( dynamicsWorld ){
// Remove the rigidbodies from the dynamics world and delete them.
for( int i = dynamicsWorld->getNumCollisionObjects() - 1; i >= 0; i-- ){
btCollisionObject* obj = dynamicsWorld->getCollisionObjectArray()[i];
btRigidBody* body = btRigidBody::upcast( obj );
if( body && body->getMotionState() ){
delete body->getMotionState();
}
dynamicsWorld->removeCollisionObject( obj );
delete obj;
}
delete dynamicsWorld;
}
if( solver )
delete solver;
if( overlappingPairCache )
delete overlappingPairCache;
if( dispatcher )
delete dispatcher;
if( collisionConfiguration )
delete collisionConfiguration;
}
// You MUST call this before calling loadWorld.
void Dungeon::initGraphics( std::string shaderPath ){
// Shader file prefix.
std::string pre = shaderPath + "/";
// Load the sky pipeline.
if( !skyPipeline.success )
skyPipeline = loadShaders( pre + "sky.vert", pre + "sky.frag", {} );
// Load the clouds pipeline.
if( !cloudsPipeline.success )
cloudsPipeline = loadShaders( pre + "clouds.vert", pre + "clouds.frag", cloudsSamplers );
// Load the outline shader pipeline.
if( !outlinePipeline.success )
outlinePipeline = loadShaders( pre + "outline.vert", pre + "outline.frag", {} );
// Load the particle shadow shader pipeline.
if( !particleShadowPipeline.success )
particleShadowPipeline = loadShaders( pre + "particle_shadow.vert", pre + "particle_shadow.frag", {} );
// Load the shadow shader pipelines.
if( !shadowPipeline.success )
shadowPipeline = loadShaders( pre + "shadow.vert", pre + "shadow.frag", {} );
if( !shadowBlurPipeline.success )
shadowBlurPipeline = loadShaders( pre + "shadow_blur.vert", pre + "shadow_blur.frag", shadowBlurSamplers );
// Load the dungeon shader pipelines.
//if( !dungeonPipeline.success )
// dungeonPipeline = loadShaders( pre + "dungeon_sky_low.vert", pre + "dungeon_sky_low.frag", dungeonSamplers );
if( !pbrDungeonPipeline.success )
pbrDungeonPipeline = loadShaders( pre + "dungeon_sky_high.vert", pre + "dungeon_sky_high.frag", pbrDungeonSamplers );
if( !skinPipeline.success )
skinPipeline = loadShaders( pre + "dungeon_skin.vert", pre + "dungeon_skin.frag", skinSamplers );
// Load the water shader pipelines.
if( !waterPipeline.success )
waterPipeline = loadShaders( pre + "water.vert", pre + "water.frag", waterSamplers );
if( !waterInstancePipeline.success )
waterInstancePipeline = loadShaders( pre + "water_instance.vert", pre + "water_instance.frag", waterInstanceSamplers );
// Create the framebuffers for the variance shadow map.
if( !shadowBufMultisample.success )
shadowBufMultisample = fgl::createFramebuffer( shadowResolution, shadowResolution, false, GL_RG32F, 2 );
if( !shadowBuf.success )
shadowBuf = fgl::createFramebuffer( shadowResolution, shadowResolution, false, GL_RG32F );
if( !shadowBufTemp.success )
shadowBufTemp = fgl::createFramebuffer( shadowResolution, shadowResolution, false, GL_RG32F );
// Create the framebuffers for the "low" shadow map.
if( !shadowBufLowMultisample.success )
shadowBufLowMultisample = fgl::createFramebuffer( shadowResolutionLow, shadowResolutionLow, false, GL_RG32F, 4 );
if( !shadowBufLow.success )
shadowBufLow = fgl::createFramebuffer( shadowResolutionLow, shadowResolutionLow, false, GL_RG32F );
if( !shadowBufLowTemp.success )
shadowBufLowTemp = fgl::createFramebuffer( shadowResolutionLow, shadowResolutionLow, false, GL_RG32F );
}
// Called internally.
// Initialize the Bullet physics engine. Will overwrite pointers, so
// make sure to call unloadWorld() first if initPhysics() was called
// before.
void Dungeon::initPhysics(){
///collision configuration contains default setup for memory, collision setup. Advanced users can create their own configuration.
collisionConfiguration = new btDefaultCollisionConfiguration();
///use the default collision dispatcher. For parallel processing you can use a diffent dispatcher (see Extras/BulletMultiThreaded)
dispatcher = new btCollisionDispatcher( collisionConfiguration );
btGImpactCollisionAlgorithm::registerAlgorithm( dispatcher );
///btDbvtBroadphase is a good general purpose broadphase. You can also try out btAxis3Sweep.
overlappingPairCache = new btDbvtBroadphase();
///the default constraint solver. For parallel processing you can use a different solver (see Extras/BulletMultiThreaded)
solver = new btSequentialImpulseConstraintSolver;
dynamicsWorld = new btDiscreteDynamicsWorld( dispatcher, overlappingPairCache, solver, collisionConfiguration );
}
// Load or append a world from a JSON file.
void Dungeon::loadWorld(
std::string dungeonPath,
std::string agentPath,
std::string avatarPath,
std::string particlePath,
std::string filePath,
const std::string &playerJSON,
bool append ){
if( !append ){
// Clear the previously loaded content.
unloadWorld();
// Default lightRange to 300.
lightRange = 300.0;
// Default lights_always_on to false.
lights_always_on = false;
// Default shadowHighRes to true.
shadowHighRes = true;
// Default maxPartitionDistance to 500.
maxPartitionDistance = 500.0;
// Reset the third-person yaw offset.
thirdPersonYawOffset = 0.0;
}
if( !ready ){
// Start physics.
initPhysics();
}
// The map file can be anywhere. All dungeon files will be loaded
// from dungeonPath. All agent files will be loaded from agentPath.
// All particle files will be loaded from particlePath
this->dungeonPath = dungeonPath;
this->agentPath = agentPath;
this->avatarPath = avatarPath;
this->particlePath = particlePath;
// playerJSON is set every call.
// Use the parameter rather than direct access to avoid bugs.
this->playerJSON = playerJSON;
// Load the map definition.
FILE *file = ModFopen( filePath.c_str(), "rb" );
if( !file ){
std::cerr << "Failed to open map definition " << filePath << std::endl;
fgl::end();
return;
}
std::string text = "";
char buf[4096];
while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
text += std::string( buf, len );
}
fclose( file );
// Parse the map definition. jsonAllocateDocument with minimal features.
auto allocatedDocument = jsonAllocateDocument( text.c_str(), text.size(), JSON_READER_REASONABLE );
if( allocatedDocument.document.error.type != JSON_OK ){
std::cerr
<< "JSON error: "
<< jsonGetErrorString( allocatedDocument.document.error.type ) << " at line "
<< allocatedDocument.document.error.line << ":"
<< allocatedDocument.document.error.column << " of "
<< filePath << "\n" << std::endl;
jsonFreeDocument( &allocatedDocument );
return;
}
auto &info = allocatedDocument.document.root;
// Convert JsonStringView to std::string.
auto viewToString = []( JsonStringView str ){
return std::string( str.data, str.size );
};
// Populate the class with map parameters.
if( info["partitions"] ){
// Load partitions.
for( auto part : info["partitions"].getArray() ){
partitions.push_back( {} );
auto &p = partitions.back();
bool has_map = false;
if( part["map"] ){
has_map = true;
p.mapFile = viewToString( part["map"].getString() );
p.mapScene = fgltf::LoadScene( ModFopen, dungeonPath + "/" + p.mapFile );
fgltf::AddSceneToDynamicsWorld(
p.mapScene,
dynamicsWorld,
fgltf::PHYSICS_TRIHARD,
&defaultAgent
);
}
if( part["translation"] ){
auto t = part["translation"].getArray();
if( t.size() >= 3 ){
p.translation = {
t[0].getDouble(),
t[1].getDouble(),
t[2].getDouble()
};
}
}
p.friction = 1.0;
if( part["friction"] ){
p.friction = part["friction"].getDouble();
}
p.restitution = 0.0;
if( part["restitution"] ){
p.restitution = part["restitution"].getDouble();
}
terra::Terrain t{};
t.mesh = fgl::newMesh;
t.needSync = false;
// Check if the partition has terrain.
if( part["terrain"] ){
JsonValue terrainDef = part["terrain"];
// Build the terrain.
if( terrainDef["texture"] ){
t.textureName =
viewToString( terrainDef["texture"].getString() );
}
if( terrainDef["heightColors"] ){
t.heightColors =
viewToString( terrainDef["heightColors"].getString() );
}
t.tileSizeX = 1.0;
t.tileSizeZ = 1.0;
if( terrainDef["tileSizeX"] ){
t.tileSizeX = terrainDef["tileSizeX"].getDouble();
}
if( terrainDef["tileSizeZ"] ){
t.tileSizeZ = terrainDef["tileSizeZ"].getDouble();
}
t.textureTileX = 1.0f;
t.textureTileZ = 1.0f;
if( terrainDef["textureTileX"] ){
t.textureTileX = terrainDef["textureTileX"].getFloat();
}
if( terrainDef["textureTileZ"] ){
t.textureTileZ = terrainDef["textureTileZ"].getFloat();
}
t.resX = 1;
t.resZ = 1;
if( terrainDef["resX"] ){
t.resX = terrainDef["resX"].getUInt();
}
if( terrainDef["resZ"] ){
t.resZ = terrainDef["resZ"].getUInt();
}
if( terrainDef["heights"] ){
std::string text;
for( auto &line : terrainDef["heights"].getArray() ){
text += viewToString( line.getString() );
}
t.heights = terra::StringToHeights( text );
size_t terrainSize = (t.resX + 1) * (t.resZ + 1);
if( t.heights.size() < terrainSize )
t.heights.resize( terrainSize );
}
t.heightmapName = "";
t.heightmap = nullptr;
if( terrainDef["heightmap"] ){
JsonValue heightmapDef = terrainDef["heightmap"];
if( heightmapDef["file"] ){
// Get the heightmap parameters.
float pixelScale = 1.0f;
if( heightmapDef["pixelScale"] ){
pixelScale = heightmapDef["pixelScale"].getFloat();
}
float whiteHeight = 1.0f;
if( heightmapDef["whiteHeight"] ){
whiteHeight = heightmapDef["whiteHeight"].getFloat();
}
float noisePeriod = 1.0f;
if( heightmapDef["noisePeriod"] ){
noisePeriod = heightmapDef["noisePeriod"].getFloat();
}
float noiseAmplitude = 1.0f;
if( heightmapDef["noiseAmplitude"] ){
noiseAmplitude = heightmapDef["noiseAmplitude"].getFloat();
}
// Load the heightmap.
std::string heightmapFile = viewToString(heightmapDef["file"].getString());
if(heightmaps.find(heightmapFile) == heightmaps.end()){
const char *file_path = std::string(dungeonPath + "/" + heightmapFile).c_str();
FILE *file = ModFopen(file_path, "rb");
if(file){
heightmaps[heightmapFile] =
terra::LoadHeightmapFromFile(file, pixelScale, whiteHeight, noisePeriod, noiseAmplitude);
fclose(file);
t.heightmapName = heightmapFile;
t.heightmap = &heightmaps[heightmapFile];
}else{
fprintf(stderr, "Failed to open %s\n", file_path);
heightmaps[heightmapFile] = {};
}
}
}
}
}
p.terrain = t;
if( p.terrain.resX > 0 && p.terrain.resZ > 0 ){
// Once Bullet gets involved, it's only safe to call
// BakeTerrainMesh in-place. Do not copy terrains with
// physics.
terra::BakeTerrainMesh(p.terrain, dynamicsWorld, p.translation, p.friction, p.restitution);
}
if( has_map ){
auto modelMat = linalg::translation_matrix( p.translation );
std::function<void(int,linalg::mat<double,4,4>,size_t)> runTree =
[&]( int n, linalg::mat<double,4,4> m, size_t depth ){
depth++;
if( depth > fgltf::max_tree_depth ){
fprintf( stderr, "Maximum tree depth exceeded. Possible malformed scene.\n" );
return;
}
fgltf::SceneNode &node = p.mapScene.nodes[n];
m = linalg::mul(
m,
linalg::translation_matrix( node.translation ),
linalg::rotation_matrix( node.rotation ),
linalg::scaling_matrix( node.scale )
);
// Call the handler function.
sceneNodeHandler( node, m );
for( int c : node.children ){
runTree( c, m, depth );
}
};
for( int r : p.mapScene.roots ){
runTree( r, modelMat, 0 );
}
// Update limb positions and parameters in the physics world.
fgltf::UpdateSceneDynamics(
p.mapScene,
modelMat,
p.friction,
p.restitution
);
}
}
ready = true;
}
if( info["nodes"] ){
// Load world nodes.
for( auto &node : info["nodes"].getArray() ){
worldNodes.push_back( {} );
auto &n = worldNodes.back();
if( node["name"] ){
n.name = viewToString( node["name"].getString() );
}
n.rotation = {0.0, 0.0, 0.0, 1.0};
if( node["rotation"] ){
auto r = node["rotation"].getArray();
if( r.size() >= 4 ){
n.rotation = {
r[0].getDouble(),
r[1].getDouble(),
r[2].getDouble(),
r[3].getDouble()
};
}else if( r.size() == 3 ){
// Accept Euler radian angles as an alternative
// representation of rotations.
// Fragile; for quick hacks and testing only.
n.rotation = fgl::eulerToQuat(
r[0].getDouble(),
r[1].getDouble(),
r[2].getDouble()
);
}
}
if( node["translation"] ){
auto t = node["translation"].getArray();
if( t.size() >= 3 ){
n.translation = {
t[0].getDouble(),
t[1].getDouble(),
t[2].getDouble()
};
}
}
// Spawn any object specified by the node.
auto m = linalg::pose_matrix( n.rotation, n.translation );
worldNodeHandler( n, m );
}
}
if( info["maxPartitionDistance"] ){
maxPartitionDistance = info["maxPartitionDistance"].getDouble();
}
if( info["maxAIDistance"] ){
maxAIDistance = info["maxAIDistance"].getDouble();
}
if( info["gravity"] ){
gravity = info["gravity"].getDouble();
}
if( info["lightRange"] ){
lightRange = info["lightRange"].getDouble();
}
if( info["lightBrightness"] ){
lightBrightness = info["lightBrightness"].getDouble();
}
if( info["lightsAlwaysOn"] ){
lights_always_on = info["lightsAlwaysOn"].getBool();
}
if( info["shadowHighRes"] ){
shadowHighRes = info["shadowHighRes"].getBool();
}
if( info["fogBrightness"] ){
fogBrightness = info["fogBrightness"].getDouble();
}
if( info["fogDensity"] ){
fogDensity = info["fogDensity"].getDouble();
}
if( info["emissiveScale"] ){
emissive_scale = info["emissiveScale"].getFloat();
}
if( info["nearClip"] ){
nearClip = info["nearClip"].getDouble();
}
if( info["farClip"] ){
farClip = info["farClip"].getDouble();
}
if( info["waterLevel"] ){
waterLevel = info["waterLevel"].getDouble();
}
if( info["waterAmplitude"] ){
waterAmplitude = info["waterAmplitude"].getDouble();
}
if( info["waterSpeed"] ){
waterSpeed = info["waterSpeed"].getDouble();
}
if( info["underwaterColor"] ){
auto c = info["underwaterColor"].getArray();
if( c.size() >= 4 ){
underwater_color = {
c[0].getFloat(),
c[1].getFloat(),
c[2].getFloat(),
c[3].getFloat()
};
}
}
if( info["waterTexture"] ){
if( waterTexture.success ){
// Unload existing water texture.
fgl::freeTexture( waterTexture );
}
waterTextureName =
viewToString( info["waterTexture"].getString() );
std::string fileName = dungeonPath + "/" + waterTextureName;
int width, height, channels;
void *img =
stbiLoadModdable( fileName, &width, &height, &channels );
if( img )
waterTexture = fgl::loadTexture( img, width, height, channels );
else
std::cerr << "Failed to load " << fileName << std::endl;
stbi_image_free( img );
}
if( info["worldFloor"] ){
worldFloor = info["worldFloor"].getDouble();
// Add the world floor plane.
Agent *a = addAgent(
btVector3( 0.0, worldFloor, 0.0 ),
btScalar( 0.0 ),
btScalar( 0.0 ),
{ 99999.0, 0.0, 99999.0 },
{ 0.0, 0.0, 0.0 },
true
);
// Work around auto-cleanup (???) TODO: FIXME
a->health = 1.0;
a->maxHealth = 1.0;
// Prevent the floor plane from registering as an agent to raycasts.
a->nodeName = "";
a->visible = false;
// Assign the floor plane's agent type.
a->type = TYPE_STATIC;
// Assign the spawn matrix.
a->spawnMat =
linalg::translation_matrix( linalg::vec<double,3>( 0.0, worldFloor, 0.0 ) );
// Set up physics properties.
btVector3 localInertia( 0, 0, 0 );
a->colShape->calculateLocalInertia( 0.0, localInertia );
btTransform startTransform;
startTransform.setIdentity();
startTransform.setOrigin( btVector3( 0.0, worldFloor, 0.0 ) );
//using motionstate is recommended, it provides interpolation capabilities, and only synchronizes 'active' objects
btDefaultMotionState* myMotionState = new btDefaultMotionState( startTransform );
btRigidBody::btRigidBodyConstructionInfo rbInfo( 0.0, myMotionState, a->colShape, localInertia );
// Use high friction for better traction.
rbInfo.m_friction = 1.0;
// Prevent agents from bouncing.
rbInfo.m_restitution = 0.0;
// Create the rigid body.
a->rigidBody = new btRigidBody( rbInfo );
// Set the agent's userPointer to the struct.
a->rigidBody->setUserPointer( a );
dynamicsWorld->addRigidBody( a->rigidBody );
}
if( info["cube"] ){
std::vector<std::string> imageFiles = {};
for( auto &fileName : info["cube"].getArray() ){
imageFiles.push_back( viewToString( fileName.getString() ) );
}
setCube( imageFiles );
}
if( info["drawCube"] ){
draw_cube = info["drawCube"].getBool();
}
// Clouds.
if( info["cloudsTexture"] ){
if( cloudsTexture.success ){
// Unload existing clouds texture.
fgl::freeTexture( cloudsTexture );
}
cloudsTextureName =
viewToString( info["cloudsTexture"].getString() );
std::string fileName = dungeonPath + "/" + cloudsTextureName;
int width, height, channels;
void *img =
stbiLoadModdable( fileName, &width, &height, &channels );
if( img )
cloudsTexture = fgl::loadTexture( img, width, height, channels );
else
std::cerr << "Failed to load " << fileName << std::endl;
stbi_image_free( img );
}
if( info["cloudSpeed"] ){
cloudSpeed = info["cloudSpeed"].getDouble();
}
if( info["drawClouds"] ){
draw_clouds = info["drawClouds"].getBool();
}
// Particles.
if( info["defaultImpact"] ){
defaultImpactName = viewToString(info["defaultImpact"].getString());
defaultImpact = loadParticles(defaultImpactName);
particleServers[defaultImpact.texture] = ParticleServer();
}
if( info["defaultBlood"] ){
defaultBloodName = viewToString(info["defaultBlood"].getString());
defaultBlood = loadParticles(defaultBloodName);
particleServers[defaultBlood.texture] = ParticleServer();
}
// onLoad (for use in user code)
if( onLoad.length() < 1 && info["onLoad"] ){
onLoad = viewToString(info["onLoad"].getString());
}
// Get appends (for pre-generated procedural content, downloads, etc.)
std::vector<std::string> appends;
if( info["appends"] ){
for( auto &s : info["appends"].getArray() ){
appends.push_back( viewToString( s.getString() ) );
}
}
// Unload the JSON document.
jsonFreeDocument( &allocatedDocument );
if( !append ){
dungeonFile = filePath;
age = 0.0;
}
// Append the appends.
for( std::string &f : appends ){
loadWorld( dungeonPath, agentPath, avatarPath, particlePath, dungeonPath + "/" + f, playerJSON, true );
}
// Create a player if one does not already exist.
if( !playerAgent )
playerAgent = loadAgent( playerAgentFile, linalg::identity );
}
std::string Dungeon::serializePartition( Partition &p ){
std::string str = "{\n";
if( p.mapFile.length() > 0 ){
str += "\t\t\t\"map\": \"" + p.mapFile + "\",\n";
}
if( p.terrain.mesh.success ){
str += "\t\t\t\"terrain\": "
+ terra::SerializeTerrain( p.terrain ) + ",\n";
}
str += "\t\t\t\"translation\": ["
+ std::to_string(p.translation.x) + ", "
+ std::to_string(p.translation.y) + ", "
+ std::to_string(p.translation.z) + "],\n";
str += "\t\t\t\"friction\": " + std::to_string(p.friction) + ",\n";
str += "\t\t\t\"restitution\": " + std::to_string(p.restitution) + "\n";
str += "\t\t}";
return str;
}
std::string Dungeon::serializeWorld(){
std::string str = "{\n";
str += "\t\"partitions\": [\n";
for( size_t i = 0; i < partitions.size(); i++ ){
str += "\t\t" + serializePartition( partitions[i] );
if( i < partitions.size() - 1 ) str += ",";
str += "\n";
}
str += "\t],\n";
str += "\t\"nodes\": [\n";
for( size_t i = 0; i < worldNodes.size(); i++ ){
auto &n = worldNodes[i];
str += "\t\t{\n\t\t\t\"name\": \"" + n.name + "\",\n";
str += "\t\t\t\"rotation\": ["
+ std::to_string(n.rotation.x) + ", "
+ std::to_string(n.rotation.y) + ", "
+ std::to_string(n.rotation.z) + ", "
+ std::to_string(n.rotation.w) + "],\n";
str += "\t\t\t\"translation\": ["
+ std::to_string(n.translation.x) + ", "
+ std::to_string(n.translation.y) + ", "
+ std::to_string(n.translation.z) + "]\n";
str += "\t\t}";
if( i < worldNodes.size() - 1 ) str += ",";
str += "\n";
}
str += "\t],\n";
str += "\t\"gravity\": " + std::to_string(gravity) + ",\n";
str += "\t\"emissiveScale\": " + std::to_string(emissive_scale) + ",\n";
str += "\t\"lightBrightness\": " + std::to_string(lightBrightness) + ",\n";
str += "\t\"lightsAlwaysOn\": ";
str += (lights_always_on ? "true,\n" : "false,\n");
str += "\t\"shadowHighRes\": ";
str += (shadowHighRes ? "true,\n" : "false,\n");
str += "\t\"fogBrightness\": " + std::to_string(fogBrightness) + ",\n";
str += "\t\"fogDensity\": " + std::to_string(fogDensity) + ",\n";
str += "\t\"nearClip\": " + std::to_string(nearClip) + ",\n";
str += "\t\"farClip\": " + std::to_string(farClip) + ",\n";
str += "\t\"underwaterColor\": ["
+ std::to_string(underwater_color.r) + ", "
+ std::to_string(underwater_color.g) + ", "
+ std::to_string(underwater_color.b) + ", "
+ std::to_string(underwater_color.a) + "],\n";
if( waterTextureName.length() > 0 )
str += "\t\"waterTexture\": \"" + waterTextureName + "\",\n";
str += "\t\"waterLevel\": " + std::to_string(waterLevel) + ",\n";
str += "\t\"waterAmplitude\": " + std::to_string(waterAmplitude) + ",\n";
str += "\t\"waterSpeed\": " + std::to_string(waterSpeed) + ",\n";
str += "\t\"worldFloor\": " + std::to_string(worldFloor) + ",\n";
str += "\t\"drawCube\": ";
str += (draw_cube ? "true,\n" : "false,\n");
if( cloudsTextureName.length() > 0 )
str += "\t\"cloudsTexture\": \"" + cloudsTextureName + "\",\n";
str += "\t\"cloudSpeed\": " + std::to_string(cloudSpeed) + ",\n";
str += "\t\"drawClouds\": ";
str += (draw_clouds ? "true,\n" : "false,\n");
if( defaultImpactName.length() > 0 )
str += "\t\"defaultImpact\": \"" + defaultImpactName + "\",\n";
if( defaultBloodName.length() > 0 )
str += "\t\"defaultBlood\": \"" + defaultBloodName + "\",\n";
if( onLoad.length() > 0 )
str += "\t\"onLoad\": \"" + onLoad + "\"\n";
str += "}\n";
return str;
}
void Dungeon::appendWeapons( std::string weaponPath, std::string filePath ){
// Load the weapons file.
FILE *file = ModFopen( filePath.c_str(), "rb" );
if( !file ){
std::cerr << "Failed to open " << filePath << std::endl;
return;
}
std::string text = "";
char buf[4096];
while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
text += std::string( buf, len );
}
fclose( file );
// Parse the map definition.
auto allocatedDocument = jsonAllocateDocumentEx( text.c_str(), text.size(), JSON_READER_JSON5 );
if( allocatedDocument.document.error.type != JSON_OK ){
std::cerr
<< "JSON error: "
<< jsonGetErrorString( allocatedDocument.document.error.type ) << " at line "
<< allocatedDocument.document.error.line << ":"
<< allocatedDocument.document.error.column << " of "
<< filePath << "\n" << std::endl;
jsonFreeDocument( &allocatedDocument );
return;
}
// Convert JsonStringView to std::string.
auto viewToString = []( JsonStringView str ){
return std::string( str.data, str.size );
};
// Loop through the weapon definitions.
for( auto &weapDef : allocatedDocument.document.root.getObject() ){
std::string weapName = viewToString( weapDef.name );
// Find localized names and descriptions for the weapon,
// starting with English.
std::map<std::string,std::string> localizedNames, localizedBlurbs;
localizedNames["en"] = weapName;
localizedBlurbs["en"] = "";
for( auto &prop : weapDef.value.getObject() ){
std::string propName = viewToString( prop.name );
if( propName.length() > 5 && propName.substr( 0, 5 ) == "name_" ){
localizedNames[propName.substr( 5 )] =
viewToString( prop.value.getString() );
}
if( propName.length() > 12 && propName.substr( 0, 12 ) == "description_" ){
localizedBlurbs[propName.substr( 12 )] =
viewToString( prop.value.getString() );
}
}
ParticleTemplate particleFire{};
if( weapDef.value["particle_fire"] ){
particleFire =
loadParticles(viewToString(weapDef.value["particle_fire"].getString()));
particleServers[particleFire.texture] = ParticleServer();
}
std::string ammo;
if( weapDef.value["ammo"] ){
ammo = viewToString( weapDef.value["ammo"].getString() );
}
std::string soundFire;
if( weapDef.value["sound_fire"] ){
soundFire =
viewToString( weapDef.value["sound_fire"].getString() );
}
std::string soundReload;
if( weapDef.value["sound_reload"] ){
soundReload =
viewToString( weapDef.value["sound_reload"].getString() );
}
std::string soundHit;
if( weapDef.value["sound_hit"] ){
soundHit =
viewToString( weapDef.value["sound_hit"].getString() );
}
std::string soundRicochet;
if( weapDef.value["sound_ricochet"] ){
soundRicochet =
viewToString( weapDef.value["sound_ricochet"].getString() );
}
fgl::Texture *icon = nullptr;
if( weapDef.value["icon"] ){
// Load the icon.
std::string iconFile = viewToString( weapDef.value["icon"].getString() );
if( sprites.find( iconFile ) == sprites.end() )
sprites[iconFile] = loadSprite( weaponPath + "/" + iconFile );
icon = &sprites[iconFile];
}
fgltf::Scene *model = nullptr;
if( weapDef.value["model"] ){
// Load the weapon model.
std::string modelFile = viewToString( weapDef.value["model"].getString() );
if( models.find( modelFile ) == models.end() )
models[modelFile] = fgltf::LoadScene( ModFopen, weaponPath + "/" + modelFile );
model = &models[modelFile];
}
linalg::vec<double,3> translation( 0.0, 0.0, 0.0 );
if( weapDef.value["translation"] ){
auto t = weapDef.value["translation"].getArray();
if( t.size() >= 3 ){
translation = {
t[0].getDouble(),
t[1].getDouble(),
t[2].getDouble()
};
}
}
linalg::vec<double,3> zoomTranslation( 0.0, 0.0, 0.0 );
if( weapDef.value["zoom_translation"] ){
auto t = weapDef.value["zoom_translation"].getArray();
if( t.size() >= 3 ){
zoomTranslation = {
t[0].getDouble(),
t[1].getDouble(),
t[2].getDouble()
};
}
}
// Animation ranges.
linalg::vec<float,2> animationFire( 0.0f, 0.0f );
if( weapDef.value["animation_fire"] ){
auto a = weapDef.value["animation_fire"].getArray();
if( a.size() >= 2 ){
animationFire = {
a[0].getFloat(),
a[1].getFloat()
};
}
}
linalg::vec<float,2> animationIdle( 0.0f, 0.0f );
if( weapDef.value["animation_idle"] ){
auto a = weapDef.value["animation_idle"].getArray();
if( a.size() >= 2 ){
animationIdle = {
a[0].getFloat(),
a[1].getFloat()
};
}
}
linalg::vec<float,2> animationReload( 0.0f, 0.0f );
if( weapDef.value["animation_reload"] ){
auto a = weapDef.value["animation_reload"].getArray();
if( a.size() >= 2 ){
animationReload = {
a[0].getFloat(),
a[1].getFloat()
};
}
}
// Fire spot.
linalg::vec<float,2> fireSpotCone( 0.0f, 0.785f );
if( weapDef.value["fire_spot_cone"] ){
auto a = weapDef.value["fire_spot_cone"].getArray();
if( a.size() >= 2 ){
fireSpotCone = {
a[0].getFloat(),
a[1].getFloat()
};
}
}
fgl::Color fireSpotColor = fgl::newColor;
if( weapDef.value["fire_spot_color"] ){
auto a = weapDef.value["fire_spot_color"].getArray();
if( a.size() >= 4 ){
fireSpotColor = {
a[0].getFloat(),
a[1].getFloat(),
a[2].getFloat(),
a[3].getFloat()
};
}
}
GLfloat fireSpotRange = 0.0f;
if( weapDef.value["fire_spot_range"] ){
fireSpotRange = weapDef.value["fire_spot_range"].getFloat();
}
double fireSpotDuration = 0.0;
if( weapDef.value["fire_spot_duration"] ){
fireSpotDuration = weapDef.value["fire_spot_duration"].getDouble();
}
// Stats.
double blockDamage = 0.0;
if( weapDef.value["block_damage"] ){
blockDamage = weapDef.value["block_damage"].getDouble();
}
double damage = 0.0;
if( weapDef.value["damage"] ){
damage = weapDef.value["damage"].getDouble();
}
double impact = 0.0;
if( weapDef.value["impact"] ){
impact = weapDef.value["impact"].getDouble();
}
double speed = 1.0;
if( weapDef.value["speed"] ){
speed = weapDef.value["speed"].getDouble();
}
double range = 1.9;
if( weapDef.value["range"] ){
range = weapDef.value["range"].getDouble();
}
double clipMax = 0.0;
if( weapDef.value["clip_max"] ){
clipMax = weapDef.value["clip_max"].getDouble();
}
double zoom = 1.0;
if( weapDef.value["zoom"] ){
zoom = weapDef.value["zoom"].getDouble();
}
double recoil = 0.0;
if( weapDef.value["recoil"] ){
recoil = weapDef.value["recoil"].getDouble();
}
bool recoilCompensator = false;
if( weapDef.value["recoil_compensator"] ){
recoilCompensator = weapDef.value["recoil_compensator"].getBool();
}
bool thirdPersonPitch = false;
if( weapDef.value["third_person_pitch"] ){
thirdPersonPitch = weapDef.value["third_person_pitch"].getBool();
}
int64_t price = 0;
if( weapDef.value["price"] ){
price = weapDef.value["price"].getInt64();
}
weapons[weapName] = {
localizedNames,
localizedBlurbs,
particleFire,
ammo,
soundFire,
soundReload,
soundHit,
soundRicochet,
icon,
model,
translation,
zoomTranslation,
animationFire,
animationIdle,
animationReload,
fireSpotCone,
fireSpotColor,
fireSpotRange,
fireSpotDuration,
blockDamage,
damage,
impact,
speed,
range,
clipMax,
zoom,
recoil,
recoilCompensator,
thirdPersonPitch,
price
};
}
jsonFreeDocument( &allocatedDocument );
}
void Dungeon::appendTrinkets( std::string trinketPath, std::string filePath ){
// Load the trinkets file.
FILE *file = ModFopen( filePath.c_str(), "rb" );
if( !file ){
std::cerr << "Failed to open " << filePath << std::endl;
return;
}
std::string text = "";
char buf[4096];
while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
text += std::string( buf, len );
}
fclose( file );
// Parse the map definition.
auto allocatedDocument = jsonAllocateDocumentEx( text.c_str(), text.size(), JSON_READER_JSON5 );
if( allocatedDocument.document.error.type != JSON_OK ){
std::cerr
<< "JSON error: "
<< jsonGetErrorString( allocatedDocument.document.error.type ) << " at line "
<< allocatedDocument.document.error.line << ":"
<< allocatedDocument.document.error.column << " of "
<< filePath << "\n" << std::endl;
jsonFreeDocument( &allocatedDocument );
return;
}
// Convert JsonStringView to std::string.
auto viewToString = []( JsonStringView str ){
return std::string( str.data, str.size );
};
// Loop through the trinket definitions.
for( auto &trinketDef : allocatedDocument.document.root.getObject() ){
std::string trinketName = viewToString( trinketDef.name );
// Find localized names and descriptions for the trinket,
// starting with English.
std::map<std::string,std::string> localizedNames, localizedBlurbs;
localizedNames["en"] = trinketName;
localizedBlurbs["en"] = "";
for( auto &prop : trinketDef.value.getObject() ){
std::string propName = viewToString( prop.name );
if( propName.length() > 5 && propName.substr( 0, 5 ) == "name_" ){
localizedNames[propName.substr( 5 )] =
viewToString( prop.value.getString() );
}
if( propName.length() > 12 && propName.substr( 0, 12 ) == "description_" ){
localizedBlurbs[propName.substr( 12 )] =
viewToString( prop.value.getString() );
}
}
fgl::Texture *icon = nullptr;
if( trinketDef.value["icon"] ){
// Load the icon.
std::string iconFile = viewToString( trinketDef.value["icon"].getString() );
if( sprites.find( iconFile ) == sprites.end() )
sprites[iconFile] = loadSprite( trinketPath + "/" + iconFile );
icon = &sprites[iconFile];
}
fgltf::Scene *model = nullptr;
if( trinketDef.value["model"] ){
// Load the weapon model.
std::string modelFile = viewToString( trinketDef.value["model"].getString() );
if( models.find( modelFile ) == models.end() )
models[modelFile] = fgltf::LoadScene( ModFopen, trinketPath + "/" + modelFile );
model = &models[modelFile];
}
// Stats.
double clipMax = 0.0;
if( trinketDef.value["clip_max"] ){
clipMax = trinketDef.value["clip_max"].getDouble();
}
int64_t price = 0;
if( trinketDef.value["price"] ){
price = trinketDef.value["price"].getInt64();
}
trinkets[trinketName] = {
localizedNames,
localizedBlurbs,
icon,
model,
clipMax,
price
};
}
jsonFreeDocument( &allocatedDocument );
}
fgltf::Scene *Dungeon::getModel( const std::string &filePath ){
if(models.find(filePath) == models.end())
models[filePath] =
fgltf::LoadScene(ModFopen, agentPath + "/" + filePath);
return &models[filePath];
}
void Dungeon::setAgentModel( Agent *a, const std::string &modelFile ){
if(ready && a){
a->modelFile = modelFile;
if(dynamicsWorld && a->type == fdungeon::TYPE_STATIC && a->model){
// Remove the rigidbodies from the dynamics world and delete them.
for(int i = dynamicsWorld->getNumCollisionObjects() - 1; i >= 0; i--){
btCollisionObject* obj =
dynamicsWorld->getCollisionObjectArray()[i];
if(obj && obj->getUserPointer() == a){
btRigidBody* body = btRigidBody::upcast(obj);
if(body && body->getMotionState())
delete body->getMotionState();
dynamicsWorld->removeCollisionObject(obj);
delete obj;
}
}
}
a->model = getModel(modelFile);
if(dynamicsWorld && a->type == fdungeon::TYPE_STATIC && a->model){
// Set up the new static model.
fgltf::AddSceneToDynamicsWorld(
*a->model,
dynamicsWorld,
fgltf::PHYSICS_TRIHARD,
&defaultAgent
);
// Set the user pointer for all of the agent's sub-nodes
// that have rigid bodies.
for(auto &n : a->model->nodes){
if(n.rigidBody) n.rigidBody->setUserPointer(a);
}
// Update limb positions and parameters in the physics world.
fgltf::UpdateSceneDynamics(
*a->model,
a->spawnMat,
1.0, // TODO: friction
a->restitution
);
}
}
}
//
Agent *Dungeon::addAgent( btVector3 position, btScalar mass, btScalar restitution, linalg::vec<double,3> scale, linalg::vec<double,3> boxTranslation, bool box ){
// Allocate an Agent object and push it to the back of the list.
Agent *a = new Agent;
if( !a ) return nullptr;
if( agents ){
Agent *agents_ptr = agents;
while( agents_ptr->next )
agents_ptr = agents_ptr->next;
agents_ptr->next = a;
a->prev = agents_ptr;
}else{
agents = a;
a->prev = nullptr;
}
a->next = nullptr;
// Parent and driver start as null.
a->parentAgent = nullptr;
a->driverAgent = nullptr;
// Initialize the agent's node name to "agent".
a->nodeName = "agent";
// The "agent" type has no special meaning.
a->type = TYPE_AGENT;
// Team 0 is the default. These are user-defined.
a->team = 0;
// Model loop -1 is the default.
a->modelLoop = -1;
bool isStatic = ( mass == 0.0 );
// Add a collision shape to the agent.
// By default, only vehicles get boxes.
a->footSphere = nullptr;
if( box ){
linalg::vec<double,3> halfExtents = scale * 0.5;
std::vector<linalg::vec<double,3>> points = {
{ halfExtents.x, halfExtents.y, halfExtents.z },
{ halfExtents.x, halfExtents.y, -halfExtents.z },
{ halfExtents.x, -halfExtents.y, halfExtents.z },
{ halfExtents.x, -halfExtents.y, -halfExtents.z },
{ -halfExtents.x, halfExtents.y, halfExtents.z },
{ -halfExtents.x, halfExtents.y, -halfExtents.z },
{ -halfExtents.x, -halfExtents.y, halfExtents.z },
{ -halfExtents.x, -halfExtents.y, -halfExtents.z }
};
btConvexHullShape *shape = new btConvexHullShape();
for( auto p : points ){
p += boxTranslation;
shape->addPoint( btVector3( p.x, p.y, p.z ) );
}
a->colShape = shape;
}else if( isStatic ){
a->colShape = nullptr;
a->rigidBody = nullptr;
}else{
// Dynamic capsule agent.
a->colShape = new btCapsuleShape( scale.x * 0.5, scale.y - scale.x );
a->footSphere = new btSphereShape( 0.1 ); // TODO: Customizable foot radius.
}
if( !isStatic ){
btVector3 localInertia( 0, 0, 0 );
a->colShape->calculateLocalInertia( mass, localInertia );
btTransform startTransform;
startTransform.setIdentity();
startTransform.setOrigin( position );
//using motionstate is recommended, it provides interpolation capabilities, and only synchronizes 'active' objects
btDefaultMotionState* myMotionState = new btDefaultMotionState( startTransform );
btRigidBody::btRigidBodyConstructionInfo rbInfo( mass, myMotionState, a->colShape, localInertia );
// Use high friction for better traction.
rbInfo.m_friction = 1.0;
rbInfo.m_restitution = restitution;
// Create the rigid body.
a->rigidBody = new btRigidBody( rbInfo );
// Lock the rotation for capsule colliders.
if( !box ) a->rigidBody->setAngularFactor( btScalar( 0 ) );
// Set the agent's userPointer to the struct.
a->rigidBody->setUserPointer( a );
dynamicsWorld->addRigidBody( a->rigidBody );
}
a->inventory = {};
a->ammo = {};
a->skills = {};
a->angle = { 0.0, 0.0, 0.0 };
a->scale = scale;
a->offsetYaw = 0.0;
// Static agents are invincible unless health and maxHealth are set.
a->health = isStatic ? 0.0 : 100.0;
a->maxHealth = a->health;
a->waterHealthRate = 0.0;
a->mass = mass;
a->restitution = restitution;
a->turn = 0.0;
a->walkSpeed = 3.0; // 3 m/s is standard for a human.
a->vehicleRaycaster = nullptr;
a->raycastVehicle = nullptr;
// Set default properties to be used by the dungeon engine.
a->eyeHeight = scale.y - scale.x * 0.5; // Center of top sphere.
a->footLength = 0.08;
a->collisionDamageThreshold = DBL_MAX;
a->collisionDamageFactor = 0.0;
a->lastVel = {0.0, 0.0, 0.0};
a->standing = 0.0;
a->weaponBob = 0.0;
a->weaponCooldown = 0.0;
a->weaponFireSpotCooldown = 0.0;
a->weaponTime = 0.0f;
a->lightOn = false;
a->visible = true;
a->model = nullptr;
a->modelDestroyed = nullptr;
return a;
}
// Parse an agent definition.
Agent *Dungeon::parseAgent( const std::string &text, std::string filePath, linalg::mat<double,4,4> spawnMat ){
auto allocatedDocument =
jsonAllocateDocumentEx( text.c_str(), text.size(), JSON_READER_JSON5 );
if( allocatedDocument.document.error.type != JSON_OK ){
std::cerr
<< "JSON error: "
<< jsonGetErrorString( allocatedDocument.document.error.type ) << " at line "
<< allocatedDocument.document.error.line << ":"
<< allocatedDocument.document.error.column << " of "
<< filePath << "\n" << std::endl;
jsonFreeDocument( &allocatedDocument );
return nullptr;
}
auto &info = allocatedDocument.document.root;
// Convert JsonStringView to std::string.
auto viewToString = []( JsonStringView str ){
return std::string( str.data, str.size );
};
auto position =
btVector3( spawnMat[3].x, spawnMat[3].y, spawnMat[3].z );
// Determine the type of agent.
int agent_type = TYPE_AGENT, agent_wheels = 0;
if( info["type"] ){
std::string t = viewToString( info["type"].getString() );
if( t == "bike" ){
agent_type = TYPE_VEHICLE;
agent_wheels = 2;
}else if( t == "plane" ){
agent_type = TYPE_VEHICLE;
agent_wheels = 3;
}else if( t == "car" ){
agent_type = TYPE_VEHICLE;
agent_wheels = 4;
}else if( t == "dynamic" ){
agent_type = TYPE_DYNAMIC;
}else if( t == "static" ){
agent_type = TYPE_STATIC;
}
}
// Get the agent's mass.
double agent_mass = agent_type == TYPE_STATIC ? 0.0 : 50.0;
if( agent_type != TYPE_STATIC && info["mass"] ){
agent_mass = info["mass"].getDouble();
}
// Get the agent's restitution.
double agent_restitution = 0.0;
if( info["restitution"] ){
agent_restitution = info["restitution"].getDouble();
}
// Get the agent's collision scale.
linalg::vec<double,3> agent_scale( 0.8, 1.7, 0.8 );
if( info["collisionScale"] ){
auto s = info["collisionScale"].getArray();
if( s.size() >= 3 ){
agent_scale = {
s[0].getDouble(),
s[1].getDouble(),
s[2].getDouble()
};
}
}
// Get the agent's collision box translation.
linalg::vec<double,3> agent_box_translation( 0.0, 0.0, 0.0 );
if( info["collisionBoxTranslation"] ){
auto t = info["collisionBoxTranslation"].getArray();
if( t.size() >= 3 ){
agent_box_translation = {
t[0].getDouble(),
t[1].getDouble(),
t[2].getDouble()
};
}
}
// Create the agent.
Agent *a = addAgent(
position,
btScalar( agent_mass ),
btScalar( agent_restitution ),
agent_scale,
agent_box_translation,
( agent_type == TYPE_VEHICLE )
);
// Re-enable dynamic agent rotation and set a reasonable angular damping.
if( agent_type == TYPE_DYNAMIC ){
a->rigidBody->setAngularFactor(btScalar(1));
a->rigidBody->setDamping(a->rigidBody->getLinearDamping(), 0.8);
}
// Assign the agent's type.
a->type = agent_type;
// Assign the spawn matrix.
a->spawnMat = spawnMat;
// Find localized names for the agent.
for( auto &prop : info.getObject() ){
std::string propName = viewToString( prop.name );
if( propName.length() > 5 && propName.substr( 0, 5 ) == "name_" ){
a->names[propName.substr( 5 )] =
viewToString( prop.value.getString() );
}
}
// Set the agent's team.
if( info["team"] ){
a->team = info["team"].getInt();
}
// Set the agent's dialogue.
if( info["dialogue"] ){
a->dialogue = viewToString( info["dialogue"].getString() );
}
// Set the agent's death callback.
if( info["onDeath"] ){
a->onDeath = viewToString( info["onDeath"].getString() );
}
// Set the agent's playlist.
if( info["playlist"] ){
a->playlist = viewToString( info["playlist"].getString() );
}
// Set the agent's weapon.
if( info["weapon"] ){
a->weapon = viewToString( info["weapon"].getString() );
// Add the weapon to the agent's inventory automatically.
agentAddWeapon( a, a->weapon );
}
// Set the agent's quick attack weapon.
// NOT added to inventory automatically. This allows quick attacks
// to not use an inventory slot.
if( info["quickWeapon"] ){
a->quickWeapon = viewToString( info["quickWeapon"].getString() );
}
// Set the agent's explode sound.
if( info["soundExplode"] ){
a->soundExplode = viewToString( info["soundExplode"].getString() );
}
// Set the agent's falling damage sound.
if( info["soundCollisionDamage"] ){
a->soundCollisionDamage = viewToString( info["soundCollisionDamage"].getString() );
}
// Set the agent's jump sound.
if( info["soundJump"] ){
a->soundJump = viewToString( info["soundJump"].getString() );
}
// Set the agent's heal sound.
if( info["soundHeal"] ){
a->soundHeal = viewToString( info["soundHeal"].getString() );
}
// Fill the agent's inventory.
a->inventory = {};
if( info["inventory"] ){
for( auto &item : info["inventory"].getArray() ){
InventoryInstance newInstance = {};
if( item["weapon"] ){
newInstance.weapon = viewToString( item["weapon"].getString() );
}
if( item["trinket"] ){
newInstance.trinket = viewToString( item["trinket"].getString() );
}
// Count defaults to 1.
newInstance.count = 1;
if( item["count"] ){
newInstance.count = item["count"].getInt64();
}
if( item["clip"] ){
newInstance.clip = item["clip"].getDouble();
}
// Add the item to the agent's inventory, overwriting if necessary.
bool added = false;
for( auto &instance : a->inventory ){
if( instance.weapon == newInstance.weapon ){
instance = newInstance;
added = true;
}
}
if( !added ) a->inventory.push_back( newInstance );
}
}
// Set the agent's ammo counts.
if( info["ammo"] ){
for( auto &ammoType : info["ammo"].getObject() ){
a->ammo[viewToString( ammoType.name )] = ammoType.value.getDouble();
}
}
// Set the agent's skills.
if( info["skills"] ){
for( auto &skill : info["skills"].getObject() ){
a->skills[viewToString( skill.name )] = skill.value.getDouble();
}
}
// Load the agent's dungeon file string.
if( info["dungeonFile"] ){
a->dungeonFile = viewToString( info["dungeonFile"].getString() );
}
// Load the agent's model.
if( info["model"] ){
a->modelFile = viewToString( info["model"].getString() );
if( a->modelFile.length() > 0 ){
if( models.find( a->modelFile ) == models.end() )
models[a->modelFile] =
fgltf::LoadScene( ModFopen, agentPath + "/" + a->modelFile );
a->model = &models[a->modelFile];
}
}
// Load the agent's destroyed model.
if( info["modelDestroyed"] ){
a->modelDestroyedFile = viewToString( info["modelDestroyed"].getString() );
if( a->modelDestroyedFile.length() > 0 ){
if( models.find( a->modelDestroyedFile ) == models.end() )
models[a->modelDestroyedFile] =
fgltf::LoadScene( ModFopen, agentPath + "/" + a->modelDestroyedFile );
a->modelDestroyed = &models[a->modelDestroyedFile];
}
}
// Load the agent's model loop parameter.
if( info["modelLoop"] ){
a->modelLoop = info["modelLoop"].getInt();
}
// Load the agent's avatar (mutually exclusive to the "model").
if( info["avatar"] ){
a->avatarFile = viewToString( info["avatar"].getString() );
if( a->avatarFile.length() > 0 )
a->avatar = Avatar( ModFopen, avatarPath + "/" + a->avatarFile );
}
// Get the center of mass translation.
if( info["centerOfMassTranslation"] ){
auto t = info["centerOfMassTranslation"].getArray();
if( t.size() >= 3 ){
btVector3 offset(
t[0].getDouble(),
t[1].getDouble(),
t[2].getDouble()
);
// TODO: Erwin never properly added this feature AFAIK.
// TODO: Cleanup code for btCompoundShape.
// https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=2209
// https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=1506
// https://pybullet.org/Bullet/BulletFull/classbtCompoundShape.html
// compoundShape->addChildShape( localTransform, shape );
// TODO: Or just shift a weight left and right, no muss no fuss!
// https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=11066
}
}
// Set the driver eye translation.
if( info["driverEyeTranslation"] ){
auto t = info["driverEyeTranslation"].getArray();
if( t.size() >= 3 ){
a->driverEyeTranslation = {
t[0].getDouble(),
t[1].getDouble(),
t[2].getDouble()
};
}
}
// Toggleable flashlight.
a->lightCone = linalg::vec<float,2>( 0.0f, 0.785f );
if( info["lightCone"] ){
auto cone = info["lightCone"].getArray();
if( cone.size() >= 2 ){
a->lightCone = {
cone[0].getFloat(),
cone[1].getFloat()
};
}
}
a->lightColor = fgl::newColor;
if( info["lightColor"] ){
auto color = info["lightColor"].getArray();
if( color.size() >= 4 ){
a->lightColor = {
color[0].getFloat(),
color[1].getFloat(),
color[2].getFloat(),
color[3].getFloat()
};
}
}
a->lightRange = 0.0f;
if( info["lightRange"] ){
a->lightRange = info["lightRange"].getFloat();
}
// Get the agent's yaw offset.
if( info["offsetYaw"] ){
a->offsetYaw = info["offsetYaw"].getDouble();
}
// Get the agent's starting health, increasing maxHealth if necessary.
if( info["health"] ){
a->health = info["health"].getDouble();
if( a->health > a->maxHealth ) a->maxHealth = a->health;
}
// Get the agent's maximum health.
if( info["maxHealth"] ){
a->maxHealth = info["maxHealth"].getDouble();
}
// Get the agent's water health rate.
if( info["waterHealthRate"] ){
a->waterHealthRate = info["waterHealthRate"].getDouble();
}
// Get the agent's walk speed.
if( info["walkSpeed"] ){
a->walkSpeed = info["walkSpeed"].getDouble();
}
// Get the agent's jump velocity.
if( info["jumpVel"] ){
a->jumpVel = info["jumpVel"].getDouble();
}
// Get the agent's collision damage threshold.
if( info["collisionDamageThreshold"] ){
a->collisionDamageThreshold = info["collisionDamageThreshold"].getDouble();
}
// Get the agent's collision damage factor.
if( info["collisionDamageFactor"] ){
a->collisionDamageFactor = info["collisionDamageFactor"].getDouble();
}
// Get the agent's sitting collision damage factor.
if( info["sittingCollisionDamageFactor"] ){
a->sittingCollisionDamageFactor = info["sittingCollisionDamageFactor"].getDouble();
}
// Get the agent's foot length.
if( info["footLength"] ){
a->footLength = info["footLength"].getDouble();
}
// Get vehicle properties.
a->passiveBrakeForce = 0.0;
a->brakeForce = 5.0;
a->thrustForce = 0.0;
a->suspensionStiffness = 30.0;
a->suspensionDamping = 2.3;
a->suspensionCompression = 4.4;
a->wheelFriction = 10.0;
a->wheelFloor = 0.0;
a->turnAcceleration = 0.0;
a->maxTurn = 3.0; // This can apply to non-vehicles as well.
a->maxLean = 0.0;
a->acceleration = 0.0;
a->topSpeed = 0.0;
if( info["passiveBrakeForce"] ){
a->passiveBrakeForce = info["passiveBrakeForce"].getDouble();
}
if( info["brakeForce"] ){
a->brakeForce = info["brakeForce"].getDouble();
}
if( info["thrustForce"] ){
a->thrustForce = info["thrustForce"].getDouble();
}
if( info["suspensionStiffness"] ){
a->suspensionStiffness = info["suspensionStiffness"].getDouble();
}
if( info["suspensionDamping"] ){
a->suspensionDamping = info["suspensionDamping"].getDouble();
}
if( info["suspensionCompression"] ){
a->suspensionCompression = info["suspensionCompression"].getDouble();
}
if( info["wheelFriction"] ){
a->wheelFriction = info["wheelFriction"].getDouble();
}
if( info["wheelFloor"] ){
a->wheelFloor = info["wheelFloor"].getDouble();
}
if( info["turnAcceleration"] ){
a->turnAcceleration = info["turnAcceleration"].getDouble();
}
if( info["maxTurn"] ){
a->maxTurn = info["maxTurn"].getDouble();
}
if( info["maxLean"] ){
a->maxLean = info["maxLean"].getDouble();
}
if( info["acceleration"] ){
a->acceleration = info["acceleration"].getDouble();
}
if( info["topSpeed"] ){
a->topSpeed = info["topSpeed"].getDouble();
}
// Set the weapon animation to idle.
auto it = weapons.find( a->weapon );
if( it != weapons.end() ){
a->weaponTime = it->second.animationIdle.x;
}
if( a->type == TYPE_VEHICLE ){
// Set up the vehicle.
// https://pybullet.org/Bullet/BulletFull/classbtRaycastVehicle.html
btRaycastVehicle::btVehicleTuning tuning; // Default tuning for now.
a->vehicleRaycaster = new btDefaultVehicleRaycaster( dynamicsWorld );
a->raycastVehicle = new btRaycastVehicle( tuning, a->rigidBody, a->vehicleRaycaster );
// Never deactivate the vehicle.
a->rigidBody->setActivationState( DISABLE_DEACTIVATION );
dynamicsWorld->addVehicle( a->raycastVehicle );
// TODO: Customizable wheel parameters.
double wheelRadius = 0.32;
double wheelWidth = 0.07;
double wheelY = wheelRadius + a->wheelFloor;
btScalar suspensionRestLength( wheelRadius );
btVector3 wheelDirectionCS0(0, -1, 0);
btVector3 wheelAxleCS(-1, 0, 0);
// Choose coordinate system. (Indices are: right, up, forward.)
a->raycastVehicle->setCoordinateSystem( 0, 1, 2 );
if( agent_wheels == 2 ){
bool isFrontWheel = true;
btVector3 connectionPointCS0(0.0, wheelY, agent_scale.z * 0.5 + agent_box_translation.z - wheelRadius);
a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
isFrontWheel = false;
connectionPointCS0 = btVector3(0.0, wheelY, agent_scale.z * -0.5 + agent_box_translation.z + wheelRadius);
a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
}else if( agent_wheels == 3 ){
bool isFrontWheel = true;
btVector3 connectionPointCS0(0.0, wheelY, agent_scale.z * 0.5 + agent_box_translation.z - wheelRadius);
a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
isFrontWheel = false;
connectionPointCS0 = btVector3(agent_scale.x * -0.5 + agent_box_translation.x + wheelWidth * 0.5, wheelY, agent_scale.z * -0.5 + agent_box_translation.z + wheelRadius);
a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
connectionPointCS0 = btVector3(agent_scale.x * 0.5 + agent_box_translation.x - wheelWidth * 0.5, wheelY, agent_scale.z * -0.5 + agent_box_translation.z + wheelRadius);
a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
}else if( agent_wheels == 4 ){
bool isFrontWheel = true;
btVector3 connectionPointCS0(agent_scale.x * 0.5 + agent_box_translation.x - wheelWidth * 0.5, wheelY, agent_scale.z * 0.5 + agent_box_translation.z - wheelRadius);
a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
connectionPointCS0 = btVector3(agent_scale.x * -0.5 + agent_box_translation.x + wheelWidth * 0.5, wheelY, agent_scale.z * 0.5 + agent_box_translation.z - wheelRadius);
a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
isFrontWheel = false;
connectionPointCS0 = btVector3(agent_scale.x * -0.5 + agent_box_translation.x + wheelWidth * 0.5, wheelY, agent_scale.z * -0.5 + agent_box_translation.z + wheelRadius);
a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
connectionPointCS0 = btVector3(agent_scale.x * 0.5 + agent_box_translation.x - wheelWidth * 0.5, wheelY, agent_scale.z * -0.5 + agent_box_translation.z + wheelRadius);
a->raycastVehicle->addWheel(connectionPointCS0, wheelDirectionCS0, wheelAxleCS, suspensionRestLength, wheelRadius, tuning, isFrontWheel);
}
for( int i = 0; i < a->raycastVehicle->getNumWheels(); i++ ){ // TODO: Customizable wheel parameters.
btWheelInfo& wheel = a->raycastVehicle->getWheelInfo(i);
// https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=2398
wheel.m_maxSuspensionTravelCm = suspensionRestLength * 100.0;
wheel.m_suspensionStiffness = a->suspensionStiffness;
wheel.m_wheelsDampingRelaxation = a->suspensionDamping;
wheel.m_wheelsDampingCompression = a->suspensionCompression;
wheel.m_frictionSlip = a->wheelFriction;
wheel.m_rollInfluence = 0.1f; //1.0f;
}
// Brake the vehicle.
brakeVehicle( a, a->brakeForce );
// Set the vehicle's orientation to its spawn orientation.
a->rigidBody->setWorldTransform( btTransform(
btMatrix3x3(spawnMat[0][0], spawnMat[1][0], spawnMat[2][0],
spawnMat[0][1], spawnMat[1][1], spawnMat[2][1],
spawnMat[0][2], spawnMat[1][2], spawnMat[2][2]),
position
) );
}else if( a->type == TYPE_STATIC && a->model ){
// Set up the static model.
fgltf::AddSceneToDynamicsWorld(
*a->model,
dynamicsWorld,
fgltf::PHYSICS_TRIHARD,
&defaultAgent
);
// Set the user pointer for all of the agent's sub-nodes that
// have rigid bodies.
for( auto &n : a->model->nodes ){
if( n.rigidBody ) n.rigidBody->setUserPointer( a );
}
// Update limb positions and parameters in the physics world.
fgltf::UpdateSceneDynamics(
*a->model,
a->spawnMat,
1.0, // TODO: friction
agent_restitution
);
}else if( a->type == TYPE_STATIC && !a->colShape && !a->rigidBody ){
// A static object without a model is presumably a static avatar.
a->colShape =
new btCapsuleShape( agent_scale.x * 0.5, agent_scale.y - agent_scale.x );
btVector3 localInertia( 0, 0, 0 );
a->colShape->calculateLocalInertia( btScalar(agent_mass), localInertia );
// Offset the position to put the bottom of the capsule at the agent's origin.
position.setY( position.getY() + a->scale.y * 0.5 );
btTransform startTransform;
startTransform.setIdentity();
startTransform.setOrigin( position );
btDefaultMotionState* myMotionState = new btDefaultMotionState( startTransform );
btRigidBody::btRigidBodyConstructionInfo rbInfo( btScalar(agent_mass), myMotionState, a->colShape, localInertia );
// Half friction feels better than full for static avatars.
rbInfo.m_friction = 0.5;
// Prevent agents from bouncing.
rbInfo.m_restitution = 0.0;
// Create the rigid body.
a->rigidBody = new btRigidBody( rbInfo );
// Lock the rotation.
a->rigidBody->setAngularFactor( btScalar(0) );
// Set the agent's userPointer to the struct.
a->rigidBody->setUserPointer( a );
dynamicsWorld->addRigidBody( a->rigidBody );
}
jsonFreeDocument( &allocatedDocument );
return a;
}
Agent *Dungeon::loadAgent( std::string filePath, linalg::mat<double,4,4> spawnMat ){
// Discard everything after ".json".
size_t ext_at = filePath.rfind( ".json" );
if( ext_at == std::string::npos ){
// No .json file extension. Add an empty agent at spawnMat.
Agent *a = addAgent(btVector3(0,0,0), 0.0, 0.0, {1.0,1.0,1.0});
a->type = TYPE_STATIC;
a->rigidBody = nullptr;
a->spawnMat = spawnMat;
a->visible = false;
return a;
}else{
// Remove excess characters after the .json file extension.
filePath.resize( ext_at + 5 );
}
// Load the agent definition.
filePath = agentPath + "/" + filePath;
FILE *file = ModFopen( filePath.c_str(), "rb" );
if( !file ){
std::cerr << "Failed to open " << filePath << std::endl;
return nullptr;
}
std::string text = "";
char buf[4096];
while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
text += std::string( buf, len );
}
fclose( file );
return parseAgent( text, filePath, spawnMat );
}
void Dungeon::loadSavedPlayerAgent( std::string filePath ){
// Load the agent definition.
FILE *file = ModFopen( filePath.c_str(), "rb" );
if( !file ){
std::cerr << "Failed to open " << filePath << std::endl;
return;
}
std::string text = "";
char buf[4096];
while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
text += std::string( buf, len );
}
fclose( file );
// JSON parsing boilerplate.
auto allocatedDocument =
jsonAllocateDocumentEx( text.c_str(), text.size(), JSON_READER_JSON5 );
if( allocatedDocument.document.error.type != JSON_OK ){
std::cerr
<< "JSON error: "
<< jsonGetErrorString( allocatedDocument.document.error.type ) << " at line "
<< allocatedDocument.document.error.line << ":"
<< allocatedDocument.document.error.column << " of "
<< filePath << "\n" << std::endl;
jsonFreeDocument( &allocatedDocument );
return;
}
auto &info = allocatedDocument.document.root;
// Convert JsonStringView to std::string.
auto viewToString = []( JsonStringView str ){
return std::string( str.data, str.size );
};
// Load the agent's dungeon file string.
std::string agentDungeon;
if( info["dungeonFile"] ){
agentDungeon = viewToString( info["dungeonFile"].getString() );
}
// Load the agent's angle.
linalg::vec<double,3> agentAngle( 0.0, 0.0, 0.0 );
if( info["angle"] ){
auto v = info["angle"].getArray();
if( v.size() >= 3 ){
agentAngle = {
v[0].getDouble(),
v[1].getDouble(),
v[2].getDouble()
};
}
}
// Free the JSON document.
jsonFreeDocument( &allocatedDocument );
// Load the agent in its specified dungeon.
loadWorld(dungeonPath, agentPath, avatarPath, particlePath, agentDungeon, text);
if(playerAgent){
// Position and rotate the agent.
positionAgent(playerAgent, playerAgent->driverEyeTranslation);
playerAgent->angle = agentAngle;
}
}
// Return a JSON string containing the agent definition.
// Vehicles currently do not serialize properly. TODO: collisionBoxTranslation
std::string Dungeon::serializeAgent( Agent *a ){
std::string str = "{\n";
for( const auto &it : a->names ){
str += "\t\"name_" + it.first + "\": \"" + it.second + "\",\n";
}
str += "\t\"type\": \"";
if( a->type == TYPE_VEHICLE && a->raycastVehicle ){
if( a->raycastVehicle->getNumWheels() == 2 ) str += "bike";
else if( a->raycastVehicle->getNumWheels() == 3 ) str += "plane";
else if( a->raycastVehicle->getNumWheels() == 4 ) str += "car";
else str += "vehicle";
}else{
if( a->type == TYPE_DYNAMIC ){
str += "dynamic";
}else if( a->type == TYPE_STATIC ){
str += "static";
}else{
str += "agent";
}
// Hijack driverEyeTranslation to store current position.
a->driverEyeTranslation = getAgentPoseMat(a)[3].xyz();
}
str += "\",\n";
str += "\t\"team\": " + std::to_string(a->team) + ",\n";
str += "\t\"dialogue\": \"" + a->dialogue + "\",\n";
str += "\t\"onDeath\": \"" + a->onDeath + "\",\n";
str += "\t\"playlist\": \"" + a->playlist + "\",\n";
str += "\t\"weapon\": \"" + a->weapon + "\",\n";
str += "\t\"quickWeapon\": \"" + a->quickWeapon + "\",\n";
str += "\t\"soundExplode\": \"" + a->soundExplode + "\",\n";
str += "\t\"soundCollisionDamage\": \"" + a->soundCollisionDamage + "\",\n";
str += "\t\"soundJump\": \"" + a->soundJump + "\",\n";
str += "\t\"soundHeal\": \"" + a->soundHeal + "\",\n";
str += "\t\"inventory\": [\n";
for( size_t i = 0; i < a->inventory.size(); i++ ){
InventoryInstance &instance = a->inventory[i];
str += "\t\t{ \"weapon\": \"" + instance.weapon + "\", ";
str += "\"trinket\": \"" + instance.trinket + "\", ";
str += "\"count\": " + std::to_string(instance.count) + ", ";
str += "\"clip\": " + std::to_string(instance.clip) + " }";
if( i < a->inventory.size() - 1 ) str += ",";
str += "\n";
}
str += "\t],\n";
str += "\t\"ammo\": { ";
size_t ct = 0;
for( const auto &ammo_it : a->ammo ){
str += "\"" + ammo_it.first + "\": "
+ std::to_string(ammo_it.second);
ct++;
if( ct < a->ammo.size() ) str += ", ";
}
str += " },\n";
str += "\t\"skills\": { ";
ct = 0;
for( const auto &skills_it : a->skills ){
str += "\"" + skills_it.first + "\": "
+ std::to_string(skills_it.second);
ct++;
if( ct < a->skills.size() ) str += ", ";
}
str += " },\n";
str += "\t\"dungeonFile\": \"" + dungeonFile + "\",\n"; // The current dungeon file.
str += "\t\"model\": \"" + a->modelFile + "\",\n";
str += "\t\"modelDestroyed\": \"" + a->modelDestroyedFile + "\",\n";
str += "\t\"modelLoop\": \"" + std::to_string(a->modelLoop) + "\",\n";
str += "\t\"avatar\": \"" + a->avatarFile + "\",\n";
str += "\t\"angle\": ["
+ std::to_string(a->angle.x) + ", "
+ std::to_string(a->angle.y) + ", "
+ std::to_string(a->angle.z) + "],\n";
str += "\t\"collisionScale\": ["
+ std::to_string(a->scale.x) + ", "
+ std::to_string(a->scale.y) + ", "
+ std::to_string(a->scale.z) + "],\n";
str += "\t\"driverEyeTranslation\": ["
+ std::to_string(a->driverEyeTranslation.x) + ", "
+ std::to_string(a->driverEyeTranslation.y) + ", "
+ std::to_string(a->driverEyeTranslation.z) + "],\n";
// Toggleable flashlight.
str += "\t\"lightCone\": ["
+ std::to_string(a->lightCone.x) + ", "
+ std::to_string(a->lightCone.y) + "],\n";
str += "\t\"lightColor\": ["
+ std::to_string(a->lightColor.r) + ", "
+ std::to_string(a->lightColor.g) + ", "
+ std::to_string(a->lightColor.b) + ", "
+ std::to_string(a->lightColor.a) + "],\n";
str += "\t\"lightRange\": " + std::to_string(a->lightRange) + ",\n";
// General numeric properties.
str += "\t\"offsetYaw\": " + std::to_string(a->offsetYaw) + ",\n";
str += "\t\"health\": " + std::to_string(a->health) + ",\n";
str += "\t\"maxHealth\": " + std::to_string(a->maxHealth) + ",\n";
str += "\t\"waterHealthRate\": " + std::to_string(a->waterHealthRate) + ",\n";
str += "\t\"mass\": " + std::to_string(a->mass) + ",\n";
str += "\t\"restitution\": " + std::to_string(a->restitution) + ",\n";
str += "\t\"walkSpeed\": " + std::to_string(a->walkSpeed) + ",\n";
str += "\t\"jumpVel\": " + std::to_string(a->jumpVel) + ",\n";
str += "\t\"collisionDamageThreshold\": " + std::to_string(a->collisionDamageThreshold) + ",\n";
str += "\t\"collisionDamageFactor\": " + std::to_string(a->collisionDamageFactor) + ",\n";
str += "\t\"sittingCollisionDamageFactor\": " + std::to_string(a->sittingCollisionDamageFactor) + ",\n";
str += "\t\"footLength\": " + std::to_string(a->footLength) + ",\n";
// Vehicle properties.
str += "\t\"passiveBrakeForce\": " + std::to_string(a->passiveBrakeForce) + ",\n";
str += "\t\"brakeForce\": " + std::to_string(a->brakeForce) + ",\n";
str += "\t\"thrustForce\": " + std::to_string(a->thrustForce) + ",\n";
str += "\t\"suspensionStiffness\": " + std::to_string(a->suspensionStiffness) + ",\n";
str += "\t\"suspensionDamping\": " + std::to_string(a->suspensionDamping) + ",\n";
str += "\t\"suspensionCompression\": " + std::to_string(a->suspensionCompression) + ",\n";
str += "\t\"wheelFriction\": " + std::to_string(a->wheelFriction) + ",\n";
str += "\t\"wheelFloor\": " + std::to_string(a->wheelFloor) + ",\n";
str += "\t\"turnAcceleration\": " + std::to_string(a->turnAcceleration) + ",\n";
str += "\t\"maxTurn\": " + std::to_string(a->maxTurn) + ",\n";
str += "\t\"maxLean\": " + std::to_string(a->maxLean) + ",\n";
str += "\t\"acceleration\": " + std::to_string(a->acceleration) + ",\n";
str += "\t\"topSpeed\": " + std::to_string(a->topSpeed) + "\n";
str += "}\n";
return str;
}
// Rotate angle a to angle b at speed.
double Dungeon::getRotationDelta(double a, double b, double speed){
double diff = a - b;
double diff2 = std::atan2(std::sin(diff), std::cos(diff));
if(diff2 > 0.0) return std::min(speed, M_PI - diff2);
else return std::max(-speed, -M_PI - diff2);
}
// Rotate an agent with a constrained X angle, taking third-person into account.
void Dungeon::rotateAgentConstrained( Agent *a, linalg::vec<double,3> rotation ){
if( a == playerAgent && topDown ){
a->angle = {0.0, 0.0, 0.0};
return;
}
double minX = -1.570796, maxX = 1.570796;
a->angle.z = fgl::wrapAngle( a->angle.z + rotation.z );
if( thirdPerson && a == playerAgent ){
minX = thirdPersonCameraMinPitch;
maxX = thirdPersonCameraMaxPitch;
thirdPersonYawOffset = fgl::wrapAngle( thirdPersonYawOffset + rotation.y );
}else{
a->angle.y = fgl::wrapAngle( a->angle.y + rotation.y );
}
a->angle.x = std::min( std::max( a->angle.x + rotation.x, minX ), maxX );
// Optional: Apply the Y angle to the rigid body. This breaks smooth
// animations when running at > 60 FPS.
/*
btTransform trans = a->rigidBody->getWorldTransform();
linalg::vec<double,4> q = fgl::eulerToQuat( 0.0, a->angle.y, 0.0 );
trans.setRotation( btQuaternion( q.x, q.y, q.z, q.w ) );
// Activate the object.
a->rigidBody->setActivationState( 1 );
a->rigidBody->setWorldTransform( trans );
if( a->rigidBody->getMotionState() )
a->rigidBody->getMotionState()->setWorldTransform( trans );
*/
}
// Rotate an agent without constraints.
// This may temporarily freeze vehicles in place.
void Dungeon::rotateAgentUnconstrained( Agent *a, linalg::vec<double,3> rotation ){
if( a->type == TYPE_VEHICLE ){
btTransform trans = getAgentPhysicsTransform( a );
btQuaternion r = trans.getRotation();
auto q = linalg::qmul(
fgl::eulerToQuat( rotation.x, rotation.y, rotation.z ),
linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() )
);
trans.setRotation( btQuaternion( q.x, q.y, q.z, q.w ) );
// Activate the vehicle.
a->rigidBody->setActivationState( 1 );
// Set the vehicle's transform.
a->rigidBody->setWorldTransform( trans );
if( a->rigidBody->getMotionState() )
a->rigidBody->getMotionState()->setWorldTransform( trans );
}else{
// Rotate the agent's eye in world space.
a->angle.x = fgl::wrapAngle( a->angle.x + rotation.x );
a->angle.y = fgl::wrapAngle( a->angle.y + rotation.y );
a->angle.z = fgl::wrapAngle( a->angle.z + rotation.z );
}
}
// Move an agent by translation. Return true if agent successfully jumped.
bool Dungeon::moveAgent( Agent *a, linalg::vec<double,3> translation, double standingTime, double d ){
if( !a->rigidBody ) return false;
// https://gamedev.stackexchange.com/questions/37282
// https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=7202
// Cast a foot ray.
btVector3 from = a->rigidBody->getWorldTransform().getOrigin();
btVector3 to(
from.getX(),
from.getY() - a->scale.y * 0.5 - a->footLength,
from.getZ()
);
//btCollisionWorld::ClosestRayResultCallback rayResult( from, to );
//dynamicsWorld->rayTest( from, to, rayResult );
//a->standing = rayResult.hasHit();
// Spherecast the foot.
btTransform fromTrans, toTrans;
fromTrans.setIdentity();
fromTrans.setOrigin( from );
toTrans.setIdentity();
toTrans.setOrigin( to );
// https://pybullet.org/Bullet/BulletFull/classbtClosestNotMeConvexResultCallback.html
// A fixed version of Bullet's official class.
btrapClosestNotMeConvexResultCallback rayResult(
a->rigidBody,
from,
to,
(btOverlappingPairCache*)overlappingPairCache
);
dynamicsWorld->convexSweepTest( a->footSphere, fromTrans, toTrans, rayResult );
if(rayResult.hasHit()){
a->standing = standingTime;
}else{
a->standing = std::max(a->standing - d, 0.0);
}
/*
a->standing = false;
// Loop through spherecast hits.
for( size_t i = 0; i < (size_t)rayResult.m_collisionObjects.size(); i++ ){
if( rayResult.m_collisionObjects[i] != a ){
a->standing = true;
break;
}
}
*/
// Get the Y angle of the translation vector plus the Y angle of the agent.
double moveAngle =
std::atan2( translation.x, translation.z ) + a->angle.y;
// Get the length of the Y plane of the translation vector.
double moveLength = linalg::length(
linalg::vec<double,2>( translation.x, translation.z )
);
// Apply the necessary adjustments in third-person mode.
if(thirdPerson && !topDown && a == playerAgent && moveLength > 0.0){
moveAngle += thirdPersonYawOffset;
// Rotate agent toward moveAngle.
double rdelta = getRotationDelta(a->angle.y, moveAngle, a->maxTurn * d);
a->angle.y = fgl::wrapAngle(a->angle.y + rdelta);
thirdPersonYawOffset = fgl::wrapAngle(thirdPersonYawOffset - rdelta);
}
btVector3 vel = a->rigidBody->getVelocityInLocalPoint( btVector3( 0, 0, 0 ) );
#if 0
// This section is non-functional due to vel not representing distance traveled.
if( fgl::charKey( 'e' ) ){
// Capsule radius.
const double r = a->scale.x * 0.5;
// Center of capsule's top sphere.
const double center = a->scale.y - r;
// Allow the eye height to drift vertically to smooth movement.
a->eyeHeight = center
+ std::min( std::max( a->eyeHeight - center - vel.getY() * d, -r ), r );
// Push the eye height towards the imaginary center position at an
// approximately linear rate.
if( a->eyeHeight < center ){
a->eyeHeight = std::min( a->eyeHeight + 5.0 * d, center );
}else if( a->eyeHeight > center ){
a->eyeHeight = std::max( a->eyeHeight - 5.0 * d, center );
}
printf( "eyeHeight: %f\n", (float)a->eyeHeight );
}
#endif
// Activate the object.
a->rigidBody->setActivationState( 1 );
//a->rigidBody->applyCentralImpulse( ??? / a->rigidBody->getInvMass() );
if(a->standing != 0.0 || waterLevel + waterWaveHeight > from.getY()){
// TODO: Set velocity relative to velocity of intersecting point.
// Vertical movement.
btVector3 moveVec = btVector3(
vel.getX(),
translation.y ? translation.y : vel.getY(),
vel.getZ()
);
// Lateral movement. Only override current velocity when desired
// movement velocity is more than half of current velocity.
if( ( translation.x || translation.z )
&& moveLength > linalg::length<double,2>( { vel.getX(), vel.getZ() } ) * 0.5 ){
moveVec = btVector3(
std::sin( moveAngle ) * moveLength,
moveVec.getY(),
std::cos( moveAngle ) * moveLength
);
// Zero friction for smooth sliding.
a->rigidBody->setFriction( 0.0 );
}else{
// Feet are touching the ground but not moving.
a->rigidBody->setFriction( 1.0 );
}
// Apply the velocity.
a->rigidBody->setLinearVelocity( moveVec );
return translation.y != 0.0;
}else{
// Set friction to 0 when feet are not touching the ground.
a->rigidBody->setFriction( 0.0 );
}
return false;
}
void Dungeon::positionAgent( Agent *a, linalg::vec<double,3> position ){
if( a->type == TYPE_STATIC || !a->rigidBody ){
a->spawnMat[3].x = position.x;
a->spawnMat[3].y = position.y;
a->spawnMat[3].z = position.z;
return;
}
btTransform trans = getAgentPhysicsTransform( a );
trans.setOrigin( btVector3( position.x, position.y, position.z ) );
a->rigidBody->setActivationState( 1 );
a->rigidBody->setWorldTransform( trans );
if( a->rigidBody->getMotionState() )
a->rigidBody->getMotionState()->setWorldTransform( trans );
}
// Accelerate an agent as a vehicle via its own space.
// TODO: acceleration = wheel torque / ( wheel radius * vehicle mass )
// "Engine force" seems arbitrary. What is it?
void Dungeon::accelerateVehicle( Agent *a, double force ){
int wheels = a->raycastVehicle->getNumWheels();
// Set appropriate brake force.
for( int i = 0; i < wheels; i++ ){
a->raycastVehicle->setBrake(
(force == 0.0) ? a->passiveBrakeForce : 0.0,
i
);
}
double speed = a->raycastVehicle->getCurrentSpeedKmHour();
// If accelerating in the same direction and speed exceeds top
// speed, set force to 0.0.
if( speed * force > 0.0 && std::abs(speed) > a->topSpeed )
force = 0.0;
// Thrust.
double thrust = (force == 0.0)
? 0.0
: ( (force > 0.0) ? a->thrustForce : -a->thrustForce );
btTransform trans = a->rigidBody->getWorldTransform();
btQuaternion r = trans.getRotation();
auto rotationMat = linalg::rotation_matrix(
linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() )
);
auto thrustVec = linalg::mul(
rotationMat,
linalg::translation_matrix( linalg::vec<double,3>( 0.0, 0.0, 1.0 ) )
)[3].xyz() * thrust;
a->rigidBody->applyCentralForce( btVector3( thrustVec.x, thrustVec.y, thrustVec.z ) );
// Spin wheels.
if( wheels == 2 ){
a->raycastVehicle->applyEngineForce( force, 1 );
}else if( wheels == 3 ){
a->raycastVehicle->applyEngineForce( force, 1 );
a->raycastVehicle->applyEngineForce( force, 2 );
}else if( wheels == 4 ){
a->raycastVehicle->applyEngineForce( force, 2 );
a->raycastVehicle->applyEngineForce( force, 3 );
}
}
void Dungeon::brakeVehicle( Agent *a, double brakeForce ){
for( int i = 0; i < a->raycastVehicle->getNumWheels(); i++ ){
a->raycastVehicle->applyEngineForce( 0.0, i );
a->raycastVehicle->setBrake( brakeForce, i );
}
}
void Dungeon::rightVehicle( Agent *a ){
auto q = fgl::eulerToQuat( 0.0, getVehicleYaw( a ), 0.0 );
//btTransform trans = getAgentPhysicsTransform( a );
btTransform trans = a->rigidBody->getWorldTransform();
trans.setRotation( btQuaternion( q.x, q.y, q.z, q.w ) );
a->rigidBody->setWorldTransform( trans );
if( a->rigidBody->getMotionState() )
a->rigidBody->getMotionState()->setWorldTransform( trans );
a->rigidBody->setAngularVelocity( btVector3( 0.0, 0.0, 0.0 ) );
}
bool Dungeon::isVehicleUpright( Agent *a ){
btTransform trans = getAgentPhysicsTransform( a );
btQuaternion r = trans.getRotation();
auto rotationMat = linalg::rotation_matrix(
linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() )
);
return ( linalg::mul(
rotationMat,
linalg::translation_matrix( linalg::vec<double,3>( 0.0, 1.0, 0.0 ) )
)[3].y > 0.3 ); // This threshold is meant to make the "upright cone" smaller.
}
double Dungeon::getVehicleLean( Agent *a ){
btTransform trans = getAgentPhysicsTransform( a );
btQuaternion r = trans.getRotation();
auto rotationMat = linalg::rotation_matrix(
linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() )
);
auto side = linalg::mul(
rotationMat,
linalg::translation_matrix( linalg::vec<double,3>( 1.0, 0.0, 0.0 ) )
)[3].xyz();
return atan2(
side.y,
linalg::length<double,2>( { side.x, side.z } )
);
}
double Dungeon::getVehiclePitch( Agent *a ){
btTransform trans = getAgentPhysicsTransform( a );
btQuaternion r = trans.getRotation();
auto rotationMat = linalg::rotation_matrix(
linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() )
);
auto end = linalg::mul(
rotationMat,
linalg::translation_matrix( linalg::vec<double,3>( 0.0, 0.0, -1.0 ) )
)[3].xyz();
return atan2(
end.y,
linalg::length<double,2>( { end.x, end.z } )
);
}
double Dungeon::getVehicleYaw( Agent *a ){
btTransform trans = getAgentPhysicsTransform( a );
btQuaternion r = trans.getRotation();
auto rotationMat = linalg::rotation_matrix(
linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() )
);
auto end = linalg::mul(
rotationMat,
linalg::translation_matrix( linalg::vec<double,3>( 0.0, 0.0, 1.0 ) )
)[3].xyz();
return std::atan2( end.x, end.z );
}
void Dungeon::setAgentCollisions( Agent *a, bool enable ){
if( !a->rigidBody ) return;
// https://pybullet.org/Bullet/BulletFull/classbtCollisionObject.html
auto flags = a->rigidBody->getCollisionFlags();
if( enable ){
a->rigidBody->setCollisionFlags( flags
& ~btCollisionObject::CF_NO_CONTACT_RESPONSE );
}else{
a->rigidBody->setCollisionFlags( flags
| btCollisionObject::CF_NO_CONTACT_RESPONSE );
}
}
void Dungeon::setAgentCrouch( Agent *a, bool enable, double d ){
if( !a->colShape ) return;
// https://pybullet.org/Bullet/phpBB3/viewtopic.php?f=9&t=13057
// https://pybullet.org/Bullet/phpBB3/viewtopic.php?f=9&t=6325
double s = enable ? 0.6 : 1.0;
//a->colShape->setLocalScaling( btVector3( 0.0, s, 0.0 ) );
double eye = a->scale.y * s - a->scale.x * 0.5;
if( a->eyeHeight < eye ){
a->eyeHeight += 6.0 * d;
if( a->eyeHeight > eye ) a->eyeHeight = eye;
}else if( a->eyeHeight > eye ){
a->eyeHeight -= 6.0 * d;
if( a->eyeHeight < eye ) a->eyeHeight = eye;
}
}
void Dungeon::unsitAgent( Agent *a ){
auto ray = getAgentGazeRay( a, 1.0 );
auto dir = ray.first - ray.second;
a->angle.y = std::atan2( dir.x, dir.z );
a->angle.z = 0.0;
setAgentCollisions( a, true );
if( a->parentAgent && a->parentAgent->driverAgent == a ){
a->parentAgent->turn = 0.0;
a->parentAgent->driverAgent = nullptr;
}
a->parentAgent = nullptr;
}
linalg::vec<double,3> Dungeon::getNodeLookAt( linalg::mat<double,4,4> m ){
// https://gamedev.stackexchange.com/questions/119702/fastest-way-to-neutralize-scale-in-the-transform-matrix
// Get a rotation quaternion from the top-left part of the matrix.
auto q = linalg::rotation_quat<double>( {
linalg::normalize<double,3>( { m[0][0], m[0][1], m[0][2] } ),
linalg::normalize<double,3>( { m[1][0], m[1][1], m[1][2] } ),
linalg::normalize<double,3>( { m[2][0], m[2][1], m[2][2] } )
} );
// Get a look-at vector from the rotation quaternion.
// This is inverse of the node's Blender orientation but points in
// the same direction as the base of a cone empty.
return linalg::qzdir( q ) * linalg::vec<double,3>( -1.0, 1.0, -1.0 );
}
btTransform Dungeon::getAgentPhysicsTransform( Agent *a ){
if( a->rigidBody->getMotionState() ){
btTransform trans;
a->rigidBody->getMotionState()->getWorldTransform( trans );
return trans;
}else{
return a->rigidBody->getWorldTransform();
}
}
//
linalg::mat<double,4,4> Dungeon::getAgentPoseMat( Agent *a ){
if( a->type == TYPE_STATIC || !a->rigidBody ) return a->spawnMat;
btTransform trans = getAgentPhysicsTransform( a );
btVector3 &o = trans.getOrigin();
// Return physical rotations for vehicles and axis-aligned rotations
// for other agents.
if( a->type == TYPE_VEHICLE || a->type == TYPE_DYNAMIC ){
btQuaternion r = trans.getRotation();
return linalg::pose_matrix(
linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() ),
linalg::vec<double,3>( o.getX(), o.getY(), o.getZ() )
);
}else{
return linalg::pose_matrix(
fgl::eulerToQuat( 0.0, a->angle.y + a->offsetYaw, 0.0 ),
linalg::vec<double,3>( o.getX(), o.getY(), o.getZ() )
);
}
}
std::pair<linalg::vec<double,3>,linalg::vec<double,3>> Dungeon::getAgentGazeRay( Agent *a, double range, bool pitch ){
if( !a->rigidBody )
return { linalg::vec<double,3>(0,0,0), linalg::vec<double,3>(0,0,0) };
linalg::vec<double,3> from, to;
if( a->parentAgent && a->parentAgent->driverAgent == a ){
// Cast a ray from the offset driver eye position.
auto eyeMat = linalg::mul(
getAgentPoseMat( a->parentAgent ),
linalg::pose_matrix(
fgl::eulerToQuat(
pitch ? a->angle.x : 0.0,
a->angle.y,
a->angle.z ),
a->parentAgent->driverEyeTranslation
)
);
from = eyeMat[3].xyz();
auto targetMat = linalg::mul(
eyeMat,
linalg::translation_matrix( linalg::vec<double,3>( 0.0, 0.0, -range ) )
);
to = targetMat[3].xyz();
}else{
// Cast a ray from the agent eye position in world space.
btTransform trans = getAgentPhysicsTransform( a );
btVector3 &o = trans.getOrigin();
from = {
o.getX(),
o.getY() - a->scale.y * 0.5 + a->eyeHeight,
o.getZ()
};
auto offset = linalg::normalize( linalg::vec<double,3>(
-std::sin( a->angle.y ),
std::tan( pitch ? a->angle.x : 0.0 ),
-std::cos( a->angle.y )
) ) * range;
to = from + offset;
}
return { from, to };
}
std::pair<void*,double> Dungeon::getAgentGazeHit( Agent *a, double range ){
// https://gamedev.stackexchange.com/questions/37282
// https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=7202
// https://pybullet.org/Bullet/BulletFull/structbtCollisionWorld_1_1ClosestRayResultCallback.html
auto ray = getAgentGazeRay( a, range );
btVector3
from( ray.first.x, ray.first.y, ray.first.z ),
to( ray.second.x, ray.second.y, ray.second.z );
/*
btCollisionWorld::ClosestRayResultCallback rayResult( from, to );
dynamicsWorld->rayTest( from, to, rayResult );
if( rayResult.hasHit() )
return rayResult.m_collisionObject->getUserPointer();
*/
// Find the closest ray hit that is not the agent's parent.
btCollisionWorld::AllHitsRayResultCallback rayResult( from, to );
dynamicsWorld->rayTest( from, to, rayResult );
bool has_hit = false;
size_t closest;
double frac = 1.0;
for( size_t i = 0; i < (size_t)rayResult.m_collisionObjects.size(); i++ ){
void *userptr = rayResult.m_collisionObjects[i]->getUserPointer();
if( userptr != a && userptr != a->parentAgent
&& rayResult.m_hitFractions[i] < frac ){
has_hit = true;
closest = i;
frac = rayResult.m_hitFractions[i];
}
}
if( has_hit )
return { rayResult.m_collisionObjects[closest]->getUserPointer(), frac * range };
return { nullptr, 0.0 };
}
// Get the agent's localized name, falling back to English.
// Fill out name_en at minimum if you want a name to display.
std::string Dungeon::getAgentLocalizedName( Agent *a, const std::string &languageCode ){
if( a->names.size() < 1 ) return "";
auto it1 = a->names.find( languageCode );
if( it1 == a->names.end() ){
auto it2 = a->names.find( "en" );
if( it2 == a->names.end() ){
return "";
}else{
return it2->second;
}
}else{
return it1->second;
}
}
// Get the weapon's localized name, first falling back to English, then
// a default name.
std::string Dungeon::getWeaponLocalizedName( Weapon *w, const std::string &languageCode ){
auto it1 = w->names.find( languageCode );
if( it1 == w->names.end() ){
auto it2 = w->names.find( "en" );
if( it2 == w->names.end() ){
return "Weapon";
}else{
return it2->second;
}
}else{
return it1->second;
}
}
//
void Dungeon::removeAgent( Agent *a, bool conservative ){ // TODO: Safely remove static objects. Make this not crash the program!
// Detach the agent from other agents.
if( a->parentAgent && a->parentAgent->driverAgent == a )
a->parentAgent->driverAgent = nullptr;
if( a->driverAgent && a->driverAgent->parentAgent == a )
unsitAgent( a->driverAgent );
if( conservative ){
// Disable the agent without removing it.
if( a->rigidBody ){
a->rigidBody->setUserPointer( &defaultAgent );
a->rigidBody->setFriction( 1.0 );
if( a->raycastVehicle ){
for(int i = 0; i < a->raycastVehicle->getNumWheels(); i++)
a->raycastVehicle->applyEngineForce( 0.0, i );
}
}
}else{
// Fully remove the agent.
if( a->prev )
a->prev->next = a->next;
if( a->next )
a->next->prev = a->prev;
if( a->colShape )
delete a->colShape;
if( a->rigidBody ){
if( a->rigidBody->getMotionState() )
delete a->rigidBody->getMotionState();
if( dynamicsWorld )
dynamicsWorld->removeRigidBody( a->rigidBody );
delete a->rigidBody;
}
if( a->footSphere )
delete a->footSphere;
if( a->vehicleRaycaster )
delete a->vehicleRaycaster;
if( a->raycastVehicle ){
if( dynamicsWorld )
dynamicsWorld->removeAction( a->raycastVehicle );
delete a->raycastVehicle;
}
delete a;
}
}
// Apply an impulse to the physics world with the ray and return the
// agent that it hits.
Agent *Dungeon::rayImpact( Agent *a, std::pair<linalg::vec<double,3>,linalg::vec<double,3>> ray, double impact, double damage, std::string defenseSkill ){
// https://pybullet.org/Bullet/BulletFull/classbtRigidBody.html
btVector3
from( ray.first.x, ray.first.y, ray.first.z ),
to( ray.second.x, ray.second.y, ray.second.z );
// Upcast any btCollisionObject to btRigidBody. No crashes from this yet.
btRigidBody *rb = nullptr;
void *userptr = &defaultAgent;
btVector3 hit;
if( a ){
// Find the closest ray hit that is not the agent's parent.
btCollisionWorld::AllHitsRayResultCallback rayResult( from, to );
dynamicsWorld->rayTest( from, to, rayResult );
bool has_hit = false;
size_t closest;
double frac = 1.0;
for( size_t i = 0; i < (size_t)rayResult.m_collisionObjects.size(); i++ ){
auto cob = rayResult.m_collisionObjects[i];
auto optr = cob->getUserPointer();
if( optr != a && (!optr || optr != a->parentAgent)
&& rayResult.m_hitFractions[i] < frac
&& ((optr && optr != &defaultAgent && ((Agent*)optr)->parentAgent) // Allow ray hits while driving.
|| !(cob->getCollisionFlags() & btCollisionObject::CF_NO_CONTACT_RESPONSE)) ){
has_hit = true;
closest = i;
frac = rayResult.m_hitFractions[i];
}
}
if( !has_hit ) return nullptr;
rb = (btRigidBody*)rayResult.m_collisionObjects[closest];
userptr = rb->getUserPointer();
hit = rayResult.m_hitPointWorld[closest];
}else{
// Agent is null. Find the closest ray hit. TODO: Actually test this use case!
btCollisionWorld::ClosestRayResultCallback rayResult( from, to );
dynamicsWorld->rayTest( from, to, rayResult );
if( !rayResult.hasHit() ) return nullptr;
userptr = rayResult.m_collisionObject->getUserPointer();
hit = rayResult.m_hitPointWorld;
}
std::string atype =
userptr ? (*(std::string*)userptr).substr( 0, 5 ) : "";
Agent *hitAgent = (Agent*)userptr;
if( atype == "agent" || atype == "spawn" ){
// Ray hit an agent.
// TODO: Headshots, special damage zones, blood vertices, destroying static objects, etc.
if( hitAgent != &defaultAgent ){
if( defenseSkill.length() > 0 && hitAgent->skills.size() > 0 ){
auto defense_skill_it = hitAgent->skills.find( defenseSkill );
if( defense_skill_it != hitAgent->skills.end() ){
// Defense skill affects damage.
// If skill == 10.0, damage is reduced by about 10%.
// If skill == 100.0, damage is reduced by about 52%.
// If skill == 500.0, damage is reduced by about 85%.
if( defense_skill_it->second > 1.0 )
damage /= ( 0.011 * defense_skill_it->second + 1.0 );
}
}
if(hitAgent->maxHealth > 0.0) hitAgent->health -= damage;
}
// TODO: Agent-specific particles.
if( hitAgent != &defaultAgent && !hitAgent->model ){
// Avatar = blood
auto ps_it = particleServers.find(defaultBlood.texture);
if(ps_it != particleServers.end()){
ps_it->second.addOmniBurst(
defaultBlood.num, defaultBlood.vel,
{hit.getX(), hit.getY(), hit.getZ()},
defaultBlood.startScale, defaultBlood.endScale,
defaultBlood.startMass, defaultBlood.endMass, defaultBlood.maxAge,
defaultBlood.damage, (void*)&defaultAgent );
}
}else{
// No avatar = default impact
auto ps_it = particleServers.find(defaultImpact.texture);
if(ps_it != particleServers.end()){
ps_it->second.addOmniBurst(
defaultImpact.num, defaultImpact.vel,
{hit.getX(), hit.getY(), hit.getZ()},
defaultImpact.startScale, defaultImpact.endScale,
defaultImpact.startMass, defaultImpact.endMass, defaultImpact.maxAge,
defaultImpact.damage, (void*)&defaultAgent );
}
}
}else{
// Ray hit a non-agent.
// Apply default impact particles.
// Old behavior: return null.
// New behavior: return &defaultAgent.
hitAgent = &defaultAgent;
auto ps_it = particleServers.find(defaultImpact.texture);
if(ps_it != particleServers.end()){
ps_it->second.addOmniBurst(
defaultImpact.num, defaultImpact.vel,
{hit.getX(), hit.getY(), hit.getZ()},
defaultImpact.startScale, defaultImpact.endScale,
defaultImpact.startMass, defaultImpact.endMass, defaultImpact.maxAge,
defaultImpact.damage, (void*)&defaultAgent );
}
}
if( rb->getInvMass() != 0.0 ){
// Apply a physical impulse to the agent.
btTransform trans = rb->getWorldTransform();
btQuaternion r = trans.getRotation();
btVector3 o = trans.getOrigin();
auto localHit = linalg::mul(
linalg::inverse( linalg::pose_matrix(
linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() ),
linalg::vec<double,3>( o.getX(), o.getY(), o.getZ() )
) ),
linalg::translation_matrix( linalg::vec<double,3>( hit.x(), hit.y(), hit.z() ) )
)[3].xyz();
rb->setActivationState( 1 );
auto impulse = linalg::normalize( ray.second - ray.first ) * impact;
rb->applyImpulse( btVector3( impulse.x, impulse.y, impulse.z ),
btVector3( localHit.x, localHit.y, localHit.z ) );
}
return hitAgent;
}
// Fire the agent's weapon. TODO: pellets, ammo cost, crouch_fire animation.
// Return { success, ray impact }
std::pair<bool,Agent*> Dungeon::agentFireWeapon( Agent *a, std::string weapon, std::string attackSkill, std::string defenseSkill ){
if( !a ) return { false, nullptr };
if( weapon.empty() ) weapon = a->weapon;
auto it = weapons.find( weapon );
if( it == weapons.end() ) return { false, nullptr };
auto &weap = it->second;
if( weap.clipMax > 0.0 ){
// The weapon requires a clip and consumes ammo.
double cost = 1.0;
// Find the weapon in the agent's inventory.
InventoryInstance *instance = nullptr;
for( auto &instance_ref : a->inventory ){
if( instance_ref.weapon == a->weapon ){
instance = &instance_ref;
break;
}
}
if( !instance || instance->count == 0 || instance->clip < cost )
return { false, nullptr };
instance->clip = std::max( instance->clip - cost, 0.0 );
}
// Offset agent angle BEFORE the shot, accuracy/recoil depending on stats.
double recoil = weap.recoil;
if( attackSkill.length() > 0 ){
auto attack_skill_it = a->skills.find( attackSkill );
if( attack_skill_it != a->skills.end() ){
// Attack skill affects recoil.
// Simplified from the quadratic regression provided by this tool:
// https://planetcalc.com/8735/
// If skill <= 1.0, recoil is the weapon's base recoil.
// If skill == 10.0, recoil is about half.
// If skill == 100.0, recoil is about one fourth.
// If skill == 500.0, recoil is about one thirteenth.
if( attack_skill_it->second > 1.0 )
recoil /= ( 0.0228 * attack_skill_it->second + 1.4 );
}
}
double angle = (std::rand() % 1024) / 1024.0
* (weap.recoilCompensator ? 6.28318 : 3.14159);
rotateAgentConstrained(
a,
{std::sin(angle) * recoil, std::cos(angle) * recoil, 0.0}
);
// Fire. TODO: crouch_fire
a->weaponCooldown = 1.0 / weap.speed;
a->weaponFireSpotCooldown = weap.fireSpotDuration;
a->weaponTime = weap.animationFire.x;
auto fire_it = a->avatar.bodyAnimations.find("stand_fire");
if( fire_it != a->avatar.bodyAnimations.end() )
a->avatar.bodyFrame = fire_it->second.x;
bool pitch = a != playerAgent || !thirdPerson || weap.thirdPersonPitch;
auto ray = getAgentGazeRay(a, weap.range, pitch);
// Emit particles.
auto ps_it = particleServers.find(weap.particleFire.texture);
if(ps_it != particleServers.end()){
double startScale = weap.particleFire.startScale,
endScale = weap.particleFire.endScale,
startMass = weap.particleFire.startMass,
endMass = weap.particleFire.endMass,
maxAge = weap.particleFire.maxAge;
double svel = (endScale - startScale) / maxAge;
double mvel = (endMass - startMass) / maxAge;
// TODO: Randomly emit weap.particleFire.num particles.
for(size_t i = 0; i < 1; ++i){
Particle part{};
part.tvel = linalg::normalize(ray.second - ray.first)
* weap.particleFire.vel + a->lastVel;
part.translation = ray.first;
part.svel = svel;
part.scale = startScale;
part.mvel = mvel;
part.mass = startMass;
part.maxAge = maxAge;
part.damage = weap.particleFire.damage;
part.userptr = (void*)a;
ps_it->second.particles.push_back(part);
}
}
return {
true,
rayImpact(a, ray, weap.impact, weap.damage, defenseSkill)
};
}
// Add a weapon to the agent's inventory.
void Dungeon::agentAddWeapon( Agent *a, const std::string &weapon ){
for( auto &instance_ref : a->inventory ){
if( instance_ref.weapon == weapon ){
instance_ref.count++;
return;
}
}
InventoryInstance instance = {};
instance.weapon = weapon;
instance.count = 1;
a->inventory.push_back( instance );
}
// Reload the clip for the specified weapon.
void Dungeon::agentReloadWeapon( Agent *a, const std::string &weapon ){
// Find the global weapon definition.
auto weap_it = weapons.find( weapon );
if( weap_it == weapons.end() ) return;
Weapon &weap = weap_it->second;
// Find the agent's backup ammo.
auto backup_it = a->ammo.find( weap.ammo );
if( backup_it == a->ammo.end() ) return;
double &backup = backup_it->second;
// Transfer from backup to clip.
for( auto &instance_ref : a->inventory ){
if( instance_ref.weapon == weapon ){
double transfer =
std::min( weap.clipMax - instance_ref.clip, backup );
if( transfer > 0.0 ){
backup -= transfer;
instance_ref.clip += transfer;
}
return;
}
}
}
// Returns whether or not reloading is possible (or advisable).
bool Dungeon::agentCanReloadCurrentWeapon( Agent *a ){
// Find the global weapon definition.
auto weap_it = weapons.find( a->weapon );
if( weap_it == weapons.end() ) return false;
// Find the agent's backup ammo.
auto backup_it = a->ammo.find( weap_it->second.ammo );
if( backup_it == a->ammo.end() ) return false;
// Find the agent's weapon instance.
for( auto &instance_ref : a->inventory ){
if( instance_ref.weapon == a->weapon ){
// Return true if there is backup ammo to fill the clip.
return ( backup_it->second > 0.0
&& weap_it->second.clipMax > instance_ref.clip );
}
}
return false;
}
// Use mouse input to rotate the playerAgent with a constrained X angle.
void Dungeon::moveMouse( double x, double y ){
if( playerAgent ){
double f = -mouseSpeed / std::max( playerZoom, 1.0 );
rotateAgentConstrained( playerAgent, { y * f, x * f, 0.0 } );
}
}
// Prepare the light buffers.
void Dungeon::prepareLightBuffers( double d ){
pointLights.clear();
spotLights.clear();
// Add partition lights at night only. Looks better and runs faster.
// Consider them part of the power grid.
// Draw them anyways if lightsAlwaysOn is true. (Underground areas etc.)
if( shadowOffset.y < 0.0 || lights_always_on ){
for( size_t i = 0; i < nearestPartitions.size(); i++ ){
if( nearestPartitions[i].first >= partitions.size() )
continue;
auto &p = partitions[nearestPartitions[i].first];
fgltf::AddToWorldLights(
p.mapScene,
linalg::translation_matrix( p.translation ),
viewMat[3].xyz(),
lightRange,
pointLights,
spotLights
);
}
}
// Add agent lights.
Agent *agent_ptr = agents;
while( agent_ptr ){
if( agent_ptr->visible && (agent_ptr->health > 0.0 || agent_ptr->maxHealth == 0.0) && agent_ptr->model
&& (agent_ptr->type != TYPE_VEHICLE || agent_ptr->driverAgent) ){
auto agentMat = getAgentPoseMat( agent_ptr );
if( agent_ptr->type == TYPE_AGENT ){
// Origin at bottom of collider.
agentMat[3].y -= agent_ptr->scale.y * 0.5;
}
// Add the model's lights to the world.
fgltf::AddToWorldLights(
*agent_ptr->model,
agentMat,
viewMat[3].xyz(),
lightRange,
pointLights,
spotLights
);
}
if( agent_ptr->lightOn ){
// Add the agent's toggleable gaze spot.
addAgentGazeSpot(
agent_ptr,
agent_ptr->lightColor,
agent_ptr->lightRange,
agent_ptr->lightCone.x,
agent_ptr->lightCone.y
);
}
if( agent_ptr != playerAgent
&& agent_ptr->weaponFireSpotCooldown > 0.0 ){
// Add the agent's weapon fire spot.
Weapon *weap = nullptr;
auto it = weapons.find( agent_ptr->weapon );
if( it != weapons.end() ) weap = &it->second;
if( weap ){
addAgentGazeSpot(
agent_ptr,
weap->fireSpotColor,
weap->fireSpotRange,
weap->fireSpotCone.x,
weap->fireSpotCone.y
);
}
// Cool down the weapon fire spot.
agent_ptr->weaponFireSpotCooldown -= d;
}
agent_ptr = agent_ptr->next;
}
playerFireSpotOn = false;
if( playerAgent && playerAgent->weaponFireSpotCooldown > 0.0 ){
// Add the player's weapon fire spot.
Weapon *weap = nullptr;
auto it = weapons.find( playerAgent->weapon );
if( it != weapons.end() ) weap = &it->second;
if( weap ){
addAgentGazeSpot(
playerAgent,
weap->fireSpotColor,
weap->fireSpotRange,
weap->fireSpotCone.x,
weap->fireSpotCone.y
);
playerFireSpotOn = true;
}
// Cool down the weapon fire spot.
playerAgent->weaponFireSpotCooldown -= d;
}
}
// Animate the water.
void Dungeon::simulateWater( float wscale, double d ){
auto GetElevation = [&]( float u, float y, float v ){
return
( stb_perlin_noise3_seed( u * 4, y * 4, v * 4, 4, 4, 4, 0 )
* 0.25f
+ stb_perlin_noise3_seed( u * 8, y * 8, v * 8, 8, 8, 8, 1 )
* 0.125f )
* waterAmplitude;
};
float yscale = 1.0f;
int wsize = pbr ? 16 : 8;
static float perlinY;
perlinY = std::fmod<float>( perlinY + waterSpeed * d, wsize );
// Ease waterWaveHeight back down.
waterWaveHeight -= 0.25 * waterAmplitude * waterSpeed * d;
size_t vert_pos = 0, idx_pos = 0;
// Generate the mesh.
for( int x = 0; x <= wsize; x++ ){
for( int z = 0; z <= wsize; z++ ){
// Calculate UV coordinates.
float
u = x / (float)wsize,
v = z / (float)wsize;
float elev_left = 0.0f, elev_up = 0.0f;
// TODO: Add STB_PERLIN_INCLUDED to stb_perlin.h.
float elev = GetElevation( u, perlinY, v );
if( elev > waterWaveHeight )
waterWaveHeight = elev;
// Steal elevations from previous vertices where possible.
if( x == 0 )
elev_left = GetElevation( -1.0f / wsize, perlinY, v );
if( z == 0 )
elev_up = GetElevation( u, perlinY, -1.0f / wsize );
size_t idx = ( x - 1 ) * ( wsize + 1 ) + z;
if( x && idx < waterMesh.vertices.size() )
elev_left = waterMesh.vertices[idx].Position[1] / yscale;
idx = x * ( wsize + 1 ) + z - 1;
if( z && idx < waterMesh.vertices.size() )
elev_up = waterMesh.vertices[idx].Position[1] / yscale;
// Generate a normal vector based on the elevation of the
// current vertex and the elevations of 2 adjacent vertices.
/*
auto vec_a = linalg::vec<float,3>( wscale / wsize, 0.0f, ( elev - elev_left ) * yscale );
auto vec_b = linalg::vec<float,3>( 0.0f, wscale / wsize, ( elev - elev_up ) * yscale );
auto norm = linalg::cross(
linalg::normalize( vec_a ),
linalg::normalize( vec_b )
);
*/
auto norm = linalg::normalize( linalg::vec<float,3>(
(elev_left - elev) / wscale * wsize,
yscale,
(elev_up - elev) / wscale * wsize
) );
// Add the vertex to the mesh.
if( vert_pos >= waterMesh.vertices.size() ){
waterMesh.vertices.resize( vert_pos + 1 );
}
waterMesh.vertices[vert_pos] = {
{ x / (float)wsize * wscale, elev * yscale, z / (float)wsize * wscale },
{ norm.x, norm.y, norm.z },
{ 1.0f, 0.0f, 0.0f }, // Vertex shaders can correct tangents.
{ u, v }
};
vert_pos++;
if( x && z ){
// Add the triangles to the vertex and neighbors.
// Determine the indices from the x and z coordinates.
if( idx_pos >= waterMesh.indices.size() ){
waterMesh.indices.resize( idx_pos + 6 );
}
auto &inds = waterMesh.indices;
inds[idx_pos ] = (fgl::Index)( ( x - 1 ) * ( wsize + 1 ) + z - 1 ); // Up and left by 1.
inds[idx_pos + 1] = (fgl::Index)( ( x - 1 ) * ( wsize + 1 ) + z ); // Left by 1.
inds[idx_pos + 2] = (fgl::Index)( x * ( wsize + 1 ) + z ); // Current position.
inds[idx_pos + 3] = (fgl::Index)( x * ( wsize + 1 ) + z );
inds[idx_pos + 4] = (fgl::Index)( x * ( wsize + 1 ) + z - 1 ); // Up by 1.
inds[idx_pos + 5] = (fgl::Index)( ( x - 1 ) * ( wsize + 1 ) + z - 1 ); // Up and left by 1.
idx_pos += 6;
}
}
}
// Resize the vectors if the new mesh is smaller than the old mesh.
if( vert_pos < waterMesh.vertices.size() )
waterMesh.vertices.resize( vert_pos );
if( idx_pos < waterMesh.indices.size() )
waterMesh.indices.resize( idx_pos );
// Upload the mesh to graphics memory.
if( waterMesh.success ){
fgl::updateMesh( waterMesh, waterMesh.vertices, waterMesh.indices, true );
}else{
waterMesh = fgl::loadMesh( waterMesh.vertices, waterMesh.indices, true );
}
// Buffer water tiles.
waterBuffer.clear();
// Move the water with the camera.
linalg::vec<double,3> player_offset = {
std::floor( viewMat[3].x / wscale ) * wscale,
0.0,
std::floor( viewMat[3].z / wscale ) * wscale
};
/*
// * * *
// * * * * *
// * * * * *
// * * * * *
// * * *
for( int x = -2; x < 3; x++ ){
for( int y = -2; y < 3; y++ ){
// Skip corners.
if( ( x == -2 || x == 2 ) && ( y == -2 || y == 2 ) )
continue;
linalg::vec<double,3> o = {
x * wscale,
waterLevel,
y * wscale
};
waterBuffer.push(
{ fgl::fogColor.r, fgl::fogColor.g, fgl::fogColor.b, 1.0f },
linalg::translation_matrix<double>( o + player_offset )
);
}
}
*/
/*
for( int x = -4; x < 5; x++ ){
for( int y = -4; y < 5; y++ ){
linalg::vec<double,3> o = {
x * wscale,
waterLevel,
y * wscale
};
waterBuffer.push(
{ fgl::fogColor.r, fgl::fogColor.g, fgl::fogColor.b, 1.0f },
linalg::translation_matrix<double>( o + player_offset )
);
}
}
*/
// Send the fog color to the water instance pipeline.
fgl::Color waterColor = fgl::getFog();
waterColor.a = 1.0f;
// Spiral out from the player.
int distance = 1, direction = 0, x = 0, y = 0;
// 100 seems to be the magic number on old NVidia cards.
// However, overdraw can cause lag, so we'll use a conservative 64.
for( int i = 0; i < 64; i++ ){
linalg::vec<double,3> o = {
x * wscale,
waterLevel,
y * wscale
};
waterBuffer.push(
waterColor,
linalg::translation_matrix<double>( o + player_offset ),
linalg::identity
);
// Move the offset coordinates.
if( direction == 0 ){
x++;
if( x >= distance ){
direction = 1;
}
}else if( direction == 1 ){
y++;
if( y >= distance ){
direction = 2;
}
}else if( direction == 2 ){
x--;
if( x <= -distance ){
direction = 3;
}
}else{
y--;
if( y <= -distance ){
direction = 0;
distance++;
}
}
}
// Add larger water planes around the center area.
for( int i = 0; i < 8; i++ ){
static int offset_x[8] = { 5, 5, -3, -11, -11, -11, -3, 5 };
static int offset_y[8] = { -3, -11, -11, -11, -3, 5, 5, 5 };
linalg::vec<double,3> o = {
offset_x[i] * wscale,
waterLevel - waterWaveHeight * 1.5,
offset_y[i] * wscale
};
waterBuffer.push(
waterColor,
linalg::mul(
linalg::translation_matrix<double>( o + player_offset ),
linalg::scaling_matrix<double>( { 8.0, 1.0, 8.0 } )
),
linalg::identity
);
}
// Add the outermost water planes leagues away on the horizon.
for( int i = 0; i < 8; i++ ){
static int offset_x[8] = { 13, 13, -11, -35, -35, -35, -11, 13 };
static int offset_y[8] = { -11, -35, -35, -35, -11, 13, 13, 13 };
linalg::vec<double,3> o = {
offset_x[i] * wscale,
waterLevel - waterWaveHeight * 4.5,
offset_y[i] * wscale
};
waterBuffer.push(
waterColor,
linalg::mul(
linalg::translation_matrix<double>( o + player_offset ),
linalg::scaling_matrix<double>( { 24.0, 2.0, 24.0 } )
),
linalg::identity
);
}
// Upload attributes.
waterBuffer.upload();
}
template<class T> linalg::mat<T,4,4> ortho(
T left,
T right,
T bottom,
T top,
T near,
T far ){
return {
{ 2 / (right - left), 0, 0, 0 },
{ 0, 2 / (top - bottom), 0, 0 },
// Oldschool OpenGL clip space.
//{ 0, 0, 2 / (near - far), 0 },
//{ -(right + left) / (right - left), -(top + bottom) / (top - bottom), -(far + near) / (far - near), 1 }
// glClipControl 0-1 depth clip space.
{ 0, 0, 1 / (near - far), 0 },
{ -(right + left) / (right - left), -(top + bottom) / (top - bottom), near / (near - far), 1 }
};
}
// Physics, shadow, and camera position handling.
void Dungeon::simulate( double maxDraw, double d, std::string defenseSkill ){
if( !dynamicsWorld )
return;
simulateWater( 16.0f, d );
dynamicsWorld->setGravity( btVector3( 0.0, gravity * -1.0, 0.0 ) );
age += d;
cloudScroll += cloudSpeed * d;
// Play in-range partition animations.
for( size_t i = 0; i < nearestPartitions.size(); i++ ){
if( nearestPartitions[i].first >= partitions.size() )
continue;
auto &p = partitions[nearestPartitions[i].first];
// Loop the glTF scene's animations.
for( size_t j = 0; j < p.mapScene.animations.size(); j++ ){
auto &anim = p.mapScene.animations[j];
float animLength = 0.0f;
for( auto &chan : anim.channels ){
animLength = std::max( animLength, chan.timeline.back() );
}
float animTime = (float)std::fmod( age, (double)animLength );
fgltf::SetSceneAnimation( p.mapScene, i, animTime );
}
if( p.mapScene.animations.size() > 0 ){
// Update the physics data.
fgltf::UpdateSceneDynamics(
p.mapScene,
linalg::translation_matrix( p.translation ),
p.friction,
p.restitution
);
}
}
if( d > 0.0 ){
int iterations = 5;
double physicsDelta = std::min( d, 0.1 ) / iterations;
for( int i = 0; i < iterations; i++ ){
if( dynamicsWorld->stepSimulation( physicsDelta, 0 ) > 0 ){
updateCallback( physicsDelta );
}
}
}else{
dynamicsWorld->stepSimulation( 0.0, 0 );
}
if( playerAgent ){
// The player agent exists. May be in vehicle fwiw.
// Check for playerAgent/portal collisions.
int numManifolds = dispatcher->getNumManifolds();
for( int i = 0; i < numManifolds; i++ ){
btPersistentManifold *mf =
dispatcher->getManifoldByIndexInternal( i );
btCollisionObject *ob0 = (btCollisionObject*)mf->getBody0();
btCollisionObject *ob1 = (btCollisionObject*)mf->getBody1();
std::string *name;
// Get the name of any object colliding with playerAgent.
if( ( ob0 == playerAgent->rigidBody
&& ( name = (std::string*)ob1->getUserPointer() ) )
|| ( ob1 == playerAgent->rigidBody
&& ( name = (std::string*)ob0->getUserPointer() ) ) ){
// Portals.
if( portalCallback && name->length() >= 6
&& name->substr( 0, 6 ) == "portal" ){
std::string param =
name->length() >= 8 ? name->substr( 7 ) : "";
if(param == "kill")
playerAgent->health = 0.0;
else
(*portalCallback)(param);
}
}
}
if( !playerAgent )
return;
}
if( playerAgent ) updateMonoCamera();
// Partition sort-em-up without reallocating nearestPartitions.
linalg::vec<double,3> cameraPosition = viewMat[3].xyz();
nearestPartitions.resize( 0 );
for( size_t i = 0; i < partitions.size(); i++ ){
nearestPartitions.push_back( {
i,
linalg::length( cameraPosition - partitions[i].translation )
} );
}
std::sort( nearestPartitions.begin(), nearestPartitions.end(),
[]( const std::pair<size_t,double> &a, const std::pair<size_t,double> &b ){
return a.second < b.second;
} );
for( size_t i = 0; i < nearestPartitions.size(); i++ ){
if( nearestPartitions[i].second > std::min(maxPartitionDistance, maxDraw) ){
nearestPartitions.resize( i );
break;
}
}
// Simulate agents.
Agent *agent_ptr = agents;
while( agent_ptr ){
// Falling damage.
if( agent_ptr->type == TYPE_AGENT && agent_ptr->rigidBody
&& agent_ptr->rigidBody->getUserPointer() != &defaultAgent ){
auto v = agent_ptr->rigidBody->getLinearVelocity();
linalg::vec<double,3> vel(v.getX(), v.getY(), v.getZ());
double dist = linalg::length( vel - agent_ptr->lastVel );
agent_ptr->lastVel = vel;
if( dist > agent_ptr->collisionDamageThreshold ){
double damage = dist * ( agent_ptr->parentAgent
? agent_ptr->sittingCollisionDamageFactor
: agent_ptr->collisionDamageFactor );
agent_ptr->health -= damage;
if( collisionDamageCallback )
(*collisionDamageCallback)( agent_ptr, damage );
}
}
// AI.
if( aiFrameCallback && agent_ptr->rigidBody
&& agent_ptr->rigidBody->getUserPointer() != &defaultAgent ){
btVector3 &o =
agent_ptr->rigidBody->getWorldTransform().getOrigin();
linalg::vec<double,3> pos( o.getX(), o.getY(), o.getZ() );
if( linalg::length( cameraPosition - pos ) > maxAIDistance ){
// TODO: Out-of-range behavior.
}else{
// Run AI. Remove agent if aiFrameCallback returns true.
if( (*aiFrameCallback)( agent_ptr, d ) ){
// Flag the agent for removal and skip to the next agent.
agent_ptr->health = 0.0;
agent_ptr = agent_ptr->next;
continue;
}
}
}
// Animations.
if( agent_ptr->model ){
fgltf::Scene &scn = *agent_ptr->model;
// Play all agent model animations.
float animLength = 0.0f;
for( size_t i = 0; i < scn.animations.size(); i++ ){
auto &anim = scn.animations[i];
for( auto &chan : anim.channels ){
animLength = std::max( animLength, chan.timeline.back() );
}
}
float animTime = agent_ptr->modelLoop
? (float)std::fmod( age, (double)animLength ) : (float)age;
for( size_t i = 0; i < scn.animations.size(); i++ ){
fgltf::SetSceneAnimation( scn, i, animTime );
}
//printf( "%f / %f\n", animTime, animLength );
}else{
// Simulate the avatar. TODO: Melee, non-physical walking.
btVector3 velVec =
agent_ptr->rigidBody ? agent_ptr->rigidBody->getLinearVelocity() : btVector3(0,0,0);
double velHoriz = linalg::length(linalg::vec<double,2>(velVec.x(), velVec.z()));
bool jumping = agent_ptr->type == TYPE_AGENT && agent_ptr->standing == 0.0;
if( agent_ptr->health > 0.0 ){
auto fire_it = agent_ptr->avatar.bodyAnimations.find("stand_fire");
if( fire_it != agent_ptr->avatar.bodyAnimations.end()
&& agent_ptr->avatar.bodyFrame >= fire_it->second.x
&& agent_ptr->avatar.bodyFrame < fire_it->second.y ){
// Standing fire animation.
agent_ptr->avatar.simulate( "stand_fire", d, false );
}else if( agent_ptr->eyeHeight < agent_ptr->scale.y - agent_ptr->scale.x * 0.5 ){
// Crouching.
if( velHoriz > 0.1 && !jumping ){
// Crawl animation.
agent_ptr->avatar.simulate( "crawl", d );
}else{
// Crouch idle animation.
agent_ptr->avatar.simulate( "crouch_idle", d );
}
}else{
// Not crouching.
if( jumping ){
// Jump animation.
agent_ptr->avatar.simulate( "jump", d );
}else if( velHoriz > 0.1 ){
if( velHoriz < agent_ptr->walkSpeed * 1.5 ){
// Walk animation.
agent_ptr->avatar.simulate( "walk", d );
}else{
// Sprint animation.
agent_ptr->avatar.simulate( "sprint", d );
}
}else{
// Idle animation.
agent_ptr->avatar.simulate( "idle", d );
}
}
}else{
// Die animation.
agent_ptr->avatar.simulate( "die", d, false );
if(agent_ptr->rigidBody){
// Zero mass if standing, otherwise fall infinitely.
if(agent_ptr->standing != 0.0){
btVector3 btzero(0,0,0);
agent_ptr->mass = 0.0;
agent_ptr->rigidBody->setMassProps(0.0, btzero);
// Zero velocity.
agent_ptr->rigidBody->setLinearVelocity(btzero);
agent_ptr->rigidBody->setAngularVelocity(btzero);
}
// Disable collisions.
setAgentCollisions(agent_ptr, false);
}
}
}
agent_ptr = agent_ptr->next;
}
// Clean up dead agents.
agent_ptr = agents;
while( agent_ptr ){
Agent *a = agent_ptr->next;
if(agent_ptr && agent_ptr != playerAgent
&& agent_ptr->health <= 0.0 && agent_ptr->maxHealth > 0.0){
// Prevent this from triggering again by setting maxHealth negative.
agent_ptr->maxHealth = -1.0;
// Run the agent's death callback.
if(portalCallback && agent_ptr->onDeath.length() > 0)
(*portalCallback)( agent_ptr->onDeath );
// Special case for static agents: Replace the physics model.
if(agent_ptr->type == fdungeon::TYPE_STATIC)
setAgentModel(agent_ptr, agent_ptr->modelDestroyedFile);
// Remove the agent from the world (though probably not completely from memory until next level load).
removeAgent( agent_ptr, true );
}
agent_ptr = a;
}
// Simulate particles.
for( auto &ps : particleServers ){
ps.second.simulate( d );
// Simulate collisions and damage for damaging particles.
for( auto &p : ps.second.particles ){
if( p.damage != 0.0 ){
// Cast a ray from the particle's previous position to its current position.
// Cap mass to 10 kg because it may be set very high in order to cheat drag.
// Ultra-high mass = ultra-high impact = objects clip through walls
auto tprev = p.translation - p.tvel * d;
if(rayImpact((Agent*)p.userptr, {tprev, p.translation},
linalg::length(p.tvel) * std::min(p.mass, 10.0), p.damage, defenseSkill)){
p.age = DBL_MAX;
}
}
}
}
// Configure the shadow map.
//shadowViewMat = linalg::pose_matrix( fgl::eulerToQuat( -0.7, 0.0, 0.0 ), { 0.0, 10.0, 20.0 } );
/*
fgl::setLightMatrix( linalg::mul(
ortho<double>( -15.0, 15.0, -15.0, 15.0, 2.0, 40.0 ),
linalg::lookat_matrix<double>( { 10.0, 7.2, 12.0 }, { 10.0, 0.0, 0.0 }, { 0.0, 1.0, 0.0 } )
) );
*/
// Avoid gimbal-locking the sun.
if( shadowOffset.x == 0.0 && shadowOffset.z == 0.0 )
shadowOffset.x = 0.0001;
// Rotate the shadow map by about 45 degrees.
// The calculation isn't perfect, but it avoids popping artifacts.
lightOriginMat = linalg::mul(
ortho<double>( -shadowScale, shadowScale, -shadowScale, shadowScale, shadowNearClip, shadowFarClip ),
linalg::lookat_matrix<double>( shadowOffset, { 0.0, 0.0, 0.0 }, { 1.0, 0.0, 1.0 } )
);
// Shoot the shadow in the gun direction 2-dimensionally. TODO: Verify this is working correctly.
auto dir = linalg::normalize( linalg::transpose( viewMat )[2].xyz() ) * linalg::vec<double,3>( -1.0, 0.0, 1.0 );
linalg::mat<double,4,4> lightMat = linalg::mul(
lightOriginMat,
linalg::translation_matrix( dir * shadowScale - viewMat[3].xyz() )
);
// Snap the light position to the nearest texel along X and Y in light space.
double snapSize = 2.0 / shadowResolution;
lightMat[3].x = std::floor( lightMat[3].x / snapSize ) * snapSize;
lightMat[3].y = std::floor( lightMat[3].y / snapSize ) * snapSize;
fgl::setLightMatrix( lightMat );
if( shadowOffset.y >= 0.0 ){
// Render to the multisample shadow map. Fill the shadow buffer.
fgl::setFramebuffer(shadowHighRes ? shadowBufMultisample : shadowBufLowMultisample);
glClearColor( 1.0f, 1.0f, 1.0f, 1.0f );
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
drawSolid( maxDraw, true );
fgl::setPipeline( particleShadowPipeline );
fgl::Texture tex = fgl::getBlankTexture();
for(auto &ps : particleServers)
ps.second.draw(tex, linalg::identity, linalg::identity, true);
// Blit the multisample shadow map to the non-multisample shadow map.
fgl::setFramebuffer(shadowHighRes ? shadowBuf : shadowBufLow);
fgl::drawFramebuffer(shadowHighRes ? shadowBufMultisample : shadowBufLowMultisample);
// Blur the shadow.
if( !shadowBlurPipeline.success ) return;
fgl::setPipeline( shadowBlurPipeline );
// Reset the texture matrix.
fgl::setTextureMatrix( linalg::identity );
auto u_blurScale =
glGetUniformLocation( shadowBlurPipeline.programObject, "u_blurScale" );
float s = 1.4 / (shadowHighRes ? shadowResolution : shadowResolutionLow);
// Shadow map blur (first pass)
glUniform2f(
u_blurScale,
s,
0.0
);
fgl::setFramebuffer(shadowHighRes ? shadowBufTemp : shadowBufLowTemp); // <-- to
fgl::drawFramebuffer(shadowHighRes ? shadowBuf : shadowBufLow); // from -->
// Shadow map blur (second pass)
glUniform2f(
u_blurScale,
0.0,
s
);
fgl::setFramebuffer(shadowHighRes ? shadowBuf : shadowBufLow); // <-- to
fgl::drawFramebuffer(shadowHighRes ? shadowBufTemp : shadowBufLowTemp); // from -->
}
// Switch to the main framebuffer.
fgl::setFramebuffer( *frameBuf, frameBufScale );
// Prepare the light buffers.
prepareLightBuffers( d );
// Update viewMatL and viewMatR.
updateStereoCamera();
}
// Create view and projection matrices based on the player's transform.
void Dungeon::updateMonoCamera(){
if( playerAgent ){
if( playerAgent->parentAgent
&& playerAgent->parentAgent->driverAgent == playerAgent ){
// In vehicle.
// Parent the camera to the parent agent.
viewMat = linalg::mul(
getAgentPoseMat( playerAgent->parentAgent ),
linalg::pose_matrix(
fgl::eulerToQuat(
playerAgent->angle.x,
playerAgent->angle.y,
playerAgent->angle.z ),
playerAgent->parentAgent->driverEyeTranslation
)
);
}else{
// Not in vehicle.
linalg::mat<double,4,4> poseMat = getAgentPoseMat( playerAgent );
// Turn camera eulers into matrices.
viewMat = linalg::pose_matrix(
topDown
? fgl::eulerToQuat(-1.570796, playerAgent->angle.y, 0.0)
: fgl::eulerToQuat(playerAgent->angle.x,
playerAgent->angle.y + (thirdPerson ? thirdPersonYawOffset : 0.0),
playerAgent->angle.z),
linalg::vec<double,3>(
poseMat[3].x,
topDown
? farClip * 0.5
: poseMat[3].y - playerAgent->scale.y * 0.5 + playerAgent->eyeHeight,
poseMat[3].z
)
);
}
}
// Offset the view matrix for third-person mode.
if( playerAgent && thirdPerson && !topDown ){
// Multiply in reverse order to move mat4 relative to its own orientation.
auto destMat = linalg::mul(
viewMat,
linalg::translation_matrix(thirdPersonCameraTranslation)
);
// Use the foot sphere to check for intersections with physical geometry.
btVector3 from(viewMat[3].x, viewMat[3].y, viewMat[3].z);
btVector3 to(destMat[3].x, destMat[3].y, destMat[3].z);
btTransform fromTrans, toTrans;
fromTrans.setIdentity();
fromTrans.setOrigin(from);
toTrans.setIdentity();
toTrans.setOrigin(to);
// https://pybullet.org/Bullet/BulletFull/classbtClosestNotMeConvexResultCallback.html
// A fixed version of Bullet's official class.
btrapClosestNotMeConvexResultCallback rayResult(
playerAgent->rigidBody,
from,
to,
(btOverlappingPairCache*)overlappingPairCache
);
dynamicsWorld->convexSweepTest(playerAgent->footSphere, fromTrans, toTrans, rayResult);
if(rayResult.hasHit()){
viewMat[3].xyz() += (destMat[3].xyz() - viewMat[3].xyz())
* rayResult.m_closestHitFraction;
}else{
viewMat = destMat;
}
}
// Create the projection matrix.
double aspect = (double)frameBuf->width / (double)frameBuf->height;
projMat = topDown
? ortho( -orthoRadius * aspect, orthoRadius * aspect, -orthoRadius, orthoRadius, nearClip, farClip )
: linalg::perspective_matrix( verticalField / playerZoom, aspect, nearClip, farClip );
}
// Update viewMatL and viewMatR based on viewMat and ipd.
void Dungeon::updateStereoCamera(){
// Make a copy of the original view matrix.
viewMatCenter = viewMat;
// Multiply in reverse order to move mat4 relative to its own orientation.
// Cheat the IPD when zooming. (This should not affect VR unless you zoom in VR. o.O)
viewMatL = linalg::mul(
viewMat,
linalg::pose_matrix(
fgl::eulerToQuat(0.0, -stereoAngle / playerZoom, 0.0),
linalg::vec<double,3>((ipd / playerZoom) * -0.5, 0.0, 0.0))
);
viewMatR = linalg::mul(
viewMat,
linalg::pose_matrix(
fgl::eulerToQuat(0.0, stereoAngle / playerZoom, 0.0),
linalg::vec<double,3>((ipd / playerZoom) * 0.5, 0.0, 0.0))
);
}
// Switch to a dungeon rendering pipeline for non-instanced objects.
fgl::Pipeline Dungeon::useDungeonPipeline( bool skinPass, bool shadowPass ){
if(!skinPipeline.success) skinPass = false;
auto &pipeline = skinPass
? skinPipeline
: ( pbr ? pbrDungeonPipeline : dungeonPipeline );
if( skinPass && shadowPass ){
// Less optimal than a skin shadow pipeline, but better than nothing.
fgl::setPipeline( skinPipeline );
return skinPipeline;
}else if( pipeline.success && !shadowPass ){
fgl::setPipeline( pipeline );
fgl::setTexture( irradiance, 4 );
// Pass the shadow map texture to the shader.
glActiveTexture( GL_TEXTURE3 );
glBindTexture( GL_TEXTURE_2D, shadowHighRes ? shadowBuf.texture : shadowBufLow.texture );
// TODO: Get rid of glGet* at render time as they can cost performance.
glUniform1f(
glGetUniformLocation( pipeline.programObject, "u_emissiveScale" ),
emissive_scale
);
glUniform4f(
glGetUniformLocation( pipeline.programObject, "u_ambient" ),
ambient_light.r * lightBrightness,
ambient_light.g * lightBrightness,
ambient_light.b * lightBrightness,
ambient_light.a
);
glUniform4f(
glGetUniformLocation( pipeline.programObject, "u_underwater" ),
underwater_color.r,
underwater_color.g,
underwater_color.b,
underwater_color.a
);
glUniform2f(
glGetUniformLocation( pipeline.programObject, "u_waterHeights" ),
(float)waterLevel,
waterWaveHeight
);
// Send the lights to the shader.
glUniform1i(
glGetUniformLocation( pipeline.programObject, "u_numPointLights" ),
(GLint)( pointLights.size() / 8 )
);
glUniform4fv(
glGetUniformLocation( pipeline.programObject, "u_pointLights" ),
(GLsizei)( pointLights.size() / 4 ),
pointLights.data()
);
glUniform1i(
glGetUniformLocation( pipeline.programObject, "u_numSpotLights" ),
(GLint)( spotLights.size() / 16 )
);
glUniform4fv(
glGetUniformLocation( pipeline.programObject, "u_spotLights" ),
(GLsizei)( spotLights.size() / 4 ),
spotLights.data()
);
return pipeline;
}else if( shadowPipeline.success && shadowPass ){
fgl::setPipeline( shadowPipeline );
return shadowPipeline;
}else{
fgl::Pipeline unlitPipeline = fgl::getUnlitPipeline();
fgl::setPipeline( unlitPipeline );
return unlitPipeline;
}
}
// Draw solid world geometry and agents.
void Dungeon::drawSolid( double maxDraw, bool shadowPass, int agent_types ){
glEnable( GL_DEPTH_TEST );
glDisable( GL_BLEND );
GLint u_outlineThickness = 0, u_outlineColor = 0;
if(outline && !shadowPass && outlinePipeline.success){
u_outlineThickness = glGetUniformLocation(outlinePipeline.programObject, "u_outlineThickness");
u_outlineColor = glGetUniformLocation(outlinePipeline.programObject, "u_outlineColor");
}
if( outline && !topDown && !shadowPass && outlinePipeline.success ){
// Draw outline hulls.
// These are cheap enough that in theory, their occlusion of
// more expensive faces behind them saves render time.
fgl::setPipeline( outlinePipeline );
glUniform1f(
u_outlineThickness,
outlineThickness / playerZoom
);
glUniform4f(
u_outlineColor,
outline_regular.r,
outline_regular.g,
outline_regular.b,
outline_regular.a
);
glCullFace( GL_FRONT );
size_t maxParts = nearestPartitions.size();
// Hard-coded limit of 9 world partitons drawn in shadow maps.
// This is currently not enabled because it breaks immersion.
//if( maxParts > 9 && shadowPass ) maxParts = 9;
for( size_t i = 0; i < maxParts; i++ ){
if( nearestPartitions[i].first >= partitions.size() )
continue;
auto &p = partitions[nearestPartitions[i].first];
// Draw glTF outline.
fgltf::DrawScene(
p.mapScene,
linalg::translation_matrix( p.translation ),
viewMat,
projMat,
farClip,
true, // Disable material switching.
fgltf::ALPHA_OPAQUE,
fgltf::MESH_RIGID
);
if( p.terrain.mesh.success ){
// Draw terrain outline.
terra::DrawTerrain(
p.terrain,
p.translation,
viewMat,
projMat
);
}
}
}
// Set a shader pipeline.
useDungeonPipeline( false, shadowPass );
fgl::Texture blankTexture = fgl::getBlankTexture();
if( shadowPass && worldFloor != 0.0 ){
// Draw the world floor (important to avoid VSM glitches).
fgl::Mesh cube = fgl::getCubeMesh();
double s = std::max( shadowScale, shadowFarClip ) * 20.0;
auto oldFog = fgl::getFog();
fgl::setFog({oldFog.r, oldFog.g, oldFog.b, 9999.0f});
//fgl::setTexture(blankTexture, 0);
fgl::drawMesh(
cube,
linalg::mul(
linalg::translation_matrix<double>( { viewMat[3].x, worldFloor, viewMat[3].z } ),
linalg::scaling_matrix<double>( { s, 0.0, s } )
),
viewMat,
projMat
);
fgl::setFog(oldFog);
}
// Draw the opaque parts of the GLTF map.
//glCullFace( shadowPass ? GL_FRONT : GL_BACK );
glCullFace( GL_BACK );
for( size_t i = 0; i < nearestPartitions.size(); i++ ){
if( nearestPartitions[i].first >= partitions.size() )
continue;
auto &p = partitions[nearestPartitions[i].first];
// Draw glTF.
fgltf::DrawScene(
p.mapScene,
linalg::translation_matrix( p.translation ),
viewMat,
projMat,
farClip,
shadowPass,
( fgltf::ALPHA_OPAQUE | fgltf::ALPHA_MASK ),
fgltf::MESH_RIGID
);
if( p.terrain.mesh.success ){
// Draw terrain.
if( !shadowPass ){
// Set diffuse texture. TODO: More efficient terrain texture access.
fgl::setTexture(
fgltf::images[fgltf::LoadImage(ModFopen, dungeonPath + "/", p.terrain.textureName)].texture,
0
);
// Set metallic/roughness texture.
fgl::setTexture( blankTexture, 1 );
// Set metallic and roughness factors.
fgl::setMetallicFactor( 0.0f );
fgl::setRoughnessFactor( 0.8f );
// Set base color and emissive factors.
fgl::setBaseColorFactor( fgl::newColor );
fgl::setEmissiveFactor( 0.0f, 0.0f, 0.0f );
}
terra::DrawTerrain(
p.terrain,
p.translation,
viewMat,
projMat
);
}
}
// Draw agents. TODO: Draw agent models with independently controlled animations.
int passes = (outline && !topDown && !shadowPass && outlinePipeline.success) ? 3 : 2;
for( int pass = 0; pass < passes; pass++ ){
// The outline occlusion optimization above probably isn't that useful here.
// Pass 0: Regular pipeline. Pass 1: Skins. Pass 2: Outlines.
if( pass == 1 ){
useDungeonPipeline( true, shadowPass );
}else if( pass == 2 ){
fgl::setPipeline( outlinePipeline );
glUniform1f(
u_outlineThickness,
outlineThickness / playerZoom
);
glUniform4f(
u_outlineColor,
outline_regular.r,
outline_regular.g,
outline_regular.b,
outline_regular.a
);
glCullFace( GL_FRONT );
}
Agent *agent_ptr = agents;
while( agent_ptr ){
if( (agent_ptr->type & agent_types)
&& agent_ptr->visible && !agent_ptr->parentAgent
&& (agent_ptr != playerAgent || thirdPerson) ){
auto agentMat = getAgentPoseMat( agent_ptr );
if( agent_ptr->type == TYPE_AGENT ){
// Origin at bottom of collider.
agentMat[3].y -= agent_ptr->scale.y * 0.5;
}
// Check if agent is within draw distance.
if(linalg::length(viewMat[3].xyz() - agentMat[3].xyz()) < maxDraw){
// Check for agent target match.
if(pass == 2 && agent_ptr == targetAgent){
glUniform4f(
u_outlineColor,
outline_highlight.r,
outline_highlight.g,
outline_highlight.b,
outline_highlight.a
);
}
if( agent_ptr->model ){
// Draw the agent model.
fgltf::DrawScene(
(agent_ptr->health > 0.0 || agent_ptr->maxHealth == 0.0 || !agent_ptr->modelDestroyed)
? *agent_ptr->model : *agent_ptr->modelDestroyed,
agentMat,
viewMat,
projMat,
farClip,
shadowPass || ( pass == 2 ),
( fgltf::ALPHA_OPAQUE | ( pass == 2 ? 0 : fgltf::ALPHA_MASK ) ),
( pass == 1 ? fgltf::MESH_SKIN : fgltf::MESH_RIGID )
);
}else{
// Set default material settings.
fgl::setBaseColorFactor( fgl::newColor );
fgl::setMetallicFactor( 0.0f );
fgl::setRoughnessFactor( 0.5f );
fgl::setEmissiveFactor( 0.0f, 0.0f, 0.0f );
// Set the metallic roughness texture.
fgl::setTexture( blankTexture, 1 );
// Set the emissive texture.
fgl::setTexture( blankTexture, 2 );
// Draw the agent's avatar.
agent_ptr->avatar.draw(
agentMat,
viewMat,
projMat,
farClip,
agent_ptr->weapon
);
}
/*
}else if( pass == 0 ){
// Draw a generic box.
fgl::drawMesh(
fgl::cubeMesh,
linalg::mul(
getAgentPoseMat( agent_ptr ),
linalg::scaling_matrix( agent_ptr->scale )
),
viewMat,
projMat
);
}
*/
if(pass == 2 && agent_ptr == targetAgent){
glUniform4f(
u_outlineColor,
outline_regular.r,
outline_regular.g,
outline_regular.b,
outline_regular.a
);
}
}
}
agent_ptr = agent_ptr->next;
}
}
// Switch to normal face culling.
glCullFace( GL_BACK );
}
void Dungeon::drawWater( bool shadowPass ){
bool have_pipeline =
waterInstancePipeline.success && waterTexture.success;
if( have_pipeline && !shadowPass ){
glBindFramebuffer( GL_FRAMEBUFFER, frameBuf->fbo );
// Switch to the water instance pipeline.
fgl::setPipeline( waterInstancePipeline );
// Use the water texture for base color and alpha.
fgl::setTexture( waterTexture, 0 );
if( radiance.success && false ){
// TODO: Radiance cubemap textures.
// Use the radiance cubemap for reflective lighting.
fgl::setTexture( radiance, 1 );
}
// Use the shadow map.
glActiveTexture( GL_TEXTURE1 );
glBindTexture( GL_TEXTURE_2D, shadowHighRes ? shadowBuf.texture : shadowBufLow.texture );
// Send the underwater color to the shader.
glUniform4f(
glGetUniformLocation( waterInstancePipeline.programObject, "u_underwater" ),
underwater_color.r,
underwater_color.g,
underwater_color.b,
underwater_color.a
);
// Send the near and far clip distances to the shader.
glUniform1f(
glGetUniformLocation( waterInstancePipeline.programObject, "u_near" ),
(float)nearClip
);
glUniform1f(
glGetUniformLocation( waterInstancePipeline.programObject, "u_far" ),
(float)farClip
);
// Send the lights to the shader.
glUniform1i(
glGetUniformLocation( waterInstancePipeline.programObject, "u_numSpotLights" ),
(GLint)( spotLights.size() / 16 )
);
glUniform4fv(
glGetUniformLocation( waterInstancePipeline.programObject, "u_spotLights" ),
(GLsizei)( spotLights.size() / 4 ),
spotLights.data()
);
}
if( !shadowPass ){
// Disable depth writes because water shaders read depth.
glDepthMask( GL_FALSE );
// Disable backface culling to view the surface from underwater.
glDisable( GL_CULL_FACE );
glEnable( GL_BLEND );
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
// Draw the instance buffer.
waterBuffer.draw( waterMesh, viewMat, projMat, fgl::getLightMatrix() );
if( shadowPass ) return;
glDepthMask( GL_TRUE );
glEnable( GL_CULL_FACE );
}
// Draw the transparent objects.
void Dungeon::drawTransparent(){
glEnable( GL_BLEND );
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Draw particles. TODO: Adjust drawing order.
for( auto &ps : particleServers ){
fgl::Texture *tex = getCacheTexture(ModFopen, particlePath + "/" + ps.first);
ps.second.draw( *tex, viewMat, projMat );
}
// Set a shader pipeline.
useDungeonPipeline( false, false );
// Draw the transparent parts of the GLTF map. (Partition by partition back-to-front.)
for( int i = (int)nearestPartitions.size() - 1; i >= 0; i-- ){
if( nearestPartitions[i].first >= partitions.size() )
continue;
auto &p = partitions[nearestPartitions[i].first];
fgltf::DrawScene(
p.mapScene,
linalg::translation_matrix( p.translation ),
viewMat,
projMat,
farClip,
false,
fgltf::ALPHA_BLEND,
fgltf::MESH_RIGID
);
}
}
// Draw everything. You should call simulate( d ) before calling draw().
void Dungeon::draw( double maxDraw, bool allowTransparent, fgl::Mesh *nodeMesh, int agent_types ){
Weapon *weap = nullptr;
linalg::mat<double,4,4> weapModelMat;
useDungeonPipeline( false, false );
auto depthCrunch =
glGetUniformLocation( fgl::getPipeline().programObject, "u_depthCrunch" );
if( playerAgent && !thirdPerson && !topDown ){
auto it = weapons.find( playerAgent->weapon );
if( it != weapons.end() ) weap = &it->second;
if( weap ){
// Get a factor from unzoomed to fully zoomed.
double f = ( weap->zoom > 1.0 )
? ( playerWeaponZoom - 1.0 ) / ( weap->zoom - 1.0 )
: 0.0;
// Calculate a model matrix.
linalg::vec<double,3> b( 0.0, playerAgent->weaponBob, 0.0 );
weapModelMat = linalg::mul(
viewMatCenter,
linalg::rotation_matrix(fgl::eulerToQuat(playerOffsetAngle.x, playerOffsetAngle.y, playerOffsetAngle.z)),
linalg::translation_matrix(linalg::lerp(weap->translation + b, weap->zoomTranslation + b, f))
);
// Set the weapon animation timestamp.
for( size_t i = 0; i < weap->model->animations.size(); i++ ){
fgltf::SetSceneAnimation( *weap->model, i, playerAgent->weaponTime );
}
}
// Hide the player's weapon spotlight.
if( playerFireSpotOn ){
glUniform1i(
glGetUniformLocation( fgl::getPipeline().programObject, "u_numSpotLights" ),
(GLint)std::max( (int)spotLights.size() / 16 - 1, 0 )
);
}
glEnable( GL_DEPTH_TEST );
int passes = ( weap && weap->model ) ? 3 : 0;
GLint u_outlineThickness = (passes == 3)
? glGetUniformLocation(outlinePipeline.programObject, "u_outlineThickness")
: 0;
for( int pass = 0; pass < passes; pass++ ){
// Pass 0: Normal pipeline. Pass 1: Skins. Pass 2: Outlines.
if( pass == 1 ){
//fgl::setPipeline( skinPipeline );
useDungeonPipeline( true, false );
}else if( pass == 2 ){
fgl::setPipeline( outlinePipeline );
glUniform1f(
u_outlineThickness,
outlineThickness / playerZoom
);
glCullFace( GL_FRONT );
}
if( depthCrunch != -1 ) glUniform1f( depthCrunch, 0.5f );
// Draw the weapon model.
fgltf::DrawScene(
*weap->model,
weapModelMat,
viewMat,
projMat,
farClip,
( pass == 2 ),
( fgltf::ALPHA_OPAQUE | ( pass == 2 ? 0 : fgltf::ALPHA_MASK ) ),
( pass == 1 ? fgltf::MESH_SKIN : fgltf::MESH_RIGID )
);
if( depthCrunch != -1 ) glUniform1f( depthCrunch, 0.0f );
}
}
// Show the player's weapon spotlight.
if( playerFireSpotOn ){
glUniform1i(
glGetUniformLocation( fgl::getPipeline().programObject, "u_numSpotLights" ),
(GLint)( spotLights.size() / 16 )
);
}
drawSolid( maxDraw, false, agent_types );
// Draw the sky after drawing solid geometry.
if( draw_cube && !topDown ){
setPipeline( skyPipeline );
glDisable( GL_CULL_FACE );
fgl::Mesh cube = fgl::getCubeMesh();
// Extract the 3x3 rotation matrix from the view matrix.
// The camera is effectively at <0,0,0> with the skybox.
drawMesh(
cube,
linalg::identity,
linalg::mat<double,4,4>(
{ viewMat[0][0], viewMat[0][1], viewMat[0][2], 0.0 },
{ viewMat[1][0], viewMat[1][1], viewMat[1][2], 0.0 },
{ viewMat[2][0], viewMat[2][1], viewMat[2][2], 0.0 },
{ 0.0, 0.0, 0.0, 1.0 }
),
projMat
);
glEnable( GL_CULL_FACE );
// Skybox.
//fgl::drawSkybox( radiance, viewMat, projMat, ambient_light );
}else{
// Solid color.
fgl::cls( fgl::getFog(), false );
}
// Draw clouds.
if( draw_clouds ){
fgl::setPipeline( cloudsPipeline );
glDepthMask( GL_FALSE );
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
fgl::setTexture( cloudsTexture, 0 );
auto old_fog = fgl::getFog();
fgl::setFog( { 0.0f, 0.0f, 0.0f, 0.01f } );
auto planeMesh = fgl::getPlaneMesh();
for( int i = 1; i <= 2; i++ ){
fgl::setTextureMatrix( linalg::mul(
linalg::translation_matrix(linalg::vec<double,3>(cloudScroll * i, 0.0, 0.0)),
linalg::scaling_matrix(linalg::vec<double,3>(6.5 / i, 6.5 / i, 6.5 / i))
) );
fgl::drawMesh(
planeMesh,
linalg::mul(
//linalg::translation_matrix(linalg::vec<double,3>(viewMat[3].x, viewMat[3].y + 20.0 - i * 0.1, viewMat[3].z)),
linalg::rotation_matrix(fgl::eulerToQuat(1.570796327, 0.0, 0.0)),
linalg::scaling_matrix(linalg::vec<double,3>(1200.0, 1200.0, 1200.0))
),
linalg::mat<double,4,4>(
{ viewMat[0][0], viewMat[0][1], viewMat[0][2], 0.0 },
{ viewMat[1][0], viewMat[1][1], viewMat[1][2], 0.0 },
{ viewMat[2][0], viewMat[2][1], viewMat[2][2], 0.0 },
{ 0.0, i * 0.1 - 20.0, 0.0, 1.0 }
),
projMat
);
}
fgl::setFog( old_fog );
fgl::setTextureMatrix( linalg::identity );
glDepthMask( GL_TRUE );
}
if( nodeMesh ){
useDungeonPipeline( false, false );
// Set default material settings.
fgl::setBaseColorFactor( {0.0f, 0.0f, 0.9f, 0.4f} );
fgl::setMetallicFactor( 0.0f );
fgl::setRoughnessFactor( 0.5f );
fgl::setEmissiveFactor( 0.0f, 0.0f, 0.1f );
fgl::Texture blankTexture = fgl::getBlankTexture();
// Set the diffuse texture.
fgl::setTexture( blankTexture, 0 );
// Set the metallic roughness texture.
fgl::setTexture( blankTexture, 1 );
// Set the emissive texture.
fgl::setTexture( blankTexture, 2 );
glCullFace( GL_BACK );
glDisable( GL_BLEND );
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
for( auto &n : worldNodes ){
auto m = linalg::pose_matrix( n.rotation, n.translation );
fgl::drawMesh( *nodeMesh, m, viewMat, projMat );
if( topDown && depthCrunch != -1 ){
// Draw a transparent version of the mesh over the top.
glUniform1f( depthCrunch, 0.9f );
glEnable( GL_BLEND );
fgl::drawMesh( *nodeMesh, m, viewMat, projMat );
glDisable( GL_BLEND );
glUniform1f( depthCrunch, 0.0f );
}
}
}
if( allowTransparent ){
drawWater();
drawTransparent();
int passes = ( weap && weap->model && !topDown ) ? 2 : 0;
for( int pass = 0; pass < passes; pass++ ){
// Pass 0: Normal pipeline. Pass 1: Skins.
if( pass == 1 ){
//fgl::setPipeline( skinPipeline );
useDungeonPipeline( true, false );
}
if( depthCrunch != -1 ) glUniform1f( depthCrunch, 0.5f );
// Hide the player's weapon spotlight.
if( playerFireSpotOn ){
glUniform1i(
glGetUniformLocation( fgl::getPipeline().programObject, "u_numSpotLights" ),
(GLint)std::max( (int)spotLights.size() / 16 - 1, 0 )
);
}
// Draw the weapon model.
fgltf::DrawScene(
*weap->model,
weapModelMat,
viewMat,
projMat,
farClip,
false,
fgltf::ALPHA_BLEND,
( pass == 1 ? fgltf::MESH_SKIN : fgltf::MESH_RIGID )
);
if( depthCrunch != -1 ) glUniform1f( depthCrunch, 0.0f );
}
}
// glGetError can cause a full pipeline sync, so disable for performance.
// TODO: Make this toggleable.
if( false ){
GLenum glerr;
while( ( glerr = glGetError() ) != GL_NO_ERROR ){
printf( "OpenGL error: 0x%.4X\n", glerr );
}
}
}
// Add a spotlight to the buffer without sending it to the shader.
void Dungeon::addAgentGazeSpot( Agent *a, fgl::Color color, GLfloat range, GLfloat innerConeAngle, GLfloat outerConeAngle ){
if( !a ) return;
auto ray = getAgentGazeRay( a, 1.0 );
auto dir = ray.first - ray.second;
// Offset the light from the eye to mitigate light glitches.
ray.first += dir * 0.05;
GLfloat lightAngleScale =
1.0f / std::max(0.001f, std::cos(innerConeAngle) - std::cos(outerConeAngle));
GLfloat lightAngleOffset = -std::cos(outerConeAngle) * lightAngleScale;
spotLights.insert( spotLights.end(), {
(GLfloat)ray.first.x, (GLfloat)ray.first.y + 0.03f, (GLfloat)ray.first.z, range,
color.r, color.g, color.b, color.a,
(GLfloat)dir.x, (GLfloat)dir.y, (GLfloat)dir.z, 0.0f,
lightAngleScale, lightAngleOffset, 0.0f, 0.0f
} );
}
// Spawn agents where specified.
void Dungeon::sceneNodeHandler( fgltf::SceneNode &node, linalg::mat<double,4,4> &m ){
// Get a look-at vector.
auto look = getNodeLookAt( m );
// Get an FPS-style Euler X angle from the look-at vector.
double angle_x = atan2(
look.y,
linalg::length<double,2>( { look.x, look.z } )
);
// Get an FPS-style Euler Y angle from the look-at vector.
double angle_y = std::atan2( look.x, look.z );
// Set the object's userPointer to the node name since that
// currently contains the most useful information.
if( node.rigidBody ){
node.rigidBody->setUserPointer( &node.name );
}
if( node.name == "spawn" ){
// Reposition the player agent or create a new one.
if( playerAgent ){
positionAgent( playerAgent, m[3].xyz() );
playerAgent->angle.x = angle_x;
playerAgent->angle.y = angle_y;
}else{
// Create an agent centered at the node's coordinates.
if( playerJSON.length() > 0 ){
playerAgent = parseAgent( playerJSON, "playerJSON", m );
}else{
playerAgent = loadAgent( playerAgentFile, m );
}
// Assign properties.
if( playerAgent ){
playerAgent->nodeName = node.name;
playerAgent->angle.x = angle_x;
playerAgent->angle.y = angle_y;
}
}
}else if( node.name.length() > 6
&& node.name.substr( 0, 5 ) == "agent" ){
// Create an arbitrary agent controlled by script.
Agent *agent = loadAgent( node.name.substr( 6 ), m );
// Assign additional properties.
if( agent ){
agent->nodeName = node.name;
agent->angle.x = angle_x;
agent->angle.y = angle_y;
}
}else if( node.name.length() >= 8
&& node.name.substr( 0, 8 ) == "waypoint" ){
// Add a waypoint.
waypoints.push_back( m[3] );
}else if( ( node.name.length() >= 5
&& node.name.substr( 0, 5 ) == "ghost" )
|| ( node.name.length() >= 6
&& node.name.substr( 0, 6 ) == "portal" ) ){
// This only runs if the node has a rigid body.
// You must make portals out of regular geometry.
if( node.rigidBody ){
// https://pybullet.org/Bullet/phpBB3/viewtopic.php?f=9&t=3997
// Disable collisions.
node.rigidBody->setCollisionFlags(
node.rigidBody->getCollisionFlags()
| btCollisionObject::CF_NO_CONTACT_RESPONSE
);
}
}
}
void Dungeon::worldNodeHandler( WorldNode &node, linalg::mat<double,4,4> &m ){
fgltf::SceneNode snode = {};
snode.name = node.name;
snode.rotation = node.rotation;
snode.translation = node.translation;
snode.rigidBody = node.rigidBody;
sceneNodeHandler( snode, m );
}
// Physics update callback. This only influences the physics engine and
// tries to converge the simulation rather than move objects over time.
// tl;dr don't put animation code in this function!
void Dungeon::updateCallback( double d ){
Agent *a = agents;
while( a ){
if( a->type != TYPE_STATIC && a->rigidBody ){
// Agent buoyancy.
double buoyancy = 20.0;
auto trans = a->rigidBody->getWorldTransform();
btVector3 o = trans.getOrigin();
double y = o.getY();
if( a->type == TYPE_VEHICLE ){
buoyancy = 12.0;
auto center =
linalg::vec<double,3>( 0.0, a->scale.y * 0.5 + a->wheelFloor, 0.0 );
btQuaternion r = trans.getRotation();
y = linalg::mul(
linalg::pose_matrix(
linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() ),
linalg::vec<double,3>( o.getX(), o.getY(), o.getZ() )
),
linalg::translation_matrix( center )
)[3].y;
}
double f = std::min(
std::max( waterLevel + waterWaveHeight * 0.25 - y, 0.0 )
/ waterWaveHeight, 1.0 );
a->rigidBody->applyCentralForce( btVector3( 0.0, f * a->mass * buoyancy, 0.0 ) );
a->rigidBody->setDamping( f * 0.3, f * 0.15 );
if( a->waterHealthRate != 0.0 ){
// Handle water health effects.
a->health += f * a->waterHealthRate * d;
}
}
if( a->type == TYPE_VEHICLE && a->driverAgent ){
steerVehicle( a );
}
a = a->next;
}
}
// Steer a vehicle. This should only be called in updateCallback.
void Dungeon::steerVehicle( Agent *a ){
if( a->raycastVehicle->getNumWheels() == 2 ){
btTransform trans = a->rigidBody->getWorldTransform();
btQuaternion r = trans.getRotation();
auto rotationMat = linalg::rotation_matrix(
linalg::vec<double,4>( r.getX(), r.getY(), r.getZ(), r.getW() )
);
auto end = linalg::mul(
rotationMat,
linalg::translation_matrix( linalg::vec<double,3>( 0.0, 0.0, 1.0 ) )
)[3].xyz();
double pitch = atan2(
end.y,
linalg::length<double,2>( { end.x, end.z } )
);
auto side = linalg::mul(
rotationMat,
linalg::translation_matrix( linalg::vec<double,3>( 1.0, 0.0, 0.0 ) )
)[3].xyz();
double lean = atan2(
side.y,
linalg::length<double,2>( { side.x, side.z } )
);
double maxLean =
std::max( a->maxLean - std::max( std::abs( pitch ) - 0.2, 0.0 ), 0.0 );
rotationMat = linalg::mul(
linalg::inverse( linalg::rotation_matrix( fgl::eulerToQuat( 0.0, 0.0, -a->turn / a->maxTurn * maxLean - lean ) ) ),
linalg::inverse( rotationMat )
);
a->rigidBody->setWorldTransform( btTransform(
btMatrix3x3(rotationMat[0][0], rotationMat[0][1], rotationMat[0][2],
rotationMat[1][0], rotationMat[1][1], rotationMat[1][2],
rotationMat[2][0], rotationMat[2][1], rotationMat[2][2]),
trans.getOrigin()
) );
a->rigidBody->setDamping(
a->rigidBody->getLinearDamping(),
a->turn == 0.0 ? 0.8 : 0.5
);
//if( a->rigidBody->getMotionState() )
// a->rigidBody->getMotionState()->setWorldTransform( trans );
}
// Set the front wheel angle to the turn value.
a->raycastVehicle->setSteeringValue( a->turn, 0 );
if( a->raycastVehicle->getNumWheels() == 4 )
a->raycastVehicle->setSteeringValue( a->turn, 1 );
}
#endif // ifndef FDUNGEON_SEPARATE_COMPILATION
} // namespace fdungeon
#endif // ifndef FDUNGEON_CPP