This file is indexed.

/usr/src/castle-game-engine-6.4/x3d/castlematerialproperties.pas is in castle-game-engine-src 6.4+dfsg1-2.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  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
{
  Copyright 2007-2017 Michalis Kamburelis.

  This file is part of "Castle Game Engine".

  "Castle Game Engine" is free software; see the file COPYING.txt,
  included in this distribution, for details about the copyright.

  "Castle Game Engine" is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

  ----------------------------------------------------------------------------
}

{ Material and texture properties from external files (TMaterialProperty,
  global MaterialProperties collection). }
unit CastleMaterialProperties;

{$I castleconf.inc}

interface

uses Classes, DOM, Generics.Collections,
  CastleUtils, CastleClassUtils, CastleSoundEngine, CastleStringUtils,
  CastleImages, CastleFindFiles;

type
  { Information for a particular material. }
  TMaterialProperty = class
  strict private
    FTextureBaseName: string;
    FFootstepsSound: TSoundType;
    FToxic: boolean;
    FToxicDamageConst, FToxicDamageRandom, FToxicDamageTime: Single;
    FNormalMap: string;
    FAlphaChannel: string;
  private
    procedure LoadFromDOMElement(Element: TDOMElement; const BaseURL: string);
  public
    { Texture basename to associate this property will all appearances
      using given texture. For now, this is the only way to associate
      property, but more are possible in the future (like MaterialNodeName). }
    property TextureBaseName: string read FTextureBaseName write FTextureBaseName;

    { Footsteps sound to make when player is walking on this material.
      stNone is no information is available. }
    property FootstepsSound: TSoundType read FFootstepsSound write FFootstepsSound;

    { Is the floor toxic when walking on it.
      Taken into account only if you assign @link(TCastleSceneManager.Player).
      @groupBegin }
    property Toxic: boolean read FToxic write FToxic;
    property ToxicDamageConst: Single read FToxicDamageConst write FToxicDamageConst;
    property ToxicDamageRandom: Single read FToxicDamageRandom write FToxicDamageRandom;
    property ToxicDamageTime: Single read FToxicDamageTime write FToxicDamageTime;
    { @groupEnd }

    { Normal map texture URL. This is a simple method to activate bump mapping,
      equivalent to using normalMap field in an Appearance node of VRML/X3D, see
      http://castle-engine.sourceforge.net/x3d_extensions.php#section_ext_bump_mapping .

      In case both VRML/X3D Appearance specifies normalMap and we have
      NormalMap defined here, the VRML/X3D Appearance is used. }
    property NormalMap: string read FNormalMap write FNormalMap;

    { Override alpha channel type for diffuse texture.
      The meaning and allowed values for this are the same as for
      alphaChannel field for texture nodes, see
      http://castle-engine.sourceforge.net/x3d_extensions.php#section_ext_alpha_channel_detection .
      Empty value (default) doesn't change the alpha channel type
      (set in VRML/X3D or auto-detected). }
    property AlphaChannel: string read FAlphaChannel write FAlphaChannel;
  end;

  { Store information that is naturally associated with a given material
    or texture in an external file. Documentation and example of such
    file is on  http://castle-engine.sourceforge.net/creating_data_material_properties.php .
    Right now this allows to define things like:

    @unorderedList(
      @itemSpacing compact
      @item footsteps,
      @item toxic ground (hurts player),
      @item bump mapping (normal maps and height maps for given texture),
      @item texture GPU-compressed and downscaled alternatives.
    )

    In the future, it should be possible to express all these properties
    in pure VRML/X3D (inside Appearance / Material / ImageTexture nodes).
    Right now, you can do this with bump mapping, see
    http://castle-engine.sourceforge.net/x3d_extensions.php#section_ext_bump_mapping ,
    but not footsteps or toxic ground.
    In the future it should also be possible to express these properties
    in 3D authoring software (like Blender), and easily export them
    to appropriate VRML/X3D nodes.
    For now, this TMaterialProperty allows us to easily customize materials
    in a way that is not possible in Blender.

    Using an external file for material properties has also long-term
    advantages: it can be shared across many 3D models, for example
    you can define footsteps sound for all grounds using the @code(grass.png)
    textures, in all levels, at once.

    You have to load an XML file by setting
    @link(TMaterialProperties.URL MaterialProperties.URL) property.
  }
  TMaterialProperties = class
  strict private
    type
      TAutoGeneratedTextures = class
      strict private
        const
          PathsIgnoreCase = true;
        var
        FAutoProcessImageURLs: boolean;
        IncludePaths: TCastleStringList; // absolute URLs
        IncludePathsRecursive: TBooleanList;
        ExcludePaths: TCastleStringList;
        { necessary for Exclude with relative dirs, like "entites/*", to work }
        FBaseURL: string;
        FCompressedFormats: TTextureCompressions;
        GatheringResult: TCastleStringList;
        FSmallestScale: Cardinal;
        procedure GatherCallback(const FileInfo: TFileInfo; var StopSearch: boolean);
        procedure LoadImageEvent(var URL: string);
        function IsAbsoluteURLMatchingRelativeMask(const URL, Mask: string): boolean;
      public
        constructor Create(const Element: TDOMElement; const BaseURL: string; const AnAutoProcessImageURLs: boolean);
        destructor Destroy; override;
        function TextureURLMatches(const URL: string): boolean;
        function AutoGeneratedTextures: TCastleStringList;
        class function GeneratedTextureURL(const URL: string;
          const UseCompression: boolean; const TextureCompression: TTextureCompression;
          const Scaling: Cardinal): string;
        property CompressedFormats: TTextureCompressions read FCompressedFormats;
        property SmallestScale: Cardinal read FSmallestScale;
      end;

      TAutoGeneratedTexturesList = {$ifdef CASTLE_OBJFPC}specialize{$endif} TObjectList<TAutoGeneratedTextures>;
      TMaterialPropertyList = {$ifdef CASTLE_OBJFPC}specialize{$endif} TObjectList<TMaterialProperty>;
    var
    FAutoGeneratedTexturesList: TAutoGeneratedTexturesList;
    FMaterialPropertyList: TMaterialPropertyList;
    FURL: string;
    FAutoProcessImageURLs: boolean;
    procedure SetURL(const Value: string);
  public
    const
      AutoGeneratedDirName = 'auto_generated';

    constructor Create(const AnAutoProcessImageURLs: boolean);
    destructor Destroy; override;

    { Load material properties from given XML file.
      Set this to empty string to unload previously loaded properties.
      See Castle1 and fps_game data for examples how this looks like,
      in @code(material_properties.xml). }
    property URL: string read FURL write SetURL;

    property FileName: string read FURL write SetURL; deprecated 'use URL';

    { Find material properties for given texture basename.
      Returns @nil if no material properties are found
      (in particular, if @link(URL) was not set yet). }
    function FindTextureBaseName(const TextureBaseName: string): TMaterialProperty;

    { Get the URLs of all textures that should have automatically
      generated GPU-compressed and downscaled counterparts.
      Returns a list of absolute URLs.
      This actually searches on disk, right now, to find the texture list,
      applying the include/exclude rules specified in material_properties.xml file.

      This is to be used by "castle-engine auto-generate-textures"
      tool, or similar tools.

      Caller is responsible for freeing the returned TCastleStringList list. }
    function AutoGeneratedTextures: TCastleStringList;

    { For given texture (absolute) URL and compression and scaling,
      return the proper (absolute) URL of auto-compressed and auto-downscaled counterpart.
      Scaling is defined just like TextureLoadingScale. }
    class function AutoGeneratedTextureURL(const TextureURL: string;
      const UseCompression: boolean;
      const TextureCompression: TTextureCompression; const Scaling: Cardinal): string;

    { Automatic compression formats generated for this texture.
      @param(TextureURL An absolute texture URL. Usually should just be taken
        from AutoGeneratedTextures returned list.) }
    function AutoCompressedTextureFormats(const TextureURL: string): TTextureCompressions;

    { Automatically generated smallest scale, for this texture.
      Return value should be intepreted just like TextureLoadingScale.
      See http://castle-engine.sourceforge.net/creating_data_material_properties.php#section_texture_scale
      @param(TextureURL An absolute texture URL. Usually should just be taken
        from AutoGeneratedTextures returned list.) }
    function AutoScale(const TextureURL: string): Cardinal;
  end;

