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