Formeln

Thursday, April 25, 2013

Procedural Meshes: The Sphere

Introduction

This tutorial is one part of a series of tutorials about generating procedural meshes. See here for an outline.

In this tutorial I will show how to create a sphere mesh procedurally.

Formula

I use the following formula to create the vertices of the sphere:

x = radius * Cos(theta) * Cos(phi)
y = radius * Cos(theta) * Sin(phi)
z = radius * Sin(theta)

If you are looking at the origin an down the positive z-axis, theta is the angle between the x-axis and the line made from this angle. Theta runs in the interval from PI/2 to -PI/2 which corresponds to the latitude if you compare it to a globe:




If you look from top down on the sphere, phi is the angle between the x-axis and the line from the angle. Phi runs from 0 to 2 * PI, which corresponds to the longitude, compared with a globe:



If you fill in the values in radian for theta and phi, you will get the according point on the sphere.

From here on it is pretty straight forward: have a double nested for-loop and iterate from the top to the bottom of the sphere in the outer loop (theta) and create the vertices to on this circle in the inner loop by circling once around the y-axis (phi).

Creating the Vertex Buffer

This is, how it looks in source code. Please note, that I swap the y and z values when creating the vertices. I find it easier to do the math in a coordinate system where the z-axis points up, while in 3D coordinate systems used in computer graphics the y-axis points up.

int numVerticesPerRow = slices + 1;
int numVerticesPerColumn = stacks + 1;

numVertices = numVerticesPerRow * numVerticesPerColumn;

vertexStride = Marshal.SizeOf(typeof(Vector3)); // 12 bytes
int SizeOfVertexBufferInBytes = numVertices * vertexStride;

vertices = new DataStream(SizeOfVertexBufferInBytes, true, true);

float theta = 0.0f;
float phi = 0.0f;

float verticalAngularStride = (float)Math.PI / (float)stacks;
float horizontalAngularStride = ((float)Math.PI * 2) / (float)slices;

for (int verticalIt = 0; verticalIt < numVerticesPerColumn; verticalIt++)
{
  // beginning on top of the sphere:
  theta = ((float)Math.PI / 2.0f) - verticalAngularStride * verticalIt;

  for (int horizontalIt = 0; horizontalIt < numVerticesPerRow; horizontalIt++)
  {
    phi = horizontalAngularStride * horizontalIt;

    // position
    float x = radius * (float)Math.Cos(theta) * (float)Math.Cos(phi);
    float y = radius * (float)Math.Cos(theta) * (float)Math.Sin(phi);
    float z = radius * (float)Math.Sin(theta);

    Vector3 position = new Vector3(x, z, y);
    vertices.Write(position);
  }
}

vertices.Position = 0;

// create the vertex layout and buffer
var elements = new[] { 
    new InputElement("POSITION", 0, Format.R32G32B32_Float, 0)
};
layout = new InputLayout(DeviceManager.Instance.device, inputSignature, elements);


vertexBuffer = new SlimDX.Direct3D11.Buffer(DeviceManager.Instance.device,
    vertices,
    SizeOfVertexBufferInBytes,
    ResourceUsage.Default,
    BindFlags.VertexBuffer,
    CpuAccessFlags.None,
    ResourceOptionFlags.None,
    0);


Creating the Index Buffer

Just like in the tutorial for the grid mesh, I am iterating through the rows of vertices and create two triangles at each position.

numIndices = slices * stacks * 6;
indices = new DataStream(2 * numIndices, true, true);

for (int verticalIt = 0; verticalIt < stacks; verticalIt++)
{
    for (int horizontalIt = 0; horizontalIt < slices; horizontalIt++)
    {
        short lt = (short)(horizontalIt + verticalIt * (numVerticesPerRow));
        short rt = (short)((horizontalIt + 1) + verticalIt * (numVerticesPerRow));

        short lb = (short)(horizontalIt + (verticalIt + 1) * (numVerticesPerRow));
        short rb = (short)((horizontalIt + 1) + (verticalIt + 1) * (numVerticesPerRow));
     
        indices.Write(lt);
        indices.Write(rt);
        indices.Write(lb);

        indices.Write(rt);
        indices.Write(rb);
        indices.Write(lb);
    }
}