{ Material and texture properties, see @link(TMaterialProperties).
  Set the @link(TMaterialProperties.URL URL) property
  to load material properties from XML file. }
function MaterialProperties: TMaterialProperties;

var
  { Use the auto-generated alternative downscaled images.
    This allows to conserve both GPU memory and loading time
    by using a downscaled images versions.

    The subset of your images which are affected by this must be declared inside
    the material_properties.xml file, which is loaded to @link(MaterialProperties).
    And the image files must be prepared earlier by the build tool call
    @code("castle-engine auto-generate-textures").
    See the http://castle-engine.sourceforge.net/creating_data_material_properties.php#section_texture_scale .

    Each size (width, height, and (for 3D images) depth) is scaled
    by 1 / 2^TextureLoadingScale.
    So value = 1 means no scaling, value = 2 means that each size is 1/2
    (texture area is 1/4), value = 3 means that each size is 1/4 and so on.

    This mechanism will @italic(not)
    automatically downscale textures at runtime. If the downscaled texture version
    should exist, according to the material_properties.xml file,
    but it doesn't, then texture loading will simply fail.
    If you want to scale the texture at runtime, use the similar @link(GLTextureScale)
    instead.

    This mechanism is independent from GLTextureScale:

    @unorderedList(
      @item(Scaling indicated by GLTextureScale is performed at runtime,
        after loading. It happens @bold(after) the results of
        TextureLoadingScale have already been applied.)

      @item(The GLTextureScale works on a different subset of textures.

        For GLTextureScale, the usage of a texture determines if it's a GUI texture
        (which cannot be scaled) or not.
        So textures loaded through TGLImage, or declared as guiTexture in X3D,
        are not affected by GLTextureScale. All other textures are affected.
        It doesn't matter from where they are loaded -- so it affects also
        texture contents created by code, or downloaded from the Internet.

        In contrast, the TextureLoadingScale works (only) on all the images
        declared as having a downscaled version in material_properties.xml.
        It is not affected by how the texture will be used.)

      @item(The GLTextureScale works only on texture formats that can be scaled.
        In particular, it cannot scale textures compressed with a GPU compression
        (S3TC and such). It silently ignores them.

        In contrast, the TextureLoadingScale can cooperate with GPU-compressed textures,
        if you also compress them automatically using the material_properties.xml
        and the build tool call @code("castle-engine auto-generate-textures").
        The downscaled image versions are generated from original (uncompressed,
        unscaled) images, and are then compressed.)

      @item(The GLTextureScale scaling is usually of worse quality, since it's
        done at runtime.

        In contrast, the downscaled textures used by TextureLoadingScale
        are generated as a preprocessing step.
        The build tool @code("castle-engine auto-generate-textures") may use
        a slower but higher-quality scaling.)
    )
  }
  TextureLoadingScale: Cardinal = 1;

