]> git.xonotic.org Git - xonotic/xonotic.git/blob - misc/tools/bsptool.pl
align here too
[xonotic/xonotic.git] / misc / tools / bsptool.pl
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5 use Image::Magick;
6 use POSIX qw/floor ceil/;
7
8 my @lumpname = qw/entities textures planes nodes leafs leaffaces leafbrushes models brushes brushsides vertices triangles effects faces lightmaps lightgrid pvs advertisements/;
9 my %lumpid = map { $lumpname[$_] => $_ } 0..@lumpname-1;
10 my $msg = "";
11 my @bsp;
12
13 # READ THE BSP
14
15 if(!@ARGV || $ARGV[0] eq '-h' || $ARGV[0] eq '--help')
16 {
17         print <<EOF;
18 Usage:
19   $0 filename.bsp [operations...]
20
21 Operations are:
22   Information requests:
23     -i                print info about the BSP file
24     -xlumpname        extract a lump (see -i)
25
26   Changes:
27     -dlumpname        delete a lump (see -i)
28     -rlumpname        replace a lump (see -i) by the data from standard input
29     -gfilename.tga    save the lightgrid as filename.tga (debugging)
30     -Gratio           scale down the lightgrid to reduce BSP file size
31     -ljpgNNN          externalize the lightmaps as JPEG, quality NNN (number from 1 to 100)
32     -lpng             externalize the lightmaps as PNG
33     -ltga             externalize the lightmaps as TGA
34     -mMESSAGE         set the BSP file comment message
35
36   Save commands:
37     -o                actually apply the changes to the BSP
38     -ofilename2.bsp   save the changes to a new BSP file
39 EOF
40         exit;
41 }
42
43 my $fn = shift @ARGV;
44 $fn =~ /(.*)\.bsp$/
45         or die "invalid input file name (must be a .bsp): $fn";
46 my $basename = $1;
47 open my $fh, "<", $fn
48         or die "$fn: $!";
49
50 read $fh, my $header, 8;
51
52 die "Invalid BSP format"
53         if $header ne "IBSP\x2e\x00\x00\x00";
54
55 for(0..16)
56 {
57         read $fh, my $lump, 8;
58         my ($offset, $length) = unpack "VV", $lump;
59
60         push @bsp, [$offset, $length, undef];
61 }
62
63 for(@bsp)
64 {
65         my ($offset, $length, $data) = @$_;
66         seek $fh, $offset, 0;
67         read $fh, $data, $length;
68         length $data == $length
69                 or die "Incomplete BSP lump at $offset\n";
70         $_->[2] = $data;
71 }
72
73 close $fh;
74
75 # STRUCT DECODING
76
77 sub DecodeLump($@)
78 {
79         my ($lump, @fields) = @_;
80         my @decoded;
81
82         my $spec = "";
83         my @decoders;
84
85         my $item;
86         my @data;
87         my $idx;
88
89         for(@fields)
90         {
91                 if(/^(\w*)=(.*?)(\d*)$/)
92                 {
93                         $spec .= "$2$3 ";
94                         my $f = $1;
95                         my $n = $3;
96                         if($n eq '')
97                         {
98                                 push @decoders, sub { $item->{$f} = $data[$idx++]; };
99                         }
100                         else
101                         {
102                                 push @decoders, sub { $item->{$f} = [ map { $data[$idx++] } 1..$n ]; };
103                         }
104                 }
105         }
106
107         my $itemlen = length pack $spec, ();
108         my $len = length $lump;
109
110         die "Invalid lump size: $len not divisible by $itemlen"
111                 if $len % $itemlen;
112
113         my $items = $len / $itemlen;
114         for(0..$items - 1)
115         {
116                 @data = unpack $spec, substr $lump, $_ * $itemlen, $itemlen;
117                 $item = {};
118                 $idx = 0;
119                 $_->() for @decoders;
120                 push @decoded, $item;
121         }
122         @decoded;
123 }
124
125 sub EncodeLump($@)
126 {
127         my ($items, @fields) = @_;
128         my @decoded;
129
130         my @encoders;
131
132         my $item;
133         my @data;
134         my $idx;
135         my $data = "";
136
137         for(@fields)
138         {
139                 if(/^(\w*)=(.*?)(\d*)$/)
140                 {
141                         my $spec = "$2$3";
142                         my $f = $1;
143                         my $n = $3;
144                         if($n eq '')
145                         {
146                                 push @encoders, sub { $data .= pack $spec, $item->{$f}; };
147                         }
148                         else
149                         {
150                                 push @encoders, sub { $data .= pack $spec, @{$item->{$f}}; };
151                         }
152                 }
153         }
154
155         for my $i(@$items)
156         {
157                 $item = $i;
158                 $_->() for @encoders;
159         }
160
161         $data;
162 }
163
164 sub EncodeDirection(@)
165 {
166         my ($x, $y, $z) = @_;
167
168         return [
169                 map { ($_ / 0.02454369260617025967) & 0xFF }
170                 (
171                         atan2(sqrt($x * $x + $y * $y), $z),
172                         atan2($y, $x)
173                 )
174         ];
175 }
176
177 sub DecodeDirection($)
178 {
179         my ($dir) = @_;
180
181         my ($pitch, $yaw) = map { $_ * 0.02454369260617025967 } @$dir; # maps 256 to 2pi
182
183         return (
184                 cos($yaw) * sin($pitch),
185                 sin($yaw) * sin($pitch),
186                 cos($pitch)
187         );
188 }
189
190 sub IntervalIntersection($$$$)
191 {
192         my ($a, $al, $b, $bl) = @_;
193         my $a0 = $a - 0.5 * $al;
194         my $a1 = $a + 0.5 * $al;
195         my $b0 = $b - 0.5 * $bl;
196         my $b1 = $b + 0.5 * $bl;
197         my $left = ($a0 > $b0) ? $a0 : $b0;
198         my $right = ($a1 > $b1) ? $b1 : $a1;
199         die "Non-intersecting intervals $a $al $b $bl"
200                 if $right < $left;
201         return $right - $left;
202 }
203
204 sub BoxIntersection(@)
205 {
206         my ($x, $y, $z, $w, $h, $d, $x2, $y2, $z2, $w2, $h2, $d2) = @_;
207         return
208                 IntervalIntersection($x, $w, $x2, $w2)
209                 *
210                 IntervalIntersection($y, $h, $y2, $h2)
211                 *
212                 IntervalIntersection($z, $d, $z2, $d2);
213 }
214
215 # OPTIONS
216
217 for(@ARGV)
218 {
219         if(/^-i$/) # info
220         {
221                 my $msgalign = [0, 3, 2, 1]->[length($msg) % 4];
222                 my $total = 17 * 8 + 8 + length($msg) + $msgalign;
223                 my $max = 0;
224                 for(0..@bsp-1)
225                 {
226                         my $nl = length $bsp[$_]->[2];
227                         my $align = [0, 3, 2, 1]->[$nl % 4];
228                         $total += $nl + $align;
229                         print "BSP lump $_ ($lumpname[$_]): offset $bsp[$_]->[0] length $bsp[$_]->[1] newlength $nl\n";
230                         my $endpos = $bsp[$_]->[0] + $bsp[$_]->[1];
231                         $max = $endpos if $max < $endpos;
232                 }
233                 print "BSP file size will change from $max to $total bytes\n";
234         }
235         elsif(/^-d(.+)$/) # delete a lump
236         {
237                 my $id = $lumpid{$1};
238                 die "invalid lump $1 to remove"
239                         unless defined $id;
240                 $bsp[$id]->[2] = "";
241         }
242         elsif(/^-r(.+)$/) # replace a lump
243         {
244                 my $id = $lumpid{$1};
245                 die "invalid lump $1 to replace"
246                         unless defined $id;
247                 $bsp[$id]->[2] = do { undef local $/; scalar <STDIN>; };
248         }
249         elsif(/^-m(.*)$/) # change the message
250         {
251                 $msg = $1;
252         }
253         elsif(/^-l(jpg|png|tga)(\d+)?$/) # externalize lightmaps (deleting the internal ones)
254         {
255                 my $ext = $1;
256                 my $quality = $2;
257                 my %lightmaps = ();
258                 my $faces = $bsp[$lumpid{faces}]->[2];
259                 my $lightmaps = $bsp[$lumpid{lightmaps}]->[2];
260                 my @values = DecodeLump $faces,
261                         qw/texture=V effect=V type=V vertex=V n_vertexes=V meshvert=V n_meshverts=V lm_index=V lm_start=f2 lm_size=f2 lm_origin=f3 lm_vec_0=f3 lm_vec_1=f3 normal=f3 size=V2/;
262                 my $oddfound = 0;
263                 for(@values)
264                 {
265                         my $l = $_->{lm_index};
266                         next if $l >= 2**31; # signed
267                         $oddfound = 1
268                                 if $l % 2;
269                         ++$lightmaps{$l};
270                 }
271                 if(!$oddfound)
272                 {
273                         $lightmaps{$_+1} = $lightmaps{$_} for keys %lightmaps;
274                 }
275                 for(sort { $a <=> $b } keys %lightmaps)
276                 {
277                         print STDERR "Lightmap $_ was used $lightmaps{$_} times\n";
278
279                         # export that lightmap
280                         my $lmsize = 128 * 128 * 3;
281                         next if length $lightmaps < ($_ + 1) * $lmsize;
282                         my $lmdata = substr $lightmaps, $_ * $lmsize, $lmsize;
283                         my $img = Image::Magick->new(size => '128x128', depth => 8, magick => 'RGB');
284                         $img->BlobToImage($lmdata);
285                         my $outfn = sprintf "%s/lm_%04d.$ext", $basename, $_;
286                         mkdir $basename;
287                         $img->Set(quality => $quality)
288                                 if defined $quality;
289                         my $err = $img->Write($outfn);
290                         die $err
291                                 if $err;
292                         print STDERR "Wrote $outfn\n";
293                 }
294
295                 # nullify the lightmap lump
296                 $bsp[$lumpid{lightmaps}]->[2] = "";
297         }
298         elsif(/^-g(.+)$/) # export light grid as an image (for debugging)
299         {
300                 my $filename = $1;
301                 my @models = DecodeLump $bsp[$lumpid{models}]->[2],
302                         qw/mins=f3 maxs=f3 face=V n_faces=V brush=V n_brushes=V/;
303                 my $entities = $bsp[$lumpid{entities}]->[2];
304                 my @entitylines = split /\r?\n/, $entities;
305                 my $gridsize = "64 64 128";
306                 for(@entitylines)
307                 {
308                         last if $_ eq '}';
309                         /^\s*"_?gridsize"\s+"(.*)"$/
310                                 and $gridsize = $1;
311                 }
312                 my @scale = map { 1 / $_ } split / /, $gridsize;
313                 my @imins = map { ceil($models[0]{mins}[$_] * $scale[$_]) } 0..2;
314                 my @imaxs = map { floor($models[0]{maxs}[$_] * $scale[$_]) } 0..2;
315                 my @isize = map { $imaxs[$_] - $imins[$_] + 1 } 0..2;
316                 my $isize = $isize[0] * $isize[1] * $isize[2];
317                 my @gridcells = DecodeLump $bsp[$lumpid{lightgrid}]->[2],
318                         qw/ambient=C3 directional=C3 dir=C2/;
319                 die "Cannot decode light grid"
320                         unless $isize == @gridcells;
321
322                 # sum up the "ambient" light over all pixels
323                 my @pixels;
324                 my $max = 1;
325                 for my $y(0..$isize[1]-1)
326                 {
327                         for my $x(0..$isize[0]-1)
328                         {
329                                 my ($r, $g, $b) = (0, 0, 0);
330                                 for my $z(0..$isize[2]-1)
331                                 {
332                                         my $cell = $gridcells[$x + $y * $isize[0] + $z * $isize[0] * $isize[1]];
333                                         $r += $cell->{ambient}->[0];
334                                         $g += $cell->{ambient}->[1];
335                                         $b += $cell->{ambient}->[2];
336                                 }
337                                 push @pixels, [$r, $g, $b];
338                                 $max = $r if $max < $r;
339                                 $max = $g if $max < $g;
340                                 $max = $b if $max < $b;
341                         }
342                 }
343                 my $pixeldata = "";
344                 for my $p(@pixels)
345                 {
346                         $pixeldata .= pack "CCC", map { 255 * $p->[$_] / $max } 0..2;
347                 }
348
349                 my $img = Image::Magick->new(size => sprintf("%dx%d", $isize[0], $isize[1]), depth => 8, magick => 'RGB');
350                 $img->BlobToImage($pixeldata);
351                 $img->Write($filename);
352                 print STDERR "Wrote $filename\n";
353         }
354         elsif(/^-G(.+)$/) # decimate light grid
355         {
356                 my $decimate = $1;
357                 my $filter = 1; # 0 = nearest, 1 = box filter
358
359                 my @models = DecodeLump $bsp[$lumpid{models}]->[2],
360                         qw/mins=f3 maxs=f3 face=V n_faces=V brush=V n_brushes=V/;
361                 my $entities = $bsp[$lumpid{entities}]->[2];
362                 my @entitylines = split /\r?\n/, $entities;
363                 my $gridsize = "64 64 128";
364                 my $gridsizeindex = undef;
365                 for(0..@entitylines-1)
366                 {
367                         my $l = $entitylines[$_];
368                         last if $l eq '}';
369                         if($l =~ /^\s*"_?gridsize"\s+"(.*)"$/)
370                         {
371                                 $gridsize = $1;
372                                 $gridsizeindex = $_;
373                         }
374                 }
375                 my @scale = map { 1 / $_ } split / /, $gridsize;
376                 my @imins = map { ceil($models[0]{mins}[$_] * $scale[$_]) } 0..2;
377                 my @imaxs = map { floor($models[0]{maxs}[$_] * $scale[$_]) } 0..2;
378                 my @isize = map { $imaxs[$_] - $imins[$_] + 1 } 0..2;
379                 my $isize = $isize[0] * $isize[1] * $isize[2];
380                 my @gridcells = DecodeLump $bsp[$lumpid{lightgrid}]->[2],
381                         qw/ambient=C3 directional=C3 dir=C2/;
382                 die "Cannot decode light grid"
383                         unless $isize == @gridcells;
384
385                 # get the new grid size values
386                 my @newscale = map { $_ / $decimate } @scale;
387                 my $newgridsize = join " ", map { 1 / $_ } @newscale;
388                 my @newimins = map { ceil($models[0]{mins}[$_] * $newscale[$_]) } 0..2;
389                 my @newimaxs = map { floor($models[0]{maxs}[$_] * $newscale[$_]) } 0..2;
390                 my @newisize = map { $newimaxs[$_] - $newimins[$_] + 1 } 0..2;
391
392                 # do the decimation
393                 my @newgridcells = ();
394                 for my $z($newimins[2]..$newimaxs[2])
395                 {
396                         # the coords are MIDPOINTS of the grid cells!
397                         my @oldz = grep { $_ >= $imins[2] && $_ <= $imaxs[2] } floor(($z - 0.5) * $decimate + 0.5) .. ceil(($z + 0.5) * $decimate - 0.5);
398                         my $innerz_raw = $z * $decimate;
399                         my $innerz = floor($innerz_raw + 0.5);
400                         $innerz = $imins[2] if $innerz < $imins[2];
401                         $innerz = $imaxs[2] if $innerz > $imaxs[2];
402                         for my $y($newimins[1]..$newimaxs[1])
403                         {
404                                 my @oldy = grep { $_ >= $imins[1] && $_ <= $imaxs[1] } floor(($y - 0.5) * $decimate + 0.5) .. ceil(($y + 0.5) * $decimate - 0.5);
405                                 my $innery_raw = $y * $decimate;
406                                 my $innery = floor($innery_raw + 0.5);
407                                 $innery = $imins[1] if $innery < $imins[1];
408                                 $innery = $imaxs[1] if $innery > $imaxs[1];
409                                 for my $x($newimins[0]..$newimaxs[0])
410                                 {
411                                         my @oldx = grep { $_ >= $imins[0] && $_ <= $imaxs[0] } floor(($x - 0.5) * $decimate + 0.5) .. ceil(($x + 0.5) * $decimate - 0.5);
412                                         my $innerx_raw = $x * $decimate;
413                                         my $innerx = floor($innerx_raw + 0.5);
414                                         $innerx = $imins[0] if $innerx < $imins[0];
415                                         $innerx = $imaxs[0] if $innerx > $imaxs[0];
416
417                                         my @vec = (0, 0, 0);
418                                         my @dir = (0, 0, 0);
419                                         my @amb = (0, 0, 0);
420                                         my $weight = 0;
421                                         my $innercell = $gridcells[($innerx - $imins[0]) + $isize[0] * ($innery - $imins[1]) + $isize[0] * $isize[1] * ($innerz - $imins[2])];
422                                         for my $Z(@oldz)
423                                         {
424                                                 for my $Y(@oldy)
425                                                 {
426                                                         for my $X(@oldx)
427                                                         {
428                                                                 my $cell = $gridcells[($X - $imins[0]) + $isize[0] * ($Y - $imins[1]) + $isize[0] * $isize[1] * ($Z - $imins[2])];
429
430                                                                 my $cellweight = BoxIntersection(
431                                                                         $X, $Y, $Z, 1, 1, 1,
432                                                                         map { $_ * $decimate } $x, $y, $z, 1, 1, 1
433                                                                 );
434
435                                                                 $dir[$_] += $cellweight * $cell->{directional}->[$_] for 0..2;
436                                                                 $amb[$_] += $cellweight * $cell->{ambient}->[$_] for 0..2;
437                                                                 my @norm = DecodeDirection $cell->{dir};
438                                                                 $vec[$_] += $cellweight * $norm[$_] for 0..2;
439                                                                 $weight += $cellweight;
440                                                         }
441                                                 }
442                                         }
443                                         if($weight)
444                                         {
445                                                 $dir[$_] /= $weight for 0..2;
446                                                 $dir[$_] *= $filter for 0..2;
447                                                 $dir[$_] += (1 - $filter) * $innercell->{directional}->[$_] for 0..2;
448
449                                                 $amb[$_] /= $weight for 0..2;
450                                                 $amb[$_] *= $filter for 0..2;
451                                                 $amb[$_] += (1 - $filter) * $innercell->{ambient}->[$_] for 0..2;
452
453                                                 my @norm = DecodeDirection $innercell->{dir};
454                                                 $vec[$_] /= $weight for 0..2;
455                                                 $vec[$_] *= $filter for 0..2;
456                                                 $vec[$_] += (1 - $filter) * $norm[$_] for 0..2;
457
458                                                 $innercell = {
459                                                         ambient => \@amb,
460                                                         directional => \@dir,
461                                                         dir => EncodeDirection @norm
462                                                 };
463                                         }
464
465                                         push @newgridcells, $innercell;
466                                 }
467                         }
468                 }
469
470                 $bsp[$lumpid{lightgrid}]->[2] = EncodeLump \@newgridcells,
471                         qw/ambient=C3 directional=C3 dir=C2/;
472                 splice @entitylines, $gridsizeindex, 1, ()
473                         if defined $gridsizeindex;
474                 splice @entitylines, 1, 0, qq{"gridsize" "$newgridsize"};
475                 $bsp[$lumpid{entities}]->[2] = join "\n", @entitylines;
476         }
477         elsif(/^-x(.+)$/) # extract lump to stdout
478         {
479                 my $id = $lumpid{$1};
480                 die "invalid lump $1 to extract"
481                         unless defined $id;
482                 print $bsp[$id]->[2];
483         }
484         elsif(/^-o(.+)?$/) # write the final BSP file
485         {
486                 my $outfile = $1;
487                 $outfile = $fn
488                         if not defined $outfile;
489                 open my $fh, ">", $outfile
490                         or die "$outfile: $!";
491                 print $fh $header;
492                 my $msgalign = [0, 3, 2, 1]->[length($msg) % 4];
493                 my $pos = 17 * 8 + tell($fh) + length($msg) + $msgalign;
494                 for(@bsp)
495                 {
496                         my $align = [0, 3, 2, 1]->[length($_->[2]) % 4];
497                         $_->[0] = $pos;
498                         $_->[1] = length $_->[2];
499                         $pos += $_->[1] + $align;
500                         print $fh pack "VV", $_->[0], $_->[1];
501                 }
502                 print $fh $msg;
503                 print $fh "\x00" x $msgalign;
504                 for(@bsp)
505                 {
506                         my $align = [0, 3, 2, 1]->[length($_->[2]) % 4];
507                         print $fh $_->[2];
508                         print $fh "\x00" x $align;
509                 }
510                 close $fh;
511                 print STDERR "Wrote $outfile\n";
512         }
513         else
514         {
515                 die "Invalid option: $_";
516         }
517 }
518
519 # TODO:
520 #   features like:
521 #     decimate light grid
522 #     edit lightmaps/grid