Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 2.8.12)
cmake_minimum_required(VERSION 3.10)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake")

project(cis565_project5_vulkan_grass_rendering)
Expand Down
66 changes: 61 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,67 @@ Vulkan Grass Rendering
==================================

**University of Pennsylvania, CIS 565: GPU Programming and Architecture, Project 5**
![](img/GrassMain.gif)
* Pavel Peev
* Tested on: Windows 11, Intel Core Ultra 5 225f @ 3.3GHz, NVIDIA GeForce RTX 5060

### Description
This project implements the paper [Responsive Real-Time Grass Rendering for General 3D Scenes](https://www.cg.tuwien.ac.at/research/publications/2017/JAHRMANN-2017-RRTG/JAHRMANN-2017-RRTG-draft.pdf) using Vulkan.

In the paper, the blades of grass are represented as 3 point cubic Bezier curves, upon which forces are applied on. The first control point of the bezier curve represents the base of the blade of grass, and is unmoving. The third control point in the bezier curve represents the tip of the grass blade, and has three forces applied to it: gravity, recovery, and wind. Finally, the second control point helps keep the blade of grass at a constant height.

We run a compute pipeline to calculate the forces and new positions of the second and third control points. During this stage, we also calculate which blades of grass can be culled with 3 culling operation: orientation, view-frustrum, and distance based culling.

Afterwards, the control points are sent to the graphics pipeline with a tesselation shader, which tesselates a quad and uses the uv positions of the points on the quad to align them to the bezier curve in a shape of our choosing (in our case, a triangular tipped rectangle). Upon completion of the tesselation, the new geometry is sent to the fragment shader to be rendered.


## Images

### Grass No Force
![](img/GrassNoForce.gif)

### Grass No Wind
![](img/GrassNoWind.gif)

### Grass with Wind
![](img/GrassWithWind.gif)

### Grass Culling
![](img/GrassCulling.gif)



## Performance Analysis



![](img/grassAnalysis.png)

The above data is done without culling. As the number of blades increases exponentially, we see the fps half. This shows that the grass simulation is very parralel, only increasing in cost logarithmically alongside the exponential increase in the amount of grass we need to render. For a grass simulation, the performance is alright, but for large scale simulations of entire environments with potentially millions or billions of grass blades, it requires other optimizations to run smoothly, such as the culling.

### Culling Performance

Number of Blades: 131072 (2^17)

| Base | Orientation | Orientation Optimal|
|---|---|---|
| 110 fps | 140 fps| 200 fps|

| Base | View-Frustrum | View-Frustrum Optimal |
|---|---|---|
| 110 fps | 150 fps| 300 fps |

| Base | Distance | Distance Optimal |
|---|---|---|
| 110 fps | 250 fps| 750 fps |

| Base | All Culling | All Culling Optimal |
|---|---|---|
| 110 fps | 300 fps| 1000 fps |

For each of the optimal measurements, we position the camera such that they can more effectively cull. For the orientation culling, we position in a spot where more grass is perpendicular to the camera. For view frustrum culling, we look at it from above and zoomed in at a patch of grass. For distance, we zoom out a lot so that about half the patches of grass are culled fully.

We see notable improvements with each of the different culling methods, with the first two giving a marginal 1.5 times performance boost generally and with the potential to double or triple performance in more specific situations. Distance seems to be the most effective, although this one is of course the most visible, as it can reduces . Of course, all 3 together provides the best performance boost, though it's important to note that it's not additive, as there is a bit of overlap between the different culling methods.

* (TODO) YOUR NAME HERE
* Tested on: (TODO) Windows 22, i7-2222 @ 2.22GHz 22GB, GTX 222 222MB (Moore 2222 Lab)

### (TODO: Your README)

*DO NOT* leave the README to the last minute! It is a crucial part of the
project, and we will not be able to grade you without a good README.
Binary file modified bin/Release/vulkan_grass_rendering.exe
Binary file not shown.
4 changes: 2 additions & 2 deletions external/GLFW/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 2.8.12)
cmake_minimum_required(VERSION 3.10)

project(GLFW C)

Expand All @@ -7,7 +7,7 @@ set(CMAKE_LEGACY_CYGWIN_WIN32 OFF)
if (NOT CMAKE_VERSION VERSION_LESS "3.0")
# Until all major package systems have moved to CMake 3,
# we stick with the older INSTALL_NAME_DIR mechanism
cmake_policy(SET CMP0042 OLD)
#cmake_policy(SET CMP0042 OLD)
endif()

if (NOT CMAKE_VERSION VERSION_LESS "3.1")
Expand Down
Binary file added img/GrassCulling.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/GrassMain.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/GrassNoForces.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/GrassNoWind.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/GrassWithWind.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/grassAnalysis.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/Blades.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Blades::Blades(Device* device, VkCommandPool commandPool, float planeDim) : Mode
indirectDraw.firstInstance = 0;