implementation

uses SysUtils, XMLRead, StrUtils, Math,
  CastleXMLUtils, CastleFilesUtils, X3DNodes,
  CastleURIUtils, CastleDownload, CastleLog;

{ TMaterialProperty --------------------------------------------------------- }

procedure TMaterialProperty.LoadFromDOMElement(Element: TDOMElement; const BaseURL: string);
var
  FootstepsSoundName: string;
  ToxicDamage: TDOMElement;
  I: TXMLElementIterator;
begin
  if not Element.AttributeString('texture_base_name', FTextureBaseName) then
    raise Exception.Create('<properties> element must have "texture_base_name" attribute');

  FootstepsSoundName := '';
  if Element.AttributeString('footsteps_sound', FootstepsSoundName) and
     (FootstepsSoundName <> '') then
    FFootstepsSound := SoundEngine.SoundFromName(FootstepsSoundName) else
    FFootstepsSound := stNone;

  if Element.AttributeString('normal_map', FNormalMap) and (FNormalMap <> '') then
    FNormalMap := CombineURI(BaseURL, FNormalMap) else
    FNormalMap := '';

  if not Element.AttributeString('alpha_channel', FAlphaChannel) then
    FAlphaChannel := '';

  I := Element.ChildrenIterator;
  try
    while I.GetNext do
      if I.Current.TagName = 'toxic' then
      begin
        FToxic := true;
        ToxicDamage := I.Current.ChildElement('damage');
        if not ToxicDamage.AttributeSingle('const', FToxicDamageConst) then
          FToxicDamageConst := 0;
        if not ToxicDamage.AttributeSingle('random', FToxicDamageRandom) then
          FToxicDamageRandom := 0;
        if not ToxicDamage.AttributeSingle('time', FToxicDamageTime) then
          FToxicDamageTime := 0;
      end else
        raise Exception.CreateFmt('Unknown element inside <property>: "%s"',
          [I.Current.TagName]);
  finally FreeAndNil(I) end;
