Got BSP loading to a visual state. Needs more testing to determine if culling/visibility testing was effective.

This commit is contained in:
connellpaxton 2024-02-18 19:29:32 -05:00
parent dfd39257c0
commit 87fbb1c603
32 changed files with 1975 additions and 116 deletions

View File

@ -83,7 +83,8 @@ void Input::handleMovementKeys(Renderer& ren) {
else else
forward = glm::vec3(glm::cos(ren.cam.phi), 0.0, glm::sin(ren.cam.phi)); forward = glm::vec3(glm::cos(ren.cam.phi), 0.0, glm::sin(ren.cam.phi));
const auto right = glm::cross(forward, glm::vec3(0.0, 1.0, 0.0)); const auto right = glm::cross(forward, glm::vec3(0.0, 1.0, 0.0));
const auto speed = glfwGetKey(in, GLFW_KEY_LEFT_SHIFT)? 2.0f : 1.0f; auto speed = glfwGetKey(in, GLFW_KEY_LEFT_SHIFT)? 2.0f : 1.0f;
speed *= ren.speed;
if(glfwGetKey(in, GLFW_KEY_UP)) { if(glfwGetKey(in, GLFW_KEY_UP)) {
ren.cam.theta -= 0.01; ren.cam.theta -= 0.01;
@ -105,28 +106,28 @@ void Input::handleMovementKeys(Renderer& ren) {
} }
/* move "forward" or "backward" */ /* move "forward" or "backward" */
if (glfwGetKey(in, GLFW_KEY_W)) { if (glfwGetKey(in, GLFW_KEY_W)) {
ren.cam.pos += forward * 0.1f * speed; ren.cam.pos += forward * 1.0f * speed;
} }
if (glfwGetKey(in, GLFW_KEY_S)) { if (glfwGetKey(in, GLFW_KEY_S)) {
ren.cam.pos += forward * -0.1f * speed; ren.cam.pos += forward * -1.0f * speed;
} }
/* move "left" or "right" */ /* move "left" or "right" */
if (glfwGetKey(in, GLFW_KEY_A)) { if (glfwGetKey(in, GLFW_KEY_A)) {
ren.cam.pos -= right * 0.1f * speed; ren.cam.pos -= right * 1.0f * speed;
} }
if (glfwGetKey(in, GLFW_KEY_D)) { if (glfwGetKey(in, GLFW_KEY_D)) {
ren.cam.pos += right * 0.1f * speed; ren.cam.pos += right * 1.0f * speed;
} }
if(glfwGetKey(in, GLFW_KEY_SPACE)) { if(glfwGetKey(in, GLFW_KEY_SPACE)) {
ren.cam.pos.y += 0.1; ren.cam.pos.y += 1.0 * speed;
} }
if(glfwGetKey(in, GLFW_KEY_LEFT_CONTROL)) { if(glfwGetKey(in, GLFW_KEY_LEFT_CONTROL)) {
ren.cam.pos.y -= 0.1; ren.cam.pos.y -= 1.0 * speed;
} }
ren.cam.theta = glm::clamp(ren.cam.theta, 0.01f, glm::pi<float>() - 0.01f); ren.cam.theta = glm::clamp(ren.cam.theta, 0.01f, glm::pi<float>() - 0.01f);

View File

@ -58,26 +58,26 @@ void Model::initVertices(Node* node, const tinygltf::Primitive& prim) {
auto loc = prim.attributes.find("POSITION"); auto loc = prim.attributes.find("POSITION");
if(loc != prim.attributes.end()) { if(loc != prim.attributes.end()) {
auto accessor = model->accessors[loc->second]; auto& accessor = model->accessors[loc->second];
const auto& view = model->bufferViews[accessor.bufferView]; const auto& view = model->bufferViews[accessor.bufferView];
pos_buff = reinterpret_cast<const float*>(&model->buffers[view.buffer].data[accessor.byteOffset+view.byteOffset]); pos_buff = reinterpret_cast<const float*>(&model->buffers[view.buffer].data[accessor.byteOffset+view.byteOffset]);
vertex_count = accessor.count; vertex_count = accessor.count;
} }
loc = prim.attributes.find("NORMAL"); loc = prim.attributes.find("NORMAL");
if(loc != prim.attributes.end()) { if(loc != prim.attributes.end()) {
auto accessor = model->accessors[loc->second]; auto& accessor = model->accessors[loc->second];
const auto& view = model->bufferViews[accessor.bufferView]; const auto& view = model->bufferViews[accessor.bufferView];
norm_buff = reinterpret_cast<const float*>(&model->buffers[view.buffer].data[accessor.byteOffset+view.byteOffset]); norm_buff = reinterpret_cast<const float*>(&model->buffers[view.buffer].data[accessor.byteOffset+view.byteOffset]);
} }
loc = prim.attributes.find("TEXCOORD_0"); loc = prim.attributes.find("TEXCOORD_0");
if(loc != prim.attributes.end()) { if(loc != prim.attributes.end()) {
auto accessor = model->accessors[loc->second]; auto& accessor = model->accessors[loc->second];
const auto& view = model->bufferViews[accessor.bufferView]; const auto& view = model->bufferViews[accessor.bufferView];
uv_buff = reinterpret_cast<const float*>(&model->buffers[view.buffer].data[accessor.byteOffset+view.byteOffset]); uv_buff = reinterpret_cast<const float*>(&model->buffers[view.buffer].data[accessor.byteOffset+view.byteOffset]);
} }
for(size_t i = 0; i < vertex_count; i++) { for(size_t i = 0; i < vertex_count; i++) {
vertices.push_back(Vertex { vertices.push_back(BasicVertex {
.pos = pos_buff? glm::make_vec3(pos_buff+i*3) : glm::vec3(0.0), .pos = pos_buff? glm::make_vec3(pos_buff+i*3) : glm::vec3(0.0),
.norm = norm_buff? glm::normalize(glm::make_vec3(norm_buff+i*3)) : glm::vec3(0.0), .norm = norm_buff? glm::normalize(glm::make_vec3(norm_buff+i*3)) : glm::vec3(0.0),
.uv = uv_buff? glm::make_vec2(uv_buff+i*2) : glm::vec2(0.0), .uv = uv_buff? glm::make_vec2(uv_buff+i*2) : glm::vec2(0.0),

View File

@ -36,7 +36,7 @@ struct Model {
std::unique_ptr<VertexBuffer> vertex_buffer; std::unique_ptr<VertexBuffer> vertex_buffer;
std::unique_ptr<Buffer> index_buffer; std::unique_ptr<Buffer> index_buffer;
std::vector<Vertex> vertices; std::vector<BasicVertex> vertices;
std::vector<uint16_t> indices; std::vector<uint16_t> indices;
/* recusively initialize nodes with an accumulative vertex and index buffer collector */ /* recusively initialize nodes with an accumulative vertex and index buffer collector */

View File

@ -71,12 +71,6 @@ void CommandBuffer::bind(vk::PipelineLayout layout, vk::ArrayProxy<vk::Descripto
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, layout, 0, desc_sets, nullptr); command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, layout, 0, desc_sets, nullptr);
} }
void CommandBuffer::bind(const VertexBuffer& vertex_buffer, uint32_t binding) {
const std::array<vk::DeviceSize, 1> offsets = {0};
command_buffer.bindVertexBuffers(binding, vertex_buffer.buffer->buffer, offsets);
}
void CommandBuffer::bind(std::shared_ptr<Model> model) { void CommandBuffer::bind(std::shared_ptr<Model> model) {
bind(*model->vertex_buffer); bind(*model->vertex_buffer);
command_buffer.bindIndexBuffer(*model->index_buffer, 0, vk::IndexType::eUint16); command_buffer.bindIndexBuffer(*model->index_buffer, 0, vk::IndexType::eUint16);
@ -87,6 +81,14 @@ void CommandBuffer::bind(Terrain* terrain) {
command_buffer.bindIndexBuffer(*terrain->index_buffer, 0, vk::IndexType::eUint32); command_buffer.bindIndexBuffer(*terrain->index_buffer, 0, vk::IndexType::eUint32);
} }
void CommandBuffer::bind(Q3BSP::BSP* bsp) {
bind(*bsp->pipeline);
bind(*bsp->vertex_buffer);
command_buffer.bindIndexBuffer(*bsp->index_buffer, 0, vk::IndexType::eUint32);
bind(bsp->pipeline->layout, bsp->pipeline->desc_set);
}
void CommandBuffer::draw(uint32_t vertex_count, uint32_t instance_count, uint32_t first_vertex, uint32_t first_instance) { void CommandBuffer::draw(uint32_t vertex_count, uint32_t instance_count, uint32_t first_vertex, uint32_t first_instance) {
command_buffer.draw(vertex_count, instance_count, first_vertex, first_instance); command_buffer.draw(vertex_count, instance_count, first_vertex, first_instance);
} }

View File

@ -7,11 +7,14 @@
#include <memory> #include <memory>
#include <Renderer/VertexBuffer.hpp>
#include <Scene/BSP.hpp>
struct Buffer; struct Buffer;
struct Image; struct Image;
struct GraphicsPipeline; struct GraphicsPipeline;
struct ComputePipeline; struct ComputePipeline;
struct VertexBuffer;
struct Model; struct Model;
struct Terrain; struct Terrain;
@ -30,9 +33,17 @@ struct CommandBuffer {
void bind(const GraphicsPipeline& pipeline); void bind(const GraphicsPipeline& pipeline);
void bind(vk::PipelineLayout layout, vk::ArrayProxy<vk::DescriptorSet> desc_sets); void bind(vk::PipelineLayout layout, vk::ArrayProxy<vk::DescriptorSet> desc_sets);
void bind(const VertexBuffer& vertex_buffer, uint32_t binding = 0); template <typename Vertex>
void bind(const GeneralVertexBuffer<Vertex>& vertex_buffer, uint32_t binding = 0) {
const std::array<vk::DeviceSize, 1> offsets = { 0 };
command_buffer.bindVertexBuffers(binding, vertex_buffer.buffer->buffer, offsets);
}
void bind(std::shared_ptr<Model> model); void bind(std::shared_ptr<Model> model);
void bind(Terrain* terrain); void bind(Terrain* terrain);
void bind(Q3BSP::BSP* bsp);
void draw(uint32_t vertex_count, uint32_t instance_count, uint32_t first_vertex = 0, uint32_t first_instance = 0); void draw(uint32_t vertex_count, uint32_t instance_count, uint32_t first_vertex = 0, uint32_t first_instance = 0);

View File

@ -9,7 +9,8 @@
#include <util/log.hpp> #include <util/log.hpp>
GraphicsPipeline::GraphicsPipeline(vk::Device dev, const std::vector<Shader>& shaders, const vk::Extent2D& extent, const RenderPass& render_pass, vk::ArrayProxy<vk::DescriptorSetLayoutBinding> bindings, const VertexBuffer& vertex_buffer, enum Type type) : dev(dev) {
GraphicsPipeline::GraphicsPipeline(vk::Device dev, const std::vector<Shader>& shaders, const vk::Extent2D& extent, const RenderPass& render_pass, vk::ArrayProxy<vk::DescriptorSetLayoutBinding> bindings, const vk::VertexInputBindingDescription& vertex_binding, const std::vector<vk::VertexInputAttributeDescription>& vertex_attrs, enum Type type) : dev(dev) {
/* create layout /* create layout
* Eventually add a graphicspipline constructor that allows specification of layouts etc * Eventually add a graphicspipline constructor that allows specification of layouts etc
* kinda like how Image::Image has all those versions * kinda like how Image::Image has all those versions
@ -62,16 +63,14 @@ GraphicsPipeline::GraphicsPipeline(vk::Device dev, const std::vector<Shader>& sh
/* vertex input setup */ /* vertex input setup */
const std::vector<vk::VertexInputBindingDescription> vertex_bindings = { const std::vector<vk::VertexInputBindingDescription> vertex_bindings = {
vertex_buffer.binding(0), vertex_binding,
}; };
auto attrs = vertex_buffer.attrs(0);
const auto vertex_input_info = vk::PipelineVertexInputStateCreateInfo{ const auto vertex_input_info = vk::PipelineVertexInputStateCreateInfo{
.vertexBindingDescriptionCount = static_cast<uint32_t>(vertex_bindings.size()), .vertexBindingDescriptionCount = static_cast<uint32_t>(vertex_bindings.size()),
.pVertexBindingDescriptions = vertex_bindings.data(), .pVertexBindingDescriptions = vertex_bindings.data(),
.vertexAttributeDescriptionCount = static_cast<uint32_t>(attrs.size()), .vertexAttributeDescriptionCount = static_cast<uint32_t>(vertex_attrs.size()),
.pVertexAttributeDescriptions = attrs.data(), .pVertexAttributeDescriptions = vertex_attrs.data(),
}; };
const auto input_asm_info = vk::PipelineInputAssemblyStateCreateInfo{ const auto input_asm_info = vk::PipelineInputAssemblyStateCreateInfo{
@ -93,9 +92,9 @@ GraphicsPipeline::GraphicsPipeline(vk::Device dev, const std::vector<Shader>& sh
const auto raster_info = vk::PipelineRasterizationStateCreateInfo { const auto raster_info = vk::PipelineRasterizationStateCreateInfo {
.depthClampEnable = vk::False, .depthClampEnable = vk::False,
.polygonMode = type == Type::eGLTF? vk::PolygonMode::eFill : vk::PolygonMode::eLine, .polygonMode = vk::PolygonMode::eLine,
.cullMode = vk::CullModeFlagBits::eNone, .cullMode = vk::CullModeFlagBits::eBack,
.frontFace = Type::eGLTF ? vk::FrontFace::eClockwise : vk::FrontFace::eCounterClockwise, .frontFace = vk::FrontFace::eCounterClockwise,
.depthBiasEnable = vk::False, .depthBiasEnable = vk::False,
.lineWidth = 1.0, .lineWidth = 1.0,
}; };

View File

@ -3,9 +3,10 @@
#define VULKAN_HPP_NO_STRUCT_CONSTRUCTORS #define VULKAN_HPP_NO_STRUCT_CONSTRUCTORS
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include <Renderer/VertexBuffer.hpp>
struct Shader; struct Shader;
struct UniformBuffer; struct UniformBuffer;
struct VertexBuffer;
struct RenderPass; struct RenderPass;
struct Texture; struct Texture;
@ -18,7 +19,7 @@ struct GraphicsPipeline {
GraphicsPipeline(vk::Device dev, const std::vector<Shader>& shaders, GraphicsPipeline(vk::Device dev, const std::vector<Shader>& shaders,
const vk::Extent2D& extent, const RenderPass& render_pass, const vk::Extent2D& extent, const RenderPass& render_pass,
vk::ArrayProxy<vk::DescriptorSetLayoutBinding> bindings, vk::ArrayProxy<vk::DescriptorSetLayoutBinding> bindings,
const VertexBuffer& vertex_buffer, enum Type type = Type::eGLTF); const vk::VertexInputBindingDescription& vertex_binding, const std::vector<vk::VertexInputAttributeDescription>& vertex_attrs, enum Type type = Type::eGLTF);
vk::Device dev; vk::Device dev;
vk::Pipeline pipeline; vk::Pipeline pipeline;
vk::PipelineLayout layout; vk::PipelineLayout layout;

View File

@ -200,9 +200,15 @@ Renderer::Renderer(Window& win) : win(win) {
}); });
std::vector<Shader> shaders = { std::vector<Shader> shaders = {
{dev, "assets/shaders/fraglight.vert.spv", vk::ShaderStageFlagBits::eVertex }, { dev, "assets/shaders/basic.vert.spv", vk::ShaderStageFlagBits::eVertex },
{ dev, "assets/shaders/fraglight.geom.spv", vk::ShaderStageFlagBits::eGeometry }, { dev, "assets/shaders/basic.geom.spv", vk::ShaderStageFlagBits::eGeometry },
{ dev, "assets/shaders/lambert.frag.spv", vk::ShaderStageFlagBits::eFragment }, { dev, "assets/shaders/basic.frag.spv", vk::ShaderStageFlagBits::eFragment },
};
std::vector<Shader> model_shaders = {
{ dev, "assets/shaders/fraglight.vert.spv", vk::ShaderStageFlagBits::eVertex },
{ dev, "assets/shaders/fraglight.geom.spv", vk::ShaderStageFlagBits::eGeometry },
{ dev, "assets/shaders/lambert.frag.spv", vk::ShaderStageFlagBits::eFragment },
}; };
std::vector<vk::DescriptorSetLayoutBinding> bindings = { std::vector<vk::DescriptorSetLayoutBinding> bindings = {
@ -210,6 +216,22 @@ Renderer::Renderer(Window& win) : win(win) {
textures[0].binding(1), textures[0].binding(1),
}; };
/* Create default pipeline */
vertex_buffer = std::make_unique<VertexBuffer>(phys_dev, dev, 0x1000000 / sizeof(BasicVertex));
vertex_pipeline = std::make_unique<GraphicsPipeline>(dev, shaders, swapchain->extent, *render_pass, bindings, vertex_buffer->binding(0), vertex_buffer->attrs(0));
vertex_pipeline->update(0, *uniform_buffer);
vertex_pipeline->update(1, textures[1]);
/* BSP loader */
bsp = std::make_unique<Q3BSP::BSP>(phys_dev, dev, "assets/maps/git.bsp");
std::vector<Shader> bsp_shaders = {
{ dev, "assets/shaders/bsp.vert.spv", vk::ShaderStageFlagBits::eVertex },
{ dev, "assets/shaders/bsp.frag.spv", vk::ShaderStageFlagBits::eFragment },
};
bsp->pipeline = std::make_unique<GraphicsPipeline>(dev, bsp_shaders, swapchain->extent, *render_pass, bindings, bsp->vertex_buffer->binding(0), bsp->vertex_buffer->attrs(0));
bsp->pipeline->update(0, *uniform_buffer);
bsp->pipeline->update(1, textures[1]);
/* initialize models */ /* initialize models */
Timer model_timer; Timer model_timer;
models.push_back(std::make_shared<Model>(phys_dev, dev, "assets/models/dragon.gltf")); models.push_back(std::make_shared<Model>(phys_dev, dev, "assets/models/dragon.gltf"));
@ -217,10 +239,10 @@ Renderer::Renderer(Window& win) : win(win) {
Log::debug("Models loaded in %lf milliseconds\n", model_timer.read()); Log::debug("Models loaded in %lf milliseconds\n", model_timer.read());
pipeline = std::make_unique<GraphicsPipeline>(dev, shaders, swapchain->extent, *render_pass, bindings, *models[0]->vertex_buffer); model_pipeline = std::make_unique<GraphicsPipeline>(dev, model_shaders, swapchain->extent, *render_pass, bindings, models[0]->vertex_buffer->binding(0), models[0]->vertex_buffer->attrs(0));
pipeline->update(0, *uniform_buffer); model_pipeline->update(0, *uniform_buffer);
pipeline->update(1, textures[1]); model_pipeline->update(1, textures[1]);
/* create Terrain */ /* create Terrain */
terrain = std::make_unique<Terrain>(phys_dev, dev, textures[1]); terrain = std::make_unique<Terrain>(phys_dev, dev, textures[1]);
@ -232,13 +254,17 @@ Renderer::Renderer(Window& win) : win(win) {
{ dev, "assets/shaders/terrain.frag.spv", vk::ShaderStageFlagBits::eFragment }, { dev, "assets/shaders/terrain.frag.spv", vk::ShaderStageFlagBits::eFragment },
}; };
terrain_pipeline = std::make_unique<GraphicsPipeline>(dev, terrain_shaders, swapchain->extent, *render_pass, bindings, *terrain->vertex_buffer, GraphicsPipeline::eTERRAIN); terrain_pipeline = std::make_unique<GraphicsPipeline>(dev, terrain_shaders, swapchain->extent, *render_pass, bindings, terrain->vertex_buffer->binding(0), terrain->vertex_buffer->attrs(0), GraphicsPipeline::eTERRAIN);
terrain_pipeline->update(0, *uniform_buffer); terrain_pipeline->update(0, *uniform_buffer);
terrain_pipeline->update(1, textures[1]); terrain_pipeline->update(1, textures[1]);
for (auto& shader : shaders) for (auto& shader : shaders)
shader.cleanup(); shader.cleanup();
for (auto& shader : bsp_shaders)
shader.cleanup();
for (auto& shader : model_shaders)
shader.cleanup();
for (auto& shader : terrain_shaders) for (auto& shader : terrain_shaders)
shader.cleanup(); shader.cleanup();
@ -338,7 +364,7 @@ void Renderer::draw() {
auto sz = win.getDimensions(); auto sz = win.getDimensions();
const auto p = glm::perspective(glm::radians(90.0f), static_cast<float>(sz.width) / static_cast<float>(sz.height), 0.01f, 2000.0f); const auto p = glm::perspective(glm::radians(90.0f), static_cast<float>(sz.width) / static_cast<float>(sz.height), 0.01f, 200000.0f);
auto uni = UniformData{ auto uni = UniformData{
.view = cam.view(), .view = cam.view(),
@ -355,6 +381,7 @@ void Renderer::draw() {
uniform_buffer->upload(uni); uniform_buffer->upload(uni);
/*
command_buffer->bind(*terrain_pipeline); command_buffer->bind(*terrain_pipeline);
command_buffer->command_buffer.setViewport(0, viewport); command_buffer->command_buffer.setViewport(0, viewport);
command_buffer->command_buffer.setScissor(0, scissor); command_buffer->command_buffer.setScissor(0, scissor);
@ -363,10 +390,16 @@ void Renderer::draw() {
command_buffer->bind(terrain.get()); command_buffer->bind(terrain.get());
command_buffer->command_buffer.drawIndexed(terrain->indices.size(), 1, 0, 0, 0); command_buffer->command_buffer.drawIndexed(terrain->indices.size(), 1, 0, 0, 0);
command_buffer->bind(*pipeline); command_buffer->bind(*model_pipeline);
command_buffer->bind(pipeline->layout, pipeline->desc_set); command_buffer->bind(model_pipeline->layout, model_pipeline->desc_set);
command_buffer->bind(models[0]); command_buffer->bind(models[0]);
command_buffer->command_buffer.drawIndexed(models[0]->indices.size(), 10, 0, 0, 0); command_buffer->command_buffer.drawIndexed(models[0]->indices.size(), 10, 0, 0, 0);*/
bsp->load_indices(cam.pos);
command_buffer->bind(bsp.get());
command_buffer->command_buffer.setViewport(0, viewport);
command_buffer->command_buffer.setScissor(0, scissor);
command_buffer->command_buffer.drawIndexed(bsp->indices.size(), 1, 0, 0, 0);
/* draw User Interface stuff */ /* draw User Interface stuff */
ui->newFrame(); ui->newFrame();
@ -432,8 +465,10 @@ Renderer::~Renderer() {
uniform_buffer.reset(); uniform_buffer.reset();
vertex_buffer.reset(); vertex_buffer.reset();
vertex_pipeline.reset();
terrain_pipeline.reset(); terrain_pipeline.reset();
pipeline.reset(); model_pipeline.reset();
bsp.reset();
for (auto& tex : textures) { for (auto& tex : textures) {

View File

@ -9,16 +9,19 @@
#include <Renderer/CommandBuffer.hpp> #include <Renderer/CommandBuffer.hpp>
#include <Renderer/RenderPass.hpp> #include <Renderer/RenderPass.hpp>
#include <Renderer/VertexBuffer.hpp>
#include <Scene/Camera.hpp> #include <Scene/Camera.hpp>
#include <Scene/BSP.hpp>
#include <Scene/Terrain.hpp> #include <Scene/Terrain.hpp>
#include <Model/Model.hpp> #include <Model/Model.hpp>
#include <UI/UI.hpp> #include <UI/UI.hpp>
struct Window; struct Window;
struct UniformBuffer; struct UniformBuffer;
struct VertexBuffer;
struct Texture; struct Texture;
/* Contains all of the Vulkan objects involved in rendering the scene */ /* Contains all of the Vulkan objects involved in rendering the scene */
@ -48,12 +51,13 @@ struct Renderer {
std::unique_ptr<CommandBuffer> command_buffer; std::unique_ptr<CommandBuffer> command_buffer;
std::unique_ptr<RenderPass> render_pass; std::unique_ptr<RenderPass> render_pass;
std::unique_ptr<GraphicsPipeline> vertex_pipeline;
std::unique_ptr<GraphicsPipeline> pipeline; std::unique_ptr<GraphicsPipeline> model_pipeline;
std::unique_ptr<GraphicsPipeline> terrain_pipeline; std::unique_ptr<GraphicsPipeline> terrain_pipeline;
std::unique_ptr<VertexBuffer> vertex_buffer; std::unique_ptr<VertexBuffer> vertex_buffer;
std::unique_ptr<UniformBuffer> uniform_buffer; std::unique_ptr<UniformBuffer> uniform_buffer;
std::unique_ptr<Q3BSP::BSP> bsp;
std::unique_ptr<Terrain> terrain; std::unique_ptr<Terrain> terrain;
std::vector<Texture> textures; std::vector<Texture> textures;

View File

@ -1,19 +0,0 @@
#include <Renderer/VertexBuffer.hpp>
#include <Memory/Buffer.hpp>
#include <tinygltf/tiny_gltf.h>
VertexBuffer::VertexBuffer(vk::PhysicalDevice phys_dev, vk::Device dev, size_t n_vertices) {
buffer = std::make_unique<Buffer>(phys_dev, dev, n_vertices * sizeof(Vertex),
vk::BufferUsageFlagBits::eVertexBuffer,
vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible);
}
void VertexBuffer::upload(const std::vector<Vertex>& data) {
buffer->upload(reinterpret_cast<const uint8_t*>(data.data()), sizeof(Vertex)*data.size());
}
void VertexBuffer::upload(const tinygltf::Buffer& buff, const tinygltf::BufferView& view) {
std::memcpy(buffer->p, buff.data.data() + view.byteOffset, view.byteLength);
}

View File

@ -8,21 +8,55 @@
#include <tinygltf/tiny_gltf.h> #include <tinygltf/tiny_gltf.h>
struct Vertex { struct BasicVertex {
glm::vec3 pos; glm::vec3 pos;
glm::vec3 norm; glm::vec3 norm;
glm::vec2 uv; glm::vec2 uv;
glm::vec3 color; glm::vec3 color;
static inline std::vector<vk::VertexInputAttributeDescription> attrs(uint32_t binding) {
return std::vector<vk::VertexInputAttributeDescription> {
{
.location = 0,
.binding = binding,
.format = vk::Format::eR32G32B32Sfloat,
.offset = offsetof(BasicVertex, pos),
}, {
.location = 1,
.binding = binding,
.format = vk::Format::eR32G32B32Sfloat,
.offset = offsetof(BasicVertex, norm),
}, {
.location = 2,
.binding = binding,
.format = vk::Format::eR32G32Sfloat,
.offset = offsetof(BasicVertex, uv),
}, {
.location = 3,
.binding = binding,
.format = vk::Format::eR32G32B32Sfloat,
.offset = offsetof(BasicVertex, color),
}
};
}
}; };
struct VertexBuffer { template<typename Vertex>
struct GeneralVertexBuffer {
std::unique_ptr<Buffer> buffer; std::unique_ptr<Buffer> buffer;
GeneralVertexBuffer(vk::PhysicalDevice phys_dev, vk::Device dev, size_t n_vertices) {
buffer = std::make_unique<Buffer>(phys_dev, dev, n_vertices * sizeof(Vertex),
vk::BufferUsageFlagBits::eVertexBuffer,
vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible);
}
VertexBuffer(vk::PhysicalDevice phys_dev, vk::Device dev, size_t n_vertices); void upload(const std::vector<Vertex>& data) {
buffer->upload(reinterpret_cast<const uint8_t*>(data.data()), sizeof(Vertex) * data.size());
void upload(const std::vector<Vertex>& data); }
void upload(const tinygltf::Buffer& buff, const tinygltf::BufferView& view); void upload(const tinygltf::Buffer& buff, const tinygltf::BufferView& view) {
std::memcpy(buffer->p, buff.data.data() + view.byteOffset, view.byteLength);
}
inline vk::VertexInputBindingDescription binding(uint32_t binding, vk::ShaderStageFlags stages = vk::ShaderStageFlagBits::eVertex) const { inline vk::VertexInputBindingDescription binding(uint32_t binding, vk::ShaderStageFlags stages = vk::ShaderStageFlagBits::eVertex) const {
return vk::VertexInputBindingDescription { return vk::VertexInputBindingDescription {
@ -33,28 +67,9 @@ struct VertexBuffer {
} }
inline std::vector<vk::VertexInputAttributeDescription> attrs(uint32_t binding) const { inline std::vector<vk::VertexInputAttributeDescription> attrs(uint32_t binding) const {
return std::vector<vk::VertexInputAttributeDescription> { return Vertex::attrs(binding);
{
.location = 0,
.binding = binding,
.format = vk::Format::eR32G32B32Sfloat,
.offset = offsetof(Vertex, pos),
}, {
.location = 1,
.binding = binding,
.format = vk::Format::eR32G32B32Sfloat,
.offset = offsetof(Vertex, norm),
},{
.location = 2,
.binding = binding,
.format = vk::Format::eR32G32Sfloat,
.offset = offsetof(Vertex, uv),
},{
.location = 3,
.binding = binding,
.format = vk::Format::eR32G32B32Sfloat,
.offset = offsetof(Vertex, color),
}
};
} }
}; };
using VertexBuffer = GeneralVertexBuffer<BasicVertex>;

View File

@ -1,8 +1,12 @@
#include <Scene/BSP.hpp> #include <Scene/BSP.hpp>
#include <Renderer/Pipeline.hpp>
#include <util/file.hpp> #include <util/file.hpp>
#include <util/log.hpp> #include <util/log.hpp>
#include <set>
#include <cstring> #include <cstring>
using namespace Q3BSP; using namespace Q3BSP;
@ -14,14 +18,93 @@ static inline void copy_data(void* file_data, std::string& dst, Lump& lump) {
template<typename T> template<typename T>
static inline void copy_data(void* file_data, std::vector<T>& dst, Lump& lump) { static inline void copy_data(void* file_data, std::vector<T>& dst, Lump& lump) {
Log::debug("Size: %zu (%zu)\n", lump.len/sizeof(T), lump.len);
dst.resize(lump.len / sizeof(T)); dst.resize(lump.len / sizeof(T));
puts("ALLOC'D");
//Log::debug("%p %p\n", dst.data(), (u8*)file_data + lump.offset); //Log::debug("%p %p\n", dst.data(), (u8*)file_data + lump.offset);
std::memcpy(dst.data(), ((u8*)file_data) + lump.offset, lump.len); std::memcpy(dst.data(), ((u8*)file_data) + lump.offset, lump.len);
} }
BSP::BSP(const std::string& fname) : filename(fname) { void BSP::load_indices(const glm::vec3& cam_pos) {
std::set<int> present_faces;
std::vector<Face> visible_faces;
auto leaf_idx = determine_leaf(cam_pos);
if (leaf_idx == last_leaf)
return;
last_leaf = leaf_idx;
auto& cam_leaf = leafs[leaf_idx];
std::vector<Leaf> visible_leafs;
for (auto& leaf : leafs) {
if (determine_visibility(cam_leaf.cluster_idx, leaf.cluster_idx))
visible_leafs.push_back(leaf);
}
Log::debug("%zu visible leafs.\n", visible_leafs.size());
for (const auto& leaf : visible_leafs) {
// Log::debug("Faces: %zu vs %zu\n", leaf.first_leaf_face_idx + leaf.n_leaf_faces, faces.size());
for (size_t i = 0; i < leaf.n_leaf_faces; i++) {
auto idx = leaf_faces[leaf.first_leaf_face_idx + i].face_idx;
if (present_faces.contains(idx))
continue;
present_faces.insert(idx);
// Log::debug("Face idx: %zu (v.s. %zu)\n", idx, faces.size());
visible_faces.push_back(faces[idx]);
}
}
Log::debug("%zu visible faces.\n", visible_leafs.size());
for (auto& face : visible_faces) {
switch (face.type) {
case Face::ePATCH:
break;
case Face::ePOLYGON:
case Face::eMESH:
for (size_t i = 0; i < face.n_mesh_vertices; i++)
indices.push_back(face.first_vertex_idx + mesh_vertices[face.first_mesh_vertex_idx+i].idx);
break;
}
}
index_buffer->upload(indices);
}
int BSP::determine_leaf(glm::vec3 cam_pos) {
/* camera coordinate transformation */
float tmp = cam_pos.y;
cam_pos.y = cam_pos.z;
cam_pos.z = tmp;
/* use SDF of planes to determine relative position with respect to partitioning planes */
int idx = 0;
/* positive values are node indices, negative values are leaf indices */
while (idx >= 0) {
const auto& plane = planes[nodes[idx].plane];
const auto dist = glm::dot(plane.norm, cam_pos) - plane.dist;
if (dist >= 0)
idx = nodes[idx].children[0];
else
idx = nodes[idx].children[1];
}
return -idx - 1;
}
bool BSP::determine_visibility(int vis, int cluster) {
if (vis_info.vectors.size() == 0 || vis < 0)
return true;
int i = (vis * vis_info.sz_vectors) + (cluster >> 3);
u8 set = vis_info.vectors[i];
return !!(set & (1 << (cluster & 0x7)));
}
BSP::BSP(vk::PhysicalDevice phys_dev, vk::Device dev, const std::string& fname) : dev(dev), filename(fname) {
file_data = file::slurpb(fname); file_data = file::slurpb(fname);
Log::debug("File size: %zu\n", file_data.size()); Log::debug("File size: %zu\n", file_data.size());
header = reinterpret_cast<Header*>(file_data.data()); header = reinterpret_cast<Header*>(file_data.data());
@ -32,11 +115,6 @@ BSP::BSP(const std::string& fname) : filename(fname) {
Log::error("BSP file missing magic!\n"); Log::error("BSP file missing magic!\n");
} }
for (size_t i = 0; i < std::size(header->lumps); i++) {
Log::debug("%i: Offset: %u | Length: %u\n", i, header->lumps[i].offset, header->lumps[i].len);
Log::debug("\tPointer: 0x%p\n", (u8*)file_data.data() + (size_t)header->lumps[i].offset);
}
copy_data(file_data.data(), entities, header->entities); copy_data(file_data.data(), entities, header->entities);
copy_data(file_data.data(), textures, header->textures); copy_data(file_data.data(), textures, header->textures);
copy_data(file_data.data(), planes, header->planes); copy_data(file_data.data(), planes, header->planes);
@ -56,7 +134,14 @@ BSP::BSP(const std::string& fname) : filename(fname) {
vis_info.sz_vectors = reinterpret_cast<u32*>(file_data.data() + header->vis_info.offset)[1]; vis_info.sz_vectors = reinterpret_cast<u32*>(file_data.data() + header->vis_info.offset)[1];
auto sz = header->vis_info.len; auto sz = header->vis_info.len;
Log::debug("Size: %u\n", sz);
vis_info.vectors.resize(sz); vis_info.vectors.resize(sz);
std::memcpy(vis_info.vectors.data(), file_data.data() + header->vis_info.offset + 2*sizeof(u32), sz); std::memcpy(vis_info.vectors.data(), file_data.data() + header->vis_info.offset + 2*sizeof(u32), sz);
vertex_buffer = std::make_unique<GeneralVertexBuffer<Vertex>>(phys_dev, dev, vertices.size());
vertex_buffer->upload(vertices);
/* set limit at 256Mi indices */
index_buffer = std::make_unique<Buffer>(phys_dev, dev, 0x1000000 * sizeof(u32),
vk::BufferUsageFlagBits::eIndexBuffer, vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostVisible
);
} }

View File

@ -6,6 +6,12 @@
#include <vector> #include <vector>
#include <string> #include <string>
#define VULKAN_HPP_NO_STRUCT_CONSTRUCTORS
#include <vulkan/vulkan.hpp>
#include <Renderer/VertexBuffer.hpp>
#include <Renderer/Pipeline.hpp>
/* contains loading functions for Quake III-style BSPs */ /* contains loading functions for Quake III-style BSPs */
namespace Q3BSP { namespace Q3BSP {
struct Lump { struct Lump {
@ -110,11 +116,42 @@ namespace Q3BSP {
}; };
struct Vertex { struct Vertex {
glm::vec3 position; glm::vec3 pos;
glm::vec2 tex_coords; glm::vec2 uv;
glm::vec2 lightmap_coords; glm::vec2 lightmap_coords;
glm::vec3 normal; glm::vec3 norm;
glm::u8vec4 color; glm::u8vec4 color;
static inline std::vector<vk::VertexInputAttributeDescription> attrs(uint32_t binding) {
return std::vector<vk::VertexInputAttributeDescription> {
{
.location = 0,
.binding = binding,
.format = vk::Format::eR32G32B32Sfloat,
.offset = offsetof(Vertex, pos),
}, {
.location = 1,
.binding = binding,
.format = vk::Format::eR32G32Sfloat,
.offset = offsetof(Vertex, uv),
}, {
.location = 2,
.binding = binding,
.format = vk::Format::eR32G32Sfloat,
.offset = offsetof(Vertex, lightmap_coords),
}, {
.location = 3,
.binding = binding,
.format = vk::Format::eR32G32B32Sfloat,
.offset = offsetof(Vertex, norm),
}, {
.location = 4,
.binding = binding,
.format = vk::Format::eR8G8B8A8Uint,
.offset = offsetof(Vertex, color),
}
};
}
}; };
struct MeshVertex { struct MeshVertex {
@ -169,8 +206,13 @@ namespace Q3BSP {
}; };
struct BSP { struct BSP {
BSP(vk::PhysicalDevice phys_dev, vk::Device dev, const std::string& fname);
void load_indices(const glm::vec3& cam_pos);
int determine_leaf(glm::vec3 cam_pos);
bool determine_visibility(int vis, int cluster);
vk::Device dev;
Header* header; Header* header;
BSP(const std::string& fname);
std::string filename; std::string filename;
std::vector<u8> file_data; std::vector<u8> file_data;
std::string entities; std::string entities;
@ -190,5 +232,17 @@ namespace Q3BSP {
std::vector<Lightmap> lightmaps; std::vector<Lightmap> lightmaps;
std::vector<Lightvol> lightvols; std::vector<Lightvol> lightvols;
VisibilityInfo vis_info; VisibilityInfo vis_info;
std::vector<u32> indices;
std::unique_ptr<Buffer> index_buffer;
std::unique_ptr<GeneralVertexBuffer<Vertex>> vertex_buffer;
std::unique_ptr<GraphicsPipeline> pipeline;
/* to eliminate needless re-loading*/
int last_leaf = -0x1337;
~BSP() {
vertex_buffer.reset();
pipeline.reset();
}
}; };
} }

View File

@ -34,7 +34,7 @@ Terrain::Terrain(vk::PhysicalDevice phys_dev, vk::Device dev, Texture& tex) : ph
for (size_t x = 0; x < patch_size; x++) for (size_t x = 0; x < patch_size; x++)
for (size_t y = 0; y < patch_size; y++) for (size_t y = 0; y < patch_size; y++)
vertices[x + y*patch_size] = (Vertex { vertices[x + y*patch_size] = (BasicVertex {
.pos = glm::vec3( .pos = glm::vec3(
2.0f * x + 1.0f - patch_size, 2.0f * x + 1.0f - patch_size,
0.0f, 0.0f,

View File

@ -16,7 +16,7 @@ struct Terrain {
std::unique_ptr<VertexBuffer> vertex_buffer; std::unique_ptr<VertexBuffer> vertex_buffer;
std::unique_ptr<Buffer> index_buffer; std::unique_ptr<Buffer> index_buffer;
std::vector<Vertex> vertices; std::vector<BasicVertex> vertices;
std::vector<uint32_t> indices; std::vector<uint32_t> indices;
Terrain(vk::PhysicalDevice phys_dev, vk::Device dev, Texture& hieghtmap); Terrain(vk::PhysicalDevice phys_dev, vk::Device dev, Texture& hieghtmap);

BIN
assets/maps/git.bsp Normal file

Binary file not shown.

View File

@ -5,8 +5,15 @@ layout (location = 1) in vec2 texCoord;
layout (location = 0) out vec4 FragColor; layout (location = 0) out vec4 FragColor;
layout (set = 0, binding = 0) uniform Matrices { layout (set = 0, binding = 0) uniform Matrices {
mat4 mvp; mat4 view;
mat4 proj;
float time; float time;
vec3 cam_pos;
vec3 cam_dir;
vec4 frustum[6];
vec2 viewport;
float tess_factor;
float tess_edge_size;
}; };
layout (set = 0, binding = 1) uniform sampler2D tex; layout (set = 0, binding = 1) uniform sampler2D tex;

Binary file not shown.

View File

@ -11,14 +11,21 @@ layout (location = 0) out vec3 _norm;
layout (location = 1) out vec2 _texCoord; layout (location = 1) out vec2 _texCoord;
layout (set = 0, binding = 0) uniform Matrices { layout (set = 0, binding = 0) uniform Matrices {
mat4 mvp; mat4 view;
mat4 proj;
float time; float time;
vec3 cam_pos;
vec3 cam_dir;
vec4 frustum[6];
vec2 viewport;
float tess_factor;
float tess_edge_size;
}; };
void main(void) { void main(void) {
for(int i = 0; i < gl_in.length(); i++) { for(int i = 0; i < gl_in.length(); i++) {
gl_Position = mvp * gl_in[i].gl_Position; gl_Position = proj * view * gl_in[i].gl_Position;
_norm = norm[i]; _norm = norm[i];
_texCoord = texCoord[i]; _texCoord = texCoord[i];
EmitVertex(); EmitVertex();

Binary file not shown.

View File

@ -7,12 +7,19 @@ layout (location = 0) out vec3 norm;
layout (location = 1) out vec2 texCoord; layout (location = 1) out vec2 texCoord;
layout (set = 0, binding = 0) uniform Matrices { layout (set = 0, binding = 0) uniform Matrices {
mat4 mvp; mat4 view;
mat4 proj;
float time; float time;
vec3 cam_pos;
vec3 cam_dir;
vec4 frustum[6];
vec2 viewport;
float tess_factor;
float tess_edge_size;
}; };
void main() { void main() {
gl_Position = vec4(aPos + (vec3(10.0) * gl_InstanceIndex), 1.0); gl_Position = proj * view * vec4(aPos, 1.0);
texCoord = aTexCoord; texCoord = aTexCoord;
norm = aNorm; norm = aNorm;
} }

Binary file not shown.

23
assets/shaders/bsp.frag Normal file
View File

@ -0,0 +1,23 @@
#version 460 core
layout (location = 0) in vec3 norm;
layout (location = 1) in vec2 texCoord;
layout (location = 0) out vec4 FragColor;
layout (set = 0, binding = 0) uniform Matrices {
mat4 view;
mat4 proj;
float time;
vec3 cam_pos;
vec3 cam_dir;
vec4 frustum[6];
vec2 viewport;
float tess_factor;
float tess_edge_size;
};
layout (set = 0, binding = 1) uniform sampler2D tex;
void main() {
FragColor = vec4((abs(norm)*0.5+vec3(0.5)).r);
}

BIN
assets/shaders/bsp.frag.spv Normal file

Binary file not shown.

33
assets/shaders/bsp.vert Normal file
View File

@ -0,0 +1,33 @@
#version 460 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
layout (location = 2) in vec2 lightmapCoord;
layout (location = 3) in vec3 aNorm;
layout (location = 0) out vec3 norm;
layout (location = 1) out vec2 texCoord;
layout (set = 0, binding = 0) uniform Matrices {
mat4 view;
mat4 proj;
float time;
vec3 cam_pos;
vec3 cam_dir;
vec4 frustum[6];
vec2 viewport;
float tess_factor;
float tess_edge_size;
};
void main() {
mat4 zup_to_yup = mat4(
1.0, 0.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 0.0, 1.0
);
gl_Position = proj * view * zup_to_yup * vec4(aPos, 1.0);
texCoord = aTexCoord;
norm = aNorm;
}

BIN
assets/shaders/bsp.vert.spv Normal file

Binary file not shown.

View File

@ -0,0 +1,944 @@
<html>
<head><script src="//archive.org/includes/analytics.js?v=cf34f82" type="text/javascript"></script>
<script type="text/javascript">window.addEventListener('DOMContentLoaded',function(){var v=archive_analytics.values;v.service='wb';v.server_name='wwwb-app217.us.archive.org';v.server_ms=413;archive_analytics.send_pageview({});});</script>
<script type="text/javascript" src="https://web-static.archive.org/_static/js/bundle-playback.js?v=t1Bf4PY_" charset="utf-8"></script>
<script type="text/javascript" src="https://web-static.archive.org/_static/js/wombat.js?v=txqj7nKC" charset="utf-8"></script>
<script>window.RufflePlayer=window.RufflePlayer||{};window.RufflePlayer.config={"autoplay":"on","unmuteOverlay":"hidden"};</script>
<script type="text/javascript" src="https://web-static.archive.org/_static/js/ruffle/ruffle.js"></script>
<script type="text/javascript">
__wm.init("https://web.archive.org/web");
__wm.wombat("http://graphics.cs.brown.edu:80/games/quake/quake3.html","20160507164611","https://web.archive.org/","web","https://web-static.archive.org/_static/",
"1462639571");
</script>
<link rel="stylesheet" type="text/css" href="https://web-static.archive.org/_static/css/banner-styles.css?v=S1zqJCYt" />
<link rel="stylesheet" type="text/css" href="https://web-static.archive.org/_static/css/iconochive.css?v=qtvMKcIJ" />
<!-- End Wayback Rewrite JS Include -->
<title>
Rendering Quake 3 Maps
</title>
</head>
<body bgcolor="#FFFFFF"><!-- BEGIN WAYBACK TOOLBAR INSERT -->
<script>__wm.rw(0);</script>
<div id="wm-ipp-base" lang="en" style="display:none;direction:ltr;">
<div id="wm-ipp" style="position:fixed;left:0;top:0;right:0;">
<div id="donato" style="position:relative;width:100%;">
<div id="donato-base">
<iframe id="donato-if" src="https://archive.org/includes/donate.php?as_page=1&amp;platform=wb&amp;referer=https%3A//web.archive.org/web/20160507164611/http%3A//graphics.cs.brown.edu/games/quake/quake3.html"
scrolling="no" frameborder="0" style="width:100%; height:100%">
</iframe>
</div>
</div><div id="wm-ipp-inside">
<div id="wm-toolbar" style="position:relative;display:flex;flex-flow:row nowrap;justify-content:space-between;">
<div id="wm-logo" style="/*width:110px;*/padding-top:12px;">
<a href="/web/" title="Wayback Machine home page"><img src="https://web-static.archive.org/_static/images/toolbar/wayback-toolbar-logo-200.png" srcset="https://web-static.archive.org/_static/images/toolbar/wayback-toolbar-logo-100.png, https://web-static.archive.org/_static/images/toolbar/wayback-toolbar-logo-150.png 1.5x, https://web-static.archive.org/_static/images/toolbar/wayback-toolbar-logo-200.png 2x" alt="Wayback Machine" style="width:100px" border="0" /></a>
</div>
<div class="c" style="display:flex;flex-flow:column nowrap;justify-content:space-between;flex:1;">
<form class="u" style="display:flex;flex-direction:row;flex-wrap:nowrap;" target="_top" method="get" action="/web/submit" name="wmtb" id="wmtb"><input type="text" name="url" id="wmtbURL" value="http://graphics.cs.brown.edu/games/quake/quake3.html" onfocus="this.focus();this.select();" style="flex:1;"/><input type="hidden" name="type" value="replay" /><input type="hidden" name="date" value="20160507164611" /><input type="submit" value="Go" />
</form>
<div style="display:flex;flex-flow:row nowrap;align-items:flex-end;">
<div class="s" id="wm-nav-captures" style="flex:1;">
</div>
<div class="k">
<a href="" id="wm-graph-anchor">
<div id="wm-ipp-sparkline" title="Explore captures for this URL" style="position: relative">
<canvas id="wm-sparkline-canvas" width="725" height="27" border="0"></canvas>
</div>
</a>
</div>
</div>
</div>
<div class="n">
<table>
<tbody>
<!-- NEXT/PREV MONTH NAV AND MONTH INDICATOR -->
<tr class="m">
<td class="b" nowrap="nowrap"><a href="https://web.archive.org/web/20150624071647/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="24 Jun 2015"><strong>Jun</strong></a></td>
<td class="c" id="displayMonthEl" title="You are here: 16:46:11 May 07, 2016">MAY</td>
<td class="f" nowrap="nowrap"><a href="https://web.archive.org/web/20160607203752/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="07 Jun 2016"><strong>Jun</strong></a></td>
</tr>
<!-- NEXT/PREV CAPTURE NAV AND DAY OF MONTH INDICATOR -->
<tr class="d">
<td class="b" nowrap="nowrap"><a href="https://web.archive.org/web/20150624071647/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="07:16:47 Jun 24, 2015"><img src="https://web-static.archive.org/_static/images/toolbar/wm_tb_prv_on.png" alt="Previous capture" width="14" height="16" border="0" /></a></td>
<td class="c" id="displayDayEl" style="width:34px;font-size:22px;white-space:nowrap;" title="You are here: 16:46:11 May 07, 2016">07</td>
<td class="f" nowrap="nowrap"><a href="https://web.archive.org/web/20160607203752/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="20:37:52 Jun 07, 2016"><img src="https://web-static.archive.org/_static/images/toolbar/wm_tb_nxt_on.png" alt="Next capture" width="14" height="16" border="0" /></a></td>
</tr>
<!-- NEXT/PREV YEAR NAV AND YEAR INDICATOR -->
<tr class="y">
<td class="b" nowrap="nowrap"><a href="https://web.archive.org/web/20150422041533/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="22 Apr 2015"><strong>2015</strong></a></td>
<td class="c" id="displayYearEl" title="You are here: 16:46:11 May 07, 2016">2016</td>
<td class="f" nowrap="nowrap"><a href="https://web.archive.org/web/20170519174315/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="19 May 2017"><strong>2017</strong></a></td>
</tr>
</tbody>
</table>
</div>
<div class="r" style="display:flex;flex-flow:column nowrap;align-items:flex-end;justify-content:space-between;">
<div id="wm-btns" style="text-align:right;height:23px;">
<span class="xxs">
<div id="wm-save-snapshot-success">success</div>
<div id="wm-save-snapshot-fail">fail</div>
<a id="wm-save-snapshot-open" href="#" title="Share via My Web Archive" >
<span class="iconochive-web"></span>
</a>
<a href="https://archive.org/account/login.php" title="Sign In" id="wm-sign-in">
<span class="iconochive-person"></span>
</a>
<span id="wm-save-snapshot-in-progress" class="iconochive-web"></span>
</span>
<a class="xxs" href="http://faq.web.archive.org/" title="Get some help using the Wayback Machine" style="top:-6px;"><span class="iconochive-question" style="color:rgb(87,186,244);font-size:160%;"></span></a>
<a id="wm-tb-close" href="#close" style="top:-2px;" title="Close the toolbar"><span class="iconochive-remove-circle" style="color:#888888;font-size:240%;"></span></a>
</div>
<div id="wm-share" class="xxs">
<a href="/web/20160507164611/http://web.archive.org/screenshot/http://graphics.cs.brown.edu/games/quake/quake3.html"
id="wm-screenshot"
title="screenshot">
<span class="wm-icon-screen-shot"></span>
</a>
<a href="#" id="wm-video" title="video">
<span class="iconochive-movies"></span>
</a>
<a id="wm-share-facebook" href="#" data-url="https://web.archive.org/web/20160507164611/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="Share on Facebook" style="margin-right:5px;" target="_blank"><span class="iconochive-facebook" style="color:#3b5998;font-size:160%;"></span></a>
<a id="wm-share-twitter" href="#" data-url="https://web.archive.org/web/20160507164611/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="Share on Twitter" style="margin-right:5px;" target="_blank"><span class="iconochive-twitter" style="color:#1dcaff;font-size:160%;"></span></a>
</div>
<div style="padding-right:2px;text-align:right;white-space:nowrap;">
<a id="wm-expand" class="wm-btn wm-closed" href="#expand" onclick="__wm.ex(event);return false;"><span id="wm-expand-icon" class="iconochive-down-solid"></span> <span class="xxs" style="font-size:80%;">About this capture</span></a>
</div>
</div>
</div>
<div id="wm-capinfo" style="border-top:1px solid #777;display:none; overflow: hidden">
<div id="wm-capinfo-notice" source="api"></div>
<div id="wm-capinfo-collected-by">
<div style="background-color:#666;color:#fff;font-weight:bold;text-align:center">COLLECTED BY</div>
<div style="padding:3px;position:relative" id="wm-collected-by-content">
<div style="display:inline-block;vertical-align:top;width:50%;">
<span class="c-logo" style="background-image:url(https://archive.org/services/img/alexacrawls);"></span>
Organization: <a style="color:#33f;" href="https://archive.org/details/alexacrawls" target="_new"><span class="wm-title">Alexa Crawls</span></a>
<div style="max-height:75px;overflow:hidden;position:relative;">
<div style="position:absolute;top:0;left:0;width:100%;height:75px;background:linear-gradient(to bottom,rgba(255,255,255,0) 0%,rgba(255,255,255,0) 90%,rgba(255,255,255,255) 100%);"></div>
Starting in 1996, <a href="http://www.alexa.com/">Alexa Internet</a> has been donating their crawl data to the Internet Archive. Flowing in every day, these data are added to the <a href="http://web.archive.org/">Wayback Machine</a> after an embargo period.
</div>
</div>
<div style="display:inline-block;vertical-align:top;width:49%;">
<span class="c-logo" style="background-image:url(https://archive.org/services/img/alexacrawls)"></span>
<div>Collection: <a style="color:#33f;" href="https://archive.org/details/alexacrawls" target="_new"><span class="wm-title">Alexa Crawls</span></a></div>
<div style="max-height:75px;overflow:hidden;position:relative;">
<div style="position:absolute;top:0;left:0;width:100%;height:75px;background:linear-gradient(to bottom,rgba(255,255,255,0) 0%,rgba(255,255,255,0) 90%,rgba(255,255,255,255) 100%);"></div>
Starting in 1996, <a href="http://www.alexa.com/">Alexa Internet</a> has been donating their crawl data to the Internet Archive. Flowing in every day, these data are added to the <a href="http://web.archive.org/">Wayback Machine</a> after an embargo period.
</div>
</div>
</div>
</div>
<div id="wm-capinfo-timestamps">
<div style="background-color:#666;color:#fff;font-weight:bold;text-align:center" title="Timestamps for the elements of this page">TIMESTAMPS</div>
<div>
<div id="wm-capresources" style="margin:0 5px 5px 5px;max-height:250px;overflow-y:scroll !important"></div>
<div id="wm-capresources-loading" style="text-align:left;margin:0 20px 5px 5px;display:none"><img src="https://web-static.archive.org/_static/images/loading.gif" alt="loading" /></div>
</div>
</div>
</div></div></div></div><div id="wm-ipp-print">The Wayback Machine - https://web.archive.org/web/20160507164611/http://graphics.cs.brown.edu:80/games/quake/quake3.html</div>
<script type="text/javascript">//<![CDATA[
__wm.bt(725,27,25,2,"web","http://graphics.cs.brown.edu/games/quake/quake3.html","20160507164611",1996,"https://web-static.archive.org/_static/",["https://web-static.archive.org/_static/css/banner-styles.css?v=S1zqJCYt","https://web-static.archive.org/_static/css/iconochive.css?v=qtvMKcIJ"], false);
__wm.rw(1);
//]]></script>
<!-- END WAYBACK TOOLBAR INSERT -->
<a name="#top">
<b><font size="6">
Rendering Quake 3 Maps
</font></b>
<br>
<br>
Morgan McGuire
<br>July 11, 2003
<br>
<br>
<!---------------------------------------------------------------------------->
<a name="Intro">
<p>
<table border="1" cellpadding="0" cellspacing="0" width="100%" bgcolor="#ffffd0">
<td width="100%">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<td>
<b><font size="4">Introduction</font></b>
</td>
<td align="right">
<a href="#top">[top]</a>
</td>
</table>
</table>
<p>
This document describes how to render the basic geometry of a Quake 3
map using OpenGL. It describes how to texture and lightmap this
geometry, but does not describe how to render shaders, effects,
characters, or movers (elevators and doors).
<p>
This is intended as a sequel to Kekoa Proudfoot's <a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/">Unofficial Quake 3 Map
Specs</a> document, which describes how to parse BSP files. It
therefore uses the same notation.
<p>This is an unofficial document. Quake 3 is a registered trademark
of <a href="https://web.archive.org/web/20160507164611/http://www.idsoftware.com/">id Software</a>, which does
not sponsor, authorize, or endorse this document.
<p>
This document describes the Quake 3 BSP file format as the author
understands it. While every effort has been made to ensure that the
contents of this document are accurate, the author does not guarantee that
any portion of this document is actually correct. In addition, the author
cannot be held responsible the consequences of the any use or misuse of the
information contained in this document.
<!---------------------------------------------------------------------------->
<a name="Overview">
<p>
<table border="1" cellpadding="0" cellspacing="0" width="100%" bgcolor="#ffffd0">
<td width="100%">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<td>
<b><font size="4">Overview</font></b>
</td>
<td align="right">
<a href="#top">[top]</a>
</td>
</table>
</table>
<p>
The rendering process has five steps:
<blockquote>
<br>1. Determine the set of visible faces.
<br>2. Partition the visible faces into <i>transparent</i> and <i>opaque</i> lists.
<br>3. Clear the frame buffer and render the sky box
<br>4. Render the opaque list in front-to-back order.
<br>5. Render the transparent list in back-to-front order.
</blockquote>
The first two steps do not involve any OpenGL calls. Step 3 renders a
cube centered at the viewer with a pre-warped texture to create the
illusion of a detailed 3D environment. The practice of creating and
rendering sky boxes is discussed elsewhere and is not detailed further
in this document. Steps 4 and 5 render the actual visible surfaces of
the map. The opaque list contains triangles that will be rendered
without alpha blending. It is sorted from front to back to take
advantage of early-out depth tests on newer graphics hardware. The
transparent list contains alpha blended surfaces which must be
rendered from back to front to generate a correct image. A
straightforward quicksort on the camera-space depth of first vertex of
each surface is sufficient for these purposes. For the kinds of maps
involved, splitting overlapping polygons for truly correct render
order or using a radix sort for faster sorting will only increase the
complexity of a renderer with improving the resulting image quality or
frame rate.
<p>
The <a href="#VisibleSurface">Visible Surface Determination</a>
section explains how to use the BSP tree and
precomputed visibility data to find visible faces.
<p>
The remaining steps are straightforward and not discussed in detail,
except for the actual face rendering. The vertex indexing scheme of
Quake 3 files can be confusing because there are two levels of
indirection. The <a href="#RenderingFaces">Rendering Faces</a>
section explains how to generate triangle sets from
the indices stored in a face.
<!---------------------------------------------------------------------------->
<a name="Data">
<p>
<table border="1" cellpadding="0" cellspacing="0" width="100%" bgcolor="#ffffd0">
<td width="100%">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<td>
<b><font size="4">Data Structures</font></b>
</td>
<td align="right">
<a href="#top">[top]</a>
</td>
</table>
</table>
<p>
A Quake 3 map contains multiple models. The first of these is always
a static mesh that is the map itself and the remainders are "movers"
like doors and elevators. This document is restricted to model[0]; it
does not address the movers.
<p>
Assume the in-memory data structures mimic those in the file
structure, and that the overarching class is named Q3Map. There are a
few cases where I used a Vector3 in this document instead of float[3]
to make the code easier to read. Not all data from the file is used
for rendering. For example, the brushs are used for collision
detection but ignored during rendering. The subset of data used
during rendering is (links are to Proudfoot's structure definitions):
<p>
<table border="0" cellspacing="0" cellpadding="0" width="100%">
<th align="left">Lump Index
<th align="left">Lump Name
<th align="left">Description
<tr>
<tr><td valign="top">1<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Textures">Textures</a>
<td>Surface descriptions (assume these have been converted to OpenGL textures).
<tr><td valign="top">2<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Planes">Planes</a>
<td>Planes used by map geometry.
<tr><td valign="top">3<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Nodes">Nodes</a>
<td>BSP tree nodes.
<tr><td valign="top">4<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Leaves">Leaves</a>
<td>BSP tree leaves.
<tr><td valign="top">5<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Leaffaces">Leaffaces</a>
<td>Lists of face indices, one list per leaf.
<tr><td valign="top">7<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Models">Models</a>
<td>Descriptions of rigid world geometry in map (we only use model[0]).
<tr><td valign="top">10<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Vertexes">Vertexes</a>
<td>Vertices used to describe faces.
<tr><td valign="top">11<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Meshverts">Meshverts</a>
<td>Lists of offsets, one list per mesh.
<tr><td valign="top">13<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Faces">Faces</a>
<td>Surface geometry.
<tr><td valign="top">14<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Lightmaps">Lightmaps</a>
<td>Packed lightmap data (assume these have been converted to an OpenGL texture)
<tr><td valign="top">16<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Visdata">Visdata</a>
<td>Cluster-cluster visibility data.
</table>
<p>
There are additional data used during rendering that do not appear in
the file. These are:
<p>
<a name="Camera">
<b><font size="4">
Camera camera
</font></b>
<p>
A camera description that contains viewer position and frustum
parameters. The Camera class must have accessors for these parameters
and a method, isVisible(float min[3], float max[3]). This method
returns true if the world space bounding box with the specified
corners has non-zero intersection with the camera's view frustum.
<p>
<a name="AlreadyVisible">
<b><font size="4">
Set&lt;int&gt; alreadyVisible
</font></b>
<p>Set of indices of faces that are already visible. This is used to
prevent the same face from being rendered multiple times. A general
set implementation is not necessary. Because the face indices are
consecutive integers, a bit-set can provide an efficient
implementation.
</td></tr>
<p>
<a name="VisibleFace">
<b><font size="4">
Array&lt;int&gt; visibleFace
</font></b>
<p>
Set of indices of faces that are visible; that is, the members of the
alreadyVisible set. For efficiency this is maintained as a separate
array instead of iterating through the set.
<p>
<a name="Patch">
<b><font size="4">
Array&lt;Patch&gt; patch
</font></b>
<p>
Patches are <a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Faces">Faces</a> that
describe sets of biquadratic Bezier surfaces. Each Patch contains an
array of <a href="Bezier">Bezier</a> instances, which are described later
in this document.
<p>
These Beziers are tessellated into triangles during loading so they
can be rendered as triangle strips. Your implementation must create
this tessellation and add an index into the patch array for patch
faces.
<p>
<!---------------------------------------------------------------------------->
<a name="Coordinates">
<p>
<table border="1" cellpadding="0" cellspacing="0" width="100%" bgcolor="#ffffd0">
<td width="100%">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<td>
<b><font size="4">Coordinate System</font></b>
</td>
<td align="right">
<a href="#top">[top]</a>
</td>
</table>
</table>
<p>
Quake 3 uses a coordinate system where the x-axis points East, the
y-axis points South, and the z-axis points vertically downward. If
you prefer a coordinate system where the y-axis points vertically
upward and the z-axis points South, you can use the following function
to convert from Quake 3 coordinates to your coordinate system.
<p>
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
<blockquote><pre>
void swizzle(Vector3&amp; v) {
float temp = v.y;
v.y = v.z;
v.z = -temp;
}</pre></blockquote></td></tr></table>
<p>
When swizzling data, you must convert the vertex positions, vertex
normals, plane normals, and all bounding box min and max vectors. The
Quake coordinate system is also scaled so that one meter is about 0.03
units. You may wish to change this scale factor. If you scale vertex
positions remember to also scale plane distances, and min and max
vectors appropriately.
<p>
Depending on the conventions of your rendering system, you may also
want to invert Quake's lightmap texture coordinates to (1 - s, 1 - t)
or (s, 1 - t). It is usually easy to tell when light map texture
coordinates need to be inverted by looking at a rendering.
<p>
<!---------------------------------------------------------------------------->
<a name="VisibleSurface">
<p>
<table border="1" cellpadding="0" cellspacing="0" width="100%" bgcolor="#ffffd0">
<td width="100%">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<td>
<b><font size="4">Visible Face Determination</font></b>
</td>
<td align="right">
<a href="#top">[top]</a>
</td>
</table>
</table>
<p>
The input to the visible face determination step is the camera (and
the map). The output is the visibleFace array, which contains the
indices of all faces that are potentially visible to that camera.
During the step, the alreadyVisible set is used to prevent a face
index from being added to the visibleFace array more than once.
<p>
Two notes before we look at this process in more detail. First, the
output is an array of <i>potentially</i> visible faces. A z-buffer
test and frustum clipping (both typically provided by hardware) are
still needed to generate the exactly visible set. Second,
as Max McGuire says in his <a href="https://web.archive.org/web/20160507164611/http://www.flipcode.com/tutorials/tut_q2levels.shtml">Quake 2 BSP File Format</a>,
<blockquote><blockquote>Many people incorrectly associate the BSP tree with the visibility
algorithm used by Quake and similar engines. As described above, the
visible surface determination is done using a precomputed PVS. The BSP
tree is primarily used to divide the map into regions and to quickly
determine which region the camera is in. As a result, it isn't that
fundamental to any of the rendering algorithms used in Quake and any
data structure giving a spatial subdivision (like an octree or a k-D
tree) could be used instead. BSP trees are very simple however, and
they are useful for some of the other non-rendering tasks in the Quake
engine.
</blockquote></blockquote>
<p>
To determine the set of visible faces:
<blockquote>
1. <a href="#VisFindCluster">Find <i>visCluster</i></a>, the index of the cluster containing the camera position.
<br>2. <a href="#SelectVisibleLeaves">Select all leaves visible from that cluster</a>.
<br>3. <a href="#FaceIterate">Iterate through all faces in those clusters</a>.
</blockquote>
<p>
<a name="VisFindCluster">
<p>
<b><font size="4">
Find the camera cluster (visCluster)
</font></b>
<p>
Recall that a Quake 3 map is divided into convex spaces called
<i>leaves</i>. Adjacent leaves are joined into <i>clusters</i>. The
map file contains precomputed visibility information at the cluster
level, which is stored in the <a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Visdata">visData</a> bit
array.
<p>
The index of the cluster containing the camera is
<code>leaf[index].cluster</code>, where <i>index</i> is the index of
the leaf containing the camera. To find the index of the leaf
containing the camera, walk the BSP tree.
<p>
The root node is index 0 in the nodeArray. Each node has a splitting
plane associated with it. This plane divides space into two child
subnodes. If the camera lies in front of the splitting plane, recurse
into the front node. Otherwise recurse into the back node. We repeat
this process until a BSP leaf is reached.
<p>
In the Node data structure, a leaf is denoted by a negative child node
index. To convert the negative index into a legal leaf index, negate
and subtract one.
<p>
The following function takes a camera position as input. It walks the
BSP tree until a leaf is found and then returns the index of that
leaf. Remember that this is return value is a leaf index, not a
cluster index. The cluster index is stored in the leaf.
<p>
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
<blockquote><pre>
int Q3Map::findLeaf(const Vector3&amp; camPos) const {
int index = 0;
while (index &gt;= 0) {
const Node&amp; node = nodeArray[index];
const Plane&amp; plane = planeArray[node.plane];
// Distance from point to a plane
const double distance =
plane.normal.dot(camPos) - plane.distance;
if (distance &gt;= 0) {
index = node.front;
} else {
index = node.back;
}
}
return -index - 1;
}
</pre></blockquote></td></tr></table>
<p>
<a name="SelectVisibleLeaves">
<p>
<b><font size="4">
Select visible leaves
</font></b>
<p>
To find all visible leaves, iterate through the entire leaf array and
cull all leaves that are not in visible clusters or are outside the
view frustum. The visible cluster test should be performed first
because it is very efficient and more likely to fail.
<p>
The visData bit array contains precomputed visibility data between
clusters. If the cluster with index <i>a</i> can potentially be seen
by a viewer in the cluster with index <i>b</i>, then bit (a + b *
visData.sz_vecs * 8) of visData.vecs has value 1. Otherwise, that bit
has value 0.
<p>
The following function uses bitwise operators to efficiently extract
the relevant bit. The inputs are the index of the cluster containing
the camera and the index of the cluster to be tested. The function
returns true if the test cluster is potentially visible, otherwise it
returns false. A call to the function typically looks like
<code>isClusterVisible(visCluster, leaf[L].cluster)</code>.
<p>
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
<blockquote><pre>
bool Q3Map::isClusterVisible(int visCluster, int testCluster) const {
if ((visData.bitsets == NULL) || (visCluster &lt; 0)) {
return true;
}
int i = (visCluster * visData.bytesPerCluster) + (testCluster &gt;&gt; 3);
uint8 visSet = visData.bitsets[i];
return (visSet &amp; (1 &lt;&lt; (testCluster &amp; 7))) != 0;
}
</pre></blockquote></td></tr></table>
<p>
In the function, the expression (testCluster &gt;&gt; 3) computes
(testCluster / 8), i.e. the byte within visData that contains
information about the given cluster. The expression (1 &lt;&lt;
(testCluster &amp; 7)) creates a bit mask that selects bit (testCluster
mod 8) within that byte.
<p>
The visData information only considers the position of the viewer and
not the orientation. Orientation is handled by the frustum culling
step. The leaf contains two corners of its bounding box, min and max.
If <code>camera.isVisible(leaf[L].min, leaf[L].max)</code> returns
false, the leaf should be dropped from consideration because it is
outside the view frustum. Note that some of the faces in the leaf are
also in adjacent leaves and may therefore still be visible-- the other
leaves will take care of that when we iterate through them.
<p>
<p>
<a name="FaceIterate">
<p>
<b><font size="4">
Iterate through faces
</font></b>
<p>
A leaf contains all faces that have non-zero intersection with the
leaf volume. The faces in leaf with index L have indices
<code>leaf[L].firstFace</code> through <code>(leaf[L].firstFace +
leaf[L].facesCount - 1)</code>.
<p>
Because a face may protrude out of the leaf, the same face may be in
multiple leaves. Use the <a href="#AlreadyVisible">alreadyVisible</a>
set to avoid touching the same face twice. A simple code snippet for
this is:
<p>
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
<blockquote><pre>
for (int i = 0; i &lt; leaf[L].facesCount; ++i) {
const int f = i + leaf[L].firstFace;
if (! alreadyVisible.contains(f)) {
alreadyVisible.insert(f);
visibleFaces.append(f);
}
}
</pre></blockquote></td></tr></table>
<p>
<p>
<!---------------------------------------------------------------------------->
<a name="#RenderingFaces">
<p>
<table border="1" cellpadding="0" cellspacing="0" width="100%" bgcolor="#ffffd0">
<td width="100%">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<td>
<b><font size="4">Rendering Faces</font></b>
</td>
<td align="right">
<a href="#top">[top]</a>
</td>
</table>
</table>
<p>
There are four kinds of <a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Faces">Faces</a> in a
Quake 3 map: polygons, patches, meshes, and billboards. Polygons and
meshes are collections of triangles. A patch is a bezier-spline patch that
must be tessellated into triangles for rendering. Billboards are polygons
whose orientation changes to always face the viewer.
<p>
<a href="#RenderMesh">Polygons and meshes</a> are rendered in the same
manner. The position, texture coordinate, and lightmap coordinate are
stored in the vertex array. Using OpenGL vertex arrays, these can be
referenced with a single index by setting the active vertex pointer
and tex coord pointers into the same array, offset by the memory
location within a Vertex for each type of coordinate.
<p>
<a href="#RenderPatch">Patches</a> are tessellated into triangles,
either during loading or per-frame, and rendered as triangle strips.
The tessellation process creates vertices that did not exist in the
original mesh, so each patch contains its own vertex array instead
of using the global one stored in the map file.
<p>
Although handling the shaders and effects that can be stored in Quake
3 maps is more complicated, simple alpha blending can be supported to
render translucent surfaces correctly. When a texture contains an
alpha channel, enable blending and select the
<code>glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)</code>
blending mode. Alpha blended faces should not be backfaced culled;
they appear to have only one polygon for both sides (there is probably
a two-sided polygon flag somewhere that is the correct place to obtain
such information).
<p>
<a name="RenderMesh">
<p>
<b><font size="4">
Render a mesh
</font></b>
<p>
Each face mesh, <i>curFace</i> of type Face describes a mesh
containing (curFace.meshVertexesCount / 3) triangles. The indices
into the vertex array for the vertices of these triangles are
themselves indirected. The <a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Meshverts>">meshVertex</a>
array stores the indices of the vertices, in the correct order to
create triangle lists. For a given face, these are
<code>meshVertex[curFace.firstMeshVertex]</code> through
<code>meshVertex[curFace.firstMeshVertex + curFace.mushVertexesCount -
1]</code>. The meshVertex values are also offet by
<code>curFace.firstVertex</code>.
<p>
This indexing scheme, although confusing, is arranged conveniently for
using glDrawElements to render the triangle lists. The following code
renders a mesh using this function.
<p>
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
<blockquote><pre>
const Face&amp; curFace = face[visibleFace[f]];
static const stride = sizeof(Vertex); // BSP Vertex, not float[3]
const int offset = face.firstVertex;
glVertexPointer(3, GL_FLOAT, stride, &amp;(vertex[offset].position));
glClientActiveTextureARB(GL_TEXTURE0_ARB);
glTexCoordPointer(2, GL_FLOAT, stride, &amp;(vertex[offset].textureCoord));
glClientActiveTextureARB(GL_TEXTURE1_ARB);
glTexCoordPointer(2, GL_FLOAT, stride, &amp;(vertex[offset].lightmapCoord));
glDrawElements(GL_TRIANGLES, curFace.meshVertexesCount,
GL_UNSIGNED_INT, &meshVertex;[curFace.firstMeshVertex]);
</pre></blockquote></td></tr></table>
<p>
In the above code, the firstMeshVertex offset is applied directly to
the vertex pointer since there is no other provision for offsetting
the indices with glDrawElements.
<a name="RenderPatch">
<p>
<b><font size="4">
Render a patch
</font></b>
<p>
Patches are surfaces defined by an array of Bezier curves. These
curves are represented by the following data structure.
<p>
<a name="Bezier">
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
<blockquote><pre>
class Bezier {
private:
int level;
Array&lt;Vertex&gt; vertex;
Array&lt;uint32&gt; indexes;
Array&lt;int32&gt; trianglesPerRow;
Array&lt;uint32*&gt; rowIndexes;
public:
Vertex controls[9];
void tessellate(int level);
void render();
};
</pre></blockquote></td></tr></table>
<p>
The controls array contains the 9 control points for this curve. The
Beziers form a grid within a patch, so adjacent Beziers will share
three of these. The Bezier curves must be tessellated prior to
rendering. The <i>level</i> of the tessellation is the number of
edges into which each side of a 2D curve is subdivided. The total
number of triangles in the tessellation is <code>(2 * pow(level,
2))</code>. The remainder of the private data is the tessellation,
stored in a form we can pass directly to glMultiDrawElements. The
pointers in the rowIndexes array point into the indexes array; they
are not referring to separately allocated memory.
<p>
The tessellate method computes the private data for rendering from the
control points (which must themselves be set up during loading of the
containing patch). Any number between five and 10 is a reasonable
subdivision level for most maps. The intent of subdivision is to
provide smoother curves on faster machines by increasing the level at
runtime. Another use for subdivision is to allocate more polygons to
larger curves-- implementors are free to provide their own metric for
choosing a good subdivision level.
<p>
The following is an implementation of the tessellate method with
structure based on the tessellator in the Paul Baker's <a href="https://web.archive.org/web/20160507164611/http://users.ox.ac.uk/~univ1234/opengl/octagon/octagon.htm">Octagon</a> project.</i>
<p>
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
<blockquote><pre>
void Bezier::tessellate(int L) {
level = L;
// The number of vertices along a side is 1 + num edges
const int L1 = L + 1;
vertex.resize(L1 * L1);
// Compute the vertices
int i;
for (i = 0; i &lt;= L; ++i) {
double a = (double)i / L;
double b = 1 - a;
vertex[i] =
controls[0] * (b * b) +
controls[3] * (2 * b * a) +
controls[6] * (a * a);
}
for (i = 1; i &lt;= L; ++i) {
double a = (double)i / L;
double b = 1.0 - a;
BSPVertex temp[3];
int j;
for (j = 0; j &lt; 3; ++j) {
int k = 3 * j;
temp[j] =
controls[k + 0] * (b * b) +
controls[k + 1] * (2 * b * a) +
controls[k + 2] * (a * a);
}
for(j = 0; j &lt;= L; ++j) {
double a = (double)j / L;
double b = 1.0 - a;
vertex[i * L1 + j]=
temp[0] * (b * b) +
temp[1] * (2 * b * a) +
temp[2] * (a * a);
}
}
// Compute the indices
int row;
indexes.resize(L * (L + 1) * 2);
for (row = 0; row &lt; L; ++row) {
for(int col = 0; col &lt;= L; ++col) {
indexes[(row * (L + 1) + col) * 2 + 1] = row * L1 + col;
indexes[(row * (L + 1) + col) * 2] = (row + 1) * L1 + col;
}
}
trianglesPerRow.resize(L);
rowIndexes.resize(L);
for (row = 0; row &lt; L; ++row) {
trianglesPerRow[row] = 2 * L1;
rowIndexes[row] = &indexes;[row * 2 * L1];
}
}</pre></blockquote></td></tr></table>
<p>
Once constructed, this data can be rendered directly with vertex arrays:
<p>
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
<blockquote><pre>
void Bezier::render() {
glVertexPointer(3, GL_FLOAT,sizeof(BSPVertex), &vertex;[0].position);
glClientActiveTextureARB(GL_TEXTURE0_ARB);
glTexCoordPointer(2, GL_FLOAT,sizeof(BSPVertex), &vertex;[0].textureCoord);
glClientActiveTextureARB(GL_TEXTURE1_ARB);
glTexCoordPointer(2, GL_FLOAT, sizeof(BSPVertex), &vertex;[0].lightmapCoord);
glMultiDrawElementsEXT(GL_TRIANGLE_STRIP, trianglesPerRow.getCArray(),
GL_UNSIGNED_INT, (const void **)(rowIndexes.getCArray()), patch.level);
}</pre></blockquote></td></tr></table>
<p>
<hr>
<table border="0" width="100%" cellspacing="0" cellpadding="0">
<td align="left" valign="top">
<em><font size="3">Copyright &copy; 2003 Morgan McGuire. All rights
reserved.</font></em>
<td align="right" valign="top">
<em><font size="3">morgan@cs.brown.edu</font></em>
</table>
<p>
<b>Acknowledgements</b>
<br>Kris Taeleman answered a question while I was working on this document, and I used the following resources:
<br>Max McGuire's <a href="https://web.archive.org/web/20160507164611/http://www.flipcode.com/tutorials/tut_q2levels.shtml">Quake 2 BSP File Format</a>,
<br>Kekoa Proudfoot's
<a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/">Unofficial Quake 3 Map Specs</a>,
<br>Ben "Digiben" Humphrey's <a href="https://web.archive.org/web/20160507164611/http://www.misofruit.co.kr/seojewoo/programming/opengl/Quake3Format.htm">Unofficial Quake 3 BSP Format</a>,
<br>Nathan Ostgard's <a href="https://web.archive.org/web/20160507164611/http://www.nathanostgard.com/tutorials/quake3/collision/">Quake 3 BSP Collision Detection</a>,
<br>Leonardo Boselli's <a href="https://web.archive.org/web/20160507164611/http://sourceforge.net/projects/apocalyx/">Apocalyx</a> source code,
<br>Paul Baker's <a href="https://web.archive.org/web/20160507164611/http://users.ox.ac.uk/~univ1234/opengl/octagon/octagon.htm">Octagon</a> source code,
<br>The Aside Software <a href="https://web.archive.org/web/20160507164611/http://talika.eii.us.es/~titan/titan/rnews.html">Titan</a> source code,
<br>The <a href="https://web.archive.org/web/20160507164611/http://etud.epita.fr:8000/~bilalt_j/q3tools/q3radiant_man/q3rmanhtml.htm">Q3Radient Manual</a>
<p>
<font size="2">
Keywords: quake 3 quake3 q3 arena quake3arena q3arena map bsp file render opengl spec specs format vertex order
</font>
</body>
</html>
<!--
FILE ARCHIVED ON 16:46:11 May 07, 2016 AND RETRIEVED FROM THE
INTERNET ARCHIVE ON 21:12:17 Feb 18, 2024.
JAVASCRIPT APPENDED BY WAYBACK MACHINE, COPYRIGHT INTERNET ARCHIVE.
ALL OTHER CONTENT MAY ALSO BE PROTECTED BY COPYRIGHT (17 U.S.C.
SECTION 108(a)(3)).
-->
<!--
playback timings (ms):
exclusion.robots: 2.144 (7)
exclusion.robots.policy: 2.002 (7)
cdx.remote: 0.758 (7)
esindex: 0.111 (7)
LoadShardBlock: 1023.154 (24)
PetaboxLoader3.datanode: 713.073 (25)
PetaboxLoader3.resolve: 88.188 (2)
load_resource: 93.243
-->

View File

@ -0,0 +1,507 @@
@import 'record.css'; /* for SPN1 */
#wm-ipp-base {
height:65px;/* initial height just in case js code fails */
padding:0;
margin:0;
border:none;
background:none transparent;
}
#wm-ipp {
z-index: 2147483647;
}
#wm-ipp, #wm-ipp * {
font-family:Lucida Grande, Helvetica, Arial, sans-serif;
font-size:12px;
line-height:1.2;
letter-spacing:0;
width:auto;
height:auto;
max-width:none;
max-height:none;
min-width:0 !important;
min-height:0;
outline:none;
float:none;
text-align:left;
border:none;
color: #000;
text-indent: 0;
position: initial;
background: none;
}
#wm-ipp div, #wm-ipp canvas {
display: block;
}
#wm-ipp div, #wm-ipp tr, #wm-ipp td, #wm-ipp a, #wm-ipp form {
padding:0;
margin:0;
border:none;
border-radius:0;
background-color:transparent;
background-image:none;
/*z-index:2147483640;*/
height:auto;
}
#wm-ipp table {
border:none;
border-collapse:collapse;
margin:0;
padding:0;
width:auto;
font-size:inherit;
}
#wm-ipp form input {
padding:1px !important;
height:auto;
display:inline;
margin:0;
color: #000;
background: none #fff;
border: 1px solid #666;
}
#wm-ipp form input[type=submit] {
padding:0 8px !important;
margin:1px 0 1px 5px !important;
width:auto !important;
border: 1px solid #000 !important;
background: #fff !important;
color: #000 !important;
}
#wm-ipp form input[type=submit]:hover {
background: #eee !important;
cursor: pointer !important;
}
#wm-ipp form input[type=submit]:active {
transform: translateY(1px);
}
#wm-ipp a {
display: inline;
}
#wm-ipp a:hover{
text-decoration:underline;
}
#wm-ipp a.wm-btn:hover {
text-decoration:none;
color:#ff0 !important;
}
#wm-ipp a.wm-btn:hover span {
color:#ff0 !important;
}
#wm-ipp #wm-ipp-inside {
margin: 0 6px;
border:5px solid #000;
border-top:none;
background-color:rgba(255,255,255,0.9);
-moz-box-shadow:1px 1px 4px #333;
-webkit-box-shadow:1px 1px 4px #333;
box-shadow:1px 1px 4px #333;
border-radius:0 0 8px 8px;
}
/* selectors are intentionally verbose to ensure priority */
#wm-ipp #wm-logo {
padding:0 10px;
vertical-align:middle;
min-width:100px;
flex: 0 0 100px;
}
#wm-ipp .c {
padding-left: 4px;
}
#wm-ipp .c .u {
margin-top: 4px !important;
}
#wm-ipp .n {
padding:0 0 0 5px !important;
vertical-align: bottom;
}
#wm-ipp .n a {
text-decoration:none;
color:#33f;
font-weight:bold;
}
#wm-ipp .n .b {
padding:0 6px 0 0 !important;
text-align:right !important;
overflow:visible;
white-space:nowrap;
color:#99a;
vertical-align:middle;
}
#wm-ipp .n .y .b {
padding:0 6px 2px 0 !important;
}
#wm-ipp .n .c {
background:#000;
color:#ff0;
font-weight:bold;
padding:0 !important;
text-align:center;
}
#wm-ipp.hi .n td.c {
color:#ec008c;
}
#wm-ipp .n td.f {
padding:0 0 0 6px !important;
text-align:left !important;
overflow:visible;
white-space:nowrap;
color:#99a;
vertical-align:middle;
}
#wm-ipp .n tr.m td {
text-transform:uppercase;
white-space:nowrap;
padding:2px 0;
}
#wm-ipp .c .s {
padding:0 5px 0 0 !important;
vertical-align:bottom;
}
#wm-ipp #wm-nav-captures {
white-space: nowrap;
}
#wm-ipp .c .s a.t {
color:#33f;
font-weight:bold;
line-height: 1.8;
}
#wm-ipp .c .s div.r {
color: #666;
font-size:9px;
white-space:nowrap;
}
#wm-ipp .c .k {
padding-bottom:1px;
}
#wm-ipp .c .s {
padding:0 5px 2px 0 !important;
}
#wm-ipp td#displayMonthEl {
padding: 2px 0 !important;
}
#wm-ipp td#displayYearEl {
padding: 0 0 2px 0 !important;
}
div#wm-ipp-sparkline {
position:relative;/* for positioning markers */
white-space:nowrap;
background-color:#fff;
cursor:pointer;
line-height:0.9;
}
#sparklineImgId, #wm-sparkline-canvas {
position:relative;
z-index:9012;
max-width:none;
}
#wm-ipp-sparkline div.yt {
position:absolute;
z-index:9010 !important;
background-color:#ff0 !important;
top: 0;
}
#wm-ipp-sparkline div.mt {
position:absolute;
z-index:9013 !important;
background-color:#ec008c !important;
top: 0;
}
#wm-ipp .r {
margin-left: 4px;
}
#wm-ipp .r a {
color:#33f;
border:none;
position:relative;
background-color:transparent;
background-repeat:no-repeat !important;
background-position:100% 100% !important;
text-decoration: none;
}
#wm-ipp #wm-capinfo {
/* prevents notice div background from sticking into round corners of
#wm-ipp-inside */
border-radius: 0 0 4px 4px;
}
#wm-ipp #wm-capinfo .c-logo {
display:block;
float:left;
margin-right:3px;
width:90px;
min-height:90px;
max-height: 290px;
border-radius:45px;
overflow:hidden;
background-position:50%;
background-size:auto 90px;
box-shadow: 0 0 2px 2px rgba(208,208,208,128) inset;
}
#wm-ipp #wm-capinfo .c-logo span {
display:inline-block;
}
#wm-ipp #wm-capinfo .c-logo img {
height:90px;
position:relative;
left:-50%;
}
#wm-ipp #wm-capinfo .wm-title {
font-size:130%;
}
#wm-ipp #wm-capinfo a.wm-selector {
display:inline-block;
color: #aaa;
text-decoration:none !important;
padding: 2px 8px;
}
#wm-ipp #wm-capinfo a.wm-selector.selected {
background-color:#666;
}
#wm-ipp #wm-capinfo a.wm-selector:hover {
color: #fff;
}
#wm-ipp #wm-capinfo.notice-only #wm-capinfo-collected-by,
#wm-ipp #wm-capinfo.notice-only #wm-capinfo-timestamps {
display: none;
}
#wm-ipp #wm-capinfo #wm-capinfo-notice .wm-capinfo-content {
background-color:#ff0;
padding:5px;
font-size:14px;
text-align:center;
}
#wm-ipp #wm-capinfo #wm-capinfo-notice .wm-capinfo-content * {
font-size:14px;
text-align:center;
}
#wm-ipp #wm-expand {
right: 1px;
bottom: -1px;
color: #ffffff;
background-color: #666 !important;
padding:0 5px 0 3px !important;
border-radius: 3px 3px 0 0 !important;
}
#wm-ipp #wm-expand span {
color: #ffffff;
}
#wm-ipp #wm-expand #wm-expand-icon {
display: inline-block;
transition: transform 0.5s;
transform-origin: 50% 45%;
}
#wm-ipp #wm-expand.wm-open #wm-expand-icon {
transform: rotate(180deg);
}
#wm-ipp #wmtb {
text-align:right;
}
#wm-ipp #wmtb #wmtbURL {
width: calc(100% - 45px);
}
#wm-ipp #wm-graph-anchor {
border-right:1px solid #ccc;
}
/* time coherence */
html.wb-highlight {
box-shadow: inset 0 0 0 3px #a50e3a !important;
}
.wb-highlight {
outline: 3px solid #a50e3a !important;
}
#wm-ipp-print {
display:none !important;
}
@media print {
#wm-ipp-base {
display:none !important;
}
#wm-ipp-print {
display:block !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
@media (max-width:414px) {
#wm-ipp .xxs {
display:none !important;
}
}
@media (min-width:1055px) {
#wm-ipp #wm-graph-anchor {
display:block !important;
}
}
@media (max-width:1054px) {
#wm-ipp #wm-graph-anchor {
display:none !important;
}
}
@media (max-width:1163px) {
#wm-logo {
display:none !important;
}
}
#wm-btns {
white-space: nowrap;
margin-top: -2px;
}
#wm-btns #wm-save-snapshot-open {
margin-right: 7px;
top: -6px;
}
#wm-btns #wm-sign-in {
box-sizing: content-box;
display: none;
margin-right: 7px;
top: -8px;
/*
round border around sign in button
*/
border: 2px #000 solid;
border-radius: 14px;
padding-right: 2px;
padding-bottom: 2px;
width: 11px;
height: 11px;
}
#wm-btns #wm-sign-in>.iconochive-person {
font-size: 12.5px;
}
#wm-save-snapshot-open > .iconochive-web {
color:#000;
font-size:160%;
}
#wm-ipp #wm-share {
display: flex;
align-items: flex-end;
justify-content: space-between;
}
#wm-share > #wm-screenshot {
display: inline-block;
margin-right: 3px;
visibility: hidden;
}
#wm-screenshot > .iconochive-image {
color:#000;
font-size:160%;
}
#wm-share > #wm-video {
display: inline-block;
margin-right: 3px;
visibility: hidden;
}
#wm-video > .iconochive-movies {
color: #000;
display: inline-block;
font-size: 150%;
margin-bottom: 2px;
}
#wm-btns #wm-save-snapshot-in-progress {
display: none;
font-size:160%;
opacity: 0.5;
position: relative;
margin-right: 7px;
top: -5px;
}
#wm-btns #wm-save-snapshot-success {
display: none;
color: green;
position: relative;
top: -7px;
}
#wm-btns #wm-save-snapshot-fail {
display: none;
color: red;
position: relative;
top: -7px;
}
.wm-icon-screen-shot {
background: url("../images/web-screenshot.svg") no-repeat !important;
background-size: contain !important;
width: 22px !important;
height: 19px !important;
display: inline-block;
}
#donato {
/* transition effect is disable so as to simplify height adjustment */
/*transition: height 0.5s;*/
height: 0;
margin: 0;
padding: 0;
border-bottom: 1px solid #999 !important;
}
body.wm-modal {
height: auto !important;
overflow: hidden !important;
}
#donato #donato-base {
width: 100%;
height: 100%;
/*bottom: 0;*/
margin: 0;
padding: 0;
position: absolute;
z-index: 2147483639;
}
body.wm-modal #donato #donato-base {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2147483640;
}
.wb-autocomplete-suggestions {
font-family: Lucida Grande, Helvetica, Arial, sans-serif;
font-size: 12px;
text-align: left;
cursor: default;
border: 1px solid #ccc;
border-top: 0;
background: #fff;
box-shadow: -1px 1px 3px rgba(0,0,0,.1);
position: absolute;
display: none;
z-index: 2147483647;
max-height: 254px;
overflow: hidden;
overflow-y: auto;
box-sizing: border-box;
}
.wb-autocomplete-suggestion {
position: relative;
padding: 0 .6em;
line-height: 23px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 1.02em;
color: #333;
}
.wb-autocomplete-suggestion b {
font-weight: bold;
}
.wb-autocomplete-suggestion.selected {
background: #f0f0f0;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,116 @@
@font-face{font-family:'Iconochive-Regular';src:url('https://archive.org/includes/fonts/Iconochive-Regular.eot?-ccsheb');src:url('https://archive.org/includes/fonts/Iconochive-Regular.eot?#iefix-ccsheb') format('embedded-opentype'),url('https://archive.org/includes/fonts/Iconochive-Regular.woff?-ccsheb') format('woff'),url('https://archive.org/includes/fonts/Iconochive-Regular.ttf?-ccsheb') format('truetype'),url('https://archive.org/includes/fonts/Iconochive-Regular.svg?-ccsheb#Iconochive-Regular') format('svg');font-weight:normal;font-style:normal}
[class^="iconochive-"],[class*=" iconochive-"]{font-family:'Iconochive-Regular'!important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
.iconochive-Uplevel:before{content:"\21b5"}
.iconochive-exit:before{content:"\1f6a3"}
.iconochive-beta:before{content:"\3b2"}
.iconochive-logo:before{content:"\1f3db"}
.iconochive-audio:before{content:"\1f568"}
.iconochive-movies:before{content:"\1f39e"}
.iconochive-software:before{content:"\1f4be"}
.iconochive-texts:before{content:"\1f56e"}
.iconochive-etree:before{content:"\1f3a4"}
.iconochive-image:before{content:"\1f5bc"}
.iconochive-web:before{content:"\1f5d4"}
.iconochive-collection:before{content:"\2211"}
.iconochive-folder:before{content:"\1f4c2"}
.iconochive-data:before{content:"\1f5c3"}
.iconochive-tv:before{content:"\1f4fa"}
.iconochive-article:before{content:"\1f5cf"}
.iconochive-question:before{content:"\2370"}
.iconochive-question-dark:before{content:"\3f"}
.iconochive-info:before{content:"\69"}
.iconochive-info-small:before{content:"\24d8"}
.iconochive-comment:before{content:"\1f5e9"}
.iconochive-comments:before{content:"\1f5ea"}
.iconochive-person:before{content:"\1f464"}
.iconochive-people:before{content:"\1f465"}
.iconochive-eye:before{content:"\1f441"}
.iconochive-rss:before{content:"\221e"}
.iconochive-time:before{content:"\1f551"}
.iconochive-quote:before{content:"\275d"}
.iconochive-disc:before{content:"\1f4bf"}
.iconochive-tv-commercial:before{content:"\1f4b0"}
.iconochive-search:before{content:"\1f50d"}
.iconochive-search-star:before{content:"\273d"}
.iconochive-tiles:before{content:"\229e"}
.iconochive-list:before{content:"\21f6"}
.iconochive-list-bulleted:before{content:"\2317"}
.iconochive-latest:before{content:"\2208"}
.iconochive-left:before{content:"\2c2"}
.iconochive-right:before{content:"\2c3"}
.iconochive-left-solid:before{content:"\25c2"}
.iconochive-right-solid:before{content:"\25b8"}
.iconochive-up-solid:before{content:"\25b4"}
.iconochive-down-solid:before{content:"\25be"}
.iconochive-dot:before{content:"\23e4"}
.iconochive-dots:before{content:"\25a6"}
.iconochive-columns:before{content:"\25af"}
.iconochive-sort:before{content:"\21d5"}
.iconochive-atoz:before{content:"\1f524"}
.iconochive-ztoa:before{content:"\1f525"}
.iconochive-upload:before{content:"\1f4e4"}
.iconochive-download:before{content:"\1f4e5"}
.iconochive-favorite:before{content:"\2605"}
.iconochive-heart:before{content:"\2665"}
.iconochive-play:before{content:"\25b6"}
.iconochive-play-framed:before{content:"\1f3ac"}
.iconochive-fullscreen:before{content:"\26f6"}
.iconochive-mute:before{content:"\1f507"}
.iconochive-unmute:before{content:"\1f50a"}
.iconochive-share:before{content:"\1f381"}
.iconochive-edit:before{content:"\270e"}
.iconochive-reedit:before{content:"\2710"}
.iconochive-gear:before{content:"\2699"}
.iconochive-remove-circle:before{content:"\274e"}
.iconochive-plus-circle:before{content:"\1f5d6"}
.iconochive-minus-circle:before{content:"\1f5d5"}
.iconochive-x:before{content:"\1f5d9"}
.iconochive-fork:before{content:"\22d4"}
.iconochive-trash:before{content:"\1f5d1"}
.iconochive-warning:before{content:"\26a0"}
.iconochive-flash:before{content:"\1f5f2"}
.iconochive-world:before{content:"\1f5fa"}
.iconochive-lock:before{content:"\1f512"}
.iconochive-unlock:before{content:"\1f513"}
.iconochive-twitter:before{content:"\1f426"}
.iconochive-facebook:before{content:"\66"}
.iconochive-googleplus:before{content:"\67"}
.iconochive-reddit:before{content:"\1f47d"}
.iconochive-tumblr:before{content:"\54"}
.iconochive-pinterest:before{content:"\1d4df"}
.iconochive-popcorn:before{content:"\1f4a5"}
.iconochive-email:before{content:"\1f4e7"}
.iconochive-embed:before{content:"\1f517"}
.iconochive-gamepad:before{content:"\1f579"}
.iconochive-Zoom_In:before{content:"\2b"}
.iconochive-Zoom_Out:before{content:"\2d"}
.iconochive-RSS:before{content:"\1f4e8"}
.iconochive-Light_Bulb:before{content:"\1f4a1"}
.iconochive-Add:before{content:"\2295"}
.iconochive-Tab_Activity:before{content:"\2318"}
.iconochive-Forward:before{content:"\23e9"}
.iconochive-Backward:before{content:"\23ea"}
.iconochive-No_Audio:before{content:"\1f508"}
.iconochive-Pause:before{content:"\23f8"}
.iconochive-No_Favorite:before{content:"\2606"}
.iconochive-Unike:before{content:"\2661"}
.iconochive-Song:before{content:"\266b"}
.iconochive-No_Flag:before{content:"\2690"}
.iconochive-Flag:before{content:"\2691"}
.iconochive-Done:before{content:"\2713"}
.iconochive-Check:before{content:"\2714"}
.iconochive-Refresh:before{content:"\27f3"}
.iconochive-Headphones:before{content:"\1f3a7"}
.iconochive-Chart:before{content:"\1f4c8"}
.iconochive-Bookmark:before{content:"\1f4d1"}
.iconochive-Documents:before{content:"\1f4da"}
.iconochive-Newspaper:before{content:"\1f4f0"}
.iconochive-Podcast:before{content:"\1f4f6"}
.iconochive-Radio:before{content:"\1f4fb"}
.iconochive-Cassette:before{content:"\1f4fc"}
.iconochive-Shuffle:before{content:"\1f500"}
.iconochive-Loop:before{content:"\1f501"}
.iconochive-Low_Audio:before{content:"\1f509"}
.iconochive-First:before{content:"\1f396"}
.iconochive-Invisible:before{content:"\1f576"}
.iconochive-Computer:before{content:"\1f5b3"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long