File: precompute_radiance_transfer.lpr

package info (click to toggle)
castle-game-engine 6.4%2Bdfsg1-2
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 194,520 kB
  • sloc: pascal: 364,585; ansic: 8,606; java: 2,851; objc: 2,601; cpp: 1,412; xml: 851; makefile: 725; sh: 563; php: 26
file content (264 lines) | stat: -rw-r--r-- 10,090 bytes parent folder | download | duplicates (2)
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
{
  Copyright 2008-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.

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

{ Process arbitrary model for PRT. Computes and adds "radianceTransfer"
  field to all geometry nodes (descending from X3DComposedGeometryNode,
  this includes most often used nodes).
  See https://castle-engine.sourceforge.io/x3d_extensions.php#section_ext_radiance_transfer
  about the radiance transfer.

  Command-line usage:
    $1 is the source model,
    $2 is the output model (output is always VRML/X3D, so use .wrl/.x3dv extension).

  Example:
    ./precompute_radiance_transfer data/chinchilla.wrl.gz chinchilla-output.x3dv
    ./radiance_transfer chinchilla-output.x3dv # test the output

  Optional parameters:

  --sh-basis-count / -c COUNT

    Says how much basis SH functions to use. PRT paper advices between
    9 and 25.

  --rays-per-vertex / -r COUNT

    How many rays per vertex to generate. This linearly affects the speed
    of the program. Default if 1000, PRT paper advices 10 * 1000 to 30 * 1000
    for best effect.

  TODO: for now, radianceTransfer is calculated for whole model.
  This means that self-shadowing takes whole model into account,
  but also that whole model must remain static (or radianceTransfer must
  be animated along with coords).

  Alternative approach is possible: calculate radianceTransfer only
  for this specific shape. Then shape must stay static (or it's
  radianceTransfer must be animated along with it's coords), but it can
  move with respect to other shapes. But note that then self-shadowing
  takes only this shape into account... TODO: make this possible,
  and document on
  https://castle-engine.sourceforge.io/x3d_extensions.php#section_ext_radiance_transfer

  We compute radianceTransfer in scene space (not in local shape
  space). This is important, otherwise incoming light SH (calculated
  when rendering at every frame) would have to be transformed (rotated)
  for each shape. Right now, it only has to be rotated once, for each scene.

  Note that your geometry nodes shouldn't use DEF/USE mechanism.
  If the same shape is instantiated many times, it will have the same
  radianceTransfer. Which is bad, since self-shadowing may be different
  on different instances...
}

program precompute_radiance_transfer;

uses SysUtils, CastleUtils, CastleVectors, CastleSceneCore, X3DNodes,
  CastleSphereSampling, CastleProgress, CastleProgressConsole,
  CastleSphericalHarmonics, CastleParameters, CastleTimeUtils, CastleShapes;

var
  Scene: TCastleSceneCore;
  Normals: TVector3List;
  SHBasisCount: Integer = 25;
  RaysPerVertex: Cardinal = 1000;

procedure ComputeTransfer(RadianceTransfer: TVector3List;
  Coord: TVector3List; const Transform: TMatrix4;
  const DiffuseColor: TVector3);
var
  I, J, SHBase: Integer;
  V, N: TVector3;
  RayDirectionPT: TVector2;
  RayDirection: TVector3;
  VertexTransfer: PVector3;
begin
  RadianceTransfer.Count := Coord.Count * SHBasisCount;

  Progress.Init(Coord.Count, 'Computing transfer for one shape');
  try
    for I := 0 to Coord.Count - 1 do
    begin
      VertexTransfer := Addr(RadianceTransfer.List^[I * SHBasisCount]);

      { V = scene-space vertex coord }
      V := Transform.MultPoint(Coord.List^[I]);

      { N = scene-space normal coord
        TODO: MultDirection will not work under non-uniform scaling matrix correctly. }
      N := Transform.MultDirection(Normals.List^[I]).Normalize;

      for SHBase := 0 to SHBasisCount - 1 do
        VertexTransfer[SHBase] := TVector3.Zero;

      { In some nasty cases, smoothed normal may be zero, see e.g. spider_queen
        legs. Leave VertexTransfer[SHBase] as zeros in this case. }

      if not N.IsZero then
      begin
        { Integrate over hemisphere (around N). Actually, we could integrate
          over the sphere, but this is (on most models) a waste of time
          (= a waste of half rays), since most rays outside of N hemisphere
          would go inside the model (where usually you do not hit anything). }

        for J := 0 to RaysPerVertex - 1 do
        begin
          RayDirectionPT := RandomHemispherePointConst;
          RayDirection := PhiThetaToXYZ(RayDirectionPT, N);
          if not Scene.InternalOctreeVisibleTriangles.IsRayCollision(V, RayDirection,
            nil, true { yes, ignore margin at start, to not hit V },
            nil) then
          begin
            { Previous RayDirectionPT assumed that (0, 0, 1) is N.
              That is, RayDirectionPT was in local vertex coords, not
              in actual world coords (like N). So now transform back
              RayDirection to RayDirectionPT, to get RayDirectionPT in world coords.
              This is an extremely important operation, without this SHBasis
              is calculated with wrong arguments, and the models are
              not lighted as they should with PRT. }

            RayDirectionPT := XYZToPhiTheta(RayDirection);
            for SHBase := 0 to SHBasisCount - 1 do
              { TVector3.DotProduct below must be >= 0, since RayDirection was
                chosen at random within hemisphere around N.
                So no need to do Max(0, TVector3.DotProduct(...)) below.

                We calculate only red component here, the rest will
                be copied from it later. }
              VertexTransfer[SHBase].Data[0] += SHBasis(SHBase, RayDirectionPT) *
                TVector3.DotProduct(N, RayDirection);
          end;
        end;

        for SHBase := 0 to SHBasisCount - 1 do
        begin
          { VertexTransfer[SHBase][0] is an integral over hemisphere,
            so normalize. }
          VertexTransfer[SHBase].Data[0] *= 2 * Pi / RaysPerVertex;

          { Calculate Green, Blue components of VertexTransfer
            (just copy from Red, since we didn't take DiffuseColor
            into account yet). }
          VertexTransfer[SHBase].Data[1] := VertexTransfer[SHBase].Data[0];
          VertexTransfer[SHBase].Data[2] := VertexTransfer[SHBase].Data[0];

          { Multiply by BRDF = DiffuseColor (since
            BRDF is simply constant, so we can simply multiply here).
            For diffuse surface, BRDF is just = DiffuseColor. }
          VertexTransfer[SHBase].Data[0] *= DiffuseColor[0];
          VertexTransfer[SHBase].Data[1] *= DiffuseColor[1];
          VertexTransfer[SHBase].Data[2] *= DiffuseColor[2];
        end;
      end;

      Progress.Step;
    end;
  finally Progress.Fini end;
end;

function DiffuseColor(State: TX3DGraphTraverseState): TVector3;
var
  MaterialInfo: TMaterialInfo;
begin
  MaterialInfo := State.MaterialInfo;
  if MaterialInfo <> nil then
    Result := MaterialInfo.DiffuseColor
  else
    Result := TMaterialInfo.DefaultDiffuseColor;
end;

const
  Options: array[0..1] of TOption =
  ( (Short: 'c'; Long: 'sh-basis-count'; Argument: oaRequired),
    (Short: 'r'; Long: 'rays-per-vertex'; Argument: oaRequired)
  );

  procedure OptionProc(OptionNum: Integer; HasArgument: boolean;
    const Argument: string; const SeparateArgs: TSeparateArgs; Data: Pointer);
  begin
    case OptionNum of
      0: SHBasisCount := StrToInt(Argument);
      1: RaysPerVertex := StrToInt(Argument);
    end;
  end;

var
  SI: TShapeTreeIterator;
  Geometry: TAbstractGeometryNode;
  State: TX3DGraphTraverseState;
  RadianceTransfer: TVector3List;
  S: string;
  TimeStart: TProcessTimerResult;
  Seconds: TFloatTime;
begin
  Parameters.Parse(Options, @OptionProc, nil);
  Parameters.CheckHigh(2);

  Progress.UserInterface := ProgressConsoleInterface;

  Scene := TCastleSceneCore.Create(nil);
  try
    Scene.Load(Parameters[1]);
    Scene.TriangleOctreeProgressTitle := 'Building octree';
    Scene.Spatial := [ssVisibleTriangles];

    TimeStart := ProcessTimer;

    SI := TShapeTreeIterator.Create(Scene.Shapes, false);
    try
      while SI.GetNext do
      begin
        Geometry := SI.Current.Geometry;
        State := SI.Current.State;

        if Geometry is TAbstractComposedGeometryNode then
          RadianceTransfer := TAbstractComposedGeometryNode(Geometry).FdRadianceTransfer.Items else
        if Geometry is TIndexedFaceSetNode_1 then
          RadianceTransfer := TIndexedFaceSetNode_1(Geometry).FdRadianceTransfer.Items else
          RadianceTransfer := nil;

        { If we used Proxy to get Geometry, then don't calculate PRT.
          We could calculate it, but it would not be used: it would not
          be saved to the actual file, only to the temporary Proxy instance.
          To make it work, we may in the future implement actually inserting
          Proxy result into the VRML file. }
        if Geometry <> SI.Current.OriginalGeometry then
          RadianceTransfer := nil;

        if RadianceTransfer <> nil then
        begin
          { For PRT, we need a normal per-vertex, so always calculate
            smooth normals. Simple, and thanks to CastleInternalNormals
            this works for all VRML/X3D coord-based nodes (and only for
            those RadianceTransfer is defined). }
          Normals := SI.Current.NormalsSmooth(true, true);
          ComputeTransfer(RadianceTransfer,
            Geometry.InternalCoordinates(State).Items,
            State.Transform, DiffuseColor(State));
        end;
      end;
    finally FreeAndNil(SI) end;

    Seconds := ProcessTimerSeconds(ProcessTimer, TimeStart);
    S := Format('SH bases %d, rays per vertex %d, done in %f secs',
      [SHBasisCount, RaysPerVertex, Seconds]);

    Writeln('Precomputing finished: ', S);

    Save3D(Scene.RootNode, Parameters[2],
      'radianceTransfer computed by precompute_radiance_transfer: ' + S, '', xeClassic);
  finally FreeAndNil(Scene) end;
end.