end;

{ TMaterialProperties.TAutoGeneratedTextures -------------------------------- }

constructor TMaterialProperties.TAutoGeneratedTextures.Create(
  const Element: TDOMElement; const BaseURL: string;
  const AnAutoProcessImageURLs: boolean);
var
  ChildElements: TXMLElementIterator;
  ChildElement, CompressElement, ScaleElement: TDOMElement;
begin
  inherited Create;
  FAutoProcessImageURLs := AnAutoProcessImageURLs;
  IncludePaths := TCastleStringList.Create;
  IncludePathsRecursive := TBooleanList.Create;
  ExcludePaths := TCastleStringList.Create;
  FBaseURL := BaseURL;

  { read from XML }

  ChildElements := Element.ChildrenIterator('include');
  try
    while ChildElements.GetNext do
    begin
      ChildElement := ChildElements.Current;
      IncludePaths.Add(ChildElement.AttributeURL('path', BaseURL));
      IncludePathsRecursive.Add(ChildElement.AttributeBooleanDef('recursive', false));
    end;
  finally FreeAndNil(ChildElements) end;

  ChildElements := Element.ChildrenIterator('exclude');
  try
    while ChildElements.GetNext do
    begin
      ChildElement := ChildElements.Current;
      ExcludePaths.Add(ChildElement.AttributeString('path'));
    end;
  finally FreeAndNil(ChildElements) end;

  CompressElement := Element.ChildElement('compress', false);
  if CompressElement <> nil then
  begin
    ChildElements := CompressElement.ChildrenIterator('format');
    try
      while ChildElements.GetNext do
        Include(FCompressedFormats, StringToTextureCompression(
          ChildElements.Current.AttributeString('name')));
    finally FreeAndNil(ChildElements) end;
  end;

  FSmallestScale := 1;
  ScaleElement := Element.ChildElement('scale', false);
  if ScaleElement <> nil then
  begin
    FSmallestScale := ScaleElement.AttributeCardinalDef('smallest', 1);
    if FSmallestScale < 1 then
      raise Exception.CreateFmt('Invalid scale smallest value "%d"', [FSmallestScale]);
  end;

  if FAutoProcessImageURLs then
    AddLoadImageListener({$ifdef CASTLE_OBJFPC}@{$endif} LoadImageEvent);
