/
TextureReplacement.cs
1214 lines (1055 loc) · 52 KB
/
TextureReplacement.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Project: Daggerfall Tools For Unity
// Copyright: Copyright (C) 2009-2021 Daggerfall Workshop
// Web Site: http://www.dfworkshop.net
// License: MIT License (http://www.opensource.org/licenses/mit-license.php)
// Source Code: https://github.com/Interkarma/daggerfall-unity
// Original Author: TheLacus
// Contributors:
//
// Notes:
//
/*
* TODO:
* - Support for Sky (SKYxx.DAT)
*/
//#define DEBUG_TEXTURE_FORMAT
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.Rendering;
using DaggerfallConnect;
using DaggerfallConnect.Arena2;
using DaggerfallWorkshop.Game.Items;
using DaggerfallWorkshop.Game.UserInterface;
using DaggerfallWorkshop.Game.Utility.ModSupport;
namespace DaggerfallWorkshop.Utility.AssetInjection
{
#region Enums and Structs
/// <summary>
/// Supported textures maps.
/// </summary>
public enum TextureMap
{
Albedo,
Normal,
Height,
Emission,
MetallicGloss,
Mask
}
public enum TextureImport
{
None,
LooseFiles,
AllLocations
}
/// <summary>
/// Textures for all frames of a billboard texture record.
/// </summary>
public struct BillboardImportedTextures
{
public bool HasImportedTextures; // Contains imported textures ?
public int FrameCount; // Number of frames
public bool IsEmissive; // Is billboard emissive ?
public List<Texture2D> Albedo; // Textures for all frames.
public List<Texture2D> Emission; // EmissionMaps for all frames.
}
/// <summary>
/// Imported textures for an archive used by a wandering npc or a foe.
/// </summary>
public struct MobileBillboardImportedTextures
{
public bool HasImportedTextures; // Contains imported textures ?
public bool IsEmissive; // Is billboard emissive ?
public Texture2D[][] Albedo; // Textures for all records and frames.
public Texture2D[][] EmissionMaps; // Emission maps for all records and frames.
}
#endregion
/// <summary>
/// Handles import and injection of custom textures and images with the purpose of providing modding support.
/// Import materials from mods and textures from mods and loose files.
/// </summary>
public static class TextureReplacement
{
#region Fields
const string extension = ".png";
// Paths
static readonly string texturesPath = Path.Combine(Application.streamingAssetsPath, "Textures");
static readonly string imgPath = Path.Combine(texturesPath, "Img");
static readonly string cifRciPath = Path.Combine(texturesPath, "CifRci");
static readonly Type customBlendModeType = typeof(MaterialReader.CustomBlendMode);
#endregion
#region Properties
static TextureFormat TextureFormat
{
get
{
return DaggerfallUnity.Instance.MaterialReader.CompressModdedTextures ?
TextureFormat.DXT5 :
TextureFormat.ARGB32;
}
}
static FilterMode MainFilterMode
{
get { return DaggerfallUnity.Instance.MaterialReader.MainFilterMode; }
}
/// <summary>
/// Path to custom textures on disk.
/// </summary>
static public string TexturesPath
{
get { return texturesPath; }
}
/// <summary>
/// Path to custom images on disk.
/// </summary>
static public string ImagesPath
{
get { return imgPath; }
}
/// <summary>
/// Path to custom Cif and Rci files on disk.
/// </summary>
static public string CifRciPath
{
get { return cifRciPath; }
}
#endregion
#region Textures Import
/// <summary>
/// Seek material from mods.
/// </summary>
/// <param name="archive">Texture archive.</param>
/// <param name="record">Record index.</param>
/// <param name="frame">Animation frame index.</param>
/// <param name="material">Imported material.</param>
/// <returns>True if material imported.</returns>
public static bool TryImportMaterial(int archive, int record, int frame, out Material material)
{
return TryImportMaterial(GetName(archive, record, frame), out material);
}
/// <summary>
/// Seek animated texture from modding locations with all frames.
/// </summary>
/// <param name="archive">Texture archive.</param>
/// <param name="record">Record index.</param>
/// <param name="texFrames">Imported texture frames.</param>
/// <returns>True if texture imported.</returns>
public static bool TryImportTexture(int archive, int record, out Texture2D[] texFrames)
{
return TryImportTexture(texturesPath, frame => GetName(archive, record, frame), out texFrames);
}
/// <summary>
/// Seek texture from modding locations.
/// </summary>
/// <param name="archive">Texture archive.</param>
/// <param name="record">Record index.</param>
/// <param name="frame">Animation frame index.</param>
/// <param name="tex">Imported texture.</param>
/// <returns>True if texture imported.</returns>
public static bool TryImportTexture(int archive, int record, int frame, out Texture2D tex)
{
return TryImportTexture(texturesPath, GetName(archive, record, frame), false, out tex);
}
/// <summary>
/// Seek texture from modding locations.
/// </summary>
/// <param name="archive">Texture archive.</param>
/// <param name="record">Record index.</param>
/// <param name="frame">Animation frame index.</param>
/// <param name="textureMap">Texture type.</param>
/// <param name="readOnly">Release copy on system memory after uploading to gpu.</param>
/// <param name="tex">Imported texture.</param>
/// <returns>True if texture imported.</returns>
public static bool TryImportTexture(int archive, int record, int frame, TextureMap textureMap, bool readOnly, out Texture2D tex)
{
return TryImportTexture(texturesPath, GetName(archive, record, frame, textureMap), readOnly, out tex);
}
/// <summary>
/// Seek texture from modding locations.
/// </summary>
/// <param name="archive">Texture archive.</param>
/// <param name="record">Record index.</param>
/// <param name="frame">Animation frame index.</param>
/// <param name="textureMap">Texture type.</param>
/// <param name="textureImport">Texture import options.</param>
/// <param name="readOnly">Release copy on system memory after uploading to gpu.</param>
/// <param name="tex">Imported texture.</param>
/// <returns>True if texture imported.</returns>
public static bool TryImportTexture(int archive, int record, int frame, TextureMap textureMap, TextureImport textureImport, bool readOnly, out Texture2D tex)
{
tex = null;
return (textureImport == TextureImport.AllLocations && TryImportTexture(texturesPath, GetName(archive, record, frame, textureMap), readOnly, out tex))
|| (textureImport == TextureImport.LooseFiles && TryImportTextureFromLooseFiles(archive, record, frame, textureMap, readOnly, out tex));
}
/// <summary>
/// Seek texture from modding locations with a specific dye.
/// </summary>
/// <param name="archive">Texture archive.</param>
/// <param name="record">Record index.</param>
/// <param name="frame">Animation frame index</param>
/// <param name="dye">Dye colour for armour, weapons, and clothing.</param>
/// <param name="textureMap">Texture type.</param>
/// <param name="tex">Imported texture.</param>
/// <returns>True if texture imported.</returns>
public static bool TryImportTexture(int archive, int record, int frame, DyeColors dye, TextureMap textureMap, out Texture2D tex)
{
return TryImportTexture(texturesPath, GetName(archive, record, frame, textureMap, dye), false, out tex);
}
/// <summary>
/// Seek texture from modding locations.
/// </summary>
/// <param name="name">Texture name.</param>
/// <param name="readOnly">Release copy on system memory after uploading to gpu.</param>
/// <param name="tex">Imported texture.</param>
/// <returns>True if texture imported.</returns>
public static bool TryImportTexture(string name, bool readOnly, out Texture2D tex)
{
return TryImportTexture(texturesPath, name, readOnly, out tex);
}
/// <summary>
/// Seek image from modding locations.
/// </summary>
/// <param name="name">Image name.</param>
/// <param name="readOnly">Release copy on system memory after uploading to gpu.</param>
/// <param name="tex">Imported image as texture.</param>
/// <returns>True if image imported.</returns>
public static bool TryImportImage(string name, bool readOnly, out Texture2D tex)
{
return TryImportTexture(imgPath, name, readOnly, out tex);
}
/// <summary>
/// Seek CifRci from modding locations.
/// </summary>
/// <param name="name">Image name.</param>
/// <param name="record">Record index.</param>
/// <param name="frame">Animation frame index</param>
/// <param name="readOnly">Release copy on system memory after uploading to gpu.</param>
/// <param name="tex">Imported image as texture.</param>
/// <returns>True if CifRci imported.</returns>
public static bool TryImportCifRci(string name, int record, int frame, bool readOnly, out Texture2D tex)
{
return TryImportTexture(cifRciPath, GetNameCifRci(name, record, frame), readOnly, out tex);
}
/// <summary>
/// Seek CifRci with a specific metaltype from modding locations.
/// </summary>
/// <param name="name">Image name.</param>
/// <param name="record">Record index.</param>
/// <param name="frame">Animation frame index</param>
/// <param name="metalType">Metal type.</param>
/// <param name="readOnly">Release copy on system memory after uploading to gpu.</param>
/// <param name="tex">Imported image as texture.</param>
/// <returns>True if CifRci imported.</returns>
public static bool TryImportCifRci(string name, int record, int frame, MetalTypes metalType, bool readOnly, out Texture2D tex)
{
return TryImportTexture(cifRciPath, GetNameCifRci(name, record, frame, metalType), readOnly, out tex);
}
/// <summary>
/// Seeks a texture array asset or an archive of individual textures to merge with <see cref="Graphics.CopyTexture(Texture, Texture)"/>.
/// NOTE: It is possible to make a texture array on the cpu with <see cref="Texture2DArray.SetPixels32(Color32[], int)"/> but current
/// implementation doesn't use this feature. It is up to the caller to potentially do it as a fallback if this method return false.
/// </summary>
/// <param name="archive">The requested texture archive.</param>
/// <param name="depth">The expected number of layer.</param>
/// <param name="textureMap">The texture type.</param>
/// <param name="fallbackColor">If provided is used silenty for missing layers; texture format must be RGBA32 or ARGB32.</param>
/// <param name="textureArray">Imported or created texture array or null.</param>
/// <returns>True if the texture array has been imported or created.</returns>
internal static bool TryImportTextureArray(int archive, int depth, TextureMap textureMap, Color32? fallbackColor, out Texture2DArray textureArray)
{
if (!DaggerfallUnity.Settings.AssetInjection)
{
textureArray = null;
return false;
}
if (ModManager.Instance && !TextureExistsAmongLooseFiles(archive, 0, 0, textureMap))
{
string[] names = { GetNameTexArray(archive, textureMap), GetName(archive, 0, 0, textureMap) };
// Seek texture array or individual textures with load order.
// If the first match is a texture array, is returned successfully.
// If the first match is the first texture in the archive, an array is created at runtime.
Texture texture;
if (ModManager.Instance.TryGetAsset(names, null, out texture) && texture.dimension == TextureDimension.Tex2DArray)
{
if ((textureArray = texture as Texture2DArray).depth == depth)
return true;
Debug.LogErrorFormat("{0}: expected depth {0} but got {1}.", textureArray.name, depth, textureArray.depth);
}
}
// Seek individual textures from mods and loose files
return TryMakeTextureArrayCopyTexture(archive, depth, textureMap, fallbackColor, out textureArray);
}
/// <summary>
/// Seek texture from loose files.
/// </summary>
/// <param name="archive">Texture archive.</param>
/// <param name="record">Record index.</param>
/// <param name="frame">Animation index.</param>
/// <param name="textureMap">Texture type.</param>
/// <param name="readOnly">Release copy on system memory after uploading to gpu.</param>
/// <param name="tex">Imported texture.</param>
/// <returns>True if texture imported.</returns>
public static bool TryImportTextureFromLooseFiles(int archive, int record, int frame, TextureMap textureMap, bool readOnly, out Texture2D tex)
{
if (DaggerfallUnity.Settings.AssetInjection)
{
string path = Path.Combine(texturesPath, GetName(archive, record, frame, textureMap));
return TryImportTextureFromDisk(path, true, textureMap == TextureMap.Normal, readOnly, out tex);
}
tex = null;
return false;
}
/// <summary>
/// Seek texture from loose files using a relative path from <see cref="TexturesPath"/>.
/// </summary>
/// <param name="relPath">Relative path to file from <see cref="TexturesPath"/>.</param>
/// <param name="mipMaps">Enable mipmaps?</param>
/// <param name="encodeAsNormalMap">Convert from RGB to DTXnm.</param>
/// <param name="tex">Imported texture.</param>
/// <returns>True if texture exists and has been imported.</returns>
[Obsolete("Use overload that accepts readOnly flag.")]
public static bool TryImportTextureFromLooseFiles(string relPath, bool mipMaps, bool encodeAsNormalMap, out Texture2D tex)
{
return TryImportTextureFromLooseFiles(Path.Combine(texturesPath, relPath), mipMaps, encodeAsNormalMap, false, out tex);
}
/// <summary>
/// Seeks a texture from loose files using a full path or a relative path from <see cref="TexturesPath"/>.
/// </summary>
/// <param name="path">Path to texture file, full or relative to <see cref="TexturesPath"/>.</param>
/// <param name="mipMaps">Enable mipmaps?</param>
/// <param name="encodeAsNormalMap">Convert from RGB to DTXnm.</param>
/// <param name="readOnly">Release copy on system memory after uploading to gpu.</param>
/// <param name="tex">Imported texture.</param>
/// <returns>True if texture exists and has been imported.</returns>
public static bool TryImportTextureFromLooseFiles(string path, bool mipMaps, bool encodeAsNormalMap, bool readOnly, out Texture2D tex)
{
return TryImportTextureFromDisk(Path.IsPathRooted(path) ? path : Path.Combine(texturesPath, path), mipMaps, encodeAsNormalMap, readOnly, out tex);
}
/// <summary>
/// Seek texture from disk using a full path.
/// </summary>
/// <param name="directory">Full path to texture file.</param>
/// <param name="fileName">Name of texture file.</param>
/// <param name="mipMaps">Enable mipmaps?</param>
/// <param name="encodeAsNormalMap">Convert from RGB to DTXnm.</param>
/// <param name="tex">Imported texture.</param>
/// <param name="readOnly">Release copy on system memory after uploading to gpu.</param>
/// <returns>True if texture exists and has been imported.</returns>
[Obsolete("Use TryImportTextureFromLooseFiles()")]
public static bool TryImportTextureFromDisk(string path, bool mipMaps, bool encodeAsNormalMap, out Texture2D tex, bool readOnly = true)
{
return TryImportTextureFromLooseFiles(path, mipMaps, encodeAsNormalMap, readOnly, out tex);
}
#endregion
#region Textures Injection
/// <summary>
/// Import additional custom components of material.
/// </summary>
/// <param name="archive">Archive index</param>
/// <param name="record">Record index</param>
/// <param name="frame">Texture frame</param>
/// <param name="material">Material.</param>
public static void CustomizeMaterial(int archive, int record, int frame, Material material)
{
// MetallicGloss map
Texture2D metallicGloss;
if (TryImportTextureFromLooseFiles(archive, record, frame, TextureMap.MetallicGloss, true, out metallicGloss))
{
metallicGloss.filterMode = MainFilterMode;
material.EnableKeyword(KeyWords.MetallicGlossMap);
material.SetTexture(Uniforms.MetallicGlossMap, metallicGloss);
}
// Height Map
Texture2D height;
if (TryImportTextureFromLooseFiles(archive, record, frame, TextureMap.Height, true, out height))
{
height.filterMode = MainFilterMode;
material.EnableKeyword(KeyWords.HeightMap);
material.SetTexture(Uniforms.HeightMap, height);
}
// Properties
string path = Path.Combine(texturesPath, GetName(archive, record, frame));
if (XMLManager.XmlFileExists(path))
{
var xml = new XMLManager(path);
// Metallic parameter
float metallic;
if (xml.TryGetFloat("metallic", out metallic))
material.SetFloat(Uniforms.Metallic, metallic);
// Smoothness parameter
float smoothness;
if (xml.TryGetFloat("smoothness", out smoothness))
material.SetFloat(Uniforms.Glossiness, smoothness);
}
}
/// <summary>
/// Gets a custom material for a static billboard with textures and configuration imported from mods.
/// </summary>
/// <param name="go">The billboard object.</param>
/// <param name="archive">Archive index.</param>
/// <param name="record">Record index.</param>
/// <param name="summary">Summary data of the billboard object.</param>
/// <param name="scale">Custom local scale for the billboard.</param>
/// <remarks>
/// Seek the texture for the first frame of the given record. If found, it imports all other frames.
/// Always creates an emission map for textures marked as emissive by TextureReader, import emission maps for others only if available.
/// </remarks>
/// <returns>A material or null.</returns>
public static Material GetStaticBillboardMaterial(GameObject go, int archive, int record, ref DaggerfallBillboard.BillboardSummary summary, out Vector2 scale)
{
scale = Vector2.one;
if (!DaggerfallUnity.Settings.AssetInjection)
return null;
//MeshRenderer meshRenderer = go.GetComponent<MeshRenderer>();
int frame = 0;
Texture2D albedo, emission;
if (summary.ImportedTextures.HasImportedTextures = LoadFromCacheOrImport(archive, record, frame, true, true, out albedo, out emission))
{
bool isEmissive = emission || DaggerfallUnity.Instance.MaterialReader.TextureReader.IsEmissive(archive, record);
// Read xml configuration
Vector2 uv = Vector2.zero;
string renderMode = null;
XMLManager xml;
if (XMLManager.TryReadXml(texturesPath, GetName(archive, record), out xml))
{
xml.TryGetString("renderMode", out renderMode);
isEmissive |= xml.GetBool("emission");
// Set billboard scale
Transform transform = go.GetComponent<Transform>();
scale = transform.localScale = xml.GetVector3("scaleX", "scaleY", transform.localScale);
// Get UV
uv = xml.GetVector2("uvX", "uvY", uv);
}
// Make material
Material material = MakeBillboardMaterial(renderMode);
summary.Rect = new Rect(uv.x, uv.y, 1 - 2 * uv.x, 1 - 2 * uv.y);
// Set textures on material; emission is always overriden, with actual texture or null.
material.SetTexture(Uniforms.MainTex, albedo);
material.SetTexture(Uniforms.EmissionMap, isEmissive ? emission ?? albedo : null);
ToggleEmission(material, isEmissive);
// Import animation frames
var albedoTextures = new List<Texture2D>();
var emissionTextures = isEmissive ? new List<Texture2D>() : null;
do
{
albedoTextures.Add(albedo);
if (isEmissive)
emissionTextures.Add(emission ?? albedo);
}
while (LoadFromCacheOrImport(archive, record, ++frame, isEmissive, true, out albedo, out emission));
// Save results
summary.ImportedTextures.FrameCount = frame;
summary.ImportedTextures.IsEmissive = isEmissive;
summary.ImportedTextures.Albedo = albedoTextures;
summary.ImportedTextures.Emission = emissionTextures;
return material;
}
return null;
}
/// <summary>
/// Gets a custom material for a mobile billboard with textures and configuration imported from mods.
/// </summary>
/// <param name="archive">Archive index.</param>
/// <param name="meshFilter">The MeshFilter of the billboard object.</param>
/// <param name="importedTextures">All the imported textures for the archive.</param>
/// <remarks>
/// Seek the texture for the first frame of the first record. If found, it imports the entire archive.
/// If this texture has an emission map the material is considered emissive and all emission maps are imported.
/// </remarks>
/// <returns>A material or null.</returns>
public static Material GetMobileBillboardMaterial(int archive, MeshFilter meshFilter, ref MobileBillboardImportedTextures importedTextures)
{
if (!DaggerfallUnity.Settings.AssetInjection)
return null;
Texture2D tex, emission;
if (importedTextures.HasImportedTextures = LoadFromCacheOrImport(archive, 0, 0, true, true, out tex, out emission))
{
string renderMode = null;
// Read xml configuration
XMLManager xml;
if (XMLManager.TryReadXml(ImagesPath, string.Format("{0:000}", archive), out xml))
{
xml.TryGetString("renderMode", out renderMode);
importedTextures.IsEmissive = xml.GetBool("emission");
}
// Make material
Material material = MakeBillboardMaterial(renderMode);
// Enable emission
ToggleEmission(material, importedTextures.IsEmissive |= emission != null);
// Load texture file to get record and frame count
string fileName = TextureFile.IndexToFileName(archive);
var textureFile = new TextureFile(Path.Combine(DaggerfallUnity.Instance.Arena2Path, fileName), FileUsage.UseMemory, true);
// Import all textures in this archive
importedTextures.Albedo = new Texture2D[textureFile.RecordCount][];
importedTextures.EmissionMaps = importedTextures.IsEmissive ? new Texture2D[textureFile.RecordCount][] : null;
for (int record = 0; record < textureFile.RecordCount; record++)
{
int frames = textureFile.GetFrameCount(record);
var frameTextures = new Texture2D[frames];
var frameEmissionMaps = importedTextures.IsEmissive ? new Texture2D[frames] : null;
for (int frame = 0; frame < frames; frame++)
{
if (record != 0 || frame != 0)
LoadFromCacheOrImport(archive, record, frame, importedTextures.IsEmissive, true, out tex, out emission);
frameTextures[frame] = tex ?? ImageReader.GetTexture(fileName, record, frame, true);
if (frameEmissionMaps != null)
frameEmissionMaps[frame] = emission ?? frameTextures[frame];
}
importedTextures.Albedo[record] = frameTextures;
if (importedTextures.EmissionMaps != null)
importedTextures.EmissionMaps[record] = frameEmissionMaps;
}
// Update UV map
SetUv(meshFilter);
return material;
}
return null;
}
/// <summary>
/// Read scale from xml and apply to given vector.
/// </summary>
public static void SetBillboardScale(int archive, int record, ref Vector2 size)
{
if (!DaggerfallUnity.Settings.AssetInjection)
return;
XMLManager xml;
if (XMLManager.TryReadXml(texturesPath, GetName(archive, record), out xml))
{
Vector2 scale = xml.GetVector2("scaleX", "scaleY", Vector2.one);
size.x *= scale.x;
size.y *= scale.y;
}
}
/// <summary>
/// Import custom texture and label settings for buttons.
/// This feature has been deprecated in favor of <see cref="DaggerfallWorkshop.Game.UserInterfaceWindows.UIWindowFactory"/>.
/// </summary>
/// <param name="button">Button</param>
/// <param name="colorName">Name of texture</param>
[Obsolete("This feature has been deprecated in favor of UIWindowFactory.")]
public static bool TryCustomizeButton(ref Button button, string colorName)
{
Texture2D tex;
if (!TryImportTexture(colorName, true, out tex))
return false;
// Load texture
button.BackgroundTexture = tex;
button.BackgroundTexture.filterMode = (FilterMode)DaggerfallUnity.Settings.GUIFilterMode;
// Load settings from Xml
XMLManager xml;
if (XMLManager.TryReadXml(texturesPath, colorName, out xml))
{
string value;
if (xml.TryGetString("customtext", out value))
{
if (value == "true") // Set custom color for text
button.Label.TextColor = xml.GetColor(button.Label.TextColor);
else if (value == "notext") // Disable text. This is useful if text is drawn on texture
button.Label.Text = string.Empty;
}
}
LogLegacyUICustomizationMessage(colorName);
return true;
}
#endregion
#region Public Helpers
/// <summary>
/// Get name for a texture.
/// </summary>
/// <param name="archive">Archive index from TEXTURE.XXX</param>
/// <param name="record">Record index.</param>
/// <param name="frame">Frame index. It's different than zero only for animations.</param>
/// <param name="textureMap">Texture type.</param>
/// <param name="dye">Color Dye.</param>
/// <returns>The name for the texture with requested options.</returns>
public static string GetName(int archive, int record, int frame = 0, TextureMap textureMap = TextureMap.Albedo, DyeColors dye = DyeColors.Unchanged)
{
string name = string.Format("{0:000}_{1}-{2}", archive, record, frame);
if (dye != DyeColors.Unchanged)
name = string.Format("{0}_{1}", name, dye);
if (textureMap != TextureMap.Albedo)
name = string.Format("{0}_{1}", name, textureMap);
return name;
}
/// <summary>
/// Gets name for a texture array.
/// </summary>
/// <param name="archive">Archive index from TEXTURE.XXX</param>
/// <param name="textureMap">Texture type.</param>
/// <returns>The name for the texture array with requested options.</returns>
public static string GetNameTexArray(int archive, TextureMap textureMap = TextureMap.Albedo)
{
string name = string.Format("{0:000}-TexArray", archive);
if (textureMap != TextureMap.Albedo)
name = string.Format("{0}_{1}", name, textureMap);
return name;
}
/// <summary>
/// Get archive and record from "archive_record-0" string.
/// </summary>
/// <param name="name">"archive_record-frame string."</param>
/// <param name="archive">Archive index.</param>
/// <param name="record">Record index.</param>
/// <returns>True if texture is a Daggerfall texture.</returns>
static public bool IsDaggerfallTexture(string name, out int archive, out int record)
{
if ((name[3] == '_') && name.EndsWith("-0", StringComparison.CurrentCulture))
{
if (Int32.TryParse(name.Substring(0, 3), out archive))
{
if ((name[5] == '-' && Int32.TryParse(name.Substring(4, 1), out record)) ||
(name[6] == '-' && Int32.TryParse(name.Substring(4, 2), out record)))
return true;
}
}
archive = record = -1;
return false;
}
/// <summary>
/// Get name for a CifRci image.
/// </summary>
/// <param name="filename">Name of CIF/RCI file.</param>
/// <param name="record">Record index.</param>
/// <param name="frame">Frame index. It's different than zero only for animations.</param>
static public string GetNameCifRci (string filename, int record, int frame = 0)
{
return string.Format("{0}_{1}-{2}", filename, record, frame);
}
/// <summary>
/// Get name for a CifRci image with a metal type.
/// </summary>
/// <param name="filename">Name of CIF/RCI file.</param>
/// <param name="record">Record index.</param>
/// <param name="frame">Frame index. It's different than zero only for animations.</param>
/// <param name="metalType">Metal type of weapon.</param>
static public string GetNameCifRci(string filename, int record, int frame, MetalTypes metalType)
{
if (metalType == MetalTypes.None)
return GetNameCifRci(filename, record, frame);
return string.Format("{0}_{1}", GetNameCifRci(filename, record, frame), metalType);
}
/// <summary>
/// Makes texture results for given material.
/// </summary>
/// <param name="material">Unity material.</param>
/// <param name="archive">Texture archive.</param>
/// <param name="record">Record index.</param>
/// <returns>Results for the given material.</returns>
public static GetTextureResults MakeResults(Material material, int archive, int record)
{
string path = Path.Combine(DaggerfallUnity.Instance.MaterialReader.TextureReader.Arena2Path, TextureFile.IndexToFileName(archive));
TextureFile textureFile = new TextureFile(path, FileUsage.UseMemory, true);
return new GetTextureResults()
{
albedoMap = GetTextureOrDefault(material, Uniforms.MainTex),
normalMap = GetTextureOrDefault(material, Uniforms.BumpMap),
emissionMap = GetTextureOrDefault(material, Uniforms.EmissionMap),
singleRect = new Rect(0, 0, 1, 1),
isWindow = ClimateSwaps.IsExteriorWindow(archive, record),
isEmissive = material.HasProperty(Uniforms.EmissionMap),
textureFile = textureFile
};
}
/// <summary>
/// Assign current filtermode to all standard shader textures of the given material.
/// </summary>
public static void AssignFiltermode(Material material)
{
FilterMode filterMode = MainFilterMode;
foreach (var property in Uniforms.Textures.Where(x => material.HasProperty(x)))
{
Texture tex = material.GetTexture(property);
if (tex) tex.filterMode = filterMode;
}
}
/// <summary>
/// Seek a texture on disk inside <see cref="TexturesPath"/> without importing it.
/// </summary>
/// <param name="archive">Texture archive.</param>
/// <param name="record">Record index.</param>
/// <param name="frame">Frame index.</param>
/// <param name="textureMap">Texture type.</param>
/// <returns>True if texture is found.</returns>
public static bool TextureExistsAmongLooseFiles(int archive, int record, int frame = 0, TextureMap textureMap = TextureMap.Albedo)
{
return DaggerfallUnity.Settings.AssetInjection
&& File.Exists(Path.Combine(texturesPath, GetName(archive, record, frame, textureMap) + extension));
}
/// <summary>
/// Get a safe size for a control based on resolution of img.
/// </summary>
public static Vector2 GetSize(Texture2D texture, string textureName, bool allowXml = false)
{
if (!DaggerfallUnity.Settings.AssetInjection)
return new Vector2(texture.width, texture.height);
if (allowXml)
{
// Get size from xml
XMLManager xml;
if (XMLManager.TryReadXml(imgPath, textureName, out xml))
{
Vector2 size;
if (xml.TryGetVector2("width", "height", out size))
return size;
}
}
// Get size from Daggerfall image
ImageData imageData = ImageReader.GetImageData(textureName, createTexture: false);
return new Vector2(imageData.width, imageData.height);
}
/// <summary>
/// Get a safe size for a control based on resolution of cif or rci.
/// </summary>
public static Vector2 GetSize(Texture2D texture, string textureName, int record, int frame = 0)
{
if (!DaggerfallUnity.Settings.AssetInjection)
return new Vector2(texture.width, texture.height);
// Get size from Daggerfall image
ImageData imageData = ImageReader.GetImageData(textureName, createTexture: false);
return new Vector2(imageData.width, imageData.height);
}
/// <summary>
/// Read size associated with a texture from xml.
/// </summary>
public static bool TryGetSize(string textureName, out Vector2 size)
{
if (DaggerfallUnity.Settings.AssetInjection)
{
string path = Path.Combine(texturesPath, textureName);
if (XMLManager.XmlFileExists(path))
{
var xml = new XMLManager(path);
if (xml.TryGetVector2("width", "height", out size))
return true;
}
}
size = new Vector2();
return false;
}
/// <summary>
/// Read configuration for a paperdoll item with custom rect.
/// </summary>
/// <param name="item">Target item or null.</param>
/// <param name="imageData">Source image data.</param>
/// <param name="rect">Rect for the item on paperdoll.</param>
internal static void OverridePaperdollItemRect(DaggerfallUnityItem item, ImageData imageData, float paperdollScale, ref Rect rect)
{
DyeColors dyeColor = item != null ? item.dyeColor : DyeColors.Unchanged;
string directory;
string name;
XMLManager xml;
if (MakeName(imageData, dyeColor, out directory, out name) && XMLManager.TryReadXml(directory, name, out xml))
rect = xml.GetRect("rect", rect, paperdollScale);
}
/// <summary>
/// Parses the ID from the name of a texture archive from classic Daggerfall.
/// </summary>
/// <param name="filename">A name with format <c>"TEXTURE.XXX"</c>.</param>
/// <returns>The number parsed from <c>"XXX"</c>.</returns>
/// <seealso cref="TextureFile.IndexToFileName(int)"/>
public static int FileNameToArchive(string filename)
{
return int.Parse(filename.Substring("TEXTURE.".Length));
}
#endregion
#region Internal Methods
internal static void LogLegacyUICustomizationMessage(string textureName)
{
Debug.LogWarningFormat("Imported texture {0} for legacy support of UI customization. This feature has been deprecated in favor of UIWindowFactory.", textureName);
}
#endregion
#region Private Methods
/// <summary>
/// Seek material from mods.
/// </summary>
/// <param name="name">Name of material.</param>
/// <param name="material">Imported material.</param>
/// <returns>True if material imported.</returns>
private static bool TryImportMaterial(string name, out Material material)
{
if (DaggerfallUnity.Settings.AssetInjection)
{
// Seek from mods
if (ModManager.Instance != null)
return ModManager.Instance.TryGetAsset(name, null, out material);
}
material = null;
return false;
}
/// <summary>
/// Seek texture from modding locations.
/// </summary>
/// <param name="path">Path on disk (loose files only).</param>
/// <param name="name">Name of texture.</param>
/// <param name="readOnly">Release copy on system memory after uploading to gpu.</param>
/// <param name="tex">Imported texture.</param>
/// <remarks>
/// The <paramref name="readOnly"/> flag is only respected by loose files. It is up to mod authors
/// to ensure that textures from asset bundles have `Read/Write Enabled` flag set when required.
/// </remarks>
/// <returns>True if texture imported.</returns>
private static bool TryImportTexture(string path, string name, bool readOnly, out Texture2D tex)
{
if (DaggerfallUnity.Settings.AssetInjection)
{
// Seek from loose files
if (TryImportTextureFromDisk(Path.Combine(path, name), false, false, readOnly, out tex))
return true;
// Seek from mods
if (ModManager.Instance && ModManager.Instance.TryGetAsset(name, null, out tex))
{
if (!readOnly && !tex.isReadable)
Debug.LogWarning($"Texture {name} is not readable.");
return true;
}
}
tex = null;
return false;
}
/// <summary>
/// Seek animated texture from modding locations with all frames.
/// </summary>
/// <param name="path">Path on disk (loose files only).</param>
/// <param name="getName">Get name of frame.</param>
/// <param name="texFrames">Imported texture frames.</param>
/// <returns>True if texture imported.</returns>
private static bool TryImportTexture(string path, Func<int, string> getName, out Texture2D[] texFrames)
{
int frame = 0;
Texture2D tex;
if (TryImportTexture(path, getName(frame), false, out tex))
{
var textures = new List<Texture2D>();
do textures.Add(tex);
while (TryImportTexture(path, getName(++frame), false, out tex));
texFrames = textures.ToArray();
return true;
}
texFrames = null;
return false;
}
/// <summary>
/// Import texture data from disk with a full path to file.
/// </summary>
/// <param name="directory">Full path to texture file.</param>
/// <param name="fileName">Name of texture file.</param>
/// <param name="mipMaps">Enable mipmaps?</param>
/// <param name="encodeAsNormalMap">Convert from RGB to DTXnm.</param>
/// <param name="readOnly">Release copy on system memory after uploading to gpu.</param>
/// <param name="tex">Imported texture.</param>
/// <returns>True if texture exists and has been imported.</returns>
private static bool TryImportTextureFromDisk(string path, bool mipMaps, bool encodeAsNormalMap, bool readOnly, out Texture2D tex)
{
if (!path.EndsWith(extension))
path += extension;
if (File.Exists(path))
{
// Load texture file
tex = new Texture2D(4, 4, TextureFormat, mipMaps);
if (!tex.LoadImage(File.ReadAllBytes(path), readOnly && !encodeAsNormalMap))
Debug.LogErrorFormat("Failed to import texture data at {0}", path);
if (encodeAsNormalMap)
{
// RGBA to DXTnm
Color32[] colours = tex.GetPixels32();
for (int i = 0; i < colours.Length; i++)
{
colours[i].a = colours[i].r;
colours[i].r = colours[i].b = colours[i].g;
}
tex.SetPixels32(colours);
tex.Apply(true, readOnly);
}
#if DEBUG_TEXTURE_FORMAT
Debug.LogFormat("{0}: format: {1}, mipmaps: {2}, mipmaps count: {3}", Path.GetFileName(path), tex.format, mipMaps, tex.mipmapCount);
#endif
tex.filterMode = (FilterMode)DaggerfallUnity.Settings.MainFilterMode;
return true;
}
tex = null;
return false;