OpenGL-Planets

Planet Generator

Project Details


Production Time - 3 weeks, part time

Team Size - Solo

Framework - OpenGL C++

Description


This was an OpenGL project I made during a course at Future Games. Originally I was just trying to learn OpenGL but towards the end of the course I decided to try to make some simple planet generation to test the open gl library I had created.


Mesh Generation


The planet mesh was generated as an "inflated cube", meaning the 6 faces of a cube where each vertex position was normalized to keep a constant distance from the center. This was probably the easiest way to do it but in hindsight it would probably have been better to use a subdivided icosahedron, that way i would have avoided the seams that come from the normals not smoothly transitioning between the seperate faces.


The mesh deformation (the "continents" on the planet) was achived by layering some simple noise at each vertex, using a set amount of layers with increasing or decreasing frequency and amplitude.I was planing to add different types of noice, maybe to create some nice ridges to look like mountain ranges, but I didn't have time.



PlanetFace::PlanetFace(const int resolution, const glm::vec3& localUp, const PlanetSettings& settings, MinMaxFloat& elevation)
{
	struct Vertex
	{
		glm::vec3 position = glm::vec3(0.0f);
		glm::vec3 normal = glm::vec3(1.0f);
	};

	std::vector< Vertex> vertices;
	std::vector< unsigned int> indices;
	const int vertexCount = resolution * resolution;
	const int indexCount = (resolution - 1) * (resolution - 1) * 6;
	vertices.resize(vertexCount);
	indices.resize(indexCount);

	const glm::vec3 axisA = glm::vec3(localUp.y, localUp.z, localUp.x);
	const glm::vec3 axisB = glm::cross(localUp, axisA);

	for (int y = 0, vi = 0, ti = 0; y < resolution; y++)
	{
		for (int x = 0; x < resolution; x++, vi++)
		{
			const glm::vec2 factor = glm::vec2(x, y) / (float)(resolution - 1);
			glm::vec3 pointOnCube = localUp + (factor.x - 0.5f) * 2.0f * axisA + (factor.y - 0.5f) * 2.0f * axisB;
			glm::vec3 pointOnSphere = glm::normalize(pointOnCube);
			glm::vec3 pointOnPlanet = settings.CalculatePointOnPlanet(pointOnSphere, elevation);

			vertices[vi] = {pointOnPlanet, glm::vec3(1.0f)};
			if (x == resolution - 1 || y == resolution - 1) continue;

			indices[ti] = vi;
			indices[ti + 1] = vi + resolution + 1;
			indices[ti + 2] = vi + resolution;

			indices[ti + 3] = vi;
			indices[ti + 4] = vi + 1;
			indices[ti + 5] = vi + resolution + 1;
			ti += 6;
		}
	}
	for (int i = 0; i < indexCount; i += 3)
	{
		//Triangle
		Vertex* v1 = &vertices[indices[i]];
		Vertex* v2 = &vertices[indices[i + 1]];
		Vertex* v3 = &vertices[indices[i + 2]];

		glm::vec3 normal = glm::normalize(glm::cross(v2->position - v1->position, v3->position - v1->position));
		v1->normal += normal;
		v2->normal += normal;
		v3->normal += normal;
	}
	for (int i = 0; i < vertexCount; i++)
		vertices[i].normal = glm::normalize(vertices[i].normal);

	const int vertexSize = sizeof(float) * 6;
	std::vector< LayoutElement> layout =
	{
		{3, GL_FLOAT, false, vertexSize, 0},
		{3, GL_FLOAT, false, vertexSize, sizeof(float) * 3},
	};
	_mesh = std::make_unique< Mesh>(&vertices[0].position[0], vertexSize, vertexCount, &indices[0], indexCount, layout);
}


    



glm::vec3 PlanetSettings::CalculatePointOnPlanet(const glm::vec3& point, MinMaxFloat& elevationMinMax) const
{
	float elevation = 0.0f;
	float firstLayerValue = 0.0f;
	if (!Noise.empty())
	{
		firstLayerValue = EvaluateNoise(point, Noise[0]);
		if (Noise[0].Enabled)
			elevation = firstLayerValue;
	}

	const int size = Noise.size();
	for (int i = 1; i < size; i++)
	{
		if (!Noise[i].Enabled) continue;
		float mask = Noise[i].UseFirstLayerAsMask ? firstLayerValue : 1.0f;
		elevation += EvaluateNoise(point, Noise[i]);
	}

	elevation = Radius * (1 + elevation);
	elevationMinMax.AddValue(elevation);
	return point * elevation;
}
float PlanetSettings::EvaluateNoise(const glm::vec3& point, const NoiseSettings& settings) const
{
	float noiseValue = 0.0f;
	float frequency = settings.BaseRoughness;
	float amplitude = 1.0f;

	for (int i = 0; i < settings.LayerCount; i++)
	{
		const glm::vec3 evaluationPoint = point * frequency + settings.Center;
		const float rawNoise = _perlin.GetValue(evaluationPoint.x, evaluationPoint.y, evaluationPoint.z);
		noiseValue += (rawNoise + 1) * 0.5f * amplitude;
		frequency *= settings.Roughness;
		amplitude *= settings.Persistence;
	}

	noiseValue = std::max(0.0f, noiseValue - settings.MinValue);
	return noiseValue * settings.Strength;
}

    

