1 /**
2  * Defines the static Assets class, a static class which manages all textures, meshes, materials, and animations.
3  */
4 module dash.components.assets;
5 import dash.core.properties, dash.components, dash.utility;
6 import dash.utility.data.serialization;
7 
8 import std..string, std.array, std.algorithm;
9 
10 import yaml;
11 import derelict.freeimage.freeimage, derelict.assimp3.assimp;
12 
13 /**
14  * Assets manages all assets that aren't code, GameObjects, or Prefabs.
15  */
16 abstract final class Assets
17 {
18 static:
19 private:
20     MaterialAsset[][Resource] materialResources;
21 
22 package:
23     MeshAsset[string] meshes;
24     TextureAsset[string] textures;
25     MaterialAsset[string] materials;
26 
27 public:
28     /// Basic quad, generally used for billboarding.
29     Mesh unitSquare;
30 
31     /**
32      * Get a reference to the asset with the given type and name.
33      */
34     AssetRefT get( AssetRefT )( string name ) if( is( AssetRefT AssetT : AssetRef!AssetT ) && !is( AssetRefT == AssetRef!AssetT ) )
35     {
36         static if( is( AssetRefT AssetT : AssetRef!AssetT ) )
37             return new AssetRefT( getAsset!AssetT( name ) );
38         else static assert( false );
39     }
40 
41     /**
42      * Get the asset with the given type and name.
43      */
44     AssetT getAsset( AssetT )( string name ) if( is( AssetT : Asset ) )
45     {
46         enum get( Type, string array ) = q{
47             static if( is( AssetT == $Type ) )
48             {
49                 if( auto result = name in $array )
50                 {
51                     result.isUsed = true;
52                     return *result;
53                 }
54                 else
55                 {
56                     errorf( "Unable to find %s in $array.", name );
57                     return null;
58                 }
59             }
60         }.replaceMap( [ "$array": array, "$Type": Type.stringof ] );
61 
62         mixin( get!( MeshAsset, q{meshes} ) );
63         mixin( get!( TextureAsset, q{textures} ) );
64         mixin( get!( MaterialAsset, q{materials} ) );
65     }
66 
67     /**
68      * Load all assets in the FilePath.ResourceHome folder.
69      */
70     void initialize()
71     {
72         DerelictFI.load();
73 
74         // Initial assimp start
75         DerelictASSIMP3.load();
76 
77         // Make sure fbxs are supported.
78         assert( aiIsExtensionSupported( ".fbx".toStringz ), "fbx format isn't supported by assimp instance!" );
79 
80         enum aiImportOptions = aiProcess_CalcTangentSpace | aiProcess_Triangulate | aiProcess_JoinIdenticalVertices | aiProcess_SortByPType;
81 
82         // Load the unitSquare
83         unitSquare = new Mesh( new MeshAsset( internalResource, aiImportFileFromMemory(
84                                         unitSquareMesh.toStringz(), unitSquareMesh.length,
85                                         aiImportOptions, "obj" ).mMeshes[0] ) );
86 
87         foreach( file; scanDirectory( Resources.Meshes ) )
88         {
89             // Load mesh
90             const aiScene* scene = aiImportFile( file.fullPath.toStringz, aiImportOptions );
91             assert( scene, "Failed to load scene file '" ~ file.fullPath ~ "' Error: " ~ aiGetErrorString().fromStringz() );
92 
93             // Add mesh
94             if( scene.mNumMeshes > 0 )
95             {
96                 if( file.baseFileName in meshes )
97                     warning( "Mesh ", file.baseFileName, " exsists more than once." );
98 
99                 auto newMesh = new MeshAsset( file, scene.mMeshes[ 0 ] );
100 
101                 if( scene.mNumAnimations > 0 )
102                     newMesh.animationData = new AnimationData( file, scene.mAnimations, scene.mNumAnimations, scene.mMeshes[ 0 ], scene.mRootNode );
103 
104                 meshes[ file.baseFileName ] = newMesh;
105             }
106             else
107             {
108                 warning( "Assimp did not contain mesh data, ensure you are loading a valid mesh." );
109             }
110 
111             // Release mesh
112             aiReleaseImport( scene );
113         }
114 
115         foreach( file; scanDirectory( Resources.Textures ) )
116         {
117             if( file.baseFileName in textures )
118                warningf( "Texture %s exists more than once.", file.baseFileName );
119 
120             textures[ file.baseFileName ] = new TextureAsset( file );
121         }
122 
123         foreach( res; scanDirectory( Resources.Materials ) )
124         {
125             auto newMat = deserializeMultiFile!MaterialAsset( res );
126 
127             foreach( mat; newMat )
128             {
129                 if( mat.name in materials )
130                     warningf( "Material %s exists more than once.", mat.name );
131 
132                 mat.resource = res;
133                 materials[ mat.name ] = mat;
134                 materialResources[ res ] ~= mat;
135             }
136         }
137 
138         meshes.rehash();
139         textures.rehash();
140         materials.rehash();
141         materialResources.rehash();
142     }
143 
144     /**
145      * Refresh the assets that have changed.
146      */
147     void refresh()
148     {
149         enum refresh( string aaName ) = q{
150             foreach_reverse( name; $aaName.keys )
151             {
152                 auto asset = $aaName[ name ];
153                 if( !asset.resource.exists )
154                 {
155                     asset.shutdown();
156                     $aaName.remove( name );
157                 }
158                 else if( asset.resource.needsRefresh )
159                 {
160                     tracef( "Refreshing %s.", name );
161                     asset.refresh();
162                 }
163             }
164         }.replace( "$aaName", aaName );
165 
166         mixin( refresh!q{meshes} );
167         mixin( refresh!q{textures} );
168 
169         // Iterate over each file, and it's materials
170         //TODO: Implement
171     }
172 
173     /**
174      * Unload and destroy all stored assets.
175      */
176     void shutdown()
177     {
178         enum shutdown( string aaName, string friendlyName ) = q{
179             foreach_reverse( name; $aaName.keys )
180             {
181                 if( !$aaName[ name ].isUsed )
182                     warningf( "$friendlyName %s not used during this run.", name );
183 
184                 $aaName[ name ].shutdown();
185                 $aaName.remove( name );
186             }
187         }.replaceMap( [ "$aaName": aaName, "$friendlyName": friendlyName ] );
188 
189         mixin( shutdown!( q{meshes}, "Mesh" ) );
190         mixin( shutdown!( q{textures}, "Texture" ) );
191         mixin( shutdown!( q{materials}, "Material" ) );
192     }
193 }
194 
195 abstract class Asset
196 {
197 private:
198     bool _isUsed;
199 
200 public:
201     /// Whether or not the material is actually used.
202     mixin( Property!( _isUsed, AccessModifier.Package ) );
203     /// The resource containing this asset.
204     @ignore
205     Resource resource;
206 
207     /**
208      * Creates asset with resource.
209      */
210     this( Resource res )
211     {
212         resource = res;
213     }
214 
215     void initialize() { }
216     void update() { }
217     void refresh() { }
218     void shutdown() { }
219 }
220 
221 /**
222  * A reference to an asset.
223  */
224 abstract class AssetRef( AssetType ) : Component if( is( AssetType : Asset ) )
225 {
226 public:
227     @ignore
228     AssetType asset;
229 
230     @rename( "Asset" )
231     string assetName;
232 
233     this() { }
234     this( AssetType ass )
235     {
236         asset = ass;
237         initialize();
238     }
239 
240     /// Is the asset null?
241     bool isNull() @property const pure @safe nothrow 
242     {
243         return asset is null;
244     }
245 
246     /// Gets a reference to it's asset.
247     override void initialize()
248     {
249         if( !asset && assetName )
250             asset = Assets.getAsset!AssetType( assetName );
251     }
252 }
253 
254 /// Obj for a 1x1 square billboard mesh
255 immutable string unitSquareMesh = q{
256 v -1.0 1.0 0.0
257 v -1.0 -1.0 0.0
258 v 1.0 1.0 0.0
259 v 1.0 -1.0 0.0
260 
261 vt 0.0 0.0
262 vt 0.0 1.0
263 vt 1.0 0.0
264 vt 1.0 1.0
265 
266 vn 0.0 0.0 1.0
267 
268 f 4/3/1 3/4/1 1/2/1
269 f 2/1/1 4/3/1 1/2/1
270 };