indices.Position = 0;

indexBuffer = new SlimDX.Direct3D11.Buffer(
    DeviceManager.Instance.device,
    indices,
    2 * numIndices,
    ResourceUsage.Default,
    BindFlags.IndexBuffer,
    CpuAccessFlags.None,
    ResourceOptionFlags.None,
    0);



Source Code

This is the source code of the sphere:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SlimDX.D3DCompiler;
using SlimDX;
using SlimDX.Direct3D11;
using SlimDX.DXGI;
using System.Runtime.InteropServices;
using System.Drawing;

namespace Apparat.Renderables
{
    public class Sphere : Renderable
    {
        SlimDX.Direct3D11.Buffer vertexBuffer;
        SlimDX.Direct3D11.Buffer indexBuffer;
        DataStream vertices;
        DataStream indices;

        InputLayout layout;

        int numVertices = 0;
        int numIndices = 0;

        int vertexStride = 0;

        ShaderSignature inputSignature;
        EffectTechnique technique;
        EffectPass pass;

        Effect effect;
        EffectMatrixVariable tmat;
        EffectVectorVariable mCol;
        EffectVectorVariable wfCol;

        float radius = 0;

        public Sphere(float radius, int slices, int stacks )
        {
            try
            {
                using (ShaderBytecode effectByteCode = ShaderBytecode.CompileFromFile(
                    "Shaders/transformEffectWireframe.fx",
                    "Render",
                    "fx_5_0",
                    ShaderFlags.EnableStrictness,
                    EffectFlags.None))
                {
                    effect = new Effect(DeviceManager.Instance.device, effectByteCode);
                    technique = effect.GetTechniqueByIndex(0);
                    pass = technique.GetPassByIndex(0);
                    inputSignature = pass.Description.Signature;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }

            this.radius = radius;

            tmat = effect.GetVariableByName("gWVP").AsMatrix();
            mCol = effect.GetVariableByName("colorSolid").AsVector();
            wfCol = effect.GetVariableByName("colorWireframe").AsVector();
         
            mCol.Set(new Color4(1, 0, 1, 0));
            wfCol.Set(new Color4(1, 0, 0, 0));

            int numVerticesPerRow = slices + 1;
            int numVerticesPerColumn = stacks + 1;

            numVertices = numVerticesPerRow * numVerticesPerColumn;

            vertexStride = Marshal.SizeOf(typeof(Vector3)); // 12 bytes
            int SizeOfVertexBufferInBytes = numVertices * vertexStride;

            vertices = new DataStream(SizeOfVertexBufferInBytes, true, true);

            float theta = 0.0f;
            float phi = 0.0f;

            float verticalAngularStride = (float)Math.PI / (float)stacks;
            float horizontalAngularStride = ((float)Math.PI * 2) / (float)slices;

            for (int verticalIt = 0; verticalIt < numVerticesPerColumn; verticalIt++)
            {
                // beginning on top of the sphere:
                theta = ((float)Math.PI / 2.0f) - verticalAngularStride * verticalIt;

                for (int horizontalIt = 0; horizontalIt < numVerticesPerRow; horizontalIt++)
                {
                    phi = horizontalAngularStride * horizontalIt;

                    // position
                    float x = radius * (float)Math.Cos(theta) * (float)Math.Cos(phi);
                    float y = radius * (float)Math.Cos(theta) * (float)Math.Sin(phi);
                    float z = radius * (float)Math.Sin(theta);

                    Vector3 position = new Vector3(x, z, y);
                    vertices.Write(position);
                }
            }

            vertices.Position = 0;

            // create the vertex layout and buffer
            var elements = new[] { 
                new InputElement("POSITION", 0, Format.R32G32B32_Float, 0)
            };
            layout = new InputLayout(DeviceManager.Instance.device, inputSignature, elements);

            
            vertexBuffer = new SlimDX.Direct3D11.Buffer(DeviceManager.Instance.device,
                vertices,
                SizeOfVertexBufferInBytes,
                ResourceUsage.Default,
                BindFlags.VertexBuffer,
                CpuAccessFlags.None,
                ResourceOptionFlags.None,
                0);

            numIndices = slices * stacks * 6;
            indices = new DataStream(2 * numIndices, true, true);

            for (int verticalIt = 0; verticalIt < stacks; verticalIt++)
            {
                for (int horizontalIt = 0; horizontalIt < slices; horizontalIt++)
                {
                    short lt = (short)(horizontalIt + verticalIt * (numVerticesPerRow));
                    short rt = (short)((horizontalIt + 1) + verticalIt * (numVerticesPerRow));

                    short lb = (short)(horizontalIt + (verticalIt + 1) * (numVerticesPerRow));
                    short rb = (short)((horizontalIt + 1) + (verticalIt + 1) * (numVerticesPerRow));
                 
                    indices.Write(lt);
                    indices.Write(rt);
                    indices.Write(lb);

                    indices.Write(rt);
                    indices.Write(rb);
                    indices.Write(lb);
                }
            }

            indices.Position = 0;

            indexBuffer = new SlimDX.Direct3D11.Buffer(
                DeviceManager.Instance.device,
                indices,
                2 * numIndices,
                ResourceUsage.Default,
                BindFlags.IndexBuffer,
                CpuAccessFlags.None,
                ResourceOptionFlags.None,
                0);


        }

        public override void render()
        {
            Matrix ViewPerspective = CameraManager.Instance.ViewPerspective;

            tmat.SetMatrix(ViewPerspective);

            // configure the Input Assembler portion of the pipeline with the vertex data
            DeviceManager.Instance.context.InputAssembler.InputLayout = layout;
            DeviceManager.Instance.context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList;
            DeviceManager.Instance.context.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(vertexBuffer, vertexStride, 0));
            DeviceManager.Instance.context.InputAssembler.SetIndexBuffer(indexBuffer, Format.R16_UInt, 0);

            technique = effect.GetTechniqueByName("Render");

            EffectTechniqueDescription techDesc;
            techDesc = technique.Description;

            for (int p = 0; p < techDesc.PassCount; ++p)
            {
                technique.GetPassByIndex(p).Apply(DeviceManager.Instance.context);
                DeviceManager.Instance.context.DrawIndexed(numIndices, 0, 0);
            }
        }