end;

destructor TMaterialProperties.TAutoGeneratedTextures.Destroy;
begin
  FreeAndNil(IncludePaths);
  FreeAndNil(IncludePathsRecursive);
  FreeAndNil(ExcludePaths);
  if FAutoProcessImageURLs then
    RemoveLoadImageListener({$ifdef CASTLE_OBJFPC}@{$endif} LoadImageEvent);
  inherited;
end;

procedure TMaterialProperties.TAutoGeneratedTextures.GatherCallback(const FileInfo: TFileInfo; var StopSearch: boolean);
begin
  if (Pos('/' + AutoGeneratedDirName + '/', FileInfo.URL) = 0) and
     IsImageMimeType(URIMimeType(FileInfo.URL), false, false) then
    GatheringResult.Add(FileInfo.URL);
end;

function TMaterialProperties.TAutoGeneratedTextures.IsAbsoluteURLMatchingRelativeMask(
  const URL, Mask: string): boolean;
var
  U: string;
begin
  U := PrefixRemove(ExtractURIPath(FBaseURL), URL, PathsIgnoreCase);
  Result := IsWild(U, Mask, PathsIgnoreCase);
end;

function TMaterialProperties.TAutoGeneratedTextures.
  AutoGeneratedTextures: TCastleStringList;

  procedure Exclude(const ExcludePathMask: string; const URLs: TCastleStringList);
  var
    I: Integer;
  begin
    I := 0;
    while I < URLs.Count do
    begin
      // Writeln('Excluding ExcludePathMask ' + ExcludePathMask +
      //   ' from ' + PrefixRemove(ExtractURIPath(FBaseURL), URLs[I], PathsIgnoreCase));
      if IsAbsoluteURLMatchingRelativeMask(URLs[I], ExcludePathMask) then
        URLs.Delete(I) else
        Inc(I);
    end;
  end;

var
  I: Integer;
  FindOptions: TFindFilesOptions;
begin
  Result := TCastleStringList.Create;
  GatheringResult := Result;

  for I := 0 to IncludePaths.Count - 1 do
  begin
    if IncludePathsRecursive[I] then
      FindOptions := [ffRecursive] else
      { not recursive, so that e.g. <include path="my_texture.png" />
	or <include path="subdir/my_texture.png" />
	should not include *all* my_texture.png files inside. }
      FindOptions := [];
    FindFiles(IncludePaths[I], false, {$ifdef CASTLE_OBJFPC}@{$endif} GatherCallback, FindOptions);
  end;

  GatheringResult := nil;

  for I := 0 to ExcludePaths.Count - 1 do
    Exclude(ExcludePaths[I], Result);
end;

function TMaterialProperties.TAutoGeneratedTextures.TextureURLMatches(const URL: string): boolean;

  { Check is URL not excluded. }
  function CheckNotExcluded: boolean;
  var
    I: Integer;
  begin
    for I := 0 to ExcludePaths.Count - 1 do
      if IsAbsoluteURLMatchingRelativeMask(URL, ExcludePaths[I]) then
        Exit(false);
    Result := true;
  end;

var
  URLName, URLPath: string;
  I: Integer;
  IncludePath, IncludeMask: string;
  PathMatches: boolean;
begin
  Result := false;
  URLPath := ExtractURIPath(URL);
  URLName := ExtractURIName(URL);
  for I := 0 to IncludePaths.Count - 1 do
  begin
    IncludePath := ExtractURIPath(IncludePaths[I]);
    IncludeMask := ExtractURIName(IncludePaths[I]);
    if IncludePathsRecursive[I] then
      PathMatches := IsPrefix(IncludePath, URLPath, PathsIgnoreCase) else
      PathMatches := AnsiSameText(IncludePath, URLPath); { assume PathsIgnoreCase=true }
    if PathMatches and IsWild(URLName, IncludeMask, PathsIgnoreCase) then
    begin
      Result := CheckNotExcluded;
      Exit;
    end;
  end;
