Building icospheres in x3d?

Has anyone made icospheres in castle game engine and x3d? If not, I will and will share my results. I think deformable icospheres would be a cool, stylistic and easy to control LOD way to handle tree leaf volume, instead of my current way of random individual leaves as transparent faces.
Untitled

Sound like a nice idea. I don’t have ready code for icospheres, I’d welcome too see CGE implementation of it :slight_smile:

Note that instead of generating one, you can also just create one in Blender and export to glTF :slight_smile: But then naturally the generation process is not configurable from code.

Right mine needs to be configurable. I am basing it on this catch 22 - Andreas Kahler's blog: Creating an icosphere mesh in code. I have the basic 12 vertex model working (it is hardcoded points). I still have bugs in the refinement process.

1 Like

It is working. For code so simple, I had a hard time. The midpoint cache dictionary caused me a lot of grief.


I need to clean it up and add support for changing the radius and I’ll post the code.

2 Likes

Here is the code. The builder is a separate class to manage temporary lists and point cache that can all be disposed after built. All you need is to call

s := ticosphere.create( owner );
s.build( radius, depth );
view.add( s );

Here is the icosphere source:

unit icosphere;

{$mode ObjFPC}{$H+}

{ IcoSphere

  builds a TCastleScene containing an icosphere given a radius and recursion depth.
  depth=0 produces the base shape with 12 vertices and 20 triangles
  each depth subdivides each triangle into 4 triangles

  based on code from http://blog.andreaskahler.com/2009/06/creating-icosphere-mesh-in-code.html }

interface

uses
  Classes, Math,
  Generics.Collections,
  CastleVectors, CastleScene, CastleUtils,
  x3dNodes;

{ low level classes used internally while building the scene. don't use directly }
type
  tpointcache = class( specialize tdictionary<int64,integer> );
  ticospherebuilder = class
     public
     constructor create( iRadius : single );
     destructor destroy; override;
     private
     Radius   : single;
     Vertices : TVector3List;
     Indexes  : TInt32List;
     middlePointIndexCache : tpointcache;
     function addVertex( const p : TVector3 ) : integer;
     function getMiddlePoint( midptcache : tpointcache;
                              p1, p2 : integer ) : integer;
     procedure refinetriangles( recursionlevel : integer );
   end;


{ this is the scene class, instantiate it as normal, then build with the
  desired parameters and add to your view/transform/scene }
type ticosphere = class( TCastleScene )

   public
     procedure build( iradius : single;
                      recursionlevel : integer );

     private

     procedure buildx3d( builder : ticospherebuilder );

   end;

implementation

function calcKey( p1, p2 : integer ) : int64;
 var smallerIndex, greaterIndex : int64;
 begin
   smallerIndex := p1;
   greaterIndex := p2;
   if p2 > p1 then
    begin
      smallerIndex := p2;
      greaterIndex := p1;
    end;
   result := (smallerindex SHL 32 ) + greaterindex;
 end;

constructor ticospherebuilder.create( iRadius : single );
 var t : single;
 begin
   Radius := iRadius;
   Vertices := TVector3List.Create;
   Indexes  := TInt32List.Create;
   middlePointIndexCache := tpointcache.Create;
   { initialize the core dodecehedron }
   t := (1.0 + sqrt(5.0))/2.0;
   AddVertex( Vector3( -1, t, 0 ));
   AddVertex( Vector3( 1, t, 0 ));
   AddVertex( Vector3( -1, -t, 0 ));
   AddVertex( Vector3( 1, -t, 0 ));

   AddVertex( Vector3( 0, -1, t ));
   AddVertex( Vector3( 0, 1, t ));
   AddVertex( Vector3( 0, -1, -t ));
   AddVertex( Vector3( 0, 1, -t ));

   AddVertex( Vector3( t, 0, -1 ));
   AddVertex( Vector3( t, 0, 1 ));
   AddVertex( Vector3( -t, 0, -1 ));
   AddVertex( Vector3( -t, 0, 1 ));

   { 5 faces around point 0 }
   Indexes.Add( 0 ); Indexes.Add( 11 ); Indexes.Add( 5 );
   Indexes.Add( 0 ); Indexes.Add( 5 ); Indexes.Add( 1 );
   Indexes.Add( 0 ); Indexes.Add( 1 ); Indexes.Add( 7 );
   Indexes.Add( 0 ); Indexes.Add( 7 ); Indexes.Add( 10 );
   Indexes.Add( 0 ); Indexes.Add( 10 ); Indexes.Add( 11 );
   { 5 adjacent faces }
   Indexes.Add( 1 ); Indexes.Add( 5 ); Indexes.Add( 9 );
   Indexes.Add( 5 ); Indexes.Add( 11 ); Indexes.Add( 4 );
   Indexes.Add( 11 ); Indexes.Add( 10); Indexes.Add( 2 );
   Indexes.Add( 10 ); Indexes.Add( 7 ); Indexes.Add( 6 );
   Indexes.Add( 7 ); Indexes.Add( 1 ); Indexes.Add( 8 );
   { 5 faces around point 3 }
   Indexes.Add( 3 ); Indexes.Add( 9 ); Indexes.Add( 4 );
   Indexes.Add( 3 ); Indexes.Add( 4 ); Indexes.Add( 2 );
   Indexes.Add( 3 ); Indexes.Add( 2 ); Indexes.Add( 6 );
   Indexes.Add( 3 ); Indexes.Add( 6 ); Indexes.Add( 8 );
   Indexes.Add( 3 ); Indexes.Add( 8 ); Indexes.Add( 9 );
   { 5 adjacent faces }
   Indexes.Add( 4 ); Indexes.Add( 9 ); Indexes.Add( 5 );
   Indexes.Add( 2 ); Indexes.Add( 4 ); Indexes.Add( 11 );
   Indexes.Add( 6 ); Indexes.Add( 2 ); Indexes.Add( 10 );
   Indexes.Add( 8 ); Indexes.Add( 6 ); Indexes.Add( 7 );
   Indexes.Add( 9 ); Indexes.Add( 8 ); Indexes.Add( 1 );
 end;

destructor ticospherebuilder.destroy;
 begin
   inherited;
   Vertices.Free;
   Indexes.Free;
   middlePointIndexCache.Free;
 end;

function ticospherebuilder.addVertex( const p : TVector3 ) : integer;
 var factor : single;
 begin
   factor := radius / sqrt( p.X * p.X + p.Y * p.Y + p.Z * p.Z );
   result := Vertices.Count;
   Vertices.Add( p * factor );
 end;

function ticospherebuilder.getMiddlePoint( midptcache : tpointcache;
                                           p1, p2 : integer ) : integer;
 var key : int64;
     pointMiddle : TVector3;
 begin
   { look to cache by key }
   key := calcKey( p1, p2 );
   if not midPtcache.trygetvalue(key, result ) then
    begin { not in cache, calculate }
      pointMiddle := ( Vertices[p1] + Vertices[p2] ) / 2;
      result := AddVertex( pointMiddle );
      midptcache.Add(key,result);
    end;
 end;

procedure ticospherebuilder.refinetriangles( recursionlevel : integer );
 var i, j, ix : integer;
     a, b, c, v1, v2, v3 : integer;
     index2 : TInt32List;
 begin
   for i := 0 to recursionlevel - 1 do
    begin
      index2 := TInt32List.create;
      j := 0;
      while j < Indexes.count do
       begin
         { replace triangle with 4 triangles }
         ix := j;
         v1 := indexes[ix]; v2 := indexes[ix+ 1]; v3 := indexes[ix + 2];
         a := getMiddlePoint(middlePointIndexCache, v1, v2);
         b := getMiddlePoint(middlePointIndexCache, v2, v3);
         c := getMiddlePoint(middlePointIndexCache, v3, v1);

         index2.add(v1); index2.add(a); index2.add(c);
         index2.add(v2); index2.add(b); index2.add(a);
         index2.add(v3); index2.add(c); index2.add(b);
         index2.add(a); index2.add(b); index2.add(c);
         j := j + 3;
       end;
      Indexes.Free;
      Indexes := index2;
    end;
 end;

//-----------------

procedure ticosphere.build( iradius : single;
                            recursionlevel : integer );
 var builder : ticospherebuilder;
 begin
   builder := ticospherebuilder.create( iradius );
   builder.refinetriangles( recursionlevel );
   buildx3d( builder );
   builder.free;
 end;

procedure ticosphere.buildx3d( builder : ticospherebuilder );
 var Triangles : TIndexedTriangleSetNode;
     Root : TX3DRootNode;
     Material: TPhysicalMaterialNode;
     Appearance: TAppearanceNode;
     Shape : TShapeNode;
     CoordinateNode : TCoordinateNode;
 begin
   Triangles := TIndexedTriangleSetNode.Create;
   Triangles.SetIndex( builder.Indexes );
   CoordinateNode := TCoordinateNode.Create;
   CoordinateNode.SetPoint( builder.Vertices );
   Triangles.Coord := CoordinateNode;

   Shape := TShapeNode.Create;
   Shape.Geometry := Triangles;

   Material := TPhysicalMaterialNode.Create;
   Material.BaseColor := vector3(0,0.5,0);

   Appearance := TAppearanceNode.Create;
   Appearance.Material := Material;
   Shape.Appearance := Appearance;

   Root := TX3DRootNode.Create;
   Root.AddChildren( Shape );

   Load(Root, true );
   renderoptions.WireframeEffect := wesolidwireframe;
   renderoptions.WireframeColor := Vector3(1,1,1);
 end;

end.
1 Like

  icosphere := ticosphere.create( self );
  icosphere.build(0.1,0);
  icosphere.translation := vector3(0,8,0);
  Mainviewport.Items.Add( icosphere );
  icosphere := ticosphere.create( self );
  icosphere.build(0.2,1);
  icosphere.translation := vector3(2,8,0);
  Mainviewport.Items.Add( icosphere );
  icosphere := ticosphere.create( self );
  icosphere.build(0.3,2);
  icosphere.translation := vector3(4,8,0);
  Mainviewport.Items.Add( icosphere );
  icosphere := ticosphere.create( self );
  icosphere.build(0.4,3);
  icosphere.translation := vector3(6,8,0);
  Mainviewport.Items.Add( icosphere );

This does depth = 0, 1, 2, 3 beyond that might start to get huge.

This is cool and very useful.

The API you have is also clear:

s := ticosphere.create( owner );
s.build( radius, depth );
view.add( s );

I went ahead and added this to the " Additional Components" page of CGE, Additional Components | Castle Game Engine , with just a link to this forum thread.

Notes:

  • Can you specify the license of your source code? Of course this is entirely up to you, but if you don’t know or don’t care, I would suggest “modified BSD 3-clause” ( castle-engine/doc/licenses/COPYING.BSD-3-clause.txt at master · castle-engine/castle-engine · GitHub ) which is also used by Castle Game Engine example code and essentially allows anyone to reuse the code as they please, but you keep the copyright.

  • If you want to go a step further, creating a GitHub repository, with src and some example, would also be cool. I recognized this may feel like an overkill for just 1 unit :slight_smile: But it is really useful, I believe others may find it useful – there’s a reason why Blender also has both “UV Sphere” and “Icosphere”, they are both useful.

Thank you for sharing!

1 Like

Anyone can use it and change it to fit their needs. I don’t need any copyright… I implemented it from someone else’s code to begin with. It still needed work, like being able to set the color. Mine has already evolved for my needs that are less general.

I think it could be the great basis for a global round world terrain system since you can control LOD seamlessly, refining nearby areas much deeper than far away ones. And you can seed the builder with fewer triangles to limit area or region built. I put an earth sized depth=5(!) icosphere under my terrain and it provided realistic curved horizons for the distance (depth=4 you could see corners). It is interesting to see how much curvature there would be for even my 2.5km wide world.

I will probably rework this to not be a TIcoSphere that is a TCastleScene, but just the builder that takes a castle scene and can add icospheres to it. This will allow me to eventually have each tree be one scene instead of separate scenes for every branch and leaf cloud. I think that is more general, but having the defined TIcoSphere is handier, like you have TCastleSphere so you don’t have to muck about in x3d.