#!/usr/bin/perl # ======================================================================== #< ac3d2threejs.pl - 12/05/2013 # AC3D parser and converter to three.js json format # Based on ac3d2Horde3D format by Niek Albers, circa 13/11/2009 # 24/10/2014 - revisit - default to add normals to off # 12/05/2013 geoff mclane http://geoffair.net/mperl # TODO: # 1: There are several TODO items noted, mainly where a more strict # parsing would help. # 2: At present a rought fix in output to ensure a "faces" : [ ] index is # not out of range of the vertices or normals lists, but should address # why this happens. # 3: Should dig into the calculation of the normals. Original code expected # only 3 refs, but have over-ridden this, but with what consequence? # 4: Would be nice if some of the 'options' below were available on the command # line, including for example the output file name. # ========================================================================== my $os = $^O; # This need to be adjusted to suit your environment my $perl_dir = '/home/geoff/bin'; my $PATH_SEP = '/'; my $temp_dir = '/tmp'; if ($os =~ /win/i) { $perl_dir = 'C:\GTools\perl'; $temp_dir = $perl_dir; $PATH_SEP = "\\"; } unshift(@INC, $perl_dir); # add location of lib_vec3.pm my $VERSION = 0.8; # 01/11/2014 - more fixes - geoff #my $VERSION = 0.7; # 12/05/2013 - lots of fixes - geoff #my $VERSION = 0.6; # last by Niek Albers, circa 13/11/2009 use strict; use warnings; #no strict 'refs'; #no warnings; use Data::Dumper; use Carp; use File::Copy; # note: in the original this 'package' was included, but I prefer it separated require 'lib_vec3.pm' or die "Unable to load 'lib_vec3.pm' Check paths in \@INC...\n"; my $pgmname = $0; if ($pgmname =~ /(\\|\/)/) { my @tmpsp = split(/(\\|\/)/,$pgmname); $pgmname = $tmpsp[-1]; } my $AC3D_FILE_VERSION = 11; my $strict_triangluation = 0; # 0=allow any number of SURF refs my $out_file = $temp_dir.$PATH_SEP."tempscene.xml"; # not used my $json_file = ''; # options my $do_welding = 0; # not sure what all this was about - it increases the vertices... my $show_welding = 0; # now not done - see $do_welding = 0; my $show_unwelding = 0; # now not done - see $do_welding = 0; my $show_objects = 1; # turn off to reduce noise my $show_calc_norms = 0; # additional noise my $show_parse_kid = 0; # mainly for diagnostics my $show_parse_numvert = 0; # likewise my $show_out_of_range = 0; # show when an index is 'fixed' my $add_line_sep = 1; # add extra line to the json my $add_normals = 0; # generate normals my $verbosity = 0; sub VERB1() { return $verbosity >= 1; } sub VERB2() { return $verbosity >= 2; } sub VERB5() { return $verbosity >= 5; } sub VERB9() { return $verbosity >= 9; } my $normal = 0; # grep { $_ eq '-n' } @ARGV; my $scene_basename; my $source = ''; my $ac3dfile; # input file my $scenefile; my $geofile; my @VERTICES; my @INDICES; my @NORMALS; my @INDPERSURF = (); my @warnings = (); my $warn_count = 0; # also used as final exit level my $index_offset = 0; my $batch_offset = 0; my $world = {}; my $material_files = {}; my $last_name = ''; # my $vertice_count = 0; # 12/05/2013 - Added token 'subdiv' and 'texoff', # and changed 'data' to what I understand it does my $TOKENIZER = { 'MATERIAL' => \&parse_material, 'OBJECT' => \&parse_object, 'kids' => \&parse_kids, 'name' => \&parse_name, 'texture' => \&parse_value, 'crease' => \&parse_value, 'numvert' => \&parse_numvert, 'numsurf' => \&parse_numsurf, 'SURF' => \&parse_surf, 'mat' => \&parse_mat, 'refs' => \&parse_refs, 'texrep' => \&parse_texrep, 'rot' => \&parse_rot, 'loc' => \&parse_loc, 'url' => \&parse_value, 'data' => \&parse_data, 'subdiv' => \&parse_value, 'texoff' => \&parse_texoff, }; # ### DEBUG ### # Just some vey specific 'test' files, so I can run the script from my editor # with a blank command line... should be OFF for release my $debug_on = 0; #my $def_file = 'C:\FG\18\blendac3d\cube.ac'; #my $def_file = 'C:\FG\18\blendac3d\f16.ac'; #my $def_file = 'C:\FG\fgdata\Aircraft\A380\Models\FlightDeck\Glareshield\efis_c.ac'; # has 'subdiv 1' #my $def_file = 'C:\FG\fgdata\Aircraft\a4\Models\attitude-mod1.ac'; # has 'texoff -0.5 0.5' #14398: ac_to_gl: Unrecognised token 'light', line 'OBJECT light' #my $def_file = 'C:\FG\fgdata\Aircraft\dhc8\Models\dhc8.ac'; #ac_to_gl: Illegal ref record. actually has several blank lines!!! could overlook, but NO! # Also fails with blender ac importer until blank lines are removed #my $def_file = 'C:\FG\fgdata\Aircraft\Dromader\Models\DropSystem.ac'; my $def_file = 'C:\FG\ac3d\DropSystem.ac'; # fixed file - no warnings ############################################################################ eval { main(@ARGV) }; die($@) if ($@); exit($warn_count); sub prt($) { print shift; } sub prtw($) { my $t = shift; push(@warnings,$t); prt($t); } # simple vector3 class - now an external package sub vec3 { vec3->new(@_) } sub basename { my $path = shift; $path =~ s/.*[\/\\]//; return [ split( /\./, $path ) ]->[0]; } sub filename { my $path = shift; $path =~ s/.*[\/\\]//; return $path; } # load a file line, and 'tokenise' it, removing # any double quotes... sub parse_line { my $line = <$ac3dfile>; return if ( !$line ); my @items = ( $line =~ /(".*?"|\S+)/g ); map { s/(^"|"$)//g } @items; return \@items; } # individual opcode parsers sub parse_header { my $items = shift; $items->[0] =~ m/^AC3D(\w)/; die "not minimally an AC3D version $AC3D_FILE_VERSION file\n" if ( hex($1) < $AC3D_FILE_VERSION ); } sub parse_roc { my $items = shift; return [ [ $items->[1], $items->[2], $items->[3] ], [ $items->[4], $items->[5], $items->[6] ], [ $items->[7], $items->[8], $items->[9] ] ]; } sub parse_value { my $items = shift; return $items->[1]; } sub parse_name { my $items = shift; $last_name = $items->[1]; return $items->[1]; } sub parse_texrep { my $items = shift; return { x => $items->[1], y => $items->[2] }; } # 12/05/2013 - added sub parse_texoff { my $items = shift; return { x => $items->[1], y => $items->[2] }; } sub parse_surf { my $items = shift; my $param = hex( $items->[1] ); my $type = $param & 0xF; my $types = { 0 => 'polygon', 1 => 'closedline', 2 => 'line' }; my $shading_sides = $param >> 4; my $shading = $shading_sides & 0x1 ? 'smooth' : 'flat'; my $sides = $shading_sides & 0x2 ? 2 : 1; my $result = { type => $types->{$type}, shading => $shading, sides => $sides, }; return $result; } # 12/05/2013 - my 'understanding' is just take the next line, BUT if # given a large 'len' could maybe be several lines, but what exactly is # the spec on this? sub parse_data { my $items = shift; my $len = $items->[1]; my $next = parse_line(); # TODO: if $strict_ac3d could check the length of the following line = len my $result = { data => $next }; return $result; } sub parse_mat { my $items = shift; my $index = $items->[1]; # TODO: if $strict_ac3d could check the index is within range return $world->{MATERIAL}->[$index]; } sub parse_loc { my $items = shift; # TODO: if $strict_ac3d could check it is 3 floats return vec3( $items->[1], $items->[2], $items->[3] ); } sub parse_numvert { my $items = shift; my $numverts = $items->[1]; my @vertices = (); # TODO: if $strict_ac3d could check it is 3 floats for ( 1 .. $numverts ) { my $itm = parse_line(); push @vertices, vec3( $itm->[0], $itm->[1], $itm->[2] ); } my $cnt = scalar @vertices; warn "parse_numvert: collected $cnt vertices ($numverts)\n" if ($show_parse_numvert); return \@vertices; } sub parse_refs { my $items = shift; my $numrefs = $items->[1]; # TODO: since source expects just 3 refs, perhaps normals calculation # also needs to be 'fixed' if this is off! if ($strict_triangluation) { die("model is not triangulated: $last_name numrefs=$numrefs\n") if ( $numrefs != 3 ); } my @indices = (); # TODO: if $strict_ac3d could check it is integer, in range, and 2 floats for ( 1 .. $numrefs ) { my $items = parse_line(); push @indices, { index => $items->[0], u => $items->[1], v => $items->[2] }; } return \@indices; } # SURF 0x10 # mat 1 # refs 3 # 1 0.8 1 # 0 1 1 # 5 0.8 0 sub parse_surface { my $items = shift; my $surface = {}; # TODO: if $strict_ac3d could check opcode order, and # index into vertices is valid! while ( my $items = parse_line() ) { my $opcode = $items->[0]; if ( exists( $TOKENIZER->{$opcode} ) ) { $surface->{$opcode} = $TOKENIZER->{$opcode}($items); } else { my $line = join(" ",@{$items}); prtw("WARNING: $.: parse_surface: unrecognised opcode [$opcode] in '$line'\n"); } last if ( $surface->{refs} ); } return $surface; } sub parse_numsurf { my $items = shift; my $numsurfs = $items->[1]; my @surfaces = (); for ( 1 .. $numsurfs ) { push @surfaces, parse_surface($items); } return \@surfaces; } sub parse_kids { my $items = shift; my $amount = $items->[1]; my @objects = (); warn "parse_kids: $amount...\n" if ($show_parse_kid && $amount); for ( 1 .. $amount ) { my $items = parse_line(); push @objects, parse_object($items); } warn "parse_kids: $amount done\n" if ($show_parse_kid && $amount); return \@objects; } sub parse_object { my $items = shift; my $type = $items->[1]; my $object = {}; my $error; $last_name = ''; $object->{type} = $type; while ( my $items = parse_line() ) { my $opcode = $items->[0]; if ( exists( $TOKENIZER->{$opcode} ) ) { $object->{$opcode} = $TOKENIZER->{$opcode}($items); $error = $@ if ($@); } else { $error = join(" ",@{$items}); prtw("WARNING: $.: parse_object: unrecognised opcode [$opcode] in '$error'\n"); } last if ( $opcode eq 'kids' ); } if ($show_objects && defined $object->{numvert}) { prt("parsed object: "); prt("$object->{name} ") if (defined $object->{name}); prt("type: $type "); prt("vertices: ".int( @{ $object->{numvert} } )." ") if (defined $object->{numvert}); prt("surfaces: ".int( @{ $object->{numsurf} } )." ") if (defined $object->{numsurf}); prt("\n"); } #if (defined $object->{name} && defined $object->{numvert}) { # warn "parsed object: $object->{name}, type: $type, vertices: " # . int( @{ $object->{numvert} } ) . "\n" if ($show_objects); #} return $object; } sub parse_material { my $items = shift; # TODO: if $strict_ac3d could check length, and verify tokens return { name => $items->[1], rgb => [ $items->[3], $items->[4], $items->[5] ], amb => [ $items->[7], $items->[8], $items->[9] ], emis => [ $items->[11], $items->[12], $items->[13] ], spec => [ $items->[15], $items->[16], $items->[17] ], shi => $items->[19], trans => $items->[21], }; } sub parse { while ( my $items = parse_line() ) { my $command = $items->[0]; if ( exists( $TOKENIZER->{$command} ) ) { push @{ $world->{$command} }, $TOKENIZER->{$command}($items); } else { my $line = join(" ",@{$items}); prtw("WARNING: $.: parse: unrecognised command [$command] in '$line'\n"); } } return $world; } # find lowest and highest vertex index sub get_min_max_count_index { my $object = shift; my $min_index = 0xffffffff; my $max_index = 0; my $index_count = 0; my ($ind,$surface,$ref); foreach $surface ( @{ $object->{numsurf} } ) { foreach $ref ( @{ $surface->{refs} } ) { $ind = $ref->{index}; $max_index = $ind if ( $max_index < $ind ); $min_index = $ind if ( $min_index > $ind ); $index_count++; } } return ( $min_index, $max_index, $index_count ); } sub trim_double($) { my $d = shift; while ($d =~ /0$/) { $d =~ s/0$//; } $d =~ s/\.$//; return $d; } # ================================================================================= ################################################################################### # my three.js replacement fucntions ################################################################################### sub write_js2 { my ($cnt,$i,$itm,@arr,$filename,$face_flag); my $QuadBit = 1; # isBitSet( $type, 0 ); my $MatBit = 2; # isBitSet( $type, 1 ); my $FUvBit = 4; # isBitSet( $type, 2 ); my $VUvBit = 8; # isBitSet( $type, 3 ); my $FNorms = 16; # isBitSet( $type, 4 ); my $VNorms = 32; # isBitSet( $type, 5 ); my $FColor = 64; # isBitSet( $type, 6 ); my $VColor = 128; # isBitSet( $type, 7 ); if (length($json_file)) { $filename = $json_file; } else { $filename = $temp_dir.$PATH_SEP."temp.$scene_basename.js"; } open( $geofile, ">$filename" ) || die("cannot write to $filename\n"); ###warn "writing to: $filename\n"; my $vertcnt = scalar @VERTICES; my $normcnt = scalar @NORMALS; my $surfcnt = scalar @INDPERSURF; my $json = "{\n"; $json .= "\n" if ($add_line_sep); $json .= "\t\"metadata\" : {\n"; $json .= "\t\t\"formatVersion\" : 3.1,\n"; $json .= "\t\t\"generatedBy\" : \"ac3d2threejs.pl $VERSION\",\n"; $json .= "\t\t\"vertices\" : $vertcnt,\n"; $json .= "\t\t\"faces\" : $surfcnt,\n"; $json .= "\t\t\"normals\" : $normcnt,\n"; $json .= "\t\t\"colors\" : 0,\n"; $json .= "\t\t\"uvs\" : [],\n"; $json .= "\t\t\"materials\" : 0,\n"; ### $json .= "\t\t\"materials\" : 1,\n"; $json .= "\t\t\"morphTargets\" : 0,\n"; $json .= "\t\t\"bones\" : 0\n"; $json .= "\t},\n"; $json .= "\n" if ($add_line_sep); $json .= "\t\"scale\" : 1.000000,\n"; $json .= "\n" if ($add_line_sep); $json .= "\t\"vertices\" : ["; for ($i = 0; $i < $vertcnt; $i++) { my $vertex = $VERTICES[$i]; $json .= trim_double($vertex->x); $json .= ','; $json .= trim_double($vertex->y); $json .= ','; $json .= trim_double($vertex->z); $json .= "," if (($i + 1) < $vertcnt); } $json .= "],\n"; $json .= "\n" if ($add_line_sep); $json .= "\t\"morphTargets\" : [],\n"; $json .= "\n" if ($add_line_sep); # Normals - positions magic = 1 $json .= "\t\"normals\" : ["; for ($i = 0; $i < $normcnt; $i++) { my $normal = $NORMALS[$i]; next if (!defined $normal); $json .= $normal->x; $json .= ','; $json .= $normal->y; $json .= ','; $json .= $normal->z; $json .= ',' if (($i + 1) < $normcnt); } $json .= "],\n"; $json .= "\n" if ($add_line_sep); $json .= "\t\"colors\" : [],\n"; $json .= "\n" if ($add_line_sep); $json .= "\t\"uvs\" : [],\n"; $json .= "\n" if ($add_line_sep); $face_flag = $QuadBit; $face_flag += $MatBit; $face_flag += $VNorms if ($normcnt > 0); my ($tmp,$cnt2,$j,$k); my ($ind,$emsg); $json .= "\t\"faces\" : ["; for ($i = 0; $i < $surfcnt; $i++) { $tmp = $INDPERSURF[$i]; $json .= ',' if ($i); $cnt2 = scalar @{$tmp}; $json .= "$face_flag"; # 35 $k = 0; for ($j = 0; $j < $cnt2; $j++) { $ind = ${$tmp}[$j]; if (($ind < 0)||($ind >= $vertcnt)) { $emsg .= "$i:$j: V $ind/$vertcnt "; $ind = $vertcnt - 1; } $json .= ",$ind"; $k++; last if ($k == 4); } while ($k < 4) { $json .= ',0'; $k++; } $json .= ',0'; $k = 0; if ($normcnt > 0) { for ($j = 0; $j < $cnt2; $j++) { $ind = ${$tmp}[$j]; if (($ind < 0)||($ind >= $normcnt)) { $emsg .= "$i:$j: N $ind/$normcnt "; $ind = $normcnt - 1; } $json .= ",$ind"; $k++; last if ($k == 4); } while ($k < 4) { $json .= ',0'; $k++; } } } $json .= "],\n"; prtw("WARNING: Out of range index: $emsg\n") if (length($emsg) && $show_out_of_range); $json .= "\n" if ($add_line_sep); $json .= "\t\"bones\" : [],\n"; $json .= "\n" if ($add_line_sep); $json .= "\t\"skinIndices\" : [],\n"; $json .= "\n" if ($add_line_sep); $json .= "\t\"skinWeights\" : [],\n"; $json .= "\n" if ($add_line_sep); $json .= "\t\"animation\" : {}"; $json .= "\n"; $json .= "\n" if ($add_line_sep); $json .= "}\n"; print $geofile $json; close($geofile); prt("Three.js json written to [$filename]\n"); } sub write_object2 { my $object = shift; my $tag = ''; my $params = {}; if ( $object->{loc} ) { $params->{tx} = $object->{loc}->x; $params->{ty} = $object->{loc}->y; $params->{tz} = $object->{loc}->z; } if ( $object->{type} eq 'world' ) { $tag = 'Group'; $params->{name} = 'world'; } elsif ( $object->{type} eq 'group' ) { $tag = 'Model'; $params->{name} = $object->{name}; $params->{geometry} = "$scene_basename/$scene_basename.geo"; $object->{in_model} = 1; } elsif ( $object->{type} eq 'poly' ) { if ( !$object->{in_model} ) { # whoops not in a model yet, create one first my %wrapper = %$object; $wrapper{kids} = [$object]; $wrapper{type} = 'group'; delete $object->{loc}; # set location for model in this case write_object2( \%wrapper ); return; } else { $tag = 'Mesh'; calc_vertex_normals($object); ### warn Dumper($object); ### my $material = basename( $object->{texture} ) || 'standard'; my $material = 'standard'; if (defined $object->{texture} ) { $material = basename( $object->{texture} ); } $params->{material} = "$scene_basename/$material.material.xml"; $params->{name} = $object->{name}; my ( $min_index, $max_index, $index_count ) = get_min_max_count_index($object); $params->{vertRStart} = $min_index + $index_offset; $params->{vertREnd} = $max_index + $index_offset; $params->{batchCount} = $index_count; $params->{batchStart} = $batch_offset; push @VERTICES, @{ $object->{numvert} }; push @NORMALS, @{ $object->{normals} }; ##push @TANGENTS, @{ $object->{tangents} }; ##push @BITANGENTS, @{ $object->{bitangents} }; ##push @TCOORDS0, @{ $object->{tcoords} }; # @INDPERSURF my @inds = (); foreach my $surface ( @{ $object->{numsurf} } ) { @inds = (); foreach my $ref ( @{ $surface->{refs} } ) { push @INDICES, $ref->{index} + $index_offset; push @inds, $ref->{index} + $index_offset; } push(@INDPERSURF, [@inds]); # keep on a per face basis } $index_offset += int( @{ $object->{numvert} } ); $batch_offset += $index_count; } } my $attributes = ''; foreach my $key ( sort keys(%$params) ) { $attributes .= qq~ $key="$params->{$key}"~; } ##print $scenefile qq~<$tag$attributes>\n~; ###print qq~<$tag$attributes>\n~; foreach my $kid ( @{ $object->{kids} } ) { $kid->{in_model} = $object->{in_model}; write_object2($kid); } ###print qq~\n~; ###print $scenefile qq~\n~; delete $object->{kids}; } sub write_object { my $tree = shift; write_object2( $tree->{OBJECT}->[0] ); } # ================================================================================= ################################################################################### sub uv_sub { my $uv1 = shift; my $uv2 = shift; return { u => $uv1->{u} - $uv2->{u}, v => $uv1->{v} - $uv2->{v} }; } # find vertices with same coordinates, this is for normal smoothing, return of list of matching indices sub find_same_vertices { my $object = shift; my $indices_map = shift; my $index = shift; my @indices = (); if (defined $object->{numvert}->[$index] && defined $indices_map) { my $vertex = $object->{numvert}->[$index]; my $vstring = $vertex->as_string; if (defined $indices_map->{ $vstring }) { @indices = @{ $indices_map->{ $vstring } }; push( @indices, $index ) if ( !grep { $_ == $index } @indices ); } } return @indices; } # create map of indices with same vertices sub make_indices_map { my $object = shift; my %indices_map; foreach my $surface ( @{ $object->{numsurf} } ) { # only average out smooth vertex normals next if ( $surface->{SURF}->{shading} ne 'smooth' ); foreach my $ref ( @{ $surface->{refs} } ) { my $v = $object->{numvert}->[ $ref->{index} ]; push @{ $indices_map{ $v->as_string } }, $ref->{index}; } } # warn Dumper(\%indices_map); return \%indices_map; } sub calc_vertex_normals { my $object = shift; my $vcnt = scalar @{ $object->{numvert} }; my $scnt = scalar @{ $object->{numsurf} }; my @normals = (); if (!$add_normals) { warn "no normals: $object->{name} v=$vcnt, s=$scnt\n" if ($show_calc_norms); $object->{normals} = \@normals; return; } warn "calculating normals: $object->{name} v=$vcnt, s=$scnt\n" if ($show_calc_norms); my ($i,$surface,$refs,$index0,$index1,$index2); my ($v0,$v1,$v2); my ($d0,$d1,$v); foreach my $surface ( @{ $object->{numsurf} } ) { $refs = scalar @{ $surface->{refs} }; if ($refs >= 3) { for ($i = 0; $i < $refs; $i++) { $index0 = $surface->{refs}->[$i]->{index}; # wrapping if (($i + 1) >= $refs) { $index1 = $surface->{refs}->[0]->{index}; $index2 = $surface->{refs}->[1]->{index}; } elsif (($i + 2) >= $refs) { $index1 = $surface->{refs}->[$i+1]->{index}; $index2 = $surface->{refs}->[0]->{index}; } else { $index1 = $surface->{refs}->[$i+1]->{index}; $index2 = $surface->{refs}->[$i+2]->{index}; } # get the three vertices $v0 = $object->{numvert}->[$index0]; $v1 = $object->{numvert}->[$index1]; $v2 = $object->{numvert}->[$index2]; # get difference $d0 = $v1 - $v0; $d1 = $v2 - $v0; # get normal $v = ( $d0 x $d1 )->normalized; $normals[$index0] = $v; # store at index } } } $object->{normals} = \@normals; } sub calc_vertex_normals_OLD { my $object = shift; my $vcnt = scalar @{ $object->{numvert} }; my $scnt = scalar @{ $object->{numsurf} }; warn "calculating normals: $object->{name} v=$vcnt, s=$scnt\n" if ($show_calc_norms); my @normals; my @tangents; my @bitangents; my $indices_map = make_indices_map($object); foreach my $surface ( @{ $object->{numsurf} } ) { my $index0 = $surface->{refs}->[0]->{index}; my $index1 = $surface->{refs}->[1]->{index}; my $index2 = $surface->{refs}->[2]->{index}; my $v0 = $object->{numvert}->[$index0]; my $v1 = $object->{numvert}->[$index1]; my $v2 = $object->{numvert}->[$index2]; my $d0 = $v1 - $v0; my $d1 = $v2 - $v0; my $v = ( $d0 x $d1 )->normalized; # # Calculate the normal for this face and add it # to the running sum of normals for each of the # three vertices involved # # and now tangents and bitangents my $Edge0uv = uv_sub( $surface->{refs}->[1], $surface->{refs}->[0] ); my $Edge1uv = uv_sub( $surface->{refs}->[2], $surface->{refs}->[0] ); my $cp = $Edge0uv->{u} * $Edge1uv->{v} - $Edge1uv->{u} * $Edge0uv->{v}; my $r = 0; if ( $cp != 0 ) { $r = 1.0 / $cp; } my $tangent = ( $d0 * $Edge1uv->{v} - $d1 * $Edge0uv->{v} ) * $r; my $bitangent = ( $d1 * $Edge0uv->{u} - $d0 * $Edge1uv->{u} ) * $r; my @indices; push @indices, find_same_vertices( $object, $indices_map, $index0 ); push @indices, find_same_vertices( $object, $indices_map, $index1 ); push @indices, find_same_vertices( $object, $indices_map, $index2 ); foreach my $index (@indices) { $normals[$index] ||= vec3(); $tangents[$index] ||= vec3(); $bitangents[$index] ||= vec3(); $normals[$index] += $v; $tangents[$index] += $tangent; $bitangents[$index] += $bitangent; } } # Normalize and fixup the vertex normal vectors, tangents en bitangents my $numVerts = int( @{ $object->{numvert} } ); for ( my $j = 0 ; $j < $numVerts ; $j++ ) { next if (! defined $normals[$j]); my $n = $normals[$j]->normalized; my $t = $tangents[$j]; # orthogonalize $tangents[$j] = ( $t - $n * ( $n * $t ) )->normalized; $normals[$j] = $n; if ( ( $n x $t ) * $bitangents[$j] < 0 ) { $bitangents[$j] = ( ( $n * -1 ) x $t )->normalized; } else { $bitangents[$j] = ( $n x $t )->normalized; } } $object->{normals} = \@normals; $object->{tangents} = \@tangents; $object->{bitangents} = \@bitangents; } sub round { my $number = shift; return int( $number + .5 * ( $number <=> 0 ) ); } # TODO: Add more command line options sub main { my @av = @_; parse_command(@av); open( $ac3dfile, "<$source" ) || die "cannot open [$source]\n"; $scene_basename = basename($source); my $header = parse_line(); parse_header($header); my $tree = parse(); write_object($tree); write_js2(); $warn_count = scalar @warnings; if ($warn_count) { foreach my $w (@warnings) { prt($w); } } warn "Done file [$source], lines $., exit $warn_count\n"; close($ac3dfile); } sub give_help() { warn "AC3D2Threejs.pl $VERSION - Convert AC3D .ac file to three.js json file.\n"; warn "usage: $pgmname [options] file.ac\n"; warn "options:\n"; warn " --help (-h or -?) = This help and exit.\n"; warn " --norm 0|1 (-n) = Disable/Enable normals generation. (def=$add_normals)\n"; warn " --out file (-o) = Give three.js json output file name.\n"; warn "If no out file given it will default to [".$temp_dir.$PATH_SEP."temp..js]\n"; prt(" --verb[n] (-v) = Bump [or set] verbosity. def=$verbosity\n"); die "enjoy!\n"; } sub need_arg { my ($arg,@av) = @_; die "ERROR: [$arg] must have a following argument!\n" if (!@av); } sub get_bool($) { my $txt = shift; return 1 if ($txt eq '1'); return 0 if ($txt eq '0'); return 1 if ($txt =~ /on/i); return 0 if ($txt =~ /off/i); return 1 if ($txt =~ /yes/i); return 0 if ($txt =~ /no/i); die "Unable to parse bool 1|0 on|off yes|no from $txt\n"; } sub parse_command() { my (@av) = @_; my ($arg,$sarg); #$arg = scalar @av; #warn "Parsing $arg commands...\n"; while (@av) { $arg = $av[0]; #warn "arg [$arg]\n"; if ($arg =~ /^-/) { $sarg = substr($arg,1); $sarg = substr($sarg,1) while ($sarg =~ /^-/); if (($sarg =~ /^h/)||($sarg =~ /^\?/)) { give_help(); } elsif ($sarg =~ /^n/) { need_arg(@av); shift @av; $sarg = $av[0]; $add_normals = get_bool($sarg); } elsif ($sarg =~ /^o/) { need_arg(@av); shift @av; $json_file = $av[0]; } elsif ($sarg =~ /^v/) { if ($sarg =~ /^v.*(\d+)$/) { $verbosity = $1; } else { while ($sarg =~ /^v/) { $verbosity++; $sarg = substr($sarg,1); } } prt("Verbosity = $verbosity\n") if (VERB1()); } else { die "Unknown arg $arg. Use -h for help\n"; } } else { $source = $arg; warn "Set source to [$source]\n"; } shift @av; } if ($debug_on) { if (length($source) == 0) { $source = $def_file; } } if (length($source) == 0) { die "Need to give AC3D file to convert. Use -h for help\n"; } } # eof - ac3d2threejs.pl