end;

procedure TMaterialProperties.TAutoGeneratedTextures.LoadImageEvent(
  var URL: string);

  { Texture has GPU-compressed and/or downscaled counterpart, according to include/exclude
    variables. So try to replace URL with something compressed and downscaled. }
  procedure ReplaceURL;
  var
    C: TTextureCompression;
    Scale: Cardinal;
  begin
    { Do not warn about it, just as we don't warn when TextureLoadingScale = 2
      but we're loading image not mentioned in <auto_generated_textures>.
    if TextureLoadingScale > SmallestScale then
      raise Exception.CreateFmt('Invalid TextureLoadingScale %d, we do not have such downscaled images. You should add or modify the <scale smallest=".." /> declaration in "material_properties.xml", and make sure thar you load the "material_properties.xml" early enough.',
        [TextureLoadingScale]); }
    Scale := Min(SmallestScale, TextureLoadingScale);

    if not SupportedTextureCompressionKnown then
      WritelnWarning('MaterialProperties', 'Cannot determine whether to use GPU compressed (and potentially downscaled) version for ' + URL + ' because the image is loaded before GPU capabilities are known')
    else
    begin
      {$ifdef IOS}
      // Do not spam log about this, there's no easy solution for this now
      //WritelnWarning('MaterialProperties', 'TODO: Not using GPU compressed (and potentially downscaled) version for ' + URL + ' because on iOS non-square compressed textures are not supported');
      {$else}
      for C in CompressedFormats do
        if C in SupportedTextureCompression then
        begin
          URL := GeneratedTextureURL(URL, true, C, Scale);
          WritelnLog('MaterialProperties', 'Using compressed (and potentially downscaled) alternative ' + URL);
          Exit;
        end;
      {$endif}
    end;

    { no GPU compression supported; still, maybe we should use a downscaled alternative }
    if Scale <> 1 then
    begin
      URL := GeneratedTextureURL(URL, false, Low(TTextureCompression), Scale);
      WritelnLog('MaterialProperties', 'Using downscaled alternative ' + URL);
    end;
  end;

begin
  if TextureURLMatches(URL) then
    ReplaceURL;
end;

class function TMaterialProperties.TAutoGeneratedTextures.GeneratedTextureURL(
  const URL: string;
  const UseCompression: boolean; const TextureCompression: TTextureCompression;
  const Scaling: Cardinal): string;
begin
  Result := ExtractURIPath(URL) + AutoGeneratedDirName + '/';
  if UseCompression then
    Result := Result + LowerCase(TextureCompressionToString(TextureCompression)) + '/' else
    Result := Result + 'uncompressed/';
  if Scaling <> 1 then
    Result := Result + 'downscaled_' + IntToStr(Scaling) + '/';
  Result := Result + ExtractURIName(URL);
  if UseCompression then
    Result := Result + '.dds' else
    Result := Result + '.png';
end;

{ TMaterialProperties ---------------------------------------------------------- }

constructor TMaterialProperties.Create(const AnAutoProcessImageURLs: boolean);
begin
  inherited Create;
  FAutoProcessImageURLs := AnAutoProcessImageURLs;
  FMaterialPropertyList := TMaterialPropertyList.Create({ owns objects } true);
  FAutoGeneratedTexturesList := TAutoGeneratedTexturesList.Create({ owns objects } true);
end;

destructor TMaterialProperties.Destroy;
begin
  FreeAndNil(FAutoGeneratedTexturesList);
  FreeAndNil(FMaterialPropertyList);
  inherited;
end;

procedure TMaterialProperties.SetURL(const Value: string);
var
  Config: TXMLDocument;
  Elements: TXMLElementIterator;
  MaterialProperty: TMaterialProperty;
  Stream: TStream;