        public override void dispose()
        {

        }
    }
}

As I am using a different shader than before which sets an uniform color for the mesh and a color for the wireframe, I will post the complete code of the shader used here. I will use this shader also for the next tutorial in which I will show how to create other geometric primitives. I use the variables mCol and wfCol to set the colors of the mesh and the wireframe in the code above via the effect framework.


matrix gWVP;
float4 colorSolid;
float4 colorWireframe;

float4 VShader(float4 position : POSITION) : SV_POSITION
{
  return mul( position, gWVP);
}

float4 PShader(float4 position : SV_POSITION) : SV_Target
{
  return colorSolid;
}

float4 PShaderWireframe(float4 position : SV_POSITION) : SV_Target
{
  return colorWireframe;
}

RasterizerState SolidState
{
  FillMode = Solid;
};

RasterizerState WireframeState
{
  FillMode = Wireframe;
  SlopeScaledDepthBias = -0.5f;
};

technique10 Render
{
  pass P0
  {
    SetVertexShader( CompileShader( vs_4_0, VShader() ));
    SetGeometryShader( NULL );
    SetPixelShader( CompileShader( ps_4_0, PShader() ));
    SetRasterizerState(SolidState);
  }

  pass P1
  {
    SetVertexShader( CompileShader( vs_4_0, VShader() ));
    SetGeometryShader( NULL );
    SetPixelShader( CompileShader( ps_4_0, PShaderWireframe() ));
    SetRasterizerState(WireframeState);
  }
}



Conclusion


This picture shows a sphere created with 8 slices and 8 stacks:


This sphere was created with 32 slices and 32 stacks:


You can download the code to this tutorial here.

No comments:

Post a Comment