Planet Shader


The shader i wrote for the planets is pretty simple; I input a color gradient as a texture, and I sample from that texture using the elevation of the vertex (length of the vertex position), I also pass in the minimum and maximum elevation that was calculated so I can get a smooth value between 0 and 1.


The shader also implements some simple light handling for directional and point lights, as well as shadow casting using a frame buffer from the directional lights point of view.



#SHADER VERTEX
#version 330 core

in vec3 a_Position;
in vec3 a_Normal;
out vec3 f_Normal;
out float f_Elevation;
out vec3 f_World;

uniform mat4 u_World;
uniform mat4 u_ViewProjection;

void main()
{
	gl_Position = u_ViewProjection * u_World * vec4(a_Position, 1.0f);
	f_Elevation = length(a_Position);
	f_Normal = normalize((u_World * vec4(a_Normal, 0.0f))).xyz;
	f_World = (u_World * vec4(a_Position, 1.0f)).xyz;
}

#SHADER FRAGMENT

#version 330 core
#define MAX_POINT_LIGHTS 10

struct DirectionalLight
{
	vec3 Direction;
	vec3 Color;
	float Intensity;
	sampler2D ShadowBuffer;
	mat4 ViewProjection;
};
struct PointLight
{
	vec3 Position;
	float Radius;
	vec3 Color;
	float Intensity;
};
struct AmbientLight
{
	float Intensity;
};
struct Emission
{
	float Intensity;
	vec3 Color;
};


uniform vec2 u_ElevationMinMax;
uniform sampler2D u_Sampler;
uniform vec3 u_EyePosition;
uniform float u_SpecularIntensity;
uniform DirectionalLight u_DirectionalLight;
uniform AmbientLight u_AmbientLight;
uniform int u_PointLightCount;
uniform PointLight u_PointLights[MAX_POINT_LIGHTS];
uniform Emission u_Emission;

out vec4 o_Color;
in vec3 f_Normal;
in vec3 f_World;
in float f_Elevation;

const float SpecExponent = 30.0f;
const float SpecIntensity = 0.4f;
const float ShadowBias = 0.01;

vec3 CalculatePointLight(PointLight light, vec3 albedo)
{
	vec3 lightDirection = normalize(f_World - light.Position);
	float intensity = 1.0f - length(f_World - light.Position) / light.Radius;
	vec3 diffuse = albedo * light.Color * light.Intensity* intensity * max(-dot(lightDirection, f_Normal), 0.0f);
	vec3 worldEye = normalize(u_EyePosition - lightDirection);
	vec3 halfwayVector = normalize(worldEye - lightDirection);
	float spec = max(dot(halfwayVector, f_Normal), 0.0f);
	spec = pow(spec, SpecExponent) * u_SpecularIntensity;
	vec3 specular = light.Color * intensity * spec;
	return diffuse + specular;
}

vec3 CalculateDirectionalLight(DirectionalLight light, vec3 albedo)
{
	vec3 diffuse = albedo * light.Color * max(-dot(light.Direction, f_Normal), 0.0f) * light.Intensity;
	vec3 worldEye = normalize(u_EyePosition - light.Direction);
	vec3 halfwayVector = normalize(worldEye - light.Direction);
	float spec = max(dot(halfwayVector, f_Normal), 0.0f);
	spec = pow(spec, SpecExponent) * u_SpecularIntensity * light.Intensity;
	vec3 specular = light.Color * spec;

	vec4 lightNDC = light.ViewProjection * vec4(f_World, 1.0f);
	lightNDC = lightNDC * 0.5f + 0.5f;
	float lightDepth = texture(light.ShadowBuffer, lightNDC.xy).x;
	float ourDepth = lightNDC.z;
	float shadow = step(ourDepth, lightDepth + ShadowBias);
	diffuse *= shadow;
	specular *= shadow;

	return diffuse + specular;
}


void main()
{
	//Color
	float value = (f_Elevation - u_ElevationMinMax.x) / (u_ElevationMinMax.y - u_ElevationMinMax.x);
	vec3 albedo= texture(u_Sampler, vec2(clamp(value, 0.0f,1.0f), 0.0f)).xyz;

	o_Color = vec4(0.0f, 0.0f, 0.0f, 1.0f);
	
	o_Color.xyz += CalculateDirectionalLight(u_DirectionalLight, albedo);

	for (int i = 0; i < u_PointLightCount; i++)
	{
		o_Color.xyz += CalculatePointLight(u_PointLights[i], albedo);
	}

	o_Color.xyz += albedo * u_AmbientLight.Intensity;
	o_Color.xyz += albedo * u_Emission.Intensity;
}

    

OpenGL Abstraction


I created some abstraction classes to make OpenGL a bit easier to use, for example some of the fundemental things you need to render a mesh are buffers and shaders, so I created a vertex and index buffer class as well as a vertex array class which are used by the mesh to easily be able to load a mesh and send it to the renderer.


The shader class loads a shader into memory and provides methods to upload uniform values, and the material class can hold uniform values while the shader is not used so they can easily be uploaded again when the shader is bound, so the user doesn't have to do that manually. Materials can also be given textures which are loaded into specifed slots when the shader is bound.


The renderer simply has a function for starting a "scene" (meaning setting up lighting data and such) then can be called to render a single mesh using a certain material.