begin
  FURL := Value;

  FMaterialPropertyList.Clear;
  FAutoGeneratedTexturesList.Clear;

  if URL = '' then Exit;

  Stream := Download(URL);
  try
    ReadXMLFile(Config, Stream, URL);
  finally FreeAndNil(Stream) end;

  try
    Check(Config.DocumentElement.TagName = 'properties',
      'Root node of material properties file must be <properties>');

    Elements := Config.DocumentElement.ChildrenIterator('property');
    try
      while Elements.GetNext do
      begin
        MaterialProperty := TMaterialProperty.Create;
        FMaterialPropertyList.Add(MaterialProperty);
        MaterialProperty.LoadFromDOMElement(Elements.Current, AbsoluteURI(URL));
      end;
    finally FreeAndNil(Elements); end;

    Elements := Config.DocumentElement.ChildrenIterator('auto_generated_textures');
    try
      while Elements.GetNext do
      begin
        FAutoGeneratedTexturesList.Add(
          TAutoGeneratedTextures.Create(Elements.Current, URL, FAutoProcessImageURLs));
      end;
    finally FreeAndNil(Elements); end;
  finally
    SysUtils.FreeAndNil(Config);
  end;
end;

function TMaterialProperties.FindTextureBaseName(const TextureBaseName: string): TMaterialProperty;
var
  I: Integer;
begin
  for I := 0 to FMaterialPropertyList.Count - 1 do
    if SameText(FMaterialPropertyList[I].TextureBaseName, TextureBaseName) then
      Exit(FMaterialPropertyList[I]);
  Result := nil;
end;

function TMaterialProperties.AutoGeneratedTextures: TCastleStringList;
var
  S: TCastleStringList;
  I, J: Integer;
begin
  Result := TCastleStringList.Create;
  try
    for I := 0 to FAutoGeneratedTexturesList.Count - 1 do
    begin
      S := FAutoGeneratedTexturesList[I].AutoGeneratedTextures;
      try
        for J := 0 to S.Count - 1 do
        begin
          if Result.IndexOf(S[J]) <> -1 then
            WritelnWarning('MaterialProperties', Format('The texture URL "%s" is under the influence of more than one <auto_generated_textures> rule. Use <include> and <exclude> to avoid it',
              [S[J]]))
          else
            Result.Add(S[J]);
        end;
      finally FreeAndNil(S) end;
    end;
  except FreeAndNil(Result); raise end;
end;

class function TMaterialProperties.AutoGeneratedTextureURL(const TextureURL: string;
  const UseCompression: boolean;
  const TextureCompression: TTextureCompression; const Scaling: Cardinal): string;
begin
  Result := TAutoGeneratedTextures.GeneratedTextureURL(TextureURL,
    UseCompression, TextureCompression, Scaling);
end;

function TMaterialProperties.AutoCompressedTextureFormats(const TextureURL: string): TTextureCompressions;
var
  I: Integer;
begin
  Result := [];
  for I := 0 to FAutoGeneratedTexturesList.Count - 1 do
    if FAutoGeneratedTexturesList[I].TextureURLMatches(TextureURL) then
      Exit(FAutoGeneratedTexturesList[I].CompressedFormats);
end;

function TMaterialProperties.AutoScale(const TextureURL: string): Cardinal;
var
  I: Integer;
begin
  Result := 1;
  for I := 0 to FAutoGeneratedTexturesList.Count - 1 do
    if FAutoGeneratedTexturesList[I].TextureURLMatches(TextureURL) then
      Exit(FAutoGeneratedTexturesList[I].SmallestScale);
end;

{ globals -------------------------------------------------------------------- }

var
  FMaterialProperties: TMaterialProperties;

function MaterialProperties: TMaterialProperties;
begin
  if FMaterialProperties = nil then
    FMaterialProperties := TMaterialProperties.Create(true);
  Result := FMaterialProperties;
end;

finalization
  FreeAndNil(FMaterialProperties);
end.