BufferUtils::CreateBufferFromData(device, commandPool, blades.data(), NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, bladesBuffer, bladesBufferMemory);
BufferUtils::CreateBuffer(device, NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, culledBladesBuffer, culledBladesBufferMemory);
BufferUtils::CreateBuffer(device, NUM_BLADES * sizeof(Blade), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, culledBladesBuffer, culledBladesBufferMemory);
BufferUtils::CreateBufferFromData(device, commandPool, &indirectDraw, sizeof(BladeDrawIndirect), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, numBladesBuffer, numBladesBufferMemory);
}

Expand Down
8 changes: 4 additions & 4 deletions src/Blades.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
#include "Model.h"

constexpr static unsigned int NUM_BLADES = 1 << 13;
constexpr static float MIN_HEIGHT = 1.3f;
constexpr static float MAX_HEIGHT = 2.5f;
constexpr static float MIN_HEIGHT = 1.3f * 1.5;
constexpr static float MAX_HEIGHT = 2.5f * 1.5;
constexpr static float MIN_WIDTH = 0.1f;
constexpr static float MAX_WIDTH = 0.14f;
constexpr static float MIN_BEND = 7.0f;
constexpr static float MAX_BEND = 13.0f;
constexpr static float MIN_BEND = 13.0f;
constexpr static float MAX_BEND = 20.0f;

struct Blade {
// Position and direction
Expand Down
169 changes: 165 additions & 4 deletions src/Renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,39 @@ void Renderer::CreateComputeDescriptorSetLayout() {
// TODO: Create the descriptor set layout for the compute pipeline
// Remember this is like a class definition stating why types of information
// will be stored at each binding
VkDescriptorSetLayoutBinding numBladesBinding = {};
numBladesBinding.binding = 0;
numBladesBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
numBladesBinding.descriptorCount = 1;
numBladesBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
numBladesBinding.pImmutableSamplers = nullptr;

VkDescriptorSetLayoutBinding bladesBufferBinding = {};
bladesBufferBinding.binding = 1;
bladesBufferBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
bladesBufferBinding.descriptorCount = 1;
bladesBufferBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
bladesBufferBinding.pImmutableSamplers = nullptr;

VkDescriptorSetLayoutBinding culledBladesBufferBinding = {};
culledBladesBufferBinding.binding = 2;
culledBladesBufferBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
culledBladesBufferBinding.descriptorCount = 1;
culledBladesBufferBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
culledBladesBufferBinding.pImmutableSamplers = nullptr;

std::vector<VkDescriptorSetLayoutBinding> bindings = { numBladesBinding, bladesBufferBinding, culledBladesBufferBinding };

// Create the descriptor set layout
VkDescriptorSetLayoutCreateInfo layoutInfo = {};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size());
layoutInfo.pBindings = bindings.data();

if (vkCreateDescriptorSetLayout(logicalDevice, &layoutInfo, nullptr, &computeDescriptorSetLayout) != VK_SUCCESS) {
throw std::runtime_error("Failed to create descriptor set layout");
}

}

void Renderer::CreateDescriptorPool() {
Expand All @@ -216,6 +249,7 @@ void Renderer::CreateDescriptorPool() {
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER , 1 },

// TODO: Add any additional types and counts of descriptors you will need to allocate
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, static_cast<uint32_t>(3 * scene->GetBlades().size())}
};

VkDescriptorPoolCreateInfo poolInfo = {};
Expand Down Expand Up @@ -320,6 +354,53 @@ void Renderer::CreateModelDescriptorSets() {
void Renderer::CreateGrassDescriptorSets() {
// TODO: Create Descriptor sets for the grass.
// This should involve creating descriptor sets which point to the model matrix of each group of grass blades

grassDescriptorSets.resize(scene->GetBlades().size());
//Since it uses the same layout as the models (model matrix + texture), we just reuse the modelDescriptorSetLayout here
VkDescriptorSetLayout layouts[] = { modelDescriptorSetLayout };
VkDescriptorSetAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = static_cast<uint32_t>(grassDescriptorSets.size());
allocInfo.pSetLayouts = layouts;

if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, grassDescriptorSets.data()) != VK_SUCCESS) {
throw std::runtime_error("Failed to allocate descriptor set");
}

std::vector<VkWriteDescriptorSet> descriptorWrites(2 * grassDescriptorSets.size());

for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) {
VkDescriptorBufferInfo grassBufferInfo = {};
grassBufferInfo.buffer = scene->GetBlades()[i]->GetModelBuffer();
grassBufferInfo.offset = 0;
grassBufferInfo.range = sizeof(ModelBufferObject);

VkDescriptorImageInfo imageInfo = {};
imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
imageInfo.imageView = scene->GetBlades()[i]->GetTextureView();
imageInfo.sampler = scene->GetBlades()[i]->GetTextureSampler();

descriptorWrites[2 * i + 0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[2 * i + 0].dstSet = grassDescriptorSets[i];
descriptorWrites[2 * i + 0].dstBinding = 0;
descriptorWrites[2 * i + 0].dstArrayElement = 0;
descriptorWrites[2 * i + 0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrites[2 * i + 0].descriptorCount = 1;
descriptorWrites[2 * i + 0].pBufferInfo = &grassBufferInfo;
descriptorWrites[2 * i + 0].pImageInfo = nullptr;
descriptorWrites[2 * i + 0].pTexelBufferView = nullptr;

descriptorWrites[2 * i + 1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[2 * i + 1].dstSet = grassDescriptorSets[i];
descriptorWrites[2 * i + 1].dstBinding = 1;
descriptorWrites[2 * i + 1].dstArrayElement = 0;
descriptorWrites[2 * i + 1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
descriptorWrites[2 * i + 1].descriptorCount = 1;
descriptorWrites[2 * i + 1].pImageInfo = &imageInfo;
}
vkUpdateDescriptorSets(logicalDevice, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);

}

void Renderer::CreateTimeDescriptorSet() {
Expand Down Expand Up @@ -360,8 +441,76 @@ void Renderer::CreateTimeDescriptorSet() {
void Renderer::CreateComputeDescriptorSets() {
// TODO: Create Descriptor sets for the compute pipeline
// The descriptors should point to Storage buffers which will hold the grass blades, the culled grass blades, and the output number of grass blades
computeDescriptorSets.resize(scene->GetBlades().size());

VkDescriptorSetLayout layouts[] = { computeDescriptorSetLayout };
VkDescriptorSetAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = 1;
allocInfo.pSetLayouts = layouts;

if (vkAllocateDescriptorSets(logicalDevice, &allocInfo, computeDescriptorSets.data()) != VK_SUCCESS) {
throw std::runtime_error("Failed to allocate descriptor set");
}

std::vector<VkWriteDescriptorSet> descriptorWrites(3 * computeDescriptorSets.size());

for (uint32_t i = 0; i < scene->GetBlades().size(); ++i) {
VkDescriptorBufferInfo numBladesBufferInfo = {};
numBladesBufferInfo.buffer = scene->GetBlades()[i]->GetNumBladesBuffer();
numBladesBufferInfo.offset = 0;
numBladesBufferInfo.range = sizeof(BladeDrawIndirect);

VkDescriptorBufferInfo bladesBufferInfo = {};
bladesBufferInfo.buffer = scene->GetBlades()[i]->GetBladesBuffer();
bladesBufferInfo.offset = 0;
bladesBufferInfo.range = NUM_BLADES * sizeof(Blade);

VkDescriptorBufferInfo culledBladesBufferInfo = {};
culledBladesBufferInfo.buffer = scene->GetBlades()[i]->GetCulledBladesBuffer();
culledBladesBufferInfo.offset = 0;
culledBladesBufferInfo.range = NUM_BLADES * sizeof(Blade);

descriptorWrites[3 * i + 0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[3 * i + 0].dstSet = computeDescriptorSets[i];
descriptorWrites[3 * i + 0].dstBinding = 0;
descriptorWrites[3 * i + 0].dstArrayElement = 0;
descriptorWrites[3 * i + 0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
descriptorWrites[3 * i + 0].descriptorCount = 1;
descriptorWrites[3 * i + 0].pBufferInfo = &numBladesBufferInfo;
descriptorWrites[3 * i + 0].pImageInfo = nullptr;
descriptorWrites[3 * i + 0].pTexelBufferView = nullptr;

descriptorWrites[3 * i + 1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[3 * i + 1].dstSet = computeDescriptorSets[i];
descriptorWrites[3 * i + 1].dstBinding = 1;
descriptorWrites[3 * i + 1].dstArrayElement = 0;
descriptorWrites[3 * i + 1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
descriptorWrites[3 * i + 1].descriptorCount = 1;
descriptorWrites[3 * i + 1].pBufferInfo = &bladesBufferInfo;
descriptorWrites[3 * i + 1].pImageInfo = nullptr;
descriptorWrites[3 * i + 1].pTexelBufferView = nullptr;

descriptorWrites[3 * i + 2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[3 * i + 2].dstSet = computeDescriptorSets[i];
descriptorWrites[3 * i + 2].dstBinding = 2;
descriptorWrites[3 * i + 2].dstArrayElement = 0;
descriptorWrites[3 * i + 2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
descriptorWrites[3 * i + 2].descriptorCount = 1;
descriptorWrites[3 * i + 2].pBufferInfo = &culledBladesBufferInfo;
descriptorWrites[3 * i + 2].pImageInfo = nullptr;
descriptorWrites[3 * i + 2].pTexelBufferView = nullptr;

}
vkUpdateDescriptorSets(logicalDevice, static_cast<uint32_t>(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);

}





void Renderer::CreateGraphicsPipeline() {
VkShaderModule vertShaderModule = ShaderModule::Create("shaders/graphics.vert.spv", logicalDevice);
VkShaderModule fragShaderModule = ShaderModule::Create("shaders/graphics.frag.spv", logicalDevice);
Expand Down Expand Up @@ -717,7 +866,7 @@ void Renderer::CreateComputePipeline() {
computeShaderStageInfo.pName = "main";

// TODO: Add the compute dsecriptor set layout you create to this list
std::vector<VkDescriptorSetLayout> descriptorSetLayouts = { cameraDescriptorSetLayout, timeDescriptorSetLayout };
std::vector<VkDescriptorSetLayout> descriptorSetLayouts = { cameraDescriptorSetLayout, timeDescriptorSetLayout, computeDescriptorSetLayout };

// Create pipeline layout
VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
Expand Down Expand Up @@ -885,6 +1034,17 @@ void Renderer::RecordComputeCommandBuffer() {

// TODO: For each group of blades bind its descriptor set and dispatch

for (int i = 0; i < scene->GetBlades().size(); i++)
{
vkCmdBindDescriptorSets(computeCommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 2, 1, &computeDescriptorSets[i], 0, nullptr);

vkCmdDispatch(computeCommandBuffer, NUM_BLADES / WORKGROUP_SIZE, 1, 1);



}


// ~ End recording ~
if (vkEndCommandBuffer(computeCommandBuffer) != VK_SUCCESS) {
throw std::runtime_error("Failed to record compute command buffer");
Expand Down Expand Up @@ -976,13 +1136,13 @@ void Renderer::RecordCommandBuffers() {
VkBuffer vertexBuffers[] = { scene->GetBlades()[j]->GetCulledBladesBuffer() };
VkDeviceSize offsets[] = { 0 };
// TODO: Uncomment this when the buffers are populated
// vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);
vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, vertexBuffers, offsets);

// TODO: Bind the descriptor set for each grass blades model

vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, grassPipelineLayout, 1, 1, &grassDescriptorSets[j], 0, nullptr);
// Draw
// TODO: Uncomment this when the buffers are populated
// vkCmdDrawIndirect(commandBuffers[i], scene->GetBlades()[j]->GetNumBladesBuffer(), 0, 1, sizeof(BladeDrawIndirect));
vkCmdDrawIndirect(commandBuffers[i], scene->GetBlades()[j]->GetNumBladesBuffer(), 0, 1, sizeof(BladeDrawIndirect));
}

// End render pass
Expand Down Expand Up @@ -1057,6 +1217,7 @@ Renderer::~Renderer() {
vkDestroyDescriptorSetLayout(logicalDevice, cameraDescriptorSetLayout, nullptr);
vkDestroyDescriptorSetLayout(logicalDevice, modelDescriptorSetLayout, nullptr);
vkDestroyDescriptorSetLayout(logicalDevice, timeDescriptorSetLayout, nullptr);
vkDestroyDescriptorSetLayout(logicalDevice, computeDescriptorSetLayout, nullptr);

vkDestroyDescriptorPool(logicalDevice, descriptorPool, nullptr);

Expand Down
6 changes: 6 additions & 0 deletions src/Renderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,18 @@ class Renderer {
VkDescriptorSetLayout cameraDescriptorSetLayout;
VkDescriptorSetLayout modelDescriptorSetLayout;
VkDescriptorSetLayout timeDescriptorSetLayout;
VkDescriptorSetLayout computeDescriptorSetLayout;


VkDescriptorPool descriptorPool;

VkDescriptorSet cameraDescriptorSet;
std::vector<VkDescriptorSet> modelDescriptorSets;
VkDescriptorSet timeDescriptorSet;
std::vector<VkDescriptorSet> computeDescriptorSets;
std::vector< VkDescriptorSet> grassDescriptorSets;



VkPipelineLayout graphicsPipelineLayout;
VkPipelineLayout grassPipelineLayout;
Expand Down
Binary file added src/images/grassBlue.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ int main() {
VkDeviceMemory grassImageMemory;
Image::FromFile(device,
transferCommandPool,
"images/grass.jpg",
"images/grassBlue.jpg",
VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_SAMPLED_BIT,
Expand All @@ -131,6 +131,8 @@ int main() {

Blades* blades = new Blades(device, transferCommandPool, planeDim);

blades->SetTexture(grassImage);

vkDestroyCommandPool(device->GetVkDevice(), transferCommandPool, nullptr);

Scene* scene = new Scene(device);
Expand Down
Loading