diff options
Diffstat (limited to 'src/utils')
-rw-r--r-- | src/utils/raytracerutils.cpp | 21 | ||||
-rw-r--r-- | src/utils/rgba.h | 10 | ||||
-rw-r--r-- | src/utils/scenedata.h | 179 | ||||
-rw-r--r-- | src/utils/scenefilereader.cpp | 1073 | ||||
-rw-r--r-- | src/utils/scenefilereader.h | 50 | ||||
-rw-r--r-- | src/utils/sceneparser.cpp | 136 | ||||
-rw-r--r-- | src/utils/sceneparser.h | 31 |
7 files changed, 1500 insertions, 0 deletions
diff --git a/src/utils/raytracerutils.cpp b/src/utils/raytracerutils.cpp new file mode 100644 index 0000000..bdb49a4 --- /dev/null +++ b/src/utils/raytracerutils.cpp @@ -0,0 +1,21 @@ +// +// Created by Michael Foiani on 11/4/23. +// + +#include "raytracer/raytracer.h" + +// Helper function to convert illumination to RGBA, applying some form of tone-mapping (e.g. clamping) in the process +RGBA RayTracer::toRGBA(const glm::vec4 &illumination) { + // Task 1 + return RGBA + { + (std::uint8_t) (255 * std::clamp(illumination.r, 0.f, 1.f)), + (std::uint8_t) (255 * std::clamp(illumination.g, 0.f, 1.f)), + (std::uint8_t) (255 * std::clamp(illumination.b, 0.f, 1.f)), + (std::uint8_t) (255 * std::clamp(illumination.b, 0.f, 1.f)) + }; +} + +bool RayTracer::floatEquals(float a, float b, float epsilon) { + return std::abs(a - b) <= epsilon; +}
\ No newline at end of file diff --git a/src/utils/rgba.h b/src/utils/rgba.h new file mode 100644 index 0000000..2103dab --- /dev/null +++ b/src/utils/rgba.h @@ -0,0 +1,10 @@ +#pragma once + +#include <cstdint> + +struct RGBA { + std::uint8_t r; + std::uint8_t g; + std::uint8_t b; + std::uint8_t a = 255; +}; diff --git a/src/utils/scenedata.h b/src/utils/scenedata.h new file mode 100644 index 0000000..043b84d --- /dev/null +++ b/src/utils/scenedata.h @@ -0,0 +1,179 @@ +#pragma once + +#include <vector> +#include <string> + +#include <glm/glm.hpp> +#include "rgba.h" + +// Enum of the types of virtual lights that might be in the scene +enum class LightType { + LIGHT_POINT, + LIGHT_DIRECTIONAL, + LIGHT_SPOT, + LIGHT_AREA +}; + +// Enum of the types of primitives that might be in the scene +enum class PrimitiveType { + PRIMITIVE_CUBE, + PRIMITIVE_CONE, + PRIMITIVE_CYLINDER, + PRIMITIVE_SPHERE, + PRIMITIVE_MESH +}; + +// Enum of the types of transformations that can be applied +enum class TransformationType { + TRANSFORMATION_TRANSLATE, + TRANSFORMATION_SCALE, + TRANSFORMATION_ROTATE, + TRANSFORMATION_MATRIX +}; + +// Type which can be used to store an RGBA color in floats [0,1] +using SceneColor = glm::vec4; + +// Struct which contains the global color coefficients of a scene. +// These are multiplied with the object-specific materials in the lighting equation. +struct SceneGlobalData { + float ka; // Ambient term + float kd; // Diffuse term + float ks; // Specular term + float kt; // Transparency; used for extra credit (refraction) +}; + +// Struct which contains raw parsed data fro a single light +struct SceneLight { + int id; + LightType type; + + SceneColor color; + glm::vec3 function; // Attenuation function + glm::vec4 dir; // Not applicable to point lights + + float penumbra; // Only applicable to spot lights, in RADIANS + float angle; // Only applicable to spot lights, in RADIANS + + float width, height; // No longer supported (area lights) +}; + +// Struct which contains data for a single light with CTM applied +struct SceneLightData { + int id; + LightType type; + + SceneColor color; + glm::vec3 function; // Attenuation function + + glm::vec4 pos; // Position with CTM applied (Not applicable to directional lights) + glm::vec4 dir; // Direction with CTM applied (Not applicable to point lights) + + float penumbra; // Only applicable to spot lights, in RADIANS + float angle; // Only applicable to spot lights, in RADIANS + + float width, height; // No longer supported (area lights) +}; + +// Struct which contains data for the camera of a scene +struct SceneCameraData { + glm::vec4 pos; + glm::vec4 look; + glm::vec4 up; + + float heightAngle; // The height angle of the camera in RADIANS + + float aperture; // Only applicable for depth of field + float focalLength; // Only applicable for depth of field +}; + +// Struct which contains data for texture mapping files +struct SceneFileMap { + SceneFileMap() : isUsed(false) {} + + bool isUsed; + std::string filename; + + float repeatU; + float repeatV; + + void clear() + { + isUsed = false; + repeatU = 0.0f; + repeatV = 0.0f; + filename = std::string(); + } +}; + +struct TextureData { + int width; + int height; + RGBA* data; +}; + +// Struct which contains data for a material (e.g. one which might be assigned to an object) +struct SceneMaterial { + SceneColor cAmbient; // Ambient term + SceneColor cDiffuse; // Diffuse term + SceneColor cSpecular; // Specular term + float shininess; // Specular exponent + + SceneColor cReflective; // Used to weight contribution of reflected ray lighting (via multiplication) + + SceneColor cTransparent; // Transparency; used for extra credit (refraction) + float ior; // Index of refraction; used for extra credit (refraction) + + SceneFileMap textureMap; // Used for texture mapping + float blend; // Used for texture mapping + + TextureData textureData; + + SceneColor cEmissive; // Not used + SceneFileMap bumpMap; // Not used + + void clear() + { + cAmbient = glm::vec4(0); + cDiffuse = glm::vec4(0); + cSpecular = glm::vec4(0); + shininess = 0; + + cReflective = glm::vec4(0); + + cTransparent = glm::vec4(0); + ior = 0; + + textureMap.clear(); + blend = 0; + + cEmissive = glm::vec4(0); + bumpMap.clear(); + } +}; + +// Struct which contains data for a single primitive in a scene +struct ScenePrimitive { + PrimitiveType type; + SceneMaterial material; + std::string meshfile; // Used for triangle meshes +}; + +// Struct which contains data for a transformation. +struct SceneTransformation { + TransformationType type; + + glm::vec3 translate; // Only applicable when translating. Defines t_x, t_y, and t_z, the amounts to translate by, along each axis. + glm::vec3 scale; // Only applicable when scaling. Defines s_x, s_y, and s_z, the amounts to scale by, along each axis. + glm::vec3 rotate; // Only applicable when rotating. Defines the axis of rotation; should be a unit vector. + float angle; // Only applicable when rotating. Defines the angle to rotate by in RADIANS, following the right-hand rule. + glm::mat4 matrix; // Only applicable when transforming by a custom matrix. This is that custom matrix. +}; + +// Struct which represents a node in the scene graph/tree, to be parsed by the student's `SceneParser`. +struct SceneNode { + std::vector<SceneTransformation*> transformations; // Note the order of transformations described in lab 5 + std::vector<ScenePrimitive*> primitives; + std::vector<SceneLight*> lights; + std::vector<SceneNode*> children; +}; diff --git a/src/utils/scenefilereader.cpp b/src/utils/scenefilereader.cpp new file mode 100644 index 0000000..ef2ad5e --- /dev/null +++ b/src/utils/scenefilereader.cpp @@ -0,0 +1,1073 @@ +#include "scenefilereader.h" +#include "scenedata.h" + +#include "glm/gtc/type_ptr.hpp" + +#include <cassert> +#include <cstring> +#include <iostream> +#include <filesystem> + +#include <QFile> +#include <QJsonArray> + +#define ERROR_AT(e) "error at line " << e.lineNumber() << " col " << e.columnNumber() << ": " +#define PARSE_ERROR(e) std::cout << ERROR_AT(e) << "could not parse <" << e.tagName().toStdString() \ + << ">" << std::endl +#define UNSUPPORTED_ELEMENT(e) std::cout << ERROR_AT(e) << "unsupported element <" \ + << e.tagName().toStdString() << ">" << std::endl; + +// Students, please ignore this file. +ScenefileReader::ScenefileReader(const std::string &name) { + file_name = name; + + memset(&m_cameraData, 0, sizeof(SceneCameraData)); + memset(&m_globalData, 0, sizeof(SceneGlobalData)); + + m_root = new SceneNode; + + m_templates.clear(); + m_nodes.clear(); + + m_nodes.push_back(m_root); +} + +ScenefileReader::~ScenefileReader() { + // Delete all Scene Nodes + for (unsigned int node = 0; node < m_nodes.size(); node++) { + for (size_t i = 0; i < (m_nodes[node])->transformations.size(); i++) + { + delete (m_nodes[node])->transformations[i]; + } + for (size_t i = 0; i < (m_nodes[node])->primitives.size(); i++) + { + delete (m_nodes[node])->primitives[i]; + } + (m_nodes[node])->transformations.clear(); + (m_nodes[node])->primitives.clear(); + (m_nodes[node])->children.clear(); + delete m_nodes[node]; + } + + m_nodes.clear(); + m_templates.clear(); +} + +SceneGlobalData ScenefileReader::getGlobalData() const { + return m_globalData; +} + +SceneCameraData ScenefileReader::getCameraData() const { + return m_cameraData; +} + +SceneNode *ScenefileReader::getRootNode() const { + return m_root; +} + +// This is where it all goes down... +bool ScenefileReader::readJSON() { + // Read the file + QFile file(file_name.c_str()); + if (!file.open(QFile::ReadOnly)) { + std::cout << "could not open " << file_name << std::endl; + return false; + } + + // Load the JSON document + QByteArray fileContents = file.readAll(); + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(fileContents, &jsonError); + if (doc.isNull()) { + std::cout << "could not parse " << file_name << std::endl; + std::cout << "parse error at line " << jsonError.offset << ": " + << jsonError.errorString().toStdString() << std::endl; + return false; + } + file.close(); + + if (!doc.isObject()) { + std::cout << "document is not an object" << std::endl; + return false; + } + + // Get the root element + QJsonObject scenefile = doc.object(); + + if (!scenefile.contains("globalData")) { + std::cout << "missing required field \"globalData\" on root object" << std::endl; + return false; + } + if (!scenefile.contains("cameraData")) { + std::cout << "missing required field \"cameraData\" on root object" << std::endl; + return false; + } + + QStringList requiredFields = {"globalData", "cameraData"}; + QStringList optionalFields = {"name", "groups", "templateGroups"}; + // If other fields are present, raise an error + QStringList allFields = requiredFields + optionalFields; + for (auto &field : scenefile.keys()) { + if (!allFields.contains(field)) { + std::cout << "unknown field \"" << field.toStdString() << "\" on root object" << std::endl; + return false; + } + } + + // Parse the global data + if (!parseGlobalData(scenefile["globalData"].toObject())) { + std::cout << "could not parse \"globalData\"" << std::endl; + return false; + } + + // Parse the camera data + if (!parseCameraData(scenefile["cameraData"].toObject())) { + std::cout << "could not parse \"cameraData\"" << std::endl; + return false; + } + + // Parse the template groups + if (scenefile.contains("templateGroups")) { + if (!parseTemplateGroups(scenefile["templateGroups"])) { + return false; + } + } + + // Parse the groups + if (scenefile.contains("groups")) { + if (!parseGroups(scenefile["groups"], m_root)) { + return false; + } + } + + std::cout << "Finished reading " << file_name << std::endl; + return true; +} + +/** + * Parse a globalData field and fill in m_globalData. + */ +bool ScenefileReader::parseGlobalData(const QJsonObject &globalData) { + QStringList requiredFields = {"ambientCoeff", "diffuseCoeff", "specularCoeff"}; + QStringList optionalFields = {"transparentCoeff"}; + QStringList allFields = requiredFields + optionalFields; + for (auto field : globalData.keys()) { + if (!allFields.contains(field)) { + std::cout << "unknown field \"" << field.toStdString() << "\" on globalData object" << std::endl; + return false; + } + } + for (auto field : requiredFields) { + if (!globalData.contains(field)) { + std::cout << "missing required field \"" << field.toStdString() << "\" on globalData object" << std::endl; + return false; + } + } + + // Parse the global data + if (globalData["ambientCoeff"].isDouble()) { + m_globalData.ka = globalData["ambientCoeff"].toDouble(); + } + else { + std::cout << "globalData ambientCoeff must be a floating-point value" << std::endl; + return false; + } + if (globalData["diffuseCoeff"].isDouble()) { + m_globalData.kd = globalData["diffuseCoeff"].toDouble(); + } + else { + std::cout << "globalData diffuseCoeff must be a floating-point value" << std::endl; + return false; + } + if (globalData["specularCoeff"].isDouble()) { + m_globalData.ks = globalData["specularCoeff"].toDouble(); + } + else { + std::cout << "globalData specularCoeff must be a floating-point value" << std::endl; + return false; + } + if (globalData.contains("transparentCoeff")) { + if (globalData["transparentCoeff"].isDouble()) { + m_globalData.kt = globalData["transparentCoeff"].toDouble(); + } + else { + std::cout << "globalData transparentCoeff must be a floating-point value" << std::endl; + return false; + } + } + + return true; +} + +/** + * Parse a Light and add a new CS123SceneLightData to m_lights. + */ +bool ScenefileReader::parseLightData(const QJsonObject &lightData, SceneNode *node) { + QStringList requiredFields = {"type", "color"}; + QStringList optionalFields = {"name", "attenuationCoeff", "direction", "penumbra", "angle", "width", "height"}; + QStringList allFields = requiredFields + optionalFields; + for (auto &field : lightData.keys()) { + if (!allFields.contains(field)) { + std::cout << "unknown field \"" << field.toStdString() << "\" on light object" << std::endl; + return false; + } + } + for (auto &field : requiredFields) { + if (!lightData.contains(field)) { + std::cout << "missing required field \"" << field.toStdString() << "\" on light object" << std::endl; + return false; + } + } + + // Create a default light + SceneLight *light = new SceneLight(); + memset(light, 0, sizeof(SceneLight)); + node->lights.push_back(light); + + light->dir = glm::vec4(0.f, 0.f, 0.f, 0.f); + light->function = glm::vec3(1, 0, 0); + + // parse the color + if (!lightData["color"].isArray()) { + std::cout << "light color must be of type array" << std::endl; + return false; + } + QJsonArray colorArray = lightData["color"].toArray(); + if (colorArray.size() != 3) { + std::cout << "light color must be of size 3" << std::endl; + return false; + } + if (!colorArray[0].isDouble() || !colorArray[1].isDouble() || !colorArray[2].isDouble()) { + std::cout << "light color must contain floating-point values" << std::endl; + return false; + } + light->color.r = colorArray[0].toDouble(); + light->color.g = colorArray[1].toDouble(); + light->color.b = colorArray[2].toDouble(); + + // parse the type + if (!lightData["type"].isString()) { + std::cout << "light type must be of type string" << std::endl; + return false; + } + std::string lightType = lightData["type"].toString().toStdString(); + + // parse directional light + if (lightType == "directional") { + light->type = LightType::LIGHT_DIRECTIONAL; + + // parse direction + if (!lightData.contains("direction")) { + std::cout << "directional light must contain field \"direction\"" << std::endl; + return false; + } + if (!lightData["direction"].isArray()) { + std::cout << "directional light direction must be of type array" << std::endl; + return false; + } + QJsonArray directionArray = lightData["direction"].toArray(); + if (directionArray.size() != 3) { + std::cout << "directional light direction must be of size 3" << std::endl; + return false; + } + if (!directionArray[0].isDouble() || !directionArray[1].isDouble() || !directionArray[2].isDouble()) { + std::cout << "directional light direction must contain floating-point values" << std::endl; + return false; + } + light->dir.x = directionArray[0].toDouble(); + light->dir.y = directionArray[1].toDouble(); + light->dir.z = directionArray[2].toDouble(); + } + else if (lightType == "point") { + light->type = LightType::LIGHT_POINT; + + // parse the attenuation coefficient + if (!lightData.contains("attenuationCoeff")) { + std::cout << "point light must contain field \"attenuationCoeff\"" << std::endl; + return false; + } + if (!lightData["attenuationCoeff"].isArray()) { + std::cout << "point light attenuationCoeff must be of type array" << std::endl; + return false; + } + QJsonArray attenuationArray = lightData["attenuationCoeff"].toArray(); + if (attenuationArray.size() != 3) { + std::cout << "point light attenuationCoeff must be of size 3" << std::endl; + return false; + } + if (!attenuationArray[0].isDouble() || !attenuationArray[1].isDouble() || !attenuationArray[2].isDouble()) { + std::cout << "ppoint light attenuationCoeff must contain floating-point values" << std::endl; + return false; + } + light->function.x = attenuationArray[0].toDouble(); + light->function.y = attenuationArray[1].toDouble(); + light->function.z = attenuationArray[2].toDouble(); + } + else if (lightType == "spot") { + QStringList pointRequiredFields = {"direction", "penumbra", "angle", "attenuationCoeff"}; + for (auto &field : pointRequiredFields) { + if (!lightData.contains(field)) { + std::cout << "missing required field \"" << field.toStdString() << "\" on spotlight object" << std::endl; + return false; + } + } + light->type = LightType::LIGHT_SPOT; + + // parse direction + if (!lightData["direction"].isArray()) { + std::cout << "spotlight direction must be of type array" << std::endl; + return false; + } + QJsonArray directionArray = lightData["direction"].toArray(); + if (directionArray.size() != 3) { + std::cout << "spotlight direction must be of size 3" << std::endl; + return false; + } + if (!directionArray[0].isDouble() || !directionArray[1].isDouble() || !directionArray[2].isDouble()) { + std::cout << "spotlight direction must contain floating-point values" << std::endl; + return false; + } + light->dir.x = directionArray[0].toDouble(); + light->dir.y = directionArray[1].toDouble(); + light->dir.z = directionArray[2].toDouble(); + + // parse attenuation coefficient + if (!lightData["attenuationCoeff"].isArray()) { + std::cout << "spotlight attenuationCoeff must be of type array" << std::endl; + return false; + } + QJsonArray attenuationArray = lightData["attenuationCoeff"].toArray(); + if (attenuationArray.size() != 3) { + std::cout << "spotlight attenuationCoeff must be of size 3" << std::endl; + return false; + } + if (!attenuationArray[0].isDouble() || !attenuationArray[1].isDouble() || !attenuationArray[2].isDouble()) { + std::cout << "spotlight direction must contain floating-point values" << std::endl; + return false; + } + light->function.x = attenuationArray[0].toDouble(); + light->function.y = attenuationArray[1].toDouble(); + light->function.z = attenuationArray[2].toDouble(); + + // parse penumbra + if (!lightData["penumbra"].isDouble()) { + std::cout << "spotlight penumbra must be of type float" << std::endl; + return false; + } + light->penumbra = lightData["penumbra"].toDouble() * M_PI / 180.f; + + // parse angle + if (!lightData["angle"].isDouble()) { + std::cout << "spotlight angle must be of type float" << std::endl; + return false; + } + light->angle = lightData["angle"].toDouble() * M_PI / 180.f; + } + else if (lightType == "area") { + light->type = LightType::LIGHT_AREA; + + QStringList pointRequiredFields = {"width", "height"}; + for (auto &field : pointRequiredFields) { + if (!lightData.contains(field)) { + std::cout << "missing required field \"" << field.toStdString() << "\" on area light object" << std::endl; + return false; + } + } + + // parse width + if (!lightData["width"].isDouble()) { + std::cout << "arealight penumbra must be of type float" << std::endl; + return false; + } + light->width = lightData["width"].toDouble(); + + // parse height + if (!lightData["height"].isDouble()) { + std::cout << "arealight height must be of type float" << std::endl; + return false; + } + light->height = lightData["height"].toDouble(); + + // parse the attenuation coefficient + if (!lightData.contains("attenuationCoeff")) { + std::cout << "area light must contain field \"attenuationCoeff\"" << std::endl; + return false; + } + if (!lightData["attenuationCoeff"].isArray()) { + std::cout << "area light attenuationCoeff must be of type array" << std::endl; + return false; + } + QJsonArray attenuationArray = lightData["attenuationCoeff"].toArray(); + if (attenuationArray.size() != 3) { + std::cout << "area light attenuationCoeff must be of size 3" << std::endl; + return false; + } + if (!attenuationArray[0].isDouble() || !attenuationArray[1].isDouble() || !attenuationArray[2].isDouble()) { + std::cout << "area light attenuationCoeff must contain floating-point values" << std::endl; + return false; + } + light->function.x = attenuationArray[0].toDouble(); + light->function.y = attenuationArray[1].toDouble(); + light->function.z = attenuationArray[2].toDouble(); + } + else { + std::cout << "unknown light type \"" << lightType << "\"" << std::endl; + return false; + } + + return true; +} + +/** + * Parse cameraData and fill in m_cameraData. + */ +bool ScenefileReader::parseCameraData(const QJsonObject &cameradata) { + QStringList requiredFields = {"position", "up", "heightAngle"}; + QStringList optionalFields = {"aperture", "focalLength", "look", "focus"}; + QStringList allFields = requiredFields + optionalFields; + for (auto &field : cameradata.keys()) { + if (!allFields.contains(field)) { + std::cout << "unknown field \"" << field.toStdString() << "\" on cameraData object" << std::endl; + return false; + } + } + for (auto &field : requiredFields) { + if (!cameradata.contains(field)) { + std::cout << "missing required field \"" << field.toStdString() << "\" on cameraData object" << std::endl; + return false; + } + } + + // Must have either look or focus, but not both + if (cameradata.contains("look") && cameradata.contains("focus")) { + std::cout << "cameraData cannot contain both \"look\" and \"focus\"" << std::endl; + return false; + } + + // Parse the camera data + if (cameradata["position"].isArray()) { + QJsonArray position = cameradata["position"].toArray(); + if (position.size() != 3) { + std::cout << "cameraData position must have 3 elements" << std::endl; + return false; + } + if (!position[0].isDouble() || !position[1].isDouble() || !position[2].isDouble()) { + std::cout << "cameraData position must be a floating-point value" << std::endl; + return false; + } + m_cameraData.pos.x = position[0].toDouble(); + m_cameraData.pos.y = position[1].toDouble(); + m_cameraData.pos.z = position[2].toDouble(); + } + else { + std::cout << "cameraData position must be an array" << std::endl; + return false; + } + + if (cameradata["up"].isArray()) { + QJsonArray up = cameradata["up"].toArray(); + if (up.size() != 3) { + std::cout << "cameraData up must have 3 elements" << std::endl; + return false; + } + if (!up[0].isDouble() || !up[1].isDouble() || !up[2].isDouble()) { + std::cout << "cameraData up must be a floating-point value" << std::endl; + return false; + } + m_cameraData.up.x = up[0].toDouble(); + m_cameraData.up.y = up[1].toDouble(); + m_cameraData.up.z = up[2].toDouble(); + } + else { + std::cout << "cameraData up must be an array" << std::endl; + return false; + } + + if (cameradata["heightAngle"].isDouble()) { + m_cameraData.heightAngle = cameradata["heightAngle"].toDouble() * M_PI / 180.f; + } + else { + std::cout << "cameraData heightAngle must be a floating-point value" << std::endl; + return false; + } + + if (cameradata.contains("aperture")) { + if (cameradata["aperture"].isDouble()) { + m_cameraData.aperture = cameradata["aperture"].toDouble(); + } + else { + std::cout << "cameraData aperture must be a floating-point value" << std::endl; + return false; + } + } + + if (cameradata.contains("focalLength")) { + if (cameradata["focalLength"].isDouble()) { + m_cameraData.focalLength = cameradata["focalLength"].toDouble(); + } + else { + std::cout << "cameraData focalLength must be a floating-point value" << std::endl; + return false; + } + } + + // Parse the look or focus + // if the focus is specified, we will convert it to a look vector later + if (cameradata.contains("look")) { + if (cameradata["look"].isArray()) { + QJsonArray look = cameradata["look"].toArray(); + if (look.size() != 3) { + std::cout << "cameraData look must have 3 elements" << std::endl; + return false; + } + if (!look[0].isDouble() || !look[1].isDouble() || !look[2].isDouble()) { + std::cout << "cameraData look must be a floating-point value" << std::endl; + return false; + } + m_cameraData.look.x = look[0].toDouble(); + m_cameraData.look.y = look[1].toDouble(); + m_cameraData.look.z = look[2].toDouble(); + } + else { + std::cout << "cameraData look must be an array" << std::endl; + return false; + } + } + else if (cameradata.contains("focus")) { + if (cameradata["focus"].isArray()) { + QJsonArray focus = cameradata["focus"].toArray(); + if (focus.size() != 3) { + std::cout << "cameraData focus must have 3 elements" << std::endl; + return false; + } + if (!focus[0].isDouble() || !focus[1].isDouble() || !focus[2].isDouble()) { + std::cout << "cameraData focus must be a floating-point value" << std::endl; + return false; + } + m_cameraData.look.x = focus[0].toDouble(); + m_cameraData.look.y = focus[1].toDouble(); + m_cameraData.look.z = focus[2].toDouble(); + } + else { + std::cout << "cameraData focus must be an array" << std::endl; + return false; + } + } + + // Convert the focus point (stored in the look vector) into a + // look vector from the camera position to that focus point. + if (cameradata.contains("focus")) { + m_cameraData.look -= m_cameraData.pos; + } + + return true; +} + +bool ScenefileReader::parseTemplateGroups(const QJsonValue &templateGroups) { + if (!templateGroups.isArray()) { + std::cout << "templateGroups must be an array" << std::endl; + return false; + } + + QJsonArray templateGroupsArray = templateGroups.toArray(); + for (auto templateGroup : templateGroupsArray) { + if (!templateGroup.isObject()) { + std::cout << "templateGroup items must be of type object" << std::endl; + return false; + } + + if (!parseTemplateGroupData(templateGroup.toObject())) { + return false; + } + } + + return true; +} + +bool ScenefileReader::parseTemplateGroupData(const QJsonObject &templateGroup) { + QStringList requiredFields = {"name"}; + QStringList optionalFields = {"translate", "rotate", "scale", "matrix", "lights", "primitives", "groups"}; + QStringList allFields = requiredFields + optionalFields; + for (auto &field : templateGroup.keys()) { + if (!allFields.contains(field)) { + std::cout << "unknown field \"" << field.toStdString() << "\" on templateGroup object" << std::endl; + return false; + } + } + + for (auto &field : requiredFields) { + if (!templateGroup.contains(field)) { + std::cout << "missing required field \"" << field.toStdString() << "\" on templateGroup object" << std::endl; + return false; + } + } + + if (!templateGroup["name"].isString()) { + std::cout << "templateGroup name must be a string" << std::endl; + } + if (m_templates.contains(templateGroup["name"].toString().toStdString())) { + std::cout << "templateGroups cannot have the same" << std::endl; + } + + SceneNode *templateNode = new SceneNode; + m_nodes.push_back(templateNode); + m_templates[templateGroup["name"].toString().toStdString()] = templateNode; + + return parseGroupData(templateGroup, templateNode); +} + +/** + * Parse a group object and create a new CS123SceneNode in m_nodes. + * NAME OF NODE CANNOT REFERENCE TEMPLATE NODE + */ +bool ScenefileReader::parseGroupData(const QJsonObject &object, SceneNode *node) { + QStringList optionalFields = {"name", "translate", "rotate", "scale", "matrix", "lights", "primitives", "groups"}; + QStringList allFields = optionalFields; + for (auto &field : object.keys()) { + if (!allFields.contains(field)) { + std::cout << "unknown field \"" << field.toStdString() << "\" on group object" << std::endl; + return false; + } + } + + // parse translation if defined + if (object.contains("translate")) { + if (!object["translate"].isArray()) { + std::cout << "group translate must be of type array" << std::endl; + return false; + } + + QJsonArray translateArray = object["translate"].toArray(); + if (translateArray.size() != 3) { + std::cout << "group translate must have 3 elements" << std::endl; + return false; + } + if (!translateArray[0].isDouble() || !translateArray[1].isDouble() || !translateArray[2].isDouble()) { + std::cout << "group translate must contain floating-point values" << std::endl; + return false; + } + + SceneTransformation *translation = new SceneTransformation(); + translation->type = TransformationType::TRANSFORMATION_TRANSLATE; + translation->translate.x = translateArray[0].toDouble(); + translation->translate.y = translateArray[1].toDouble(); + translation->translate.z = translateArray[2].toDouble(); + + node->transformations.push_back(translation); + } + + // parse rotation if defined + if (object.contains("rotate")) { + if (!object["rotate"].isArray()) { + std::cout << "group rotate must be of type array" << std::endl; + return false; + } + + QJsonArray rotateArray = object["rotate"].toArray(); + if (rotateArray.size() != 4) { + std::cout << "group rotate must have 4 elements" << std::endl; + return false; + } + if (!rotateArray[0].isDouble() || !rotateArray[1].isDouble() || !rotateArray[2].isDouble() || !rotateArray[3].isDouble()) { + std::cout << "group rotate must contain floating-point values" << std::endl; + return false; + } + + SceneTransformation *rotation = new SceneTransformation(); + rotation->type = TransformationType::TRANSFORMATION_ROTATE; + rotation->rotate.x = rotateArray[0].toDouble(); + rotation->rotate.y = rotateArray[1].toDouble(); + rotation->rotate.z = rotateArray[2].toDouble(); + rotation->angle = rotateArray[3].toDouble() * M_PI / 180.f; + + node->transformations.push_back(rotation); + } + + // parse scale if defined + if (object.contains("scale")) { + if (!object["scale"].isArray()) { + std::cout << "group scale must be of type array" << std::endl; + return false; + } + + QJsonArray scaleArray = object["scale"].toArray(); + if (scaleArray.size() != 3) { + std::cout << "group scale must have 3 elements" << std::endl; + return false; + } + if (!scaleArray[0].isDouble() || !scaleArray[1].isDouble() || !scaleArray[2].isDouble()) { + std::cout << "group scale must contain floating-point values" << std::endl; + return false; + } + + SceneTransformation *scale = new SceneTransformation(); + scale->type = TransformationType::TRANSFORMATION_SCALE; + scale->scale.x = scaleArray[0].toDouble(); + scale->scale.y = scaleArray[1].toDouble(); + scale->scale.z = scaleArray[2].toDouble(); + + node->transformations.push_back(scale); + } + + // parse matrix if defined + if (object.contains("matrix")) { + if (!object["matrix"].isArray()) { + std::cout << "group matrix must be of type array of array" << std::endl; + return false; + } + + QJsonArray matrixArray = object["matrix"].toArray(); + if (matrixArray.size() != 4) { + std::cout << "group matrix must be 4x4" << std::endl; + return false; + } + + SceneTransformation *matrixTransformation = new SceneTransformation(); + matrixTransformation->type = TransformationType::TRANSFORMATION_MATRIX; + + float *matrixPtr = glm::value_ptr(matrixTransformation->matrix); + int rowIndex = 0; + for (auto row : matrixArray) { + if (!row.isArray()) { + std::cout << "group matrix must be of type array of array" << std::endl; + return false; + } + + QJsonArray rowArray = row.toArray(); + if (rowArray.size() != 4) { + std::cout << "group matrix must be 4x4" << std::endl; + return false; + } + + int colIndex = 0; + for (auto val : rowArray) { + if (!val.isDouble()) { + std::cout << "group matrix must contain all floating-point values" << std::endl; + return false; + } + + // fill in column-wise + matrixPtr[colIndex * 4 + rowIndex] = (float)val.toDouble(); + colIndex++; + } + rowIndex++; + } + + node->transformations.push_back(matrixTransformation); + } + + // parse lights if any + if (object.contains("lights")) { + if (!object["lights"].isArray()) { + std::cout << "group lights must be of type array" << std::endl; + return false; + } + QJsonArray lightsArray = object["lights"].toArray(); + for (auto light : lightsArray) { + if (!light.isObject()) { + std::cout << "light must be of type object" << std::endl; + return false; + } + + if (!parseLightData(light.toObject(), node)) { + return false; + } + } + } + + // parse primitives if any + if (object.contains("primitives")) { + if (!object["primitives"].isArray()) { + std::cout << "group primitives must be of type array" << std::endl; + return false; + } + QJsonArray primitivesArray = object["primitives"].toArray(); + for (auto primitive : primitivesArray) { + if (!primitive.isObject()) { + std::cout << "primitive must be of type object" << std::endl; + return false; + } + + if (!parsePrimitive(primitive.toObject(), node)) { + return false; + } + } + } + + // parse children groups if any + if (object.contains("groups")) { + if (!parseGroups(object["groups"], node)) { + return false; + } + } + + return true; +} + +bool ScenefileReader::parseGroups(const QJsonValue &groups, SceneNode *parent) { + if (!groups.isArray()) { + std::cout << "groups must be of type array" << std::endl; + return false; + } + + QJsonArray groupsArray = groups.toArray(); + for (auto group : groupsArray) { + if (!group.isObject()) { + std::cout << "group items must be of type object" << std::endl; + return false; + } + + QJsonObject groupData = group.toObject(); + if (groupData.contains("name")) { + if (!groupData["name"].isString()) { + std::cout << "group name must be of type string" << std::endl; + return false; + } + + // if its a reference to a template group append it + std::string groupName = groupData["name"].toString().toStdString(); + if (m_templates.contains(groupName)) { + parent->children.push_back(m_templates[groupName]); + continue; + } + } + + SceneNode *node = new SceneNode; + m_nodes.push_back(node); + parent->children.push_back(node); + + if (!parseGroupData(group.toObject(), node)) { + return false; + } + } + + return true; +} + +/** + * Parse an <object type="primitive"> tag into node. + */ +bool ScenefileReader::parsePrimitive(const QJsonObject &prim, SceneNode *node) { + QStringList requiredFields = {"type"}; + QStringList optionalFields = { + "meshFile", "ambient", "diffuse", "specular", "reflective", "transparent", "shininess", "ior", + "blend", "textureFile", "textureU", "textureV", "bumpMapFile", "bumpMapU", "bumpMapV"}; + + QStringList allFields = requiredFields + optionalFields; + for (auto field : prim.keys()) { + if (!allFields.contains(field)) { + std::cout << "unknown field \"" << field.toStdString() << "\" on primitive object" << std::endl; + return false; + } + } + for (auto field : requiredFields) { + if (!prim.contains(field)) { + std::cout << "missing required field \"" << field.toStdString() << "\" on primitive object" << std::endl; + return false; + } + } + + if (!prim["type"].isString()) { + std::cout << "primitive type must be of type string" << std::endl; + return false; + } + std::string primType = prim["type"].toString().toStdString(); + + // Default primitive + ScenePrimitive *primitive = new ScenePrimitive(); + SceneMaterial &mat = primitive->material; + mat.clear(); + primitive->type = PrimitiveType::PRIMITIVE_CUBE; + mat.textureMap.isUsed = false; + mat.bumpMap.isUsed = false; + mat.cDiffuse.r = mat.cDiffuse.g = mat.cDiffuse.b = 1; + node->primitives.push_back(primitive); + + std::filesystem::path basepath = std::filesystem::path(file_name).parent_path().parent_path(); + if (primType == "sphere") + primitive->type = PrimitiveType::PRIMITIVE_SPHERE; + else if (primType == "cube") + primitive->type = PrimitiveType::PRIMITIVE_CUBE; + else if (primType == "cylinder") + primitive->type = PrimitiveType::PRIMITIVE_CYLINDER; + else if (primType == "cone") + primitive->type = PrimitiveType::PRIMITIVE_CONE; + else if (primType == "mesh") { + primitive->type = PrimitiveType::PRIMITIVE_MESH; + if (!prim.contains("meshFile")) { + std::cout << "primitive type mesh must contain field meshFile" << std::endl; + return false; + } + if (!prim["meshFile"].isString()) { + std::cout << "primitive meshFile must be of type string" << std::endl; + return false; + } + + std::filesystem::path relativePath(prim["meshFile"].toString().toStdString()); + primitive->meshfile = (basepath / relativePath).string(); + } + else { + std::cout << "unknown primitive type \"" << primType << "\"" << std::endl; + return false; + } + + if (prim.contains("ambient")) { + if (!prim["ambient"].isArray()) { + std::cout << "primitive ambient must be of type array" << std::endl; + return false; + } + QJsonArray ambientArray = prim["ambient"].toArray(); + if (ambientArray.size() != 3) { + std::cout << "primitive ambient array must be of size 3" << std::endl; + return false; + } + + for (int i = 0; i < 3; i++) { + if (!ambientArray[i].isDouble()) { + std::cout << "primitive ambient must contain floating-point values" << std::endl; + return false; + } + + mat.cAmbient[i] = ambientArray[i].toDouble(); + } + } + + if (prim.contains("diffuse")) { + if (!prim["diffuse"].isArray()) { + std::cout << "primitive diffuse must be of type array" << std::endl; + return false; + } + QJsonArray diffuseArray = prim["diffuse"].toArray(); + if (diffuseArray.size() != 3) { + std::cout << "primitive diffuse array must be of size 3" << std::endl; + return false; + } + + for (int i = 0; i < 3; i++) { + if (!diffuseArray[i].isDouble()) { + std::cout << "primitive diffuse must contain floating-point values" << std::endl; + return false; + } + + mat.cDiffuse[i] = diffuseArray[i].toDouble(); + } + } + + if (prim.contains("specular")) { + if (!prim["specular"].isArray()) { + std::cout << "primitive specular must be of type array" << std::endl; + return false; + } + QJsonArray specularArray = prim["specular"].toArray(); + if (specularArray.size() != 3) { + std::cout << "primitive specular array must be of size 3" << std::endl; + return false; + } + + for (int i = 0; i < 3; i++) { + if (!specularArray[i].isDouble()) { + std::cout << "primitive specular must contain floating-point values" << std::endl; + return false; + } + + mat.cSpecular[i] = specularArray[i].toDouble(); + } + } + + if (prim.contains("reflective")) { + if (!prim["reflective"].isArray()) { + std::cout << "primitive reflective must be of type array" << std::endl; + return false; + } + QJsonArray reflectiveArray = prim["reflective"].toArray(); + if (reflectiveArray.size() != 3) { + std::cout << "primitive reflective array must be of size 3" << std::endl; + return false; + } + + for (int i = 0; i < 3; i++) { + if (!reflectiveArray[i].isDouble()) { + std::cout << "primitive reflective must contain floating-point values" << std::endl; + return false; + } + + mat.cReflective[i] = reflectiveArray[i].toDouble(); + } + } + + if (prim.contains("transparent")) { + if (!prim["transparent"].isArray()) { + std::cout << "primitive transparent must be of type array" << std::endl; + return false; + } + QJsonArray transparentArray = prim["transparent"].toArray(); + if (transparentArray.size() != 3) { + std::cout << "primitive transparent array must be of size 3" << std::endl; + return false; + } + + for (int i = 0; i < 3; i++) { + if (!transparentArray[i].isDouble()) { + std::cout << "primitive transparent must contain floating-point values" << std::endl; + return false; + } + + mat.cTransparent[i] = transparentArray[i].toDouble(); + } + } + + if (prim.contains("shininess")) { + if (!prim["shininess"].isDouble()) { + std::cout << "primitive shininess must be of type float" << std::endl; + return false; + } + + mat.shininess = (float) prim["shininess"].toDouble(); + } + + if (prim.contains("ior")) { + if (!prim["ior"].isDouble()) { + std::cout << "primitive ior must be of type float" << std::endl; + return false; + } + + mat.ior = (float) prim["ior"].toDouble(); + } + + if (prim.contains("blend")) { + if (!prim["blend"].isDouble()) { + std::cout << "primitive blend must be of type float" << std::endl; + return false; + } + + mat.blend = (float)prim["blend"].toDouble(); + } + + if (prim.contains("textureFile")) { + if (!prim["textureFile"].isString()) { + std::cout << "primitive textureFile must be of type string" << std::endl; + return false; + } + std::filesystem::path fileRelativePath(prim["textureFile"].toString().toStdString()); + + mat.textureMap.filename = (basepath / fileRelativePath).string(); + mat.textureMap.repeatU = prim.contains("textureU") && prim["textureU"].isDouble() ? prim["textureU"].toDouble() : 1; + mat.textureMap.repeatV = prim.contains("textureV") && prim["textureV"].isDouble() ? prim["textureV"].toDouble() : 1; + mat.textureMap.isUsed = true; + } + + if (prim.contains("bumpMapFile")) { + if (!prim["bumpMapFile"].isString()) { + std::cout << "primitive bumpMapFile must be of type string" << std::endl; + return false; + } + std::filesystem::path fileRelativePath(prim["bumpMapFile"].toString().toStdString()); + + mat.bumpMap.filename = (basepath / fileRelativePath).string(); + mat.bumpMap.repeatU = prim.contains("bumpMapU") && prim["bumpMapU"].isDouble() ? prim["bumpMapU"].toDouble() : 1; + mat.bumpMap.repeatV = prim.contains("bumpMapV") && prim["bumpMapV"].isDouble() ? prim["bumpMapV"].toDouble() : 1; + mat.bumpMap.isUsed = true; + } + + return true; +} diff --git a/src/utils/scenefilereader.h b/src/utils/scenefilereader.h new file mode 100644 index 0000000..e51f4e5 --- /dev/null +++ b/src/utils/scenefilereader.h @@ -0,0 +1,50 @@ +#pragma once + +#include "scenedata.h" + +#include <vector> +#include <map> + +#include <QJsonDocument> +#include <QJsonObject> + +// This class parses the scene graph specified by the CS123 Xml file format. +class ScenefileReader { +public: + // Create a ScenefileReader, passing it the scene file. + ScenefileReader(const std::string &filename); + + // Clean up all data for the scene + ~ScenefileReader(); + + // Parse the XML scene file. Returns false if scene is invalid. + bool readJSON(); + + SceneGlobalData getGlobalData() const; + + SceneCameraData getCameraData() const; + + SceneNode *getRootNode() const; + +private: + // The filename should be contained within this parser implementation. + // If you want to parse a new file, instantiate a different parser. + bool parseGlobalData(const QJsonObject &globaldata); + bool parseCameraData(const QJsonObject &cameradata); + bool parseTemplateGroups(const QJsonValue &templateGroups); + bool parseTemplateGroupData(const QJsonObject &templateGroup); + bool parseGroups(const QJsonValue &groups, SceneNode *parent); + bool parseGroupData(const QJsonObject &object, SceneNode *node); + bool parsePrimitive(const QJsonObject &prim, SceneNode *node); + bool parseLightData(const QJsonObject &lightData, SceneNode *node); + + std::string file_name; + + mutable std::map<std::string, SceneNode *> m_templates; + + SceneGlobalData m_globalData; + SceneCameraData m_cameraData; + + SceneNode *m_root; + std::vector<SceneNode *> m_nodes; +}; diff --git a/src/utils/sceneparser.cpp b/src/utils/sceneparser.cpp new file mode 100644 index 0000000..74c605a --- /dev/null +++ b/src/utils/sceneparser.cpp @@ -0,0 +1,136 @@ +#include "sceneparser.h" +#include "scenefilereader.h" +#include <glm/gtx/transform.hpp> +#include <QImage> +#include <iostream> + + +/** + * @brief Stores the image specified from the input file in this class's + * `std::vector<RGBA> m_image`. + * @param file: file path to an image + * @return True if successfully loads image, False otherwise. + */ +TextureData loadTextureFromFile(const QString &file) { + QImage myTexture; + + int width; int height; + if (!myTexture.load(file)) { + std::cout<<"Failed to load in image: " << file.toStdString() << std::endl; + return TextureData{0, 0, nullptr}; + } + myTexture = myTexture.convertToFormat(QImage::Format_RGBX8888); + width = myTexture.width(); + height = myTexture.height(); + + RGBA* texture = new RGBA[width*height]; + QByteArray arr = QByteArray::fromRawData((const char*) myTexture.bits(), myTexture.sizeInBytes()); + + for (int i = 0; i < arr.size() / 4.f; i++){ + texture[i] = RGBA{(std::uint8_t) arr[4*i], (std::uint8_t) arr[4*i+1], (std::uint8_t) arr[4*i+2], (std::uint8_t) arr[4*i+3]}; + } + + return TextureData{width, height, texture}; +} + +// helper to handle recursive creation of tree +void initTree(SceneNode* currentNode, std::vector<RenderShapeData> *shapes, std::vector<SceneLightData> *lights, glm::mat4 currentCTM) { + for (auto t : currentNode->transformations) { + switch (t->type) + { + case TransformationType::TRANSFORMATION_TRANSLATE: + currentCTM *= glm::translate(glm::vec3(t->translate[0], t->translate[1], t->translate[2])); + break; + case TransformationType::TRANSFORMATION_SCALE: + currentCTM *= glm::scale(glm::vec3(t->scale[0], t->scale[1], t->scale[2])); + break; + case TransformationType::TRANSFORMATION_ROTATE: + currentCTM *= glm::rotate(t->angle, glm::vec3(t->rotate[0], t->rotate[1], t->rotate[2])); + break; + case TransformationType::TRANSFORMATION_MATRIX: + currentCTM *= glm::mat4(t->matrix); + break; + default: + std::cout << "Invalid transformation type" << std::endl; + break; + } + } + + + for(auto primitive : currentNode->primitives) { + primitive->material.textureData = loadTextureFromFile(QString::fromStdString(primitive->material.textureMap.filename)); + RenderShapeData rsd = {*primitive, currentCTM, glm::inverse(currentCTM)}; + shapes->push_back(rsd); + } + + // add the lights + for(auto l : currentNode->lights) { + SceneLightData sld{}; + sld.id = l->id; + sld.color = l->color; + sld.function = l->function; + + switch (l->type) + { + case LightType::LIGHT_POINT: + sld.type = LightType::LIGHT_POINT; + sld.pos = currentCTM * glm::vec4(0.f, 0.f, 0.f, 1.f); + sld.dir = glm::vec4(0.0f); + break; + case LightType::LIGHT_DIRECTIONAL: + sld.type = LightType::LIGHT_DIRECTIONAL; + sld.pos = glm::vec4(0.0f); + sld.dir = glm::vec4(currentCTM * l->dir); + break; + case LightType::LIGHT_SPOT: + sld.type = LightType::LIGHT_SPOT; + sld.pos = currentCTM * glm::vec4(0.f, 0.f, 0.f, 1.f); + sld.dir = currentCTM * l->dir; + sld.penumbra = l->penumbra; + sld.angle = l->angle; + break; + case LightType::LIGHT_AREA: + sld.type = LightType::LIGHT_AREA; + sld.pos = currentCTM * glm::vec4(0.f, 0.f, 0.f, 1.f); + sld.width = l->width; + sld.height = l->height; + break; + default: + std::cout << "Invalid light type" << std::endl; + continue; + } + + lights->push_back(sld); + } + + for (auto child : currentNode->children) { + initTree(child, shapes, lights, currentCTM); + } + +} + + +bool SceneParser::parse(std::string filepath, RenderData &renderData) { + ScenefileReader fileReader = ScenefileReader(filepath); + bool success = fileReader.readJSON(); + if (!success) { + return false; + } + + // TODO: Use your Lab 5 code here + // Task 5: populate renderData with global data, and camera data; + renderData.globalData = fileReader.getGlobalData(); + renderData.cameraData = fileReader.getCameraData(); + + // Task 6: populate renderData's list of primitives and their transforms. + // This will involve traversing the scene graph, and we recommend you + // create a helper function to do so! + SceneNode* root = fileReader.getRootNode(); + renderData.shapes.clear(); + renderData.lights.clear(); + auto currentCTM = glm::mat4(1.0f); + + initTree(root, &renderData.shapes, &renderData.lights, currentCTM); + + return true; +} diff --git a/src/utils/sceneparser.h b/src/utils/sceneparser.h new file mode 100644 index 0000000..699d6fb --- /dev/null +++ b/src/utils/sceneparser.h @@ -0,0 +1,31 @@ +#pragma once + +#include "scenedata.h" +#include <vector> +#include <string> +#include "rgba.h" + +// Struct which contains data for a single primitive, to be used for rendering +struct RenderShapeData { + ScenePrimitive primitive; + glm::mat4 ctm; // the cumulative transformation matrix + glm::mat4 inverseCTM; +}; + +// Struct which contains all the data needed to render a scene +struct RenderData { + SceneGlobalData globalData; + SceneCameraData cameraData; + + std::vector<SceneLightData> lights; + std::vector<RenderShapeData> shapes; +}; + +class SceneParser { +public: + // Parse the scene and store the results in renderData. + // @param filepath The path of the scene file to load. + // @param renderData On return, this will contain the metadata of the loaded scene. + // @return A boolean value indicating whether the parse was successful. + static bool parse(std::string filepath, RenderData &renderData); +}; |