aboutsummaryrefslogtreecommitdiffstats
path: root/ardor3d-extras
diff options
context:
space:
mode:
authorneothemachine <[email protected]>2012-12-05 17:03:16 +0100
committerneothemachine <[email protected]>2012-12-05 17:03:16 +0100
commit9dd02f103042cb8a196f8a3ed2278da443e345bf (patch)
tree422449f0c62ff9518316ce5d4219bb2b12f0ed15 /ardor3d-extras
parent2b26b12fd794de0f03a064a10024a3d9f5583756 (diff)
move all files from trunk to root folder
Diffstat (limited to 'ardor3d-extras')
-rw-r--r--ardor3d-extras/.classpath7
-rw-r--r--ardor3d-extras/.project17
-rw-r--r--ardor3d-extras/.settings/org.eclipse.jdt.core.prefs270
-rw-r--r--ardor3d-extras/.settings/org.eclipse.jdt.ui.prefs109
-rw-r--r--ardor3d-extras/pom.xml32
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/AtlasNode.java89
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/AtlasPacker.java33
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/AtlasTextureParameter.java24
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/TexturePacker.java402
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/TextureParameter.java163
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/InteractManager.java242
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/data/SpatialState.java28
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/AllowScaleFilter.java44
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/MinMaxScaleFilter.java49
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/PlaneBoundaryFilter.java50
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/UpdateFilter.java43
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/AbstractInteractWidget.java197
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/CompoundInteractWidget.java341
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/InteractArrow.java106
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/InteractMatrix.java15
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/InteractRing.java188
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/MoveMultiPlanarWidget.java230
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/MovePlanarWidget.java240
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/MoveWidget.java301
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/RotateWidget.java348
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/SimpleScaleWidget.java184
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2DataStore.java52
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Frame.java36
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Header.java78
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Importer.java420
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Normals.java190
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjDataStore.java39
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjGeometryStore.java369
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjImporter.java515
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjIndexSet.java87
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjMaterial.java97
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjSetManager.java53
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/KeyframeController.java762
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvEdgeInfo.java36
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvFaceInfo.java45
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvStripInfo.java323
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvStripStartInfo.java26
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvStripifier.java1297
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvTriangleStripper.java564
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/PrimitiveGroup.java62
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/VertexCache.java81
-rw-r--r--ardor3d-extras/src/main/java/com/ardor3d/extension/useful/TrailMesh.java358
47 files changed, 9242 insertions, 0 deletions
diff --git a/ardor3d-extras/.classpath b/ardor3d-extras/.classpath
new file mode 100644
index 0000000..3e03a65
--- /dev/null
+++ b/ardor3d-extras/.classpath
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src/main/java"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/ardor3d-core"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/ardor3d-extras/.project b/ardor3d-extras/.project
new file mode 100644
index 0000000..bd2b737
--- /dev/null
+++ b/ardor3d-extras/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>ardor3d-extras</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/ardor3d-extras/.settings/org.eclipse.jdt.core.prefs b/ardor3d-extras/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..9f60ae4
--- /dev/null
+++ b/ardor3d-extras/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,270 @@
+#Tue Apr 06 11:30:21 CDT 2010
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_assignment=0
+org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80
+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=0
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=1
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=false
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
+org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert
+org.eclipse.jdt.core.formatter.comment.line_length=120
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
+org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.join_lines_in_comments=true
+org.eclipse.jdt.core.formatter.join_wrapped_lines=true
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.lineSplit=120
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1
+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true
+org.eclipse.jdt.core.formatter.tabulation.char=space
+org.eclipse.jdt.core.formatter.tabulation.size=4
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
diff --git a/ardor3d-extras/.settings/org.eclipse.jdt.ui.prefs b/ardor3d-extras/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..89b3b50
--- /dev/null
+++ b/ardor3d-extras/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,109 @@
+cleanup.add_default_serial_version_id=true
+cleanup.add_generated_serial_version_id=false
+cleanup.add_missing_annotations=true
+cleanup.add_missing_deprecated_annotations=true
+cleanup.add_missing_methods=true
+cleanup.add_missing_nls_tags=false
+cleanup.add_missing_override_annotations=true
+cleanup.add_serial_version_id=true
+cleanup.always_use_blocks=true
+cleanup.always_use_parentheses_in_expressions=true
+cleanup.always_use_this_for_non_static_field_access=false
+cleanup.always_use_this_for_non_static_method_access=false
+cleanup.convert_to_enhanced_for_loop=false
+cleanup.correct_indentation=true
+cleanup.format_source_code=true
+cleanup.format_source_code_changes_only=false
+cleanup.make_local_variable_final=true
+cleanup.make_parameters_final=true
+cleanup.make_private_fields_final=true
+cleanup.make_type_abstract_if_missing_method=false
+cleanup.make_variable_declarations_final=true
+cleanup.never_use_blocks=false
+cleanup.never_use_parentheses_in_expressions=false
+cleanup.organize_imports=true
+cleanup.qualify_static_field_accesses_with_declaring_class=false
+cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+cleanup.qualify_static_member_accesses_with_declaring_class=true
+cleanup.qualify_static_method_accesses_with_declaring_class=false
+cleanup.remove_private_constructors=true
+cleanup.remove_trailing_whitespaces=true
+cleanup.remove_trailing_whitespaces_all=true
+cleanup.remove_trailing_whitespaces_ignore_empty=false
+cleanup.remove_unnecessary_casts=true
+cleanup.remove_unnecessary_nls_tags=true
+cleanup.remove_unused_imports=true
+cleanup.remove_unused_local_variables=false
+cleanup.remove_unused_private_fields=true
+cleanup.remove_unused_private_members=false
+cleanup.remove_unused_private_methods=true
+cleanup.remove_unused_private_types=true
+cleanup.sort_members=false
+cleanup.sort_members_all=false
+cleanup.use_blocks=true
+cleanup.use_blocks_only_for_return_and_throw=false
+cleanup.use_parentheses_in_expressions=false
+cleanup.use_this_for_non_static_field_access=true
+cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+cleanup.use_this_for_non_static_method_access=true
+cleanup.use_this_for_non_static_method_access_only_if_necessary=true
+cleanup_profile=_ArdorLabs
+cleanup_settings_version=2
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+formatter_profile=_ArdorLabs
+formatter_settings_version=11
+org.eclipse.jdt.ui.javadoc=false
+org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates><template autoinsert\="true" context\="gettercomment_context" deleted\="false" description\="Comment for getter method" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.gettercomment" name\="gettercomment">/**\r\n * @return the ${bare_field_name}\r\n */</template><template autoinsert\="true" context\="settercomment_context" deleted\="false" description\="Comment for setter method" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.settercomment" name\="settercomment">/**\r\n * @param ${param} the ${bare_field_name} to set\r\n */</template><template autoinsert\="true" context\="constructorcomment_context" deleted\="false" description\="Comment for created constructors" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.constructorcomment" name\="constructorcomment">/**\r\n * ${tags}\r\n */</template><template autoinsert\="true" context\="filecomment_context" deleted\="false" description\="Comment for created Java files" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.filecomment" name\="filecomment">/**\r\n * \r\n */</template><template autoinsert\="false" context\="typecomment_context" deleted\="false" description\="Comment for created types" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.typecomment" name\="typecomment">/**\r\n * ${tags}\r\n */</template><template autoinsert\="true" context\="fieldcomment_context" deleted\="false" description\="Comment for fields" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.fieldcomment" name\="fieldcomment">/**\r\n * \r\n */</template><template autoinsert\="true" context\="methodcomment_context" deleted\="false" description\="Comment for non-overriding methods" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.methodcomment" name\="methodcomment">/**\r\n * ${tags}\r\n */</template><template autoinsert\="false" context\="overridecomment_context" deleted\="false" description\="Comment for overriding methods" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.overridecomment" name\="overridecomment"/><template autoinsert\="true" context\="delegatecomment_context" deleted\="false" description\="Comment for delegate methods" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.delegatecomment" name\="delegatecomment">/**\r\n * ${tags}\r\n * ${see_to_target}\r\n */</template><template autoinsert\="false" context\="newtype_context" deleted\="false" description\="Newly created files" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.newtype" name\="newtype">/**\r\n * Copyright (c) 2008-2012 Ardor Labs, Inc.\r\n *\r\n * This file is part of Ardor3D.\r\n *\r\n * Ardor3D is free software\: you can redistribute it and/or modify it \r\n * under the terms of its license which may be found in the accompanying\r\n * LICENSE file or at &lt;http\://www.ardor3d.com/LICENSE&gt;.\r\n */\r\n\r\n${filecomment}\r\n${package_declaration}\r\n\r\n${typecomment}\r\n${type_declaration}</template><template autoinsert\="true" context\="classbody_context" deleted\="false" description\="Code in new class type bodies" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.classbody" name\="classbody">\r\n</template><template autoinsert\="true" context\="interfacebody_context" deleted\="false" description\="Code in new interface type bodies" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.interfacebody" name\="interfacebody">\r\n</template><template autoinsert\="true" context\="enumbody_context" deleted\="false" description\="Code in new enum type bodies" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.enumbody" name\="enumbody">\r\n</template><template autoinsert\="true" context\="annotationbody_context" deleted\="false" description\="Code in new annotation type bodies" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.annotationbody" name\="annotationbody">\r\n</template><template autoinsert\="true" context\="catchblock_context" deleted\="false" description\="Code in new catch blocks" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.catchblock" name\="catchblock">// ${todo} Auto-generated catch block\r\n${exception_var}.printStackTrace();</template><template autoinsert\="true" context\="methodbody_context" deleted\="false" description\="Code in created method stubs" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.methodbody" name\="methodbody">// ${todo} Auto-generated method stub\r\n${body_statement}</template><template autoinsert\="true" context\="constructorbody_context" deleted\="false" description\="Code in created constructor stubs" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.constructorbody" name\="constructorbody">${body_statement}\r\n// ${todo} Auto-generated constructor stub</template><template autoinsert\="true" context\="getterbody_context" deleted\="false" description\="Code in created getters" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.getterbody" name\="getterbody">return ${field};</template><template autoinsert\="true" context\="setterbody_context" deleted\="false" description\="Code in created setters" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.setterbody" name\="setterbody">${field} \= ${param};</template><template autoinsert\="true" context\="gettercomment_context" deleted\="false" description\="Comment for getter function" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.gettercomment" name\="gettercomment">/**\r\n * @return the ${bare_field_name}\r\n */</template><template autoinsert\="true" context\="settercomment_context" deleted\="false" description\="Comment for setter function" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.settercomment" name\="settercomment">/**\r\n * @param ${param} the ${bare_field_name} to set\r\n */</template><template autoinsert\="true" context\="constructorcomment_context" deleted\="false" description\="Comment for created constructors" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.constructorcomment" name\="constructorcomment">/**\r\n * ${tags}\r\n */</template><template autoinsert\="true" context\="filecomment_context" deleted\="false" description\="Comment for created JavaScript files" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.filecomment" name\="filecomment">/**\r\n * \r\n */</template><template autoinsert\="true" context\="typecomment_context" deleted\="false" description\="Comment for created types" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.typecomment" name\="typecomment">/**\r\n * @author ${user}\r\n *\r\n * ${tags}\r\n */</template><template autoinsert\="true" context\="fieldcomment_context" deleted\="false" description\="Comment for vars" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.fieldcomment" name\="fieldcomment">/**\r\n * \r\n */</template><template autoinsert\="true" context\="methodcomment_context" deleted\="false" description\="Comment for non-overriding function" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.methodcomment" name\="methodcomment">/**\r\n * ${tags}\r\n */</template><template autoinsert\="true" context\="overridecomment_context" deleted\="false" description\="Comment for overriding functions" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.overridecomment" name\="overridecomment">/* (non-JSDoc)\r\n * ${see_to_overridden}\r\n */</template><template autoinsert\="true" context\="delegatecomment_context" deleted\="false" description\="Comment for delegate functions" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.delegatecomment" name\="delegatecomment">/**\r\n * ${tags}\r\n * ${see_to_target}\r\n */</template><template autoinsert\="true" context\="newtype_context" deleted\="false" description\="Newly created files" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.newtype" name\="newtype">${filecomment}\r\n${package_declaration}\r\n\r\n${typecomment}\r\n${type_declaration}</template><template autoinsert\="true" context\="classbody_context" deleted\="false" description\="Code in new class type bodies" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.classbody" name\="classbody">\r\n</template><template autoinsert\="true" context\="catchblock_context" deleted\="false" description\="Code in new catch blocks" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.catchblock" name\="catchblock">// ${todo} Auto-generated catch block\r\n${exception_var}.printStackTrace();</template><template autoinsert\="true" context\="methodbody_context" deleted\="false" description\="Code in created function stubs" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.methodbody" name\="methodbody">// ${todo} Auto-generated function stub\r\n${body_statement}</template><template autoinsert\="true" context\="constructorbody_context" deleted\="false" description\="Code in created constructor stubs" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.constructorbody" name\="constructorbody">${body_statement}\r\n// ${todo} Auto-generated constructor stub</template><template autoinsert\="true" context\="getterbody_context" deleted\="false" description\="Code in created getters" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.getterbody" name\="getterbody">return ${field};</template><template autoinsert\="true" context\="setterbody_context" deleted\="false" description\="Code in created setters" enabled\="true" id\="org.eclipse.wst.jsdt.ui.text.codetemplates.setterbody" name\="setterbody">${field} \= ${param};</template></templates>
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=true
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.convert_to_enhanced_for_loop=false
+sp_cleanup.correct_indentation=true
+sp_cleanup.format_source_code=true
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.make_local_variable_final=true
+sp_cleanup.make_parameters_final=true
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=true
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.organize_imports=true
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=true
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=true
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_trailing_whitespaces=true
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_casts=true
+sp_cleanup.remove_unnecessary_nls_tags=true
+sp_cleanup.remove_unused_imports=true
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.use_blocks=true
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_parentheses_in_expressions=true
+sp_cleanup.use_this_for_non_static_field_access=true
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=true
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
diff --git a/ardor3d-extras/pom.xml b/ardor3d-extras/pom.xml
new file mode 100644
index 0000000..97b6c1e
--- /dev/null
+++ b/ardor3d-extras/pom.xml
@@ -0,0 +1,32 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.ardor3d</groupId>
+ <artifactId>ardor3d</artifactId>
+ <version>0.9-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>ardor3d-extras</artifactId>
+ <packaging>bundle</packaging>
+ <name>Ardor 3D Extras Package</name>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>1.6</source>
+ <target>1.6</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ <dependencies>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>ardor3d-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/AtlasNode.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/AtlasNode.java
new file mode 100644
index 0000000..f047859
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/AtlasNode.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.atlas;
+
+import com.ardor3d.math.Rectangle2;
+
+public class AtlasNode {
+ private boolean isLeaf = true;
+ private boolean isSet = false;
+ private final AtlasNode child[] = new AtlasNode[2];
+ private Rectangle2 localRectangle;
+
+ private AtlasNode() {}
+
+ public AtlasNode(final int width, final int height) {
+ localRectangle = new Rectangle2(0, 0, width, height);
+ }
+
+ public AtlasNode insert(final Rectangle2 rectangle) {
+ if (!isLeaf) {
+ final AtlasNode newNode = child[0].insert(rectangle);
+ if (newNode != null) {
+ return newNode;
+ }
+
+ return child[1].insert(rectangle);
+ } else {
+ if (isSet) {
+ return null;
+ }
+
+ if (rectangle.getWidth() > localRectangle.getWidth() || rectangle.getHeight() > localRectangle.getHeight()) {
+ return null;
+ }
+
+ if (rectangle.getWidth() == localRectangle.getWidth()
+ && rectangle.getHeight() == localRectangle.getHeight()) {
+ isSet = true;
+ return this;
+ }
+
+ isLeaf = false;
+
+ child[0] = new AtlasNode();
+ child[1] = new AtlasNode();
+
+ final int dw = localRectangle.getWidth() - rectangle.getWidth();
+ final int dh = localRectangle.getHeight() - rectangle.getHeight();
+
+ if (dw > dh) {
+ child[0].localRectangle = new Rectangle2(localRectangle.getX(), localRectangle.getY(),
+ rectangle.getWidth(), localRectangle.getHeight());
+ child[1].localRectangle = new Rectangle2(localRectangle.getX() + rectangle.getWidth(),
+ localRectangle.getY(), dw, localRectangle.getHeight());
+ } else {
+ child[0].localRectangle = new Rectangle2(localRectangle.getX(), localRectangle.getY(),
+ localRectangle.getWidth(), rectangle.getHeight());
+ child[1].localRectangle = new Rectangle2(localRectangle.getX(), localRectangle.getY()
+ + rectangle.getHeight(), localRectangle.getWidth(), dh);
+ }
+
+ return child[0].insert(rectangle);
+ }
+ }
+
+ public AtlasNode getChild(final int childIndex) {
+ return child[childIndex];
+ }
+
+ public Rectangle2 getRectangle() {
+ return localRectangle;
+ }
+
+ public boolean isLeaf() {
+ return isLeaf;
+ }
+
+ public boolean isSet() {
+ return isSet;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/AtlasPacker.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/AtlasPacker.java
new file mode 100644
index 0000000..cf11c06
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/AtlasPacker.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.atlas;
+
+import com.ardor3d.math.Rectangle2;
+
+public class AtlasPacker {
+ private final AtlasNode rootNode;
+
+ public AtlasPacker(final int width, final int height) {
+ rootNode = new AtlasNode(width, height);
+ }
+
+ public AtlasNode insert(final int width, final int height) {
+ return rootNode.insert(new Rectangle2(0, 0, width, height));
+ }
+
+ public AtlasNode insert(final Rectangle2 image) {
+ return rootNode.insert(image);
+ }
+
+ public AtlasNode getRootNode() {
+ return rootNode;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/AtlasTextureParameter.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/AtlasTextureParameter.java
new file mode 100644
index 0000000..4c2d5a2
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/AtlasTextureParameter.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.atlas;
+
+import com.ardor3d.image.Texture.ApplyMode;
+import com.ardor3d.image.Texture.MagnificationFilter;
+import com.ardor3d.image.Texture.MinificationFilter;
+import com.ardor3d.image.Texture.WrapMode;
+
+public class AtlasTextureParameter {
+ public MinificationFilter minificationFilter = MinificationFilter.Trilinear;
+ public MagnificationFilter magnificationFilter = MagnificationFilter.Bilinear;
+ public WrapMode wrapMode = WrapMode.EdgeClamp;
+ public ApplyMode applyMode = ApplyMode.Modulate;
+ public boolean compress = false;
+} \ No newline at end of file
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/TexturePacker.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/TexturePacker.java
new file mode 100644
index 0000000..0e6771c
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/TexturePacker.java
@@ -0,0 +1,402 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.atlas;
+
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import com.ardor3d.image.Image;
+import com.ardor3d.image.ImageDataFormat;
+import com.ardor3d.image.PixelDataType;
+import com.ardor3d.image.Texture;
+import com.ardor3d.image.Texture.WrapAxis;
+import com.ardor3d.image.Texture.WrapMode;
+import com.ardor3d.image.TextureStoreFormat;
+import com.ardor3d.math.ColorRGBA;
+import com.ardor3d.math.Rectangle2;
+import com.ardor3d.math.type.ReadOnlyColorRGBA;
+import com.ardor3d.renderer.state.RenderState.StateType;
+import com.ardor3d.renderer.state.TextureState;
+import com.ardor3d.scenegraph.Mesh;
+import com.ardor3d.util.TextureManager;
+import com.ardor3d.util.geom.BufferUtils;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+/**
+ * A tool that uses the AtlasNode/AtlasPacker algorithm to pack textures into texture atlases. It modifies the uv
+ * coordinates of the meshes, and tries to pack the atlases in a way that works with the wrap modes of the textures
+ * involved.
+ * <p>
+ * Simple use case:
+ * <p>
+ * <blockquote>
+ *
+ * <pre>
+ * // Create a texture atlas packer with maximum atlas size of 256x256
+ * final TexturePacker packer = new TexturePacker(256, 256);
+ *
+ * // Add meshes into atlas (lots of different ways of doing this if you have other source/target texture indices)
+ * packer.insert(mesh1);
+ * packer.insert(mesh2);
+ *
+ * // Create all the atlases (also possible to set filters etc here through the AtlasTextureParameter)
+ * packer.createAtlases();
+ * </pre>
+ *
+ * </blockquote>
+ */
+public class TexturePacker {
+ private static final Logger logger = Logger.getLogger(TexturePacker.class.getName());
+
+ private final int atlasWidth;
+ private final int atlasHeight;
+ private int nrTextures = 0;
+ private final boolean useAlpha = false;
+
+ private final Map<TextureParameter, List<TextureParameter>> cachedAtlases;
+ private final List<AtlasPacker> packers;
+ private final List<ByteBuffer> dataBuffers;
+
+ private final List<Texture> textures = Lists.newArrayList();
+
+ public TexturePacker(final int atlasWidth, final int atlasHeight) {
+ this.atlasWidth = atlasWidth;
+ this.atlasHeight = atlasHeight;
+
+ cachedAtlases = Maps.newHashMap();
+ packers = Lists.newArrayList();
+ dataBuffers = Lists.newArrayList();
+
+ addPacker();
+ }
+
+ public void insert(final Mesh mesh) {
+ insert(mesh, 0);
+ }
+
+ public void insert(final Mesh mesh, final int textureIndex) {
+ insert(mesh, textureIndex, 0);
+ }
+
+ public void insert(final Mesh mesh, final int textureIndex, final int targetTextureIndex) {
+ final TextureParameter param = new TextureParameter(mesh, textureIndex, targetTextureIndex);
+ insert(param);
+ }
+
+ public void insert(final TextureParameter parameterObject) {
+ if (parameterObject.getTextureCoords() == null) {
+ TexturePacker.logger.warning("Skipping mesh! - No texture coords found at index "
+ + parameterObject.getTextureIndex() + " for mesh: " + parameterObject);
+ return;
+ }
+ if (parameterObject.getTexture() == null) {
+ TexturePacker.logger.warning("Skipping mesh! - No texture found at index "
+ + parameterObject.getTextureIndex() + " for mesh: " + parameterObject);
+ return;
+ }
+ final ImageDataFormat format = parameterObject.getTexture().getImage().getDataFormat();
+ if (format != ImageDataFormat.RGB && format != ImageDataFormat.RGBA) {
+ TexturePacker.logger.warning("Skipping mesh! - Only RGB and RGBA texture formats supported currently: "
+ + parameterObject);
+ return;
+ }
+
+ List<TextureParameter> list = cachedAtlases.get(parameterObject);
+ if (list != null) {
+ final TextureParameter cachedParameter = list.get(0);
+
+ final float diffX = cachedParameter.getDiffX();
+ final float diffY = cachedParameter.getDiffY();
+ final float offsetX = cachedParameter.getOffsetX();
+ final float offsetY = cachedParameter.getOffsetY();
+
+ final FloatBuffer destination = parameterObject.getTextureCoords();
+ for (int i = 0; i < destination.limit(); i += 2) {
+ destination.put(i, destination.get(i) * diffX + offsetX);
+ destination.put(i + 1, destination.get(i + 1) * diffY + offsetY);
+ }
+
+ parameterObject.setAtlasIndex(cachedParameter.getAtlasIndex());
+
+ list.add(parameterObject);
+
+ return;
+ }
+
+ final int textureWidth = parameterObject.getWidth();
+ final int textureHeight = parameterObject.getHeight();
+
+ final int totalWidth = textureWidth + 2;
+ final int totalHeight = textureHeight + 2;
+
+ if (totalWidth > atlasWidth || totalHeight > atlasHeight) {
+ System.err.println("Could not fit texture into atlas!");
+ return;
+ }
+
+ AtlasNode node = null;
+ int index = 0;
+ for (int i = 0; i < packers.size(); i++) {
+ final AtlasPacker packer = packers.get(i);
+
+ node = packer.insert(totalWidth, totalHeight);
+ if (node != null) {
+ index = i;
+ break;
+ }
+ }
+
+ if (node == null) {
+ index = addPacker();
+ node = packers.get(index).insert(totalWidth, totalHeight);
+ }
+
+ if (node == null) {
+ System.err.println("Could not fit texture into any atlas!");
+ return;
+ }
+
+ list = Lists.newArrayList();
+ cachedAtlases.put(parameterObject, list);
+ list.add(parameterObject);
+
+ nrTextures++;
+
+ parameterObject.setAtlasIndex(index);
+
+ final Rectangle2 rectangle = node.getRectangle();
+ final ByteBuffer data = dataBuffers.get(index);
+ final ByteBuffer lightData = parameterObject.getTexture().getImage().getData(0);
+
+ boolean hasAlpha = false;
+ if (format == ImageDataFormat.RGBA) {
+ hasAlpha = true;
+ }
+
+ for (int y = 0; y < textureHeight; y++) {
+ for (int x = 0; x < textureWidth; x++) {
+ setDataPixel(rectangle, textureWidth, textureHeight, lightData, data, y, x, hasAlpha);
+ }
+ }
+
+ final WrapMode mode = parameterObject.getTexture().getWrap(WrapAxis.S);
+ switch (mode) {
+ case BorderClamp:
+ case MirrorBorderClamp:
+ final ReadOnlyColorRGBA col = parameterObject.getTexture().getBorderColor();
+ borderClamp(data, rectangle, textureWidth, textureHeight, parameterObject, col);
+ break;
+
+ case Clamp:
+ case MirrorClamp:
+ borderClamp(data, rectangle, textureWidth, textureHeight, parameterObject, ColorRGBA.BLACK);
+ break;
+
+ case EdgeClamp:
+ case MirrorEdgeClamp:
+ edgeClamp(data, rectangle, textureWidth, textureHeight, parameterObject);
+ break;
+
+ case MirroredRepeat:
+ case Repeat:
+ repeat(data, rectangle, textureWidth, textureHeight, parameterObject);
+ break;
+ default:
+ }
+
+ final float invAtlasWidth = 1f / atlasWidth;
+ final float invAtlasHeight = 1f / atlasHeight;
+
+ final float diffX = textureWidth * invAtlasWidth;
+ final float diffY = textureHeight * invAtlasHeight;
+
+ final float offsetX = (rectangle.getX() + 1) * invAtlasWidth;
+ final float offsetY = (rectangle.getY() + 1) * invAtlasHeight;
+
+ parameterObject.setDiffX(diffX);
+ parameterObject.setDiffY(diffY);
+ parameterObject.setOffsetX(offsetX);
+ parameterObject.setOffsetY(offsetY);
+
+ final FloatBuffer destination = parameterObject.getTextureCoords();
+ for (int i = 0; i < destination.limit(); i += 2) {
+ destination.put(i, destination.get(i) * diffX + offsetX);
+ destination.put(i + 1, destination.get(i + 1) * diffY + offsetY);
+ }
+ }
+
+ private void repeat(final ByteBuffer data, final Rectangle2 rectangle, final int textureWidth,
+ final int textureHeight, final TextureParameter parameterObject) {
+ for (int y = 0; y < textureHeight; y++) {
+ localCopyBuffer(data, rectangle, textureWidth, y + 1, 0, y + 1);
+ localCopyBuffer(data, rectangle, 1, y + 1, textureWidth + 1, y + 1);
+ }
+ for (int x = 0; x < textureWidth; x++) {
+ localCopyBuffer(data, rectangle, x + 1, textureHeight, x + 1, 0);
+ localCopyBuffer(data, rectangle, x + 1, 1, x + 1, textureHeight + 1);
+ }
+ localCopyBuffer(data, rectangle, textureWidth, textureHeight, 0, 0);
+ localCopyBuffer(data, rectangle, 1, textureHeight, textureWidth + 1, 0);
+ localCopyBuffer(data, rectangle, textureWidth, 1, 0, textureHeight + 1);
+ localCopyBuffer(data, rectangle, 1, 1, textureWidth + 1, textureHeight + 1);
+ }
+
+ private void edgeClamp(final ByteBuffer data, final Rectangle2 rectangle, final int textureWidth,
+ final int textureHeight, final TextureParameter parameterObject) {
+ for (int y = 0; y < textureHeight; y++) {
+ localCopyBuffer(data, rectangle, 1, y + 1, 0, y + 1);
+ localCopyBuffer(data, rectangle, textureWidth, y + 1, textureWidth + 1, y + 1);
+ }
+ for (int x = 0; x < textureWidth; x++) {
+ localCopyBuffer(data, rectangle, x + 1, 1, x + 1, 0);
+ localCopyBuffer(data, rectangle, x + 1, textureHeight, x + 1, textureHeight + 1);
+ }
+ localCopyBuffer(data, rectangle, 1, 1, 0, 0);
+ localCopyBuffer(data, rectangle, textureWidth, 1, textureWidth + 1, 0);
+ localCopyBuffer(data, rectangle, 1, textureHeight, 0, textureHeight + 1);
+ localCopyBuffer(data, rectangle, textureWidth, textureHeight, textureWidth + 1, textureHeight + 1);
+ }
+
+ private void borderClamp(final ByteBuffer data, final Rectangle2 rectangle, final int textureWidth,
+ final int textureHeight, final TextureParameter parameterObject, final ReadOnlyColorRGBA col) {
+ for (int y = 0; y < textureHeight; y++) {
+ setColor(data, rectangle, 0, y + 1, col);
+ setColor(data, rectangle, textureWidth + 1, y + 1, col);
+ }
+ for (int x = 0; x < textureWidth; x++) {
+ setColor(data, rectangle, x + 1, 0, col);
+ setColor(data, rectangle, x + 1, textureHeight + 1, col);
+ }
+ setColor(data, rectangle, 0, 0, col);
+ setColor(data, rectangle, textureWidth + 1, 0, col);
+ setColor(data, rectangle, 0, textureHeight + 1, col);
+ setColor(data, rectangle, textureWidth + 1, textureHeight + 1, col);
+ }
+
+ public void createAtlases() {
+ createAtlases(new AtlasTextureParameter());
+ }
+
+ public void createAtlases(final AtlasTextureParameter atlasTextureParameter) {
+ for (final ByteBuffer data : dataBuffers) {
+ data.rewind();
+
+ final ImageDataFormat fmt = useAlpha ? ImageDataFormat.RGBA : ImageDataFormat.RGB;
+ final Image image = new Image(fmt, PixelDataType.UnsignedByte, atlasWidth, atlasHeight, data, null);
+
+ final TextureStoreFormat format = atlasTextureParameter.compress ? TextureStoreFormat.GuessCompressedFormat
+ : TextureStoreFormat.GuessNoCompressedFormat;
+ final Texture texture = TextureManager.loadFromImage(image, atlasTextureParameter.minificationFilter,
+ format);
+ texture.setMagnificationFilter(atlasTextureParameter.magnificationFilter);
+
+ texture.setWrap(atlasTextureParameter.wrapMode);
+ texture.setApply(atlasTextureParameter.applyMode);
+
+ textures.add(texture);
+ }
+
+ for (final List<TextureParameter> paramList : cachedAtlases.values()) {
+ for (final TextureParameter param : paramList) {
+ final Texture texture = textures.get(param.getAtlasIndex());
+ final TextureState ts = (TextureState) param.getMesh().getLocalRenderState(StateType.Texture);
+ ts.setTexture(texture, param.getTargetTextureIndex());
+ ts.setNeedsRefresh(true);
+ }
+ }
+
+ TexturePacker.logger.info(nrTextures + " textures packed into " + packers.size() + " atlases.");
+ }
+
+ private int addPacker() {
+ final AtlasPacker packer = new AtlasPacker(atlasWidth, atlasHeight);
+ packers.add(packer);
+
+ final int size = atlasWidth * atlasHeight * (useAlpha ? 4 : 3); // dimensions * 4 bytes per pixel
+ final ByteBuffer data = BufferUtils.createByteBuffer(size);
+
+ dataBuffers.add(data);
+
+ return packers.size() - 1;
+ }
+
+ private void localCopyBuffer(final ByteBuffer dataAsFloatBuffer, final Rectangle2 rectangle, final int xFrom,
+ final int yFrom, final int xTo, final int yTo) {
+ final int componentsTarget = useAlpha ? 4 : 3;
+
+ final int sourceIndex = ((yFrom + rectangle.getY()) * atlasWidth + xFrom + rectangle.getX()) * componentsTarget;
+ final int targetIndex = ((yTo + rectangle.getY()) * atlasWidth + xTo + rectangle.getX()) * componentsTarget;
+
+ fillDataBuffer(dataAsFloatBuffer, dataAsFloatBuffer, sourceIndex, targetIndex, useAlpha);
+ }
+
+ private void setDataPixel(final Rectangle2 rectangle, final int width, final int height,
+ final ByteBuffer lightData, final ByteBuffer dataAsFloatBuffer, final int y, final int x,
+ final boolean sourceAlpha) {
+ final int componentsSource = sourceAlpha ? 4 : 3;
+ final int componentsTarget = useAlpha ? 4 : 3;
+
+ final int sourceIndex = (y * width + x) * componentsSource;
+ final int targetIndex = ((y + rectangle.getY() + 1) * atlasWidth + x + rectangle.getX() + 1) * componentsTarget;
+
+ fillDataBuffer(lightData, dataAsFloatBuffer, sourceIndex, targetIndex, sourceAlpha);
+ }
+
+ private void fillDataBuffer(final ByteBuffer lightData, final ByteBuffer dataAsFloatBuffer, final int sourceIndex,
+ final int targetIndex, final boolean sourceAlpha) {
+ dataAsFloatBuffer.put(targetIndex, lightData.get(sourceIndex));
+ dataAsFloatBuffer.put(targetIndex + 1, lightData.get(sourceIndex + 1));
+ dataAsFloatBuffer.put(targetIndex + 2, lightData.get(sourceIndex + 2));
+ if (useAlpha) {
+ if (sourceAlpha) {
+ dataAsFloatBuffer.put(targetIndex + 3, lightData.get(sourceIndex + 3));
+ } else {
+ dataAsFloatBuffer.put(targetIndex + 3, (byte) (255 & 0xFF));
+ }
+ }
+ }
+
+ private void setColor(final ByteBuffer dataAsFloatBuffer, final Rectangle2 rectangle, final int x, final int y,
+ final ReadOnlyColorRGBA color) {
+ final int componentsTarget = useAlpha ? 4 : 3;
+
+ final int targetIndex = ((y + rectangle.getY()) * atlasWidth + x + rectangle.getX()) * componentsTarget;
+
+ setColor(dataAsFloatBuffer, targetIndex, color, useAlpha);
+ }
+
+ private void setColor(final ByteBuffer dataAsFloatBuffer, final int targetIndex, final ReadOnlyColorRGBA color,
+ final boolean sourceAlpha) {
+ dataAsFloatBuffer.put(targetIndex, (byte) (color.getRed() * 255));
+ dataAsFloatBuffer.put(targetIndex + 1, (byte) (color.getGreen() * 255));
+ dataAsFloatBuffer.put(targetIndex + 2, (byte) (color.getBlue() * 255));
+ if (sourceAlpha) {
+ dataAsFloatBuffer.put(targetIndex + 3, (byte) (color.getAlpha() * 255));
+ }
+ }
+
+ public List<Texture> getTextures() {
+ return textures;
+ }
+
+ public int getAtlasWidth() {
+ return atlasWidth;
+ }
+
+ public int getAtlasHeight() {
+ return atlasHeight;
+ }
+
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/TextureParameter.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/TextureParameter.java
new file mode 100644
index 0000000..5679195
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/atlas/TextureParameter.java
@@ -0,0 +1,163 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.atlas;
+
+import java.nio.FloatBuffer;
+
+import com.ardor3d.image.Texture;
+import com.ardor3d.renderer.state.RenderState;
+import com.ardor3d.renderer.state.RenderState.StateType;
+import com.ardor3d.renderer.state.TextureState;
+import com.ardor3d.scenegraph.Mesh;
+import com.ardor3d.scenegraph.event.DirtyType;
+import com.ardor3d.util.Ardor3dException;
+import com.ardor3d.util.TextureKey;
+
+public class TextureParameter {
+
+ private final Mesh mesh;
+ private final Texture texture;
+ private final int textureIndex;
+ private final int targetTextureIndex;
+ private final TextureKey textureKey;
+
+ private int atlasIndex;
+ private float diffX;
+ private float diffY;
+ private float offsetX;
+ private float offsetY;
+
+ public TextureParameter(final Mesh mesh, final int textureIndex, final int targetTextureIndex) {
+ if (mesh == null) {
+ throw new IllegalArgumentException("Mesh is null");
+ }
+
+ this.mesh = mesh;
+ this.textureIndex = textureIndex;
+ this.targetTextureIndex = targetTextureIndex;
+
+ if (mesh.isDirty(DirtyType.RenderState)) {
+ mesh.updateWorldRenderStates(false);
+ mesh.clearDirty(DirtyType.RenderState);
+ }
+
+ final RenderState textureState = mesh.getWorldRenderState(StateType.Texture);
+ if (textureState == null) {
+ throw new Ardor3dException("No texture state found for mesh: " + mesh);
+ }
+
+ texture = ((TextureState) textureState).getTexture(textureIndex);
+ textureKey = texture != null ? texture.getTextureKey() : null;
+ }
+
+ public Mesh getMesh() {
+ return mesh;
+ }
+
+ public Texture getTexture() {
+ return texture;
+ }
+
+ public FloatBuffer getTextureCoords() {
+ return mesh.getMeshData().getTextureBuffer(textureIndex);
+ }
+
+ public int getWidth() {
+ return texture.getImage().getWidth();
+ }
+
+ public int getHeight() {
+ return texture.getImage().getHeight();
+ }
+
+ public void setAtlasIndex(final int atlasIndex) {
+ this.atlasIndex = atlasIndex;
+ }
+
+ public int getAtlasIndex() {
+ return atlasIndex;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (textureKey == null ? 0 : textureKey.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof TextureParameter)) {
+ return false;
+ }
+ final TextureParameter other = (TextureParameter) obj;
+ if (textureKey == null) {
+ if (other.textureKey != null) {
+ return false;
+ }
+ } else if (!textureKey.equals(other.textureKey)) {
+ return false;
+ }
+ return true;
+ }
+
+ public int getTargetTextureIndex() {
+ return targetTextureIndex;
+ }
+
+ public float getDiffX() {
+ return diffX;
+ }
+
+ public void setDiffX(final float diffX) {
+ this.diffX = diffX;
+ }
+
+ public float getDiffY() {
+ return diffY;
+ }
+
+ public void setDiffY(final float diffY) {
+ this.diffY = diffY;
+ }
+
+ public float getOffsetX() {
+ return offsetX;
+ }
+
+ public void setOffsetX(final float offsetX) {
+ this.offsetX = offsetX;
+ }
+
+ public float getOffsetY() {
+ return offsetY;
+ }
+
+ public void setOffsetY(final float offsetY) {
+ this.offsetY = offsetY;
+ }
+
+ public int getTextureIndex() {
+ return textureIndex;
+ }
+
+ @Override
+ public String toString() {
+ return "TextureParameter [mesh=" + mesh + ", textureIndex=" + textureIndex + "]";
+ }
+} \ No newline at end of file
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/InteractManager.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/InteractManager.java
new file mode 100644
index 0000000..68cb450
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/InteractManager.java
@@ -0,0 +1,242 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.ardor3d.extension.interact.data.SpatialState;
+import com.ardor3d.extension.interact.filter.UpdateFilter;
+import com.ardor3d.extension.interact.widget.AbstractInteractWidget;
+import com.ardor3d.framework.Canvas;
+import com.ardor3d.input.PhysicalLayer;
+import com.ardor3d.input.logical.BasicTriggersApplier;
+import com.ardor3d.input.logical.InputTrigger;
+import com.ardor3d.input.logical.LogicalLayer;
+import com.ardor3d.input.logical.TriggerAction;
+import com.ardor3d.input.logical.TwoInputStates;
+import com.ardor3d.renderer.Renderer;
+import com.ardor3d.scenegraph.Spatial;
+import com.ardor3d.util.ReadOnlyTimer;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Lists;
+
+public class InteractManager {
+
+ /**
+ * List of widgets currently managed by this manager.
+ */
+ protected final List<AbstractInteractWidget> _widgets = Lists.newArrayList();
+
+ /**
+ * The logical layer used by this manager to receive input events prior to forwarding them to the scene.
+ */
+ protected final LogicalLayer _logicalLayer = new LogicalLayer();
+
+ /**
+ * Internal flag indicating whether the last input event was consumed by the manager. This is used to decide if we
+ * will forward the event to the next LogicalLayer.
+ */
+ protected AtomicBoolean _inputConsumed = new AtomicBoolean(false);
+
+ /**
+ * The widget currently active.
+ */
+ protected AbstractInteractWidget _activeWidget;
+
+ /**
+ * The current Spatial being targeted for interaction.
+ */
+ protected Spatial _spatialTarget;
+
+ /**
+ * Spatial state tracking.
+ */
+ protected SpatialState _state = new SpatialState();
+
+ /**
+ * List of filters to modify state prior to applying to a Spatial target.
+ */
+ protected List<UpdateFilter> _filters = Lists.newArrayList();
+
+ public InteractManager() {
+ setupLogicalLayer();
+ }
+
+ public void update(final ReadOnlyTimer timer) {
+ for (final AbstractInteractWidget widget : _widgets) {
+ if (!widget.isActiveUpdateOnly() || widget == _activeWidget) {
+ widget.update(timer, this);
+ }
+ }
+ }
+
+ public void render(final Renderer renderer) {
+ for (final AbstractInteractWidget widget : _widgets) {
+ if (!widget.isActiveRenderOnly() || widget == _activeWidget) {
+ widget.render(renderer, this);
+ }
+ }
+ }
+
+ protected void offerInputToWidgets(final Canvas source, final TwoInputStates inputStates) {
+ if (_activeWidget != null) {
+ _activeWidget.processInput(source, inputStates, _inputConsumed, this);
+ }
+
+ if (!_inputConsumed.get()) {
+ for (final AbstractInteractWidget widget : _widgets) {
+ if (widget != _activeWidget && !widget.isActiveInputOnly()) {
+ widget.processInput(source, inputStates, _inputConsumed, this);
+ if (_inputConsumed.get()) {
+ break;
+ }
+ }
+ }
+ }
+
+ if (_spatialTarget != null && _inputConsumed.get()) {
+ // apply any filters to our state
+ for (final UpdateFilter filter : _filters) {
+ filter.applyFilter(this);
+ }
+
+ // apply state to target
+ _state.applyState(_spatialTarget);
+
+ // fire update event
+ fireTargetDataUpdated();
+ }
+ }
+
+ /**
+ * Set up our logical layer with a trigger that hands input to the manager and saves whether it was "consumed".
+ */
+ private void setupLogicalLayer() {
+ _logicalLayer.registerTrigger(new InputTrigger(new Predicate<TwoInputStates>() {
+ public boolean apply(final TwoInputStates arg0) {
+ // always trigger this.
+ return true;
+ }
+ }, new TriggerAction() {
+ public void perform(final Canvas source, final TwoInputStates inputStates, final double tpf) {
+ if (_spatialTarget != null) {
+ _state.getTransform().set(_spatialTarget.getTransform());
+ }
+ _inputConsumed.set(false);
+ offerInputToWidgets(source, inputStates);
+ }
+ }));
+ }
+
+ /**
+ * Convenience method for setting up the manager's connection to the Ardor3D input system, along with a forwarding
+ * address for input events that the manager does not care about.
+ *
+ * @param canvas
+ * the canvas to register with
+ * @param physicalLayer
+ * the physical layer to register with
+ * @param forwardTo
+ * a LogicalLayer to send unconsumed (by the manager) input events to.
+ */
+ public void setupInput(final Canvas canvas, final PhysicalLayer physicalLayer, final LogicalLayer forwardTo) {
+ // Set up this logical layer to listen for events from the given canvas and PhysicalLayer
+ _logicalLayer.registerInput(canvas, physicalLayer);
+
+ // Set up forwarding for events not consumed.
+ if (forwardTo != null) {
+ _logicalLayer.setApplier(new BasicTriggersApplier() {
+
+ @Override
+ public void checkAndPerformTriggers(final Set<InputTrigger> triggers, final Canvas source,
+ final TwoInputStates states, final double tpf) {
+ super.checkAndPerformTriggers(triggers, source, states, tpf);
+
+ if (!_inputConsumed.get()) {
+ // nothing consumed
+ forwardTo.getApplier().checkAndPerformTriggers(forwardTo.getTriggers(), source, states, tpf);
+ } else {
+ // consumed, do nothing.
+ }
+ }
+ });
+ }
+ }
+
+ public void addWidget(final AbstractInteractWidget widget) {
+ _widgets.add(widget);
+ }
+
+ public void removeWidget(final AbstractInteractWidget widget) {
+ if (_activeWidget == widget) {
+ _activeWidget = _widgets.isEmpty() ? null : _widgets.get(0);
+ }
+ _widgets.remove(widget);
+ }
+
+ public void clearWidgets() {
+ _widgets.clear();
+ }
+
+ public void addFilter(final UpdateFilter filter) {
+ _filters.add(filter);
+ }
+
+ public void removeFilter(final UpdateFilter filter) {
+ _filters.remove(filter);
+ }
+
+ public void clearFilters() {
+ _filters.clear();
+ }
+
+ public LogicalLayer getLogicalLayer() {
+ return _logicalLayer;
+ }
+
+ public void setActiveWidget(final AbstractInteractWidget widget) {
+ _activeWidget = widget;
+ }
+
+ public AbstractInteractWidget getActiveWidget() {
+ return _activeWidget;
+ }
+
+ public void setSpatialTarget(final Spatial target) {
+ if (_spatialTarget != target) {
+ _spatialTarget = target;
+ fireTargetChanged();
+ }
+ }
+
+ public void fireTargetChanged() {
+ for (final AbstractInteractWidget widget : _widgets) {
+ widget.targetChanged(this);
+ }
+ }
+
+ public void fireTargetDataUpdated() {
+ for (final AbstractInteractWidget widget : _widgets) {
+ widget.targetDataUpdated(this);
+ }
+ }
+
+ public Spatial getSpatialTarget() {
+ return _spatialTarget;
+ }
+
+ public SpatialState getSpatialState() {
+ return _state;
+ }
+
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/data/SpatialState.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/data/SpatialState.java
new file mode 100644
index 0000000..d96ab5a
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/data/SpatialState.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact.data;
+
+import com.ardor3d.math.Transform;
+import com.ardor3d.scenegraph.Spatial;
+
+public class SpatialState {
+
+ protected Transform _transform = new Transform();
+
+ public Transform getTransform() {
+ return _transform;
+ }
+
+ public void applyState(final Spatial target) {
+ target.setTransform(_transform);
+ }
+
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/AllowScaleFilter.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/AllowScaleFilter.java
new file mode 100644
index 0000000..a9798ce
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/AllowScaleFilter.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact.filter;
+
+import com.ardor3d.extension.interact.InteractManager;
+import com.ardor3d.extension.interact.data.SpatialState;
+import com.ardor3d.math.type.ReadOnlyVector3;
+
+public class AllowScaleFilter implements UpdateFilter {
+
+ protected boolean _xAxis, _yAxis, _zAxis;
+
+ public AllowScaleFilter(final boolean xAxis, final boolean yAxis, final boolean zAxis) {
+ _xAxis = xAxis;
+ _yAxis = yAxis;
+ _zAxis = zAxis;
+ }
+
+ @Override
+ public void applyFilter(final InteractManager manager) {
+ final ReadOnlyVector3 oldScale = manager.getSpatialTarget().getScale();
+ final SpatialState state = manager.getSpatialState();
+ final ReadOnlyVector3 scale = state.getTransform().getScale();
+
+ state.getTransform().setScale( //
+ _xAxis ? scale.getX() : oldScale.getX(), //
+ _yAxis ? scale.getY() : oldScale.getY(), //
+ _zAxis ? scale.getZ() : oldScale.getZ());
+ }
+
+ @Override
+ public void beginDrag(final InteractManager manager) {}
+
+ @Override
+ public void endDrag(final InteractManager manager) {}
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/MinMaxScaleFilter.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/MinMaxScaleFilter.java
new file mode 100644
index 0000000..7336bab
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/MinMaxScaleFilter.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact.filter;
+
+import com.ardor3d.extension.interact.InteractManager;
+import com.ardor3d.extension.interact.data.SpatialState;
+import com.ardor3d.math.MathUtils;
+import com.ardor3d.math.Vector3;
+import com.ardor3d.math.type.ReadOnlyVector3;
+
+public class MinMaxScaleFilter implements UpdateFilter {
+ protected Vector3 _minScale = new Vector3();
+ protected Vector3 _maxScale = new Vector3();
+
+ public MinMaxScaleFilter(final double min, final double max) {
+ _minScale.set(min, min, min);
+ _maxScale.set(max, max, max);
+ }
+
+ public MinMaxScaleFilter(final ReadOnlyVector3 min, final ReadOnlyVector3 max) {
+ _minScale.set(min);
+ _maxScale.set(max);
+ }
+
+ @Override
+ public void applyFilter(final InteractManager manager) {
+ final SpatialState state = manager.getSpatialState();
+ final ReadOnlyVector3 scale = state.getTransform().getScale();
+ final double x = MathUtils.clamp(scale.getX(), _minScale.getX(), _maxScale.getX());
+ final double y = MathUtils.clamp(scale.getY(), _minScale.getY(), _maxScale.getY());
+ final double z = MathUtils.clamp(scale.getZ(), _minScale.getZ(), _maxScale.getZ());
+
+ state.getTransform().setScale(x, y, z);
+ }
+
+ @Override
+ public void beginDrag(final InteractManager manager) {}
+
+ @Override
+ public void endDrag(final InteractManager manager) {}
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/PlaneBoundaryFilter.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/PlaneBoundaryFilter.java
new file mode 100644
index 0000000..00f28c5
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/PlaneBoundaryFilter.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact.filter;
+
+import java.util.Arrays;
+
+import com.ardor3d.extension.interact.InteractManager;
+import com.ardor3d.extension.interact.data.SpatialState;
+import com.ardor3d.math.Vector3;
+import com.ardor3d.math.type.ReadOnlyPlane;
+
+public class PlaneBoundaryFilter implements UpdateFilter {
+ private final ReadOnlyPlane[] _planes;
+ private final Vector3 _calcVectorA = new Vector3();
+ private final Vector3 _calcVectorB = new Vector3();
+
+ public PlaneBoundaryFilter(final ReadOnlyPlane... planes) {
+ _planes = Arrays.copyOf(planes, planes.length);
+ }
+
+ @Override
+ public void applyFilter(final InteractManager manager) {
+ final SpatialState state = manager.getSpatialState();
+ _calcVectorA.set(state.getTransform().getTranslation());
+ for (final ReadOnlyPlane plane : _planes) {
+ final double distance = plane.pseudoDistance(_calcVectorA);
+ if (distance < 0) {
+ // push us back to the plane.
+ _calcVectorB.set(plane.getNormal()).multiplyLocal(-distance);
+ _calcVectorA.addLocal(_calcVectorB);
+ }
+ }
+
+ state.getTransform().setTranslation(_calcVectorA);
+ }
+
+ @Override
+ public void beginDrag(final InteractManager manager) {}
+
+ @Override
+ public void endDrag(final InteractManager manager) {}
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/UpdateFilter.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/UpdateFilter.java
new file mode 100644
index 0000000..5f1e336
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/filter/UpdateFilter.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact.filter;
+
+import com.ardor3d.extension.interact.InteractManager;
+import com.ardor3d.extension.interact.data.SpatialState;
+
+/**
+ * Filter used to modify {@link SpatialState} information prior to it being applied to a Spatial by the
+ * {@link InteractManager}.
+ */
+public interface UpdateFilter {
+
+ /**
+ * Called after a successful application of mouse/key input.
+ *
+ * @param manager
+ */
+ void applyFilter(InteractManager manager);
+
+ /**
+ * Callback for when a control begins a drag operation.
+ *
+ * @param manager
+ */
+ void beginDrag(InteractManager manager);
+
+ /**
+ * Callback for when a control ends a drag operation.
+ *
+ * @param manager
+ */
+ void endDrag(InteractManager manager);
+
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/AbstractInteractWidget.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/AbstractInteractWidget.java
new file mode 100644
index 0000000..39fbbc5
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/AbstractInteractWidget.java
@@ -0,0 +1,197 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact.widget;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.ardor3d.extension.interact.InteractManager;
+import com.ardor3d.extension.interact.filter.UpdateFilter;
+import com.ardor3d.framework.Canvas;
+import com.ardor3d.input.MouseButton;
+import com.ardor3d.input.logical.TwoInputStates;
+import com.ardor3d.intersection.PickingUtil;
+import com.ardor3d.intersection.PrimitivePickResults;
+import com.ardor3d.math.Ray3;
+import com.ardor3d.math.Vector2;
+import com.ardor3d.math.Vector3;
+import com.ardor3d.renderer.Camera;
+import com.ardor3d.renderer.Renderer;
+import com.ardor3d.scenegraph.Node;
+import com.ardor3d.util.ReadOnlyTimer;
+import com.google.common.collect.Lists;
+
+public abstract class AbstractInteractWidget {
+
+ protected Node _handle;
+ protected boolean _flipPickRay, _dragging = false;
+ protected MouseButton _dragButton = MouseButton.LEFT;
+
+ protected boolean _activeInputOnly = true;
+ protected boolean _activeRenderOnly = true;
+ protected boolean _activeUpdateOnly = true;
+
+ protected Ray3 _calcRay = new Ray3();
+ protected final Vector3 _calcVec3A = new Vector3();
+ protected final Vector3 _calcVec3B = new Vector3();
+ protected final Vector3 _calcVec3C = new Vector3();
+ protected final Vector3 _calcVec3D = new Vector3();
+ protected PrimitivePickResults _results = new PrimitivePickResults();
+
+ protected InteractMatrix _interactMatrix = InteractMatrix.World;
+
+ /**
+ * List of filters to modify state after applying input.
+ */
+ protected List<UpdateFilter> _filters = Lists.newArrayList();
+
+ public AbstractInteractWidget() {
+ _results.setCheckDistance(true);
+ }
+
+ /**
+ * Use the given inputstates to determine if and how to activate this widget. If the widget uses the given input,
+ * inputConsumed should be set to "true" and applyFilters should be called by this method.
+ *
+ * @param source
+ * the canvas that is our input source.
+ * @param inputStates
+ * the current and previous state of our input devices.
+ * @param inputConsumed
+ * an atomic boolean used to indicate back to the caller of this function that we have consumed the given
+ * inputStates. If set to true, no other widgets will be offered this input, nor will any other scene
+ * input triggers attached to the manager.
+ * @param manager
+ * our interact manager.
+ */
+ public void processInput(final Canvas source, final TwoInputStates inputStates, final AtomicBoolean inputConsumed,
+ final InteractManager manager) {}
+
+ protected void applyFilters(final InteractManager manager) {
+ // apply any filters to our state
+ for (final UpdateFilter filter : _filters) {
+ filter.applyFilter(manager);
+ }
+ }
+
+ public void beginDrag(final InteractManager manager) {
+ _dragging = true;
+ for (final UpdateFilter filter : _filters) {
+ filter.beginDrag(manager);
+ }
+ }
+
+ public void endDrag(final InteractManager manager) {
+ _dragging = false;
+ for (final UpdateFilter filter : _filters) {
+ filter.endDrag(manager);
+ }
+ }
+
+ public void update(final ReadOnlyTimer timer, final InteractManager manager) {
+ _handle.updateGeometricState(timer.getTimePerFrame());
+ }
+
+ public void render(final Renderer renderer, final InteractManager manager) {}
+
+ public void targetChanged(final InteractManager manager) {}
+
+ public void targetDataUpdated(final InteractManager manager) {}
+
+ public void receivedControl(final InteractManager manager) {
+ if (_dragging) {
+ endDrag(manager);
+ }
+ }
+
+ public void lostControl(final InteractManager manager) {}
+
+ public boolean isActiveInputOnly() {
+ return _activeInputOnly;
+ }
+
+ public void setActiveInputOnly(final boolean activeOnly) {
+ _activeInputOnly = activeOnly;
+ }
+
+ public boolean isActiveRenderOnly() {
+ return _activeRenderOnly;
+ }
+
+ public void setActiveRenderOnly(final boolean activeOnly) {
+ _activeRenderOnly = activeOnly;
+ }
+
+ public boolean isActiveUpdateOnly() {
+ return _activeUpdateOnly;
+ }
+
+ public void setActiveUpdateOnly(final boolean activeOnly) {
+ _activeUpdateOnly = activeOnly;
+ }
+
+ public boolean isFlipPickRay() {
+ return _flipPickRay;
+ }
+
+ public void setFlipPickRay(final boolean flip) {
+ _flipPickRay = flip;
+ }
+
+ public MouseButton getDragButton() {
+ return _dragButton;
+ }
+
+ public void setDragButton(final MouseButton button) {
+ _dragButton = button;
+ }
+
+ public Node getHandle() {
+ return _handle;
+ }
+
+ protected Vector3 getLastPick() {
+ if (_results.getNumber() > 0 && _results.getPickData(0).getIntersectionRecord().getNumberOfIntersections() > 0) {
+ return _results.getPickData(0).getIntersectionRecord().getIntersectionPoint(0);
+ }
+ return null;
+ }
+
+ protected void findPick(final Vector2 mouseLoc, final Camera camera) {
+ getPickRay(mouseLoc, camera);
+ _results.clear();
+ PickingUtil.findPick(_handle, _calcRay, _results);
+ }
+
+ protected void getPickRay(final Vector2 mouseLoc, final Camera camera) {
+ camera.getPickRay(mouseLoc, _flipPickRay, _calcRay);
+ }
+
+ public void setInteractMatrix(final InteractMatrix matrix) {
+ _interactMatrix = matrix;
+ }
+
+ public InteractMatrix getInteractMatrix() {
+ return _interactMatrix;
+ }
+
+ public void addFilter(final UpdateFilter filter) {
+ _filters.add(filter);
+ }
+
+ public void removeFilter(final UpdateFilter filter) {
+ _filters.remove(filter);
+ }
+
+ public void clearFilters() {
+ _filters.clear();
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/CompoundInteractWidget.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/CompoundInteractWidget.java
new file mode 100644
index 0000000..2018f8c
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/CompoundInteractWidget.java
@@ -0,0 +1,341 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact.widget;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.ardor3d.extension.interact.InteractManager;
+import com.ardor3d.extension.interact.filter.UpdateFilter;
+import com.ardor3d.extension.interact.widget.MovePlanarWidget.MovePlane;
+import com.ardor3d.framework.Canvas;
+import com.ardor3d.image.Texture2D;
+import com.ardor3d.input.ButtonState;
+import com.ardor3d.input.MouseState;
+import com.ardor3d.input.logical.TwoInputStates;
+import com.ardor3d.math.Vector2;
+import com.ardor3d.math.type.ReadOnlyColorRGBA;
+import com.ardor3d.renderer.Camera;
+import com.ardor3d.renderer.Renderer;
+import com.ardor3d.scenegraph.Node;
+import com.ardor3d.scenegraph.Spatial;
+import com.ardor3d.util.ReadOnlyTimer;
+import com.google.common.collect.Maps;
+
+public class CompoundInteractWidget extends AbstractInteractWidget {
+ private static final String MOVE_KEY = "Move";
+ private static final String ROTATE_KEY = "Rotate";
+ private static final String MOVE_PLANAR_KEY = "MovePlanar";
+ private static final String MOVE_MULTIPLANAR_KEY = "MoveMultiPlanar";
+
+ public static double MIN_SCALE = 0.000001;
+
+ protected Map<String, AbstractInteractWidget> _widgets = Maps.newHashMap();
+
+ protected AbstractInteractWidget _lastInputWidget = null;
+
+ protected InteractMatrix _interactMatrix;
+
+ public CompoundInteractWidget() {
+ _handle = new Node("handleRoot");
+ }
+
+ @Override
+ public void addFilter(final UpdateFilter filter) {
+ for(final AbstractInteractWidget widget : _widgets.values()) {
+ widget.addFilter(filter);
+ }
+ super.addFilter(filter);
+ }
+
+ @Override
+ public void removeFilter(final UpdateFilter filter) {
+ for(final AbstractInteractWidget widget : _widgets.values()) {
+ widget.removeFilter(filter);
+ }
+ super.removeFilter(filter);
+ }
+
+ @Override
+ public void clearFilters() {
+ for(final AbstractInteractWidget widget : _widgets.values()) {
+ widget.clearFilters();
+ }
+ super.clearFilters();
+ }
+
+ public CompoundInteractWidget withMoveXAxis() {
+ verifyMoveWidget().withXAxis();
+ return this;
+ }
+
+ public CompoundInteractWidget withMoveXAxis(final ReadOnlyColorRGBA color) {
+ verifyMoveWidget().withXAxis(color);
+ return this;
+ }
+
+ public CompoundInteractWidget withMoveXAxis(final ReadOnlyColorRGBA color, final double scale, final double width,
+ final double lengthGap, final double tipGap) {
+ verifyMoveWidget().withXAxis(color, scale, width, lengthGap, tipGap);
+ return this;
+ }
+
+ public CompoundInteractWidget withMoveYAxis() {
+ verifyMoveWidget().withYAxis();
+ return this;
+ }
+
+ public CompoundInteractWidget withMoveYAxis(final ReadOnlyColorRGBA color) {
+ verifyMoveWidget().withYAxis(color);
+ return this;
+ }
+
+ public CompoundInteractWidget withMoveYAxis(final ReadOnlyColorRGBA color, final double scale, final double width,
+ final double lengthGap, final double tipGap) {
+ verifyMoveWidget().withYAxis(color, scale, width, lengthGap, tipGap);
+ return this;
+ }
+
+ public CompoundInteractWidget withMoveZAxis() {
+ verifyMoveWidget().withZAxis();
+ return this;
+ }
+
+ public CompoundInteractWidget withMoveZAxis(final ReadOnlyColorRGBA color) {
+ verifyMoveWidget().withZAxis(color);
+ return this;
+ }
+
+ public CompoundInteractWidget withMoveZAxis(final ReadOnlyColorRGBA color, final double scale, final double width,
+ final double lengthGap, final double tipGap) {
+ verifyMoveWidget().withZAxis(color, scale, width, lengthGap, tipGap);
+ return this;
+ }
+
+ public CompoundInteractWidget withRotateXAxis() {
+ verifyRotateWidget().withXAxis();
+ return this;
+ }
+
+ public CompoundInteractWidget withRotateXAxis(final ReadOnlyColorRGBA color) {
+ verifyRotateWidget().withXAxis(color);
+ return this;
+ }
+
+ public CompoundInteractWidget withRotateXAxis(final ReadOnlyColorRGBA color, final float scale, final float width) {
+ verifyRotateWidget().withXAxis(color, scale, width);
+ return this;
+ }
+
+ public CompoundInteractWidget withRotateYAxis() {
+ verifyRotateWidget().withYAxis();
+ return this;
+ }
+
+ public CompoundInteractWidget withRotateYAxis(final ReadOnlyColorRGBA color) {
+ verifyRotateWidget().withYAxis(color);
+ return this;
+ }
+
+ public CompoundInteractWidget withRotateYAxis(final ReadOnlyColorRGBA color, final float scale, final float width) {
+ verifyRotateWidget().withYAxis(color, scale, width);
+ return this;
+ }
+
+ public CompoundInteractWidget withRotateZAxis() {
+ verifyRotateWidget().withZAxis();
+ return this;
+ }
+
+ public CompoundInteractWidget withRotateZAxis(final ReadOnlyColorRGBA color) {
+ verifyRotateWidget().withZAxis(color);
+ return this;
+ }
+
+ public CompoundInteractWidget withRotateZAxis(final ReadOnlyColorRGBA color, final float scale, final float width) {
+ verifyRotateWidget().withZAxis(color, scale, width);
+ return this;
+ }
+
+ public CompoundInteractWidget withRingTexture(final Texture2D texture) {
+ verifyRotateWidget().setTexture(texture);
+ return this;
+ }
+
+ public CompoundInteractWidget withMultiPlanarHandle() {
+ MoveMultiPlanarWidget widget = (MoveMultiPlanarWidget) _widgets
+ .get(CompoundInteractWidget.MOVE_MULTIPLANAR_KEY);
+ if (widget != null) {
+ widget.getHandle().removeFromParent();
+ }
+
+ widget = new MoveMultiPlanarWidget();
+ _widgets.put(CompoundInteractWidget.MOVE_MULTIPLANAR_KEY, widget);
+ _handle.attachChild(widget.getHandle());
+
+ return this;
+ }
+
+ public CompoundInteractWidget withMultiPlanarHandle(final double extent) {
+ MoveMultiPlanarWidget widget = (MoveMultiPlanarWidget) _widgets
+ .get(CompoundInteractWidget.MOVE_MULTIPLANAR_KEY);
+ if (widget != null) {
+ widget.getHandle().removeFromParent();
+ }
+
+ widget = new MoveMultiPlanarWidget(extent);
+ _widgets.put(CompoundInteractWidget.MOVE_MULTIPLANAR_KEY, widget);
+ _handle.attachChild(widget.getHandle());
+
+ return this;
+ }
+
+ public CompoundInteractWidget withPlanarHandle(final MovePlane plane, final ReadOnlyColorRGBA color) {
+ MovePlanarWidget widget = (MovePlanarWidget) _widgets.get(CompoundInteractWidget.MOVE_PLANAR_KEY);
+ if (widget != null) {
+ widget.getHandle().removeFromParent();
+ }
+
+ widget = new MovePlanarWidget().withPlane(plane).withDefaultHandle(.5, .25, color);
+ _widgets.put(CompoundInteractWidget.MOVE_PLANAR_KEY, widget);
+ _handle.attachChild(widget.getHandle());
+
+ return this;
+ }
+
+ public CompoundInteractWidget withPlanarHandle(final MovePlane plane, final double radius, final double height,
+ final ReadOnlyColorRGBA color) {
+ MovePlanarWidget widget = (MovePlanarWidget) _widgets.get(CompoundInteractWidget.MOVE_PLANAR_KEY);
+ if (widget != null) {
+ widget.getHandle().removeFromParent();
+ }
+
+ widget = new MovePlanarWidget().withPlane(plane).withDefaultHandle(radius, height, color);
+ _widgets.put(CompoundInteractWidget.MOVE_PLANAR_KEY, widget);
+ _handle.attachChild(widget.getHandle());
+
+ return this;
+ }
+
+ private MoveWidget verifyMoveWidget() {
+ MoveWidget moveWidget = (MoveWidget) _widgets.get(CompoundInteractWidget.MOVE_KEY);
+ if (moveWidget == null) {
+ moveWidget = new MoveWidget();
+ _widgets.put(CompoundInteractWidget.MOVE_KEY, moveWidget);
+ _handle.attachChild(moveWidget.getHandle());
+ }
+ return moveWidget;
+ }
+
+ private RotateWidget verifyRotateWidget() {
+ RotateWidget rotateWidget = (RotateWidget) _widgets.get(CompoundInteractWidget.ROTATE_KEY);
+ if (rotateWidget == null) {
+ rotateWidget = new RotateWidget();
+ _widgets.put(CompoundInteractWidget.ROTATE_KEY, rotateWidget);
+ _handle.attachChild(rotateWidget.getHandle());
+ }
+ return rotateWidget;
+ }
+
+ @Override
+ public void targetChanged(final InteractManager manager) {
+ for (final AbstractInteractWidget widget : _widgets.values()) {
+ widget.targetChanged(manager);
+ }
+ }
+
+ @Override
+ public void targetDataUpdated(final InteractManager manager) {
+ for (final AbstractInteractWidget widget : _widgets.values()) {
+ widget.targetDataUpdated(manager);
+ }
+ }
+
+ @Override
+ public void receivedControl(final InteractManager manager) {
+ for (final AbstractInteractWidget widget : _widgets.values()) {
+ widget.receivedControl(manager);
+ }
+ }
+
+ @Override
+ public void render(final Renderer renderer, final InteractManager manager) {
+ for (final AbstractInteractWidget widget : _widgets.values()) {
+ widget.render(renderer, manager);
+ }
+ }
+
+ @Override
+ public void processInput(final Canvas source, final TwoInputStates inputStates, final AtomicBoolean inputConsumed,
+ final InteractManager manager) {
+ // Make sure we have something to modify
+ if (manager.getSpatialTarget() == null) {
+ return;
+ }
+ // Make sure we are dragging.
+ final MouseState current = inputStates.getCurrent().getMouseState();
+ final MouseState previous = inputStates.getPrevious().getMouseState();
+
+ if (current.getButtonState(_dragButton) != ButtonState.DOWN) {
+ if (_lastInputWidget != null) {
+ _lastInputWidget.processInput(source, inputStates, inputConsumed, manager);
+ _lastInputWidget = null;
+ }
+ return;
+ }
+
+ if (_lastInputWidget == null) {
+ final Camera camera = source.getCanvasRenderer().getCamera();
+ final Vector2 oldMouse = new Vector2(previous.getX(), previous.getY());
+ findPick(oldMouse, camera);
+ if (_results.getNumber() <= 0) {
+ return;
+ }
+
+ final Spatial picked = (Spatial) _results.getPickData(0).getTarget();
+ if (picked == null) {
+ return;
+ }
+
+ for (final AbstractInteractWidget widget : _widgets.values()) {
+ if (picked.hasAncestor(widget.getHandle())) {
+ _lastInputWidget = widget;
+ break;
+ }
+ }
+ }
+ _lastInputWidget.processInput(source, inputStates, inputConsumed, manager);
+
+ // apply our filters, if any, now that we've made updates.
+ applyFilters(manager);
+ }
+
+ @Override
+ public void setInteractMatrix(final InteractMatrix matrix) {
+ _interactMatrix = matrix;
+ for (final AbstractInteractWidget widget : _widgets.values()) {
+ widget.setInteractMatrix(matrix);
+ }
+ }
+
+ @Override
+ public InteractMatrix getInteractMatrix() {
+ return _interactMatrix;
+ }
+
+ @Override
+ public void update(final ReadOnlyTimer timer, final InteractManager manager) {
+ for (final AbstractInteractWidget widget : _widgets.values()) {
+ widget.update(timer, manager);
+ }
+ }
+
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/InteractArrow.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/InteractArrow.java
new file mode 100644
index 0000000..0fb6fa6
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/InteractArrow.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact.widget;
+
+import java.io.IOException;
+
+import com.ardor3d.math.MathUtils;
+import com.ardor3d.math.Quaternion;
+import com.ardor3d.scenegraph.shape.Arrow;
+import com.ardor3d.scenegraph.shape.Cylinder;
+import com.ardor3d.scenegraph.shape.Pyramid;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+
+/**
+ * <code>InteractArrow</code> is basically a cylinder with a pyramid on top. It extends the basic Arrow shape to include
+ * a customizable gap between arrow head and base, and base and origin. This shape points along the +zaxis instead.
+ */
+public class InteractArrow extends Arrow {
+
+ protected double _lengthGap = 0;
+ protected double _tipGap = 0;
+
+ protected static final Quaternion rotator = new Quaternion().applyRotationX(MathUtils.HALF_PI);
+
+ public InteractArrow() {}
+
+ public InteractArrow(final String name) {
+ this(name, 1, .25);
+ }
+
+ public InteractArrow(final String name, final double length, final double width) {
+ this(name, length, width, 0, 0);
+ }
+
+ public InteractArrow(final String name, final double length, final double width, final double lengthGap,
+ final double tipGap) {
+ super(name);
+ _length = length;
+ _width = width;
+ _lengthGap = lengthGap;
+ _tipGap = tipGap;
+
+ buildArrow();
+ }
+
+ @Override
+ public void buildArrow() {
+ detachAllChildren();
+
+ // Start with cylinder base:
+ final Cylinder base = new Cylinder("base", 4, 16, _width * 0.75, _length - _lengthGap);
+ base.getMeshData().translatePoints(0, 0, (_lengthGap + _length) * 0.5);
+ attachChild(base);
+ base.updateModelBound();
+
+ // Add the pyramid tip.
+ final double tipLength = _length / 2.0;
+ final Pyramid tip = new Pyramid("tip", 2 * _width, tipLength);
+ tip.getMeshData().translatePoints(0, _tipGap + _length + 0.5 * tipLength, 0);
+ tip.getMeshData().rotatePoints(InteractArrow.rotator);
+ tip.getMeshData().rotateNormals(InteractArrow.rotator);
+
+ attachChild(tip);
+ tip.updateModelBound();
+
+ }
+
+ public double getLengthGap() {
+ return _lengthGap;
+ }
+
+ public void setLengthGap(final double lengthGap) {
+ _lengthGap = lengthGap;
+ }
+
+ public double getTipGap() {
+ return _tipGap;
+ }
+
+ public void setTipGap(final double tipGap) {
+ _tipGap = tipGap;
+ }
+
+ @Override
+ public void write(final OutputCapsule capsule) throws IOException {
+ super.write(capsule);
+ capsule.write(_lengthGap, "lengthGap", 0);
+ capsule.write(_tipGap, "tipGap", 0);
+ }
+
+ @Override
+ public void read(final InputCapsule capsule) throws IOException {
+ super.read(capsule);
+ _lengthGap = capsule.readDouble("lengthGap", 0);
+ _tipGap = capsule.readDouble("tipGap", 0);
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/InteractMatrix.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/InteractMatrix.java
new file mode 100644
index 0000000..2574770
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/InteractMatrix.java
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact.widget;
+
+public enum InteractMatrix {
+ World, Local;
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/InteractRing.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/InteractRing.java
new file mode 100644
index 0000000..be5a53f
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/InteractRing.java
@@ -0,0 +1,188 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact.widget;
+
+import java.io.IOException;
+import java.nio.FloatBuffer;
+
+import com.ardor3d.image.Texture2D;
+import com.ardor3d.math.MathUtils;
+import com.ardor3d.renderer.IndexMode;
+import com.ardor3d.renderer.state.RenderState;
+import com.ardor3d.renderer.state.TextureState;
+import com.ardor3d.scenegraph.FloatBufferData;
+import com.ardor3d.scenegraph.Mesh;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+import com.ardor3d.util.geom.BufferUtils;
+
+/**
+ * Textured ring geometry, intended for use as a rotational handle.
+ */
+public class InteractRing extends Mesh {
+ protected float _innerRadius, _outerRadius;
+ protected int _tessRings = 2;
+ protected int _tessSteps = 32;
+ protected float _texMul = 4.0f;
+ protected float _concaveValue = 0;
+
+ public InteractRing() {}
+
+ public InteractRing(final String name, final int tessRings, final int tessSteps, final float radius,
+ final float width) {
+ this(name, tessRings, tessSteps, radius, width, 0);
+ }
+
+ public InteractRing(final String name, final int tessRings, final int tessSteps, final float radius,
+ final float width, final float concaveValue) {
+ super(name);
+ _tessRings = tessRings;
+ _tessSteps = tessSteps;
+ _concaveValue = concaveValue;
+ setRadius(radius, width);
+ }
+
+ public void setRadius(final float radius, final float width) {
+ _innerRadius = radius;
+ _outerRadius = radius + width;
+ updateGeometry();
+ }
+
+ /**
+ * @param vMult
+ * new multiplier for v direction of texture coords (around ring)
+ */
+ public void setTextureMultiplier(final float vMult) {
+ _texMul = vMult;
+ updateGeometry();
+ }
+
+ public void setConcaveValue(final float value) {
+ _concaveValue = value;
+ updateGeometry();
+ }
+
+ /**
+ * Convenience method for setting texture without managing TextureState.
+ *
+ * @param texture
+ * the new texture to set on unit 0.
+ */
+ public void setTexture(final Texture2D texture) {
+ TextureState ts = (TextureState) getLocalRenderState(RenderState.StateType.Texture);
+ if (ts == null) {
+ ts = new TextureState();
+ ts.setEnabled(true);
+ setRenderState(ts);
+ }
+ ts.setTexture(texture, 0);
+ }
+
+ /**
+ *
+ */
+ public void updateGeometry() {
+ final int numPairs = _tessSteps + 1;
+ final int totalVerts = _tessRings * numPairs * 2;
+
+ FloatBuffer crdBuf = getMeshData().getVertexBuffer();
+ if (crdBuf == null || totalVerts != crdBuf.limit() / 3) { // allocate new buffers
+ getMeshData().setVertexBuffer(BufferUtils.createFloatBuffer(totalVerts * 3));
+ getMeshData().setNormalBuffer(BufferUtils.createFloatBuffer(totalVerts * 3));
+ getMeshData().setTextureCoords(new FloatBufferData(BufferUtils.createFloatBuffer(totalVerts * 2), 2), 0);
+ crdBuf = getMeshData().getVertexBuffer();
+ }
+ final FloatBuffer nrmBuf = getMeshData().getNormalBuffer();
+ final FloatBufferData tc = getMeshData().getTextureCoords(0);
+ final FloatBuffer txcBuf = tc.getBuffer();
+ calculateVertexData(_tessRings, numPairs, totalVerts, crdBuf, nrmBuf, txcBuf);
+
+ updateModelBound();
+ }
+
+ protected void normalize(final int i, final float[] nrm) {
+ final float length = (float) MathUtils
+ .sqrt(nrm[i] * nrm[i] + nrm[i + 1] * nrm[i + 1] + nrm[i + 2] * nrm[i + 2]);
+ nrm[i] /= length;
+ nrm[i + 1] /= length;
+ nrm[i + 2] /= length;
+ }
+
+ protected void calculateVertexData(final int numStrips, final int numPairs, final int totalVerts,
+ final FloatBuffer crdBuf, final FloatBuffer nrmBuf, final FloatBuffer txcBuf) {
+ // we are generating strips
+ getMeshData().setIndexMode(IndexMode.TriangleStrip);
+
+ final float astep = (float) (Math.PI * 2 / _tessSteps);
+ final float sstep = 1.0f / numStrips;
+ final float rrange = _outerRadius - _innerRadius;
+ final float rstep = rrange / numStrips;
+ float xa, ya;
+ float r0, r1;
+ float nadd0, nadd1;
+ float tc;
+ final float up = 1;
+ final float[] nrm = new float[6];
+ crdBuf.rewind();
+ nrmBuf.rewind();
+ txcBuf.rewind();
+ for (int s = 0; s < numStrips; s++) {
+ nadd0 = _concaveValue * (s + 0 - numStrips * 0.5f) / numStrips;
+ nadd1 = _concaveValue * (s + 1 - numStrips * 0.5f) / numStrips;
+ for (int a = 0; a < numPairs; a++) {
+ xa = (float) Math.cos(a * astep);
+ ya = (float) Math.sin(a * astep);
+ r0 = _innerRadius + (s + 0) * rstep;
+ r1 = _innerRadius + (s + 1) * rstep;
+
+ crdBuf.put(xa * r0).put(ya * r0).put(0);
+ crdBuf.put(xa * r1).put(ya * r1).put(0);
+
+ nrm[0] = nadd0 * xa;
+ nrm[1] = nadd0 * ya;
+ nrm[2] = up;
+ nrm[3] = nadd1 * xa;
+ nrm[4] = nadd1 * ya;
+ nrm[5] = up;
+ normalize(0, nrm);
+ normalize(3, nrm);
+ nrmBuf.put(nrm[0]).put(nrm[1]).put(nrm[2]);
+ nrmBuf.put(nrm[3]).put(nrm[4]).put(nrm[5]);
+
+ tc = a * _texMul / _tessSteps;
+ txcBuf.put((s + 0) * sstep).put(tc);
+ txcBuf.put((s + 1) * sstep).put(tc);
+ }
+ }
+ }
+
+ @Override
+ public void write(final OutputCapsule capsule) throws IOException {
+ super.write(capsule);
+ capsule.write(_innerRadius, "innerRadius", 0f);
+ capsule.write(_outerRadius, "outerRadius", 0f);
+ capsule.write(_tessRings, "tessRings", 2);
+ capsule.write(_tessSteps, "tessSteps", 16);
+ capsule.write(_texMul, "texMul", 1f);
+ capsule.write(_concaveValue, "concaveValue", 0f);
+ }
+
+ @Override
+ public void read(final InputCapsule capsule) throws IOException {
+ super.read(capsule);
+ _innerRadius = capsule.readFloat("innerRadius", 0f);
+ _outerRadius = capsule.readFloat("outerRadius", 0f);
+ _tessRings = capsule.readInt("tessRings", 2);
+ _tessSteps = capsule.readInt("tessSteps", 16);
+ _texMul = capsule.readFloat("texMul", 1f);
+ _concaveValue = capsule.readFloat("concaveValue", 0f);
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/MoveMultiPlanarWidget.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/MoveMultiPlanarWidget.java
new file mode 100644
index 0000000..7cf7dd4
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/MoveMultiPlanarWidget.java
@@ -0,0 +1,230 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact.widget;
+
+import java.nio.FloatBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.ardor3d.extension.interact.InteractManager;
+import com.ardor3d.framework.Canvas;
+import com.ardor3d.input.ButtonState;
+import com.ardor3d.input.MouseState;
+import com.ardor3d.input.logical.TwoInputStates;
+import com.ardor3d.intersection.PickData;
+import com.ardor3d.intersection.PrimitiveKey;
+import com.ardor3d.math.ColorRGBA;
+import com.ardor3d.math.Matrix3;
+import com.ardor3d.math.Plane;
+import com.ardor3d.math.Transform;
+import com.ardor3d.math.Vector2;
+import com.ardor3d.math.Vector3;
+import com.ardor3d.renderer.Camera;
+import com.ardor3d.renderer.Renderer;
+import com.ardor3d.renderer.queue.RenderBucketType;
+import com.ardor3d.renderer.state.BlendState;
+import com.ardor3d.renderer.state.MaterialState;
+import com.ardor3d.renderer.state.MaterialState.ColorMaterial;
+import com.ardor3d.renderer.state.ShadingState;
+import com.ardor3d.renderer.state.ShadingState.ShadingMode;
+import com.ardor3d.renderer.state.ZBufferState;
+import com.ardor3d.renderer.state.ZBufferState.TestFunction;
+import com.ardor3d.scenegraph.Mesh;
+import com.ardor3d.scenegraph.Node;
+import com.ardor3d.scenegraph.Spatial;
+import com.ardor3d.scenegraph.shape.Box;
+import com.ardor3d.util.geom.BufferUtils;
+
+public class MoveMultiPlanarWidget extends AbstractInteractWidget {
+ public static double MIN_SCALE = 0.000001;
+
+ public MoveMultiPlanarWidget() {
+ this(0.5);
+ }
+
+ public MoveMultiPlanarWidget(final double extent) {
+ _handle = new Node("moveHandle");
+
+ final BlendState blend = new BlendState();
+ blend.setBlendEnabled(true);
+ _handle.setRenderState(blend);
+
+ final ZBufferState zstate = new ZBufferState();
+ zstate.setFunction(TestFunction.LessThanOrEqualTo);
+ _handle.setRenderState(zstate);
+
+ _handle.getSceneHints().setRenderBucketType(RenderBucketType.Transparent);
+ _handle.updateGeometricState(0);
+
+ createDefaultHandle(extent);
+ }
+
+ protected void createDefaultHandle(final double extent) {
+ final Box grip = new Box("grip", Vector3.ZERO, extent, extent, extent);
+ grip.updateModelBound();
+ _handle.attachChild(grip);
+
+ // setup some colors, just at the corner of the primitives since we will use flat shading.
+ grip.setSolidColor(ColorRGBA.WHITE);
+ final FloatBuffer colors = grip.getMeshData().getColorBuffer();
+ BufferUtils.setInBuffer(ColorRGBA.MAGENTA, colors, 0);
+ BufferUtils.setInBuffer(ColorRGBA.CYAN, colors, 4);
+ BufferUtils.setInBuffer(ColorRGBA.MAGENTA, colors, 8);
+ BufferUtils.setInBuffer(ColorRGBA.CYAN, colors, 12);
+ BufferUtils.setInBuffer(ColorRGBA.YELLOW, colors, 16);
+ BufferUtils.setInBuffer(ColorRGBA.YELLOW, colors, 20);
+
+ // set flat shading
+ final ShadingState shade = new ShadingState();
+ shade.setShadingMode(ShadingMode.Flat);
+ grip.setRenderState(shade);
+
+ // setup a material state to use the colors from the vertices.
+ final MaterialState material = new MaterialState();
+ material.setColorMaterial(ColorMaterial.Diffuse);
+ grip.setRenderState(material);
+ }
+
+ @Override
+ public void targetChanged(final InteractManager manager) {
+ if (_dragging) {
+ endDrag(manager);
+ }
+ final Spatial target = manager.getSpatialTarget();
+ if (target != null) {
+ _handle.setScale(Math.max(MoveMultiPlanarWidget.MIN_SCALE, target.getWorldBound().getRadius()
+ + target.getWorldTranslation().subtract(target.getWorldBound().getCenter(), _calcVec3A).length()));
+ }
+ targetDataUpdated(manager);
+ }
+
+ @Override
+ public void targetDataUpdated(final InteractManager manager) {
+ final Spatial target = manager.getSpatialTarget();
+ if (target == null) {
+ _handle.setScale(1.0);
+ _handle.setRotation(Matrix3.IDENTITY);
+ } else {
+ // update scale of widget using bounding radius
+ target.updateGeometricState(0);
+
+ // update arrow rotations from target
+ if (_interactMatrix == InteractMatrix.Local) {
+ _handle.setRotation(target.getWorldRotation());
+ } else {
+ _handle.setRotation(Matrix3.IDENTITY);
+ }
+ }
+ }
+
+ @Override
+ public void render(final Renderer renderer, final InteractManager manager) {
+ final Spatial spat = manager.getSpatialTarget();
+ if (spat == null) {
+ return;
+ }
+
+ _handle.setTranslation(spat.getWorldTranslation());
+ _handle.updateGeometricState(0);
+
+ renderer.draw(_handle);
+ }
+
+ @Override
+ public void processInput(final Canvas source, final TwoInputStates inputStates, final AtomicBoolean inputConsumed,
+ final InteractManager manager) {
+ // Make sure we have something to modify
+ if (manager.getSpatialTarget() == null) {
+ return;
+ }
+
+ // Make sure we are dragging.
+ final MouseState current = inputStates.getCurrent().getMouseState();
+ final MouseState previous = inputStates.getPrevious().getMouseState();
+
+ if (current.getButtonState(_dragButton) != ButtonState.DOWN) {
+ if (_dragging) {
+ endDrag(manager);
+ }
+ return;
+ }
+ // if we're already dragging, make sure we only act on drags that started with a positive pick.
+ else if (!current.getButtonsPressedSince(previous).contains(_dragButton) && !_dragging) {
+ return;
+ }
+
+ final Camera camera = source.getCanvasRenderer().getCamera();
+ final Vector2 oldMouse = new Vector2(previous.getX(), previous.getY());
+ // Make sure we are dragging over the handle
+ if (!_dragging) {
+ findPick(oldMouse, camera);
+ final Vector3 lastPick = getLastPick();
+ if (lastPick == null) {
+ return;
+ } else {
+ beginDrag(manager);
+ }
+ }
+
+ // we've established that our mouse is being held down, and started over our arrow. So consume.
+ inputConsumed.set(true);
+
+ // check if we've moved at all
+ if (current == previous || current.getDx() == 0 && current.getDy() == 0) {
+ return;
+ }
+
+ // act on drag
+ final PickData pickData = _results.getPickData(0);
+ final Spatial picked = (Spatial) pickData.getTarget();
+ if (picked instanceof Mesh && pickData.getIntersectionRecord().getNumberOfIntersections() > 0) {
+ final PrimitiveKey key = pickData.getIntersectionRecord().getIntersectionPrimitive(0);
+ ((Mesh) picked).getMeshData().getPrimitiveVertices(key.getPrimitiveIndex(), key.getSection(),
+ new Vector3[] { _calcVec3A, _calcVec3B, _calcVec3C });
+ picked.localToWorld(_calcVec3A, _calcVec3A);
+ picked.localToWorld(_calcVec3B, _calcVec3B);
+ picked.localToWorld(_calcVec3C, _calcVec3C);
+ final Vector3 loc = getNewOffset(oldMouse, current, camera, manager);
+ final Transform transform = manager.getSpatialState().getTransform();
+ transform.setTranslation(loc.addLocal(transform.getTranslation()));
+
+ // apply our filters, if any, now that we've made updates.
+ applyFilters(manager);
+ }
+ }
+
+ protected Vector3 getNewOffset(final Vector2 oldMouse, final MouseState current, final Camera camera,
+ final InteractManager manager) {
+
+ // make plane object
+ final Plane pickPlane = new Plane().setPlanePoints(_calcVec3A, _calcVec3B, _calcVec3C);
+
+ // find out where we were hitting the plane before
+ getPickRay(oldMouse, camera);
+ if (!_calcRay.intersectsPlane(pickPlane, _calcVec3A)) {
+ return _calcVec3A.zero();
+ }
+
+ // find out where we are hitting the plane now
+ getPickRay(new Vector2(current.getX(), current.getY()), camera);
+ if (!_calcRay.intersectsPlane(pickPlane, _calcVec3B)) {
+ return _calcVec3A.zero();
+ }
+
+ // convert to target coord space
+ final Node parent = manager.getSpatialTarget().getParent();
+ if (parent != null) {
+ parent.getWorldTransform().applyInverse(_calcVec3A);
+ parent.getWorldTransform().applyInverse(_calcVec3B);
+ }
+
+ return _calcVec3B.subtractLocal(_calcVec3A);
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/MovePlanarWidget.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/MovePlanarWidget.java
new file mode 100644
index 0000000..7389f7f
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/MovePlanarWidget.java
@@ -0,0 +1,240 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact.widget;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.ardor3d.extension.interact.InteractManager;
+import com.ardor3d.framework.Canvas;
+import com.ardor3d.input.ButtonState;
+import com.ardor3d.input.MouseState;
+import com.ardor3d.input.logical.TwoInputStates;
+import com.ardor3d.math.MathUtils;
+import com.ardor3d.math.Matrix3;
+import com.ardor3d.math.Plane;
+import com.ardor3d.math.Transform;
+import com.ardor3d.math.Vector2;
+import com.ardor3d.math.Vector3;
+import com.ardor3d.math.type.ReadOnlyColorRGBA;
+import com.ardor3d.renderer.Camera;
+import com.ardor3d.renderer.Renderer;
+import com.ardor3d.renderer.queue.RenderBucketType;
+import com.ardor3d.renderer.state.BlendState;
+import com.ardor3d.renderer.state.ZBufferState;
+import com.ardor3d.renderer.state.ZBufferState.TestFunction;
+import com.ardor3d.scenegraph.Node;
+import com.ardor3d.scenegraph.Spatial;
+import com.ardor3d.scenegraph.shape.Cylinder;
+
+public class MovePlanarWidget extends AbstractInteractWidget {
+ public static double MIN_SCALE = 0.000001;
+
+ protected MovePlane _plane = MovePlane.XZ;
+
+ public enum MovePlane {
+ XY, XZ, YZ
+ }
+
+ public MovePlanarWidget() {
+ _handle = new Node("moveHandle");
+
+ final BlendState blend = new BlendState();
+ blend.setBlendEnabled(true);
+ _handle.setRenderState(blend);
+
+ final ZBufferState zstate = new ZBufferState();
+ zstate.setFunction(TestFunction.LessThanOrEqualTo);
+ _handle.setRenderState(zstate);
+
+ _handle.getSceneHints().setRenderBucketType(RenderBucketType.Transparent);
+ _handle.updateGeometricState(0);
+ }
+
+ public MovePlanarWidget withDefaultHandle(final double radius, final double height, final ReadOnlyColorRGBA color) {
+ final Cylinder handle = new Cylinder("handle", 2, 16, radius, height, true);
+ handle.setDefaultColor(color);
+ switch (_plane) {
+ case XZ:
+ handle.setRotation(new Matrix3().fromAngleNormalAxis(MathUtils.HALF_PI, Vector3.UNIT_X));
+ break;
+ case YZ:
+ handle.setRotation(new Matrix3().fromAngleNormalAxis(MathUtils.HALF_PI, Vector3.UNIT_Y));
+ break;
+ default:
+ // do nothing
+ break;
+ }
+ handle.updateModelBound();
+ withHandle(handle);
+ return this;
+ }
+
+ public MovePlanarWidget withPlane(final MovePlane plane) {
+ _plane = plane;
+ return this;
+ }
+
+ public MovePlanarWidget withHandle(final Spatial handle) {
+ _handle.attachChild(handle);
+ return this;
+ }
+
+ @Override
+ public void targetChanged(final InteractManager manager) {
+ if (_dragging) {
+ endDrag(manager);
+ }
+ final Spatial target = manager.getSpatialTarget();
+ if (target != null) {
+ _handle.setScale(Math.max(MovePlanarWidget.MIN_SCALE, target.getWorldBound().getRadius()
+ + target.getWorldTranslation().subtract(target.getWorldBound().getCenter(), _calcVec3A).length()));
+ }
+ targetDataUpdated(manager);
+ }
+
+ @Override
+ public void targetDataUpdated(final InteractManager manager) {
+ final Spatial target = manager.getSpatialTarget();
+ if (target == null) {
+ _handle.setScale(1.0);
+ _handle.setRotation(Matrix3.IDENTITY);
+ } else {
+ // update scale of widget using bounding radius
+ target.updateGeometricState(0);
+
+ // update arrow rotations from target
+ if (_interactMatrix == InteractMatrix.Local) {
+ _handle.setRotation(target.getWorldRotation());
+ } else {
+ _handle.setRotation(Matrix3.IDENTITY);
+ }
+ }
+ }
+
+ @Override
+ public void render(final Renderer renderer, final InteractManager manager) {
+ final Spatial spat = manager.getSpatialTarget();
+ if (spat == null) {
+ return;
+ }
+
+ _handle.setTranslation(spat.getWorldTranslation());
+ _handle.updateGeometricState(0);
+
+ renderer.draw(_handle);
+ }
+
+ @Override
+ public void processInput(final Canvas source, final TwoInputStates inputStates, final AtomicBoolean inputConsumed,
+ final InteractManager manager) {
+ // Make sure we have something to modify
+ if (manager.getSpatialTarget() == null) {
+ return;
+ }
+
+ // Make sure we are dragging.
+ final MouseState current = inputStates.getCurrent().getMouseState();
+ final MouseState previous = inputStates.getPrevious().getMouseState();
+
+ if (current.getButtonState(_dragButton) != ButtonState.DOWN) {
+ if (_dragging) {
+ endDrag(manager);
+ }
+ return;
+ }
+ // if we're already dragging, make sure we only act on drags that started with a positive pick.
+ else if (!current.getButtonsPressedSince(previous).contains(_dragButton) && !_dragging) {
+ return;
+ }
+
+ final Camera camera = source.getCanvasRenderer().getCamera();
+ final Vector2 oldMouse = new Vector2(previous.getX(), previous.getY());
+ // Make sure we are dragging over the handle
+ if (!_dragging) {
+ findPick(oldMouse, camera);
+ final Vector3 lastPick = getLastPick();
+ if (lastPick == null) {
+ return;
+ } else {
+ beginDrag(manager);
+ }
+ }
+
+ // we've established that our mouse is being held down, and started over our arrow. So consume.
+ inputConsumed.set(true);
+
+ // check if we've moved at all
+ if (current == previous || current.getDx() == 0 && current.getDy() == 0) {
+ return;
+ }
+
+ // act on drag
+ final Spatial picked = (Spatial) _results.getPickData(0).getTarget();
+ if (picked != null) {
+ final Vector3 loc = getNewOffset(oldMouse, current, camera, manager);
+ final Transform transform = manager.getSpatialState().getTransform();
+ transform.setTranslation(loc.addLocal(transform.getTranslation()));
+
+ // apply our filters, if any, now that we've made updates.
+ applyFilters(manager);
+ }
+ }
+
+ protected Vector3 getNewOffset(final Vector2 oldMouse, final MouseState current, final Camera camera,
+ final InteractManager manager) {
+
+ // calculate a plane
+ _calcVec3A.set(_handle.getWorldTranslation());
+ switch (_plane) {
+ case XY:
+ _calcVec3B.set(Vector3.UNIT_X);
+ _calcVec3C.set(Vector3.UNIT_Y);
+ break;
+ case XZ:
+ _calcVec3B.set(Vector3.UNIT_X);
+ _calcVec3C.set(Vector3.UNIT_Z);
+ break;
+ case YZ:
+ _calcVec3B.set(Vector3.UNIT_Y);
+ _calcVec3C.set(Vector3.UNIT_Z);
+ break;
+ }
+
+ // rotate to arrow plane
+ _handle.getRotation().applyPost(_calcVec3B, _calcVec3B);
+ _handle.getRotation().applyPost(_calcVec3C, _calcVec3C);
+
+ // make plane object
+ final Plane pickPlane = new Plane().setPlanePoints(_calcVec3A, _calcVec3B.addLocal(_calcVec3A),
+ _calcVec3C.addLocal(_calcVec3A));
+
+ // find out where we were hitting the plane before
+ getPickRay(oldMouse, camera);
+ if (!_calcRay.intersectsPlane(pickPlane, _calcVec3A)) {
+ return _calcVec3A.zero();
+ }
+
+ // find out where we are hitting the plane now
+ getPickRay(new Vector2(current.getX(), current.getY()), camera);
+ if (!_calcRay.intersectsPlane(pickPlane, _calcVec3B)) {
+ return _calcVec3A.zero();
+ }
+
+ // convert to target coord space
+ final Node parent = manager.getSpatialTarget().getParent();
+ if (parent != null) {
+ parent.getWorldTransform().applyInverse(_calcVec3A);
+ parent.getWorldTransform().applyInverse(_calcVec3B);
+ }
+
+ return _calcVec3B.subtractLocal(_calcVec3A);
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/MoveWidget.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/MoveWidget.java
new file mode 100644
index 0000000..dfdb2b1
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/MoveWidget.java
@@ -0,0 +1,301 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact.widget;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.ardor3d.extension.interact.InteractManager;
+import com.ardor3d.framework.Canvas;
+import com.ardor3d.input.ButtonState;
+import com.ardor3d.input.MouseState;
+import com.ardor3d.input.logical.TwoInputStates;
+import com.ardor3d.math.ColorRGBA;
+import com.ardor3d.math.Line3;
+import com.ardor3d.math.MathUtils;
+import com.ardor3d.math.Matrix3;
+import com.ardor3d.math.Plane;
+import com.ardor3d.math.Quaternion;
+import com.ardor3d.math.Transform;
+import com.ardor3d.math.Vector2;
+import com.ardor3d.math.Vector3;
+import com.ardor3d.math.type.ReadOnlyColorRGBA;
+import com.ardor3d.renderer.Camera;
+import com.ardor3d.renderer.Renderer;
+import com.ardor3d.renderer.queue.RenderBucketType;
+import com.ardor3d.renderer.state.BlendState;
+import com.ardor3d.renderer.state.ZBufferState;
+import com.ardor3d.renderer.state.ZBufferState.TestFunction;
+import com.ardor3d.scenegraph.Node;
+import com.ardor3d.scenegraph.Spatial;
+
+public class MoveWidget extends AbstractInteractWidget {
+ public static double MIN_SCALE = 0.000001;
+
+ protected InteractArrow _lastArrow = null;
+
+ protected InteractArrow _xArrow = null;
+ protected InteractArrow _yArrow = null;
+ protected InteractArrow _zArrow = null;
+
+ protected ColorRGBA _xColor = new ColorRGBA(1, 0, 0, .65f);
+ protected ColorRGBA _yColor = new ColorRGBA(0, 1, 0, .65f);
+ protected ColorRGBA _zColor = new ColorRGBA(0, 0, 1, .65f);
+
+ protected InteractMatrix _interactMatrix = InteractMatrix.World;
+
+ public MoveWidget() {
+ _handle = new Node("moveHandle");
+
+ final BlendState blend = new BlendState();
+ blend.setBlendEnabled(true);
+ _handle.setRenderState(blend);
+
+ final ZBufferState zstate = new ZBufferState();
+ zstate.setFunction(TestFunction.LessThanOrEqualTo);
+ _handle.setRenderState(zstate);
+
+ _handle.getSceneHints().setRenderBucketType(RenderBucketType.Transparent);
+ _handle.updateGeometricState(0);
+ }
+
+ public MoveWidget withXAxis() {
+ return withXAxis(_xColor);
+ }
+
+ public MoveWidget withXAxis(final ReadOnlyColorRGBA color) {
+ return withXAxis(color, 1.0, 0.15, 0, 0);
+ }
+
+ public MoveWidget withXAxis(final ReadOnlyColorRGBA color, final double scale, final double width,
+ final double lengthGap, final double tipGap) {
+ if (_xArrow != null) {
+ _xArrow.removeFromParent();
+ }
+ _xColor.set(color);
+ _xArrow = new InteractArrow("xMoveArrow", scale, width, lengthGap, tipGap);
+ _xArrow.setDefaultColor(color);
+ final Quaternion rotate = new Quaternion().fromAngleAxis(MathUtils.HALF_PI, Vector3.UNIT_Y);
+ _xArrow.setRotation(rotate);
+ _handle.attachChild(_xArrow);
+ return this;
+ }
+
+ public MoveWidget withYAxis() {
+ return withYAxis(_yColor);
+ }
+
+ public MoveWidget withYAxis(final ReadOnlyColorRGBA color) {
+ return withYAxis(color, 1.0, 0.15, 0, 0);
+ }
+
+ public MoveWidget withYAxis(final ReadOnlyColorRGBA color, final double scale, final double width,
+ final double lengthGap, final double tipGap) {
+ if (_yArrow != null) {
+ _yArrow.removeFromParent();
+ }
+ _yColor.set(color);
+ _yArrow = new InteractArrow("yMoveArrow", scale, width, lengthGap, tipGap);
+ _yArrow.setDefaultColor(color);
+ final Quaternion rotate = new Quaternion().fromAngleAxis(MathUtils.HALF_PI, Vector3.NEG_UNIT_X);
+ _yArrow.setRotation(rotate);
+ _handle.attachChild(_yArrow);
+ return this;
+ }
+
+ public MoveWidget withZAxis() {
+ return withZAxis(new ColorRGBA(0, 0, 1, .65f));
+ }
+
+ public MoveWidget withZAxis(final ReadOnlyColorRGBA color) {
+ return withZAxis(color, 1.0, 0.15, 0, 0);
+ }
+
+ public MoveWidget withZAxis(final ReadOnlyColorRGBA color, final double scale, final double width,
+ final double lengthGap, final double tipGap) {
+ if (_zArrow != null) {
+ _zArrow.removeFromParent();
+ }
+ _zColor.set(color);
+ _zArrow = new InteractArrow("zMoveArrow", scale, width, lengthGap, tipGap);
+ _zArrow.setDefaultColor(color);
+ _handle.attachChild(_zArrow);
+ return this;
+ }
+
+ @Override
+ public void targetChanged(final InteractManager manager) {
+ if (_dragging) {
+ endDrag(manager);
+ }
+ final Spatial target = manager.getSpatialTarget();
+ if (target != null) {
+ _handle.setScale(Math.max(MoveWidget.MIN_SCALE, target.getWorldBound().getRadius()
+ + target.getWorldTranslation().subtract(target.getWorldBound().getCenter(), _calcVec3A).length()));
+ }
+ targetDataUpdated(manager);
+ }
+
+ @Override
+ public void targetDataUpdated(final InteractManager manager) {
+ final Spatial target = manager.getSpatialTarget();
+ if (target == null) {
+ _handle.setScale(1.0);
+ _handle.setRotation(Matrix3.IDENTITY);
+ } else {
+ // update scale of widget using bounding radius
+ target.updateGeometricState(0);
+
+ // update arrow rotations from target
+ if (_interactMatrix == InteractMatrix.Local) {
+ _handle.setRotation(target.getWorldRotation());
+ } else {
+ _handle.setRotation(Matrix3.IDENTITY);
+ }
+ }
+ }
+
+ @Override
+ public void render(final Renderer renderer, final InteractManager manager) {
+ final Spatial spat = manager.getSpatialTarget();
+ if (spat == null) {
+ return;
+ }
+
+ _handle.setTranslation(spat.getWorldTranslation());
+ _handle.updateGeometricState(0);
+
+ renderer.draw(_handle);
+ }
+
+ @Override
+ public void processInput(final Canvas source, final TwoInputStates inputStates, final AtomicBoolean inputConsumed,
+ final InteractManager manager) {
+ // Make sure we have something to modify
+ if (manager.getSpatialTarget() == null) {
+ return;
+ }
+
+ // Make sure we are dragging.
+ final MouseState current = inputStates.getCurrent().getMouseState();
+ final MouseState previous = inputStates.getPrevious().getMouseState();
+
+ if (current.getButtonState(_dragButton) != ButtonState.DOWN) {
+ if (_dragging) {
+ endDrag(manager);
+ }
+ return;
+ }
+ // if we're already dragging, make sure we only act on drags that started with a positive pick.
+ else if (!current.getButtonsPressedSince(previous).contains(_dragButton) && !_dragging) {
+ return;
+ }
+
+ final Camera camera = source.getCanvasRenderer().getCamera();
+ final Vector2 oldMouse = new Vector2(previous.getX(), previous.getY());
+ // Make sure we are dragging over the handle
+ if (!_dragging) {
+ findPick(oldMouse, camera);
+ final Vector3 lastPick = getLastPick();
+ if (lastPick == null) {
+ return;
+ } else {
+ beginDrag(manager);
+ }
+ }
+
+ // we've established that our mouse is being held down, and started over our arrow. So consume.
+ inputConsumed.set(true);
+
+ // check if we've moved at all
+ if (current == previous || current.getDx() == 0 && current.getDy() == 0) {
+ return;
+ }
+
+ // act on drag
+ final Spatial picked = (Spatial) _results.getPickData(0).getTarget();
+ if (picked != null && picked.getParent() instanceof InteractArrow) {
+ final InteractArrow arrow = (InteractArrow) picked.getParent();
+ _lastArrow = arrow;
+ final Vector3 loc = getNewOffset(arrow, oldMouse, current, camera, manager);
+ final Transform transform = manager.getSpatialState().getTransform();
+ transform.setTranslation(loc.addLocal(transform.getTranslation()));
+
+ // apply our filters, if any, now that we've made updates.
+ applyFilters(manager);
+ }
+ }
+
+ protected Vector3 getNewOffset(final InteractArrow arrow, final Vector2 oldMouse, final MouseState current,
+ final Camera camera, final InteractManager manager) {
+
+ // calculate a plane running through the Arrow and facing the camera.
+ _calcVec3A.set(_handle.getWorldTranslation());
+ _calcVec3B.set(_calcVec3A).addLocal(camera.getLeft());
+ _calcVec3C.set( //
+ arrow == _xArrow ? Vector3.UNIT_X : //
+ arrow == _yArrow ? Vector3.UNIT_Y : //
+ Vector3.UNIT_Z);
+
+ // rotate to arrow plane
+ _handle.getRotation().applyPost(_calcVec3C, _calcVec3C);
+ final Line3 arrowLine = new Line3(_calcVec3A, _calcVec3C.normalize(_calcVec3D));
+
+ // make plane object
+ final Plane pickPlane = new Plane().setPlanePoints(_calcVec3A, _calcVec3B, _calcVec3C.addLocal(_calcVec3A));
+
+ // find out where we were hitting the plane before
+ getPickRay(oldMouse, camera);
+ if (!_calcRay.intersectsPlane(pickPlane, _calcVec3A)) {
+ return _calcVec3A.zero();
+ }
+
+ // find out where we are hitting the plane now
+ getPickRay(new Vector2(current.getX(), current.getY()), camera);
+ if (!_calcRay.intersectsPlane(pickPlane, _calcVec3B)) {
+ return _calcVec3A.zero();
+ }
+
+ // Cast us to the line along our arrow
+ arrowLine.distanceSquared(_calcVec3A, _calcVec3C);
+ arrowLine.distanceSquared(_calcVec3B, _calcVec3D);
+
+ // convert to target coord space
+ final Node parent = manager.getSpatialTarget().getParent();
+ if (parent != null) {
+ parent.getWorldTransform().applyInverse(_calcVec3C);
+ parent.getWorldTransform().applyInverse(_calcVec3D);
+ }
+
+ return _calcVec3D.subtractLocal(_calcVec3C);
+ }
+
+ @Override
+ public void setInteractMatrix(final InteractMatrix matrix) {
+ _interactMatrix = matrix;
+ }
+
+ @Override
+ public InteractMatrix getInteractMatrix() {
+ return _interactMatrix;
+ }
+
+ public InteractArrow getXArrow() {
+ return _xArrow;
+ }
+
+ public InteractArrow getYRing() {
+ return _yArrow;
+ }
+
+ public InteractArrow getZRing() {
+ return _zArrow;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/RotateWidget.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/RotateWidget.java
new file mode 100644
index 0000000..50c3a1a
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/RotateWidget.java
@@ -0,0 +1,348 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact.widget;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.ardor3d.extension.interact.InteractManager;
+import com.ardor3d.framework.Canvas;
+import com.ardor3d.image.Texture2D;
+import com.ardor3d.input.ButtonState;
+import com.ardor3d.input.MouseState;
+import com.ardor3d.input.logical.TwoInputStates;
+import com.ardor3d.math.ColorRGBA;
+import com.ardor3d.math.MathUtils;
+import com.ardor3d.math.Matrix3;
+import com.ardor3d.math.Plane;
+import com.ardor3d.math.Quaternion;
+import com.ardor3d.math.Transform;
+import com.ardor3d.math.Vector2;
+import com.ardor3d.math.Vector3;
+import com.ardor3d.math.type.ReadOnlyColorRGBA;
+import com.ardor3d.math.type.ReadOnlyMatrix3;
+import com.ardor3d.math.type.ReadOnlyQuaternion;
+import com.ardor3d.renderer.Camera;
+import com.ardor3d.renderer.Renderer;
+import com.ardor3d.renderer.queue.RenderBucketType;
+import com.ardor3d.renderer.state.BlendState;
+import com.ardor3d.renderer.state.ZBufferState;
+import com.ardor3d.renderer.state.ZBufferState.TestFunction;
+import com.ardor3d.scenegraph.Node;
+import com.ardor3d.scenegraph.Spatial;
+
+public class RotateWidget extends AbstractInteractWidget {
+ public static double MIN_SCALE = 0.000001;
+
+ protected Matrix3 _calcMat3 = new Matrix3();
+ protected Matrix3 _rotateStore = new Matrix3();
+
+ protected InteractRing _lastRing = null;
+
+ protected InteractRing _xRing = null;
+ protected InteractRing _yRing = null;
+ protected InteractRing _zRing = null;
+
+ public RotateWidget() {
+ _handle = new Node("rotationHandle");
+
+ final BlendState blend = new BlendState();
+ blend.setBlendEnabled(true);
+ _handle.setRenderState(blend);
+
+ final ZBufferState zstate = new ZBufferState();
+ zstate.setFunction(TestFunction.LessThanOrEqualTo);
+ _handle.setRenderState(zstate);
+
+ _handle.getSceneHints().setRenderBucketType(RenderBucketType.Transparent);
+ _handle.updateGeometricState(0);
+ }
+
+ /**
+ * Call this after creating the rings you want to use.
+ *
+ * @param texture
+ * @return
+ */
+ public void setTexture(final Texture2D texture) {
+ if (_xRing != null) {
+ _xRing.setTexture(texture);
+ }
+ if (_yRing != null) {
+ _yRing.setTexture(texture);
+ }
+ if (_zRing != null) {
+ _zRing.setTexture(texture);
+ }
+ }
+
+ public RotateWidget withXAxis() {
+ return withXAxis(new ColorRGBA(1, 0, 0, .65f));
+ }
+
+ public RotateWidget withXAxis(final ReadOnlyColorRGBA color) {
+ return withXAxis(color, 1.0f, 0.15f);
+ }
+
+ public RotateWidget withXAxis(final ReadOnlyColorRGBA color, final float scale, final float width) {
+ if (_xRing != null) {
+ _xRing.removeFromParent();
+ }
+ _xRing = new InteractRing("xRotRing", 4, 32, scale, width);
+ _xRing.setDefaultColor(color);
+ final Quaternion rotate = new Quaternion().fromAngleAxis(MathUtils.HALF_PI, Vector3.UNIT_Y);
+ _xRing.getMeshData().rotatePoints(rotate);
+ _xRing.getMeshData().rotateNormals(rotate);
+ _handle.attachChild(_xRing);
+ return this;
+ }
+
+ public RotateWidget withYAxis() {
+ return withYAxis(new ColorRGBA(0, 1, 0, .65f));
+ }
+
+ public RotateWidget withYAxis(final ReadOnlyColorRGBA color) {
+ return withYAxis(color, 1.0f, 0.15f);
+ }
+
+ public RotateWidget withYAxis(final ReadOnlyColorRGBA color, final float scale, final float width) {
+ if (_yRing != null) {
+ _yRing.removeFromParent();
+ }
+ _yRing = new InteractRing("yRotRing", 4, 32, scale, width);
+ _yRing.setDefaultColor(color);
+ final Quaternion rotate = new Quaternion().fromAngleAxis(MathUtils.HALF_PI, Vector3.NEG_UNIT_X);
+ _yRing.getMeshData().rotatePoints(rotate);
+ _yRing.getMeshData().rotateNormals(rotate);
+ _handle.attachChild(_yRing);
+ return this;
+ }
+
+ public RotateWidget withZAxis() {
+ return withZAxis(new ColorRGBA(0, 0, 1, .65f));
+ }
+
+ public RotateWidget withZAxis(final ReadOnlyColorRGBA color) {
+ return withZAxis(color, 1.0f, 0.15f);
+ }
+
+ public RotateWidget withZAxis(final ReadOnlyColorRGBA color, final float scale, final float width) {
+ if (_zRing != null) {
+ _zRing.removeFromParent();
+ }
+ _zRing = new InteractRing("zRotRing", 4, 32, scale, width);
+ _zRing.setDefaultColor(color);
+ _handle.attachChild(_zRing);
+ return this;
+ }
+
+ @Override
+ public void targetChanged(final InteractManager manager) {
+ if (_dragging) {
+ endDrag(manager);
+ }
+ final Spatial target = manager.getSpatialTarget();
+ if (target != null) {
+ _handle.setScale(Math.max(RotateWidget.MIN_SCALE, target.getWorldBound().getRadius()
+ + target.getWorldTranslation().subtract(target.getWorldBound().getCenter(), _calcVec3A).length()));
+ }
+ targetDataUpdated(manager);
+ }
+
+ protected void setRingRotations(final ReadOnlyMatrix3 rot) {
+ if (_xRing != null) {
+ _xRing.setRotation(rot);
+ }
+ if (_yRing != null) {
+ _yRing.setRotation(rot);
+ }
+ if (_zRing != null) {
+ _zRing.setRotation(rot);
+ }
+ }
+
+ @Override
+ public void targetDataUpdated(final InteractManager manager) {
+ final Spatial target = manager.getSpatialTarget();
+ if (target == null) {
+ _handle.setScale(1.0);
+ setRingRotations(Matrix3.IDENTITY);
+ } else {
+ // update scale of widget using bounding radius
+ target.updateGeometricState(0);
+
+ // update ring rotations from target
+ if (_interactMatrix == InteractMatrix.Local) {
+ setRingRotations(target.getWorldRotation());
+ } else {
+ setRingRotations(Matrix3.IDENTITY);
+ if (_lastRing != null) {
+ _lastRing.setRotation(_rotateStore);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void render(final Renderer renderer, final InteractManager manager) {
+ final Spatial spat = manager.getSpatialTarget();
+ if (spat == null) {
+ return;
+ }
+
+ _handle.setTranslation(spat.getWorldTranslation());
+ _handle.updateGeometricState(0);
+
+ renderer.draw(_handle);
+ }
+
+ @Override
+ public void processInput(final Canvas source, final TwoInputStates inputStates, final AtomicBoolean inputConsumed,
+ final InteractManager manager) {
+ // Make sure we have something to modify
+ if (manager.getSpatialTarget() == null) {
+ return;
+ }
+
+ // Make sure we are dragging.
+ final MouseState current = inputStates.getCurrent().getMouseState();
+ final MouseState previous = inputStates.getPrevious().getMouseState();
+
+ if (current.getButtonsReleasedSince(previous).contains(_dragButton)) {
+ _rotateStore.setIdentity();
+ if (_interactMatrix != InteractMatrix.Local) {
+ setRingRotations(Matrix3.IDENTITY);
+ }
+ }
+
+ if (current.getButtonState(_dragButton) != ButtonState.DOWN) {
+ if (_dragging) {
+ endDrag(manager);
+ }
+ return;
+ }
+ // if we're already dragging, make sure we only act on drags that started with a positive pick.
+ else if (!current.getButtonsPressedSince(previous).contains(_dragButton) && !_dragging) {
+ return;
+ }
+
+ final Camera camera = source.getCanvasRenderer().getCamera();
+ final Vector2 oldMouse = new Vector2(previous.getX(), previous.getY());
+ // Make sure we are dragging over the handle
+ if (!_dragging) {
+ findPick(oldMouse, camera);
+ final Vector3 lastPick = getLastPick();
+ if (lastPick == null) {
+ return;
+ } else {
+ beginDrag(manager);
+ }
+ }
+
+ // we've established that our mouse is being held down, and started over our arrow. So consume.
+ inputConsumed.set(true);
+
+ // check if we've moved at all
+ if (current == previous || current.getDx() == 0 && current.getDy() == 0) {
+ return;
+ }
+
+ // act on drag
+ final Spatial picked = (Spatial) _results.getPickData(0).getTarget();
+ if (picked instanceof InteractRing) {
+ final InteractRing ring = (InteractRing) picked;
+ _lastRing = ring;
+ final ReadOnlyQuaternion rot = getNewAxisRotation(ring, oldMouse, current, camera, manager);
+ final Transform transform = manager.getSpatialState().getTransform();
+ rot.toRotationMatrix(_calcMat3).multiply(transform.getMatrix(), _calcMat3);
+ transform.setRotation(_calcMat3);
+
+ // apply our filters, if any, now that we've made updates.
+ applyFilters(manager);
+ }
+ }
+
+ protected ReadOnlyQuaternion getNewAxisRotation(final InteractRing ring, final Vector2 oldMouse,
+ final MouseState current, final Camera camera, final InteractManager manager) {
+ // calculate a plane running through the ring we picked
+ _calcVec3A.set(_handle.getWorldTranslation());
+ if (ring == _zRing || ring == _yRing) {
+ _calcVec3B.set(Vector3.UNIT_X);
+ } else {
+ _calcVec3B.set(Vector3.UNIT_Z);
+ }
+
+ if (ring == _zRing || ring == _xRing) {
+ _calcVec3C.set(Vector3.UNIT_Y);
+ } else {
+ _calcVec3C.set(Vector3.UNIT_Z);
+ }
+
+ // rotate to ring plane
+ ring.getRotation().applyPost(_calcVec3B, _calcVec3B);
+ ring.getRotation().applyPost(_calcVec3C, _calcVec3C);
+
+ // make plane object
+ final Plane pickPlane = new Plane().setPlanePoints(_calcVec3A, _calcVec3B.addLocal(_calcVec3A),
+ _calcVec3C.addLocal(_calcVec3A));
+
+ // find out where we were hitting the plane before
+ getPickRay(oldMouse, camera);
+ if (!_calcRay.intersectsPlane(pickPlane, _calcVec3A)) {
+ return Quaternion.IDENTITY;
+ }
+
+ // find out where we are hitting the plane now
+ getPickRay(new Vector2(current.getX(), current.getY()), camera);
+ if (!_calcRay.intersectsPlane(pickPlane, _calcVec3B)) {
+ return Quaternion.IDENTITY;
+ }
+
+ // convert to vectors
+ _calcVec3A.subtractLocal(_handle.getWorldTranslation());
+ _calcVec3B.subtractLocal(_handle.getWorldTranslation());
+
+ // apply to our interact matrix if used
+ if (_interactMatrix == InteractMatrix.World) {
+ _rotateStore.multiplyLocal(new Quaternion().fromVectorToVector(_calcVec3A, _calcVec3B).toRotationMatrix(
+ _calcMat3));
+ }
+
+ // convert to target coord space
+ final Node parent = manager.getSpatialTarget().getParent();
+ if (parent != null) {
+ parent.getWorldTransform().applyInverseVector(_calcVec3A);
+ parent.getWorldTransform().applyInverseVector(_calcVec3B);
+ }
+
+ // return a rotation to take us to the new rotation
+ return new Quaternion().fromVectorToVector(_calcVec3A, _calcVec3B);
+ }
+
+ @Override
+ public void setInteractMatrix(final InteractMatrix matrix) {
+ if (_interactMatrix != matrix) {
+ _lastRing = null;
+ _interactMatrix = matrix;
+ }
+ }
+
+ public InteractRing getXRing() {
+ return _xRing;
+ }
+
+ public InteractRing getYRing() {
+ return _yRing;
+ }
+
+ public InteractRing getZRing() {
+ return _zRing;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/SimpleScaleWidget.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/SimpleScaleWidget.java
new file mode 100644
index 0000000..e03dda1
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/interact/widget/SimpleScaleWidget.java
@@ -0,0 +1,184 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.interact.widget;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.ardor3d.extension.interact.InteractManager;
+import com.ardor3d.framework.Canvas;
+import com.ardor3d.input.ButtonState;
+import com.ardor3d.input.MouseState;
+import com.ardor3d.input.logical.TwoInputStates;
+import com.ardor3d.math.ColorRGBA;
+import com.ardor3d.math.Plane;
+import com.ardor3d.math.Quaternion;
+import com.ardor3d.math.Vector2;
+import com.ardor3d.math.Vector3;
+import com.ardor3d.math.type.ReadOnlyColorRGBA;
+import com.ardor3d.math.type.ReadOnlyVector3;
+import com.ardor3d.renderer.Camera;
+import com.ardor3d.renderer.Renderer;
+import com.ardor3d.renderer.queue.RenderBucketType;
+import com.ardor3d.renderer.state.BlendState;
+import com.ardor3d.renderer.state.ZBufferState;
+import com.ardor3d.renderer.state.ZBufferState.TestFunction;
+import com.ardor3d.scenegraph.Spatial;
+import com.ardor3d.scenegraph.shape.Arrow;
+
+public class SimpleScaleWidget extends AbstractInteractWidget {
+ public static double MIN_SCALE = 0.000001;
+
+ protected ReadOnlyVector3 _arrowDirection;
+
+ public SimpleScaleWidget() {}
+
+ public SimpleScaleWidget withArrow(final ReadOnlyVector3 arrowDirection) {
+ return withArrow(arrowDirection, new ColorRGBA(1.0f, 0.0f, 0.0f, 0.4f), 0, 0);
+ }
+
+ public SimpleScaleWidget withArrow(final ReadOnlyVector3 arrowDirection, final ReadOnlyColorRGBA color) {
+ return withArrow(arrowDirection, color, 0, 0);
+ }
+
+ public SimpleScaleWidget withArrow(final ReadOnlyVector3 arrowDirection, final ReadOnlyColorRGBA color,
+ final double lengthGap, final double tipGap) {
+ _arrowDirection = new Vector3(arrowDirection);
+ _handle = new InteractArrow("scaleHandle", 1.0, 0.125, lengthGap, tipGap);
+ if (!_arrowDirection.equals(Vector3.UNIT_Z)) {
+ _handle.setRotation(new Quaternion().fromVectorToVector(Vector3.UNIT_Z, _arrowDirection));
+ }
+
+ final BlendState blend = new BlendState();
+ blend.setBlendEnabled(true);
+ _handle.setRenderState(blend);
+
+ ((Arrow) _handle).setDefaultColor(color);
+
+ final ZBufferState zstate = new ZBufferState();
+ zstate.setWritable(false);
+ zstate.setFunction(TestFunction.Always);
+ _handle.setRenderState(zstate);
+
+ _handle.getSceneHints().setRenderBucketType(RenderBucketType.PostBucket);
+ _handle.updateGeometricState(0);
+ return this;
+ }
+
+ @Override
+ public void targetChanged(final InteractManager manager) {
+ if (_dragging) {
+ endDrag(manager);
+ }
+ targetDataUpdated(manager);
+ }
+
+ @Override
+ public void targetDataUpdated(final InteractManager manager) {
+ final Spatial target = manager.getSpatialTarget();
+ if (target == null) {
+ _handle.setScale(1.0);
+ } else {
+ _handle.setScale(Math.max(SimpleScaleWidget.MIN_SCALE, target.getWorldBound().getRadius()));
+ }
+ }
+
+ @Override
+ public void render(final Renderer renderer, final InteractManager manager) {
+ final Spatial spat = manager.getSpatialTarget();
+ if (spat == null) {
+ return;
+ }
+
+ _handle.setTranslation(spat.getWorldTranslation());
+ _handle.updateGeometricState(0);
+
+ renderer.draw(_handle);
+ }
+
+ @Override
+ public void processInput(final Canvas source, final TwoInputStates inputStates, final AtomicBoolean inputConsumed,
+ final InteractManager manager) {
+ // Make sure we have something to modify
+ if (manager.getSpatialTarget() == null) {
+ return;
+ }
+
+ // Make sure we are dragging.
+ final MouseState current = inputStates.getCurrent().getMouseState();
+ final MouseState previous = inputStates.getPrevious().getMouseState();
+ if (current.getButtonState(_dragButton) != ButtonState.DOWN) {
+ if (_dragging) {
+ endDrag(manager);
+ }
+ return;
+ }
+ // if we're already dragging, make sure we only act on drags that started with a positive pick.
+ else if (!current.getButtonsPressedSince(previous).contains(_dragButton) && !_dragging) {
+ return;
+ }
+
+ final Camera camera = source.getCanvasRenderer().getCamera();
+ final Vector2 oldMouse = new Vector2(previous.getX(), previous.getY());
+ // Make sure we are dragging over the arrow
+ if (!_dragging) {
+ findPick(oldMouse, camera);
+ final Vector3 lastPick = getLastPick();
+ if (lastPick == null) {
+ return;
+ } else {
+ beginDrag(manager);
+ }
+ }
+
+ // we've established that our mouse is being held down, and started over our arrow. So consume.
+ inputConsumed.set(true);
+
+ // check if we've moved at all
+ if (current == previous || current.getDx() == 0 && current.getDy() == 0) {
+ return;
+ }
+
+ // act on drag
+ final double scale = getNewScale(oldMouse, current, camera, manager);
+
+ // Set new scale on spatial state
+ manager.getSpatialState().getTransform().setScale(scale);
+
+ // apply our filters, if any, now that we've made updates.
+ applyFilters(manager);
+ }
+
+ protected double getNewScale(final Vector2 oldMouse, final MouseState current, final Camera camera,
+ final InteractManager manager) {
+ // calculate a plane running through the Arrow and facing the camera.
+ _calcVec3A.set(_handle.getWorldTranslation());
+ _calcVec3B.set(_calcVec3A).addLocal(camera.getLeft());
+ _calcVec3C.set(_calcVec3A).addLocal(_arrowDirection);
+ final Plane pickPlane = new Plane().setPlanePoints(_calcVec3A, _calcVec3B, _calcVec3C);
+
+ // find out where we were hitting the plane before
+ getPickRay(oldMouse, camera);
+ _calcRay.intersectsPlane(pickPlane, _calcVec3A);
+ final double oldHeight = _calcVec3A.getY();
+
+ // find out where we are hitting the plane now
+ getPickRay(new Vector2(current.getX(), current.getY()), camera);
+ _calcRay.intersectsPlane(pickPlane, _calcVec3A);
+ final double newHeight = _calcVec3A.getY();
+
+ // Use distance between points against arrow length to determine how big we need to grow our bounding radius
+ final double delta = newHeight - oldHeight;
+
+ final double oldRadius = manager.getSpatialTarget().getWorldBound().getRadius();
+
+ return manager.getSpatialTarget().getScale().getY() * (1.0 + delta / oldRadius);
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2DataStore.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2DataStore.java
new file mode 100644
index 0000000..52af183
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2DataStore.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.md2;
+
+import java.util.List;
+
+import com.ardor3d.extension.model.util.KeyframeController;
+import com.ardor3d.scenegraph.Mesh;
+import com.google.common.collect.Lists;
+
+public class Md2DataStore {
+
+ private final Mesh _mainMesh;
+ private final KeyframeController<Mesh> _controller;
+
+ private final List<String> _frameNames = Lists.newArrayList();
+
+ private final List<String> _skinNames = Lists.newArrayList();
+
+ public Md2DataStore(final Mesh mainMesh, final KeyframeController<Mesh> controller) {
+ _mainMesh = mainMesh;
+ _controller = controller;
+ }
+
+ public Mesh getScene() {
+ return _mainMesh;
+ }
+
+ public KeyframeController<Mesh> getController() {
+ return _controller;
+ }
+
+ public List<String> getFrameNames() {
+ return _frameNames;
+ }
+
+ public int getFrameIndex(final String frameName) {
+ return _frameNames.indexOf(frameName);
+ }
+
+ public List<String> getSkinNames() {
+ return _skinNames;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Frame.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Frame.java
new file mode 100644
index 0000000..f6e75d2
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Frame.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.md2;
+
+import com.ardor3d.math.Vector3;
+import com.ardor3d.math.type.ReadOnlyVector3;
+
+final class Md2Frame {
+
+ /** frame name */
+ String name; // char [16]
+
+ /** scale factor */
+ final Vector3 scale = new Vector3(1, 1, 1);
+
+ /** translation vector */
+ final Vector3 translate = new Vector3();
+
+ /** vertex data */
+ byte[] vertData;
+
+ Md2Frame(final byte[] vertData, final String name, final ReadOnlyVector3 scale, final ReadOnlyVector3 translate) {
+ this.vertData = vertData;
+ this.scale.set(scale);
+ this.translate.set(translate);
+ this.name = name;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Header.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Header.java
new file mode 100644
index 0000000..992c018
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Header.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.md2;
+
+/**
+ * Header of MD2: see also http://tfc.duke.free.fr/coding/md2-specs-en.html
+ */
+final class Md2Header {
+ /** identifier of the file: magic number: "IDP2" */
+ final int magic;
+ /** version number of the file (must be 8) */
+ final int version;
+
+ /** texture width in pixels */
+ final int skinWidth;
+ /** texture height in pixels */
+ final int skinHeight;
+
+ /** size in bytes of a frame */
+ final int frameSize;
+
+ /** number of textures associated with the model */
+ final int numSkins;
+ /** number of vertices per frame */
+ final int numVertices;
+ /** number of texture coordinates */
+ final int numTexCoords;
+ /** number of triangles */
+ final int numTriangles;
+ /** number of gl commands */
+ final int numGlCommands;
+ /** number of animation frames */
+ final int numFrames;
+
+ /** offset in the file for the texture data */
+ final int offsetSkins;
+ /** offset in the file for the texture coords */
+ final int offsetTexCoords;
+ /** offset in the file for the face data */
+ final int offsetTriangles;
+ /** offset in the file for the frames data */
+ final int offsetFrames;
+ /** offset in the file for the gl commands data */
+ final int offsetGlCommands;
+ /** offset of EOF */
+ final int offsetEnd;
+
+ Md2Header(final int magic, final int version, final int skinWidth, final int skinHeight, final int frameSize,
+ final int numSkins, final int numVertices, final int numTexCoords, final int numTriangles,
+ final int numGlCommands, final int numFrames, final int offsetSkins, final int offsetTexCoords,
+ final int offsetTriangles, final int offsetFrames, final int offsetGlCommands, final int offsetEnd) {
+ this.magic = magic;
+ this.version = version;
+ this.skinWidth = skinWidth;
+ this.skinHeight = skinHeight;
+ this.frameSize = frameSize;
+ this.numSkins = numSkins;
+ this.numVertices = numVertices;
+ this.numTexCoords = numTexCoords;
+ this.numTriangles = numTriangles;
+ this.numGlCommands = numGlCommands;
+ this.numFrames = numFrames;
+ this.offsetSkins = offsetSkins;
+ this.offsetTexCoords = offsetTexCoords;
+ this.offsetTriangles = offsetTriangles;
+ this.offsetFrames = offsetFrames;
+ this.offsetGlCommands = offsetGlCommands;
+ this.offsetEnd = offsetEnd;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Importer.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Importer.java
new file mode 100644
index 0000000..6fbc793
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Importer.java
@@ -0,0 +1,420 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.md2;
+
+import java.io.InputStream;
+import java.util.List;
+
+import com.ardor3d.bounding.BoundingBox;
+import com.ardor3d.extension.model.util.KeyframeController;
+import com.ardor3d.image.Texture;
+import com.ardor3d.image.TextureStoreFormat;
+import com.ardor3d.image.Texture.MinificationFilter;
+import com.ardor3d.math.Vector3;
+import com.ardor3d.renderer.IndexMode;
+import com.ardor3d.renderer.state.TextureState;
+import com.ardor3d.scenegraph.FloatBufferData;
+import com.ardor3d.scenegraph.Mesh;
+import com.ardor3d.scenegraph.MeshData;
+import com.ardor3d.util.Ardor3dException;
+import com.ardor3d.util.LittleEndianRandomAccessDataInput;
+import com.ardor3d.util.TextureManager;
+import com.ardor3d.util.resource.ResourceLocator;
+import com.ardor3d.util.resource.ResourceLocatorTool;
+import com.ardor3d.util.resource.ResourceSource;
+import com.google.common.collect.Lists;
+
+public class Md2Importer {
+
+ private final Vector3 calcVert = new Vector3();
+
+ private boolean _loadTextures = true;
+ private ResourceLocator _textureLocator;
+ private ResourceLocator _modelLocator;
+
+ // texture defaults
+ private MinificationFilter _minificationFilter = MinificationFilter.Trilinear;
+ private boolean _useCompression = true;
+ private boolean _flipTextureVertically = false;
+
+ public boolean isLoadTextures() {
+ return _loadTextures;
+ }
+
+ public Md2Importer setLoadTextures(final boolean loadTextures) {
+ _loadTextures = loadTextures;
+ return this;
+ }
+
+ public Md2Importer setTextureLocator(final ResourceLocator locator) {
+ _textureLocator = locator;
+ return this;
+ }
+
+ public Md2Importer setModelLocator(final ResourceLocator locator) {
+ _modelLocator = locator;
+ return this;
+ }
+
+ public Md2Importer setFlipTextureVertically(final boolean flipTextureVertically) {
+ _flipTextureVertically = flipTextureVertically;
+ return this;
+ }
+
+ public boolean isFlipTextureVertically() {
+ return _flipTextureVertically;
+ }
+
+ public Md2Importer setUseCompression(final boolean useCompression) {
+ _useCompression = useCompression;
+ return this;
+ }
+
+ public boolean isUseCompression() {
+ return _useCompression;
+ }
+
+ public Md2Importer setMinificationFilter(final MinificationFilter minificationFilter) {
+ _minificationFilter = minificationFilter;
+ return this;
+ }
+
+ public MinificationFilter getMinificationFilter() {
+ return _minificationFilter;
+ }
+
+ /**
+ * Reads an MD2 file from the given resource
+ *
+ * @param resource
+ * a resource pointing to the model we wish to load.
+ * @return an Md2DataStore data object containing the scene and other useful elements.
+ */
+ public Md2DataStore load(final ResourceSource resource) {
+ if (resource == null) {
+ throw new NullPointerException("Unable to load null resource");
+ }
+
+ try {
+ final InputStream md2Stream = resource.openStream();
+ if (md2Stream == null) {
+ throw new NullPointerException("Unable to load null streams");
+ }
+ // final Md2DataStore store = new Md2DataStore();
+ final LittleEndianRandomAccessDataInput bis = new LittleEndianRandomAccessDataInput(md2Stream);
+
+ // parse the header
+ final Md2Header header = new Md2Header(bis.readInt(), bis.readInt(), bis.readInt(), bis.readInt(), bis
+ .readInt(), bis.readInt(), bis.readInt(), bis.readInt(), bis.readInt(), bis.readInt(), bis
+ .readInt(), bis.readInt(), bis.readInt(), bis.readInt(), bis.readInt(), bis.readInt(), bis
+ .readInt());
+
+ // Check magic word and version
+ if (header.magic != ('2' << 24) + ('P' << 16) + ('D' << 8) + 'I') {
+ throw new Ardor3dException("Not an MD2 file.");
+ }
+ if (header.version != 8) {
+ throw new Ardor3dException("Invalid file version (Version not 8)!");
+ }
+
+ // Parse out texture names
+ final String[] texNames = new String[header.numSkins];
+ bis.seek(header.offsetSkins);
+ for (int i = 0; i < header.numSkins; i++) {
+ texNames[i] = bis.readString(64);
+ }
+
+ // Parse out tex coords
+ final float[] texCoords = new float[2 * header.numTexCoords];
+ bis.seek(header.offsetTexCoords);
+ final float inverseWidth = 1f / header.skinWidth;
+ final float inverseHeight = 1f / header.skinHeight;
+ for (int i = 0; i < header.numTexCoords; i++) {
+ texCoords[i * 2 + 0] = bis.readShort() * inverseWidth;
+ texCoords[i * 2 + 1] = bis.readShort() * inverseHeight;
+ }
+
+ // Parse out triangles
+ final short[] triangles = new short[header.numTriangles * 6];
+ bis.seek(header.offsetTriangles);
+ for (int i = 0; i < header.numTriangles; i++) {
+ triangles[i * 6 + 0] = bis.readShort(); // vert index 0
+ triangles[i * 6 + 1] = bis.readShort(); // vert index 1
+ triangles[i * 6 + 2] = bis.readShort(); // vert index 2
+ triangles[i * 6 + 3] = bis.readShort(); // texcoord index 0
+ triangles[i * 6 + 4] = bis.readShort(); // texcoord index 1
+ triangles[i * 6 + 5] = bis.readShort(); // texcoord index 2
+ }
+
+ // Parse out gl commands
+ final Md2GlCommand[] commands = new Md2GlCommand[header.numGlCommands];
+ bis.seek(header.offsetGlCommands);
+ int length, absLength;
+ Md2GlCommand cmd;
+ final List<Integer> fanIndices = Lists.newArrayList();
+ final List<Integer> stripIndices = Lists.newArrayList();
+ for (int i = 0; i < header.numGlCommands; i++) {
+ length = bis.readInt();
+ if (length == 0) {
+ break;
+ }
+ absLength = Math.abs(length);
+ commands[i] = cmd = new Md2GlCommand(length >= 0 ? IndexMode.TriangleStrip : IndexMode.TriangleFan,
+ absLength);
+ if (cmd.mode == IndexMode.TriangleFan) {
+ fanIndices.add(i);
+ } else {
+ stripIndices.add(i);
+ }
+ for (int j = 0; j < absLength; j++) {
+ cmd.texCoords[j * 2 + 0] = bis.readFloat();
+ cmd.texCoords[j * 2 + 1] = bis.readFloat();
+ cmd.vertIndices[j] = bis.readInt();
+ }
+ }
+
+ // Parse out frames
+ final Md2Frame[] frames = new Md2Frame[header.numFrames];
+ bis.seek(header.offsetFrames);
+ final Vector3 scale = new Vector3();
+ final Vector3 translate = new Vector3();
+ for (int i = 0; i < header.numFrames; i++) {
+ scale.set(bis.readFloat(), bis.readFloat(), bis.readFloat());
+ translate.set(bis.readFloat(), bis.readFloat(), bis.readFloat());
+ final String name = bis.readString(16);
+ final byte[] vertData = new byte[header.numVertices * 4];
+ bis.readFully(vertData);
+ frames[i] = new Md2Frame(vertData, name, scale, translate);
+ }
+
+ // make index modes/counts to be used throughout meshes
+ int vertexCount = 0;
+ int fanIndex = stripIndices.size() != 0 ? 1 : 0;
+ final IndexMode[] modes = new IndexMode[fanIndices.size() + fanIndex];
+ final int[] counts = new int[modes.length];
+ for (final Integer index : fanIndices) {
+ counts[fanIndex] = commands[index].vertIndices.length;
+ modes[fanIndex] = IndexMode.TriangleFan;
+ vertexCount += counts[fanIndex];
+ fanIndex++;
+ }
+ if (stripIndices.size() != 0) {
+ int triCounts = 0;
+ int vertCount;
+ int extra = 0;
+ for (final Integer index : stripIndices) {
+ vertCount = commands[index].vertIndices.length;
+ extra = vertCount % 2 == 1 ? 3 : 2;
+ triCounts += vertCount + extra;
+ }
+ counts[0] = triCounts - extra + 1;
+ modes[0] = IndexMode.TriangleStrip;
+ vertexCount += counts[0];
+ }
+
+ vertexCount++;
+
+ // Create each frame as a Mesh using glcommands if given
+ final Mesh[] meshes = new Mesh[header.numFrames];
+ MeshData mData;
+ for (int i = 0; i < header.numFrames; i++) {
+ final Md2Frame frame = frames[i];
+
+ meshes[i] = new Mesh(frames[i].name);
+ mData = meshes[i].getMeshData();
+ mData.setIndexLengths(counts);
+ mData.setIndexModes(modes);
+
+ final FloatBufferData verts = new FloatBufferData(vertexCount * 3, 3);
+ final FloatBufferData norms = new FloatBufferData(vertexCount * 3, 3);
+ final FloatBufferData texs = new FloatBufferData(vertexCount * 3, 2);
+ mData.setVertexCoords(verts);
+ mData.setNormalCoords(norms);
+ mData.setTextureCoords(texs, 0);
+
+ // go through the triangle strips/fans and add them in
+ // first the strips
+ if (stripIndices.size() != 0) {
+ for (int maxJ = stripIndices.size(), j = 0; j < maxJ; j++) {
+ cmd = commands[stripIndices.get(j)];
+ if (cmd.vertIndices.length < 3) {
+ continue;
+ }
+
+ addVert(cmd, frame, 0, verts);
+ norms.getBuffer().put(0).put(0).put(0);
+ texs.getBuffer().put(0).put(0);
+
+ // add strip verts / normals
+ for (int k = 0; k < cmd.vertIndices.length; k++) {
+ addVert(cmd, frame, k, verts);
+ addNormal(cmd, frame, k, norms);
+ }
+
+ // add strip tex coords
+ texs.getBuffer().put(cmd.texCoords);
+
+ // if we're not the last strip, add a vert or two for degenerate triangle connector
+ if (j != maxJ - 1) {
+ addVert(cmd, frame, cmd.vertIndices.length - 1, verts);
+ norms.getBuffer().put(0).put(0).put(0);
+ texs.getBuffer().put(0).put(0);
+ if (cmd.vertIndices.length % 2 == 1) {
+ // extra vert to maintain wind order
+ addVert(cmd, frame, cmd.vertIndices.length - 1, verts);
+ norms.getBuffer().put(0).put(0).put(0);
+ texs.getBuffer().put(0).put(0);
+ }
+ }
+ }
+ }
+ // Now the fans
+ // XXX: could add these to the strip instead
+ for (final int j : fanIndices) {
+ cmd = commands[j];
+ texs.getBuffer().put(cmd.texCoords[0]).put(cmd.texCoords[1]);
+ addNormal(cmd, frame, 0, norms);
+ addVert(cmd, frame, 0, verts);
+ for (int k = cmd.vertIndices.length; --k >= 1;) {
+ texs.getBuffer().put(cmd.texCoords[k * 2]).put(cmd.texCoords[k * 2 + 1]);
+ addNormal(cmd, frame, k, norms);
+ addVert(cmd, frame, k, verts);
+ }
+ }
+ }
+
+ // Clone frame 0 as mesh for initial mesh
+ final Mesh mesh = meshes[0].makeCopy(false);
+ mesh.setModelBound(new BoundingBox());
+
+ // Use resource name for mesh
+ mesh.setName(resource.getName());
+
+ // Add controller
+ final KeyframeController<Mesh> controller = new KeyframeController<Mesh>();
+ mesh.addController(controller);
+ controller.setMorphingMesh(mesh);
+ controller.setInterpTex(false);
+ int i = 0;
+ for (final Mesh meshX : meshes) {
+ controller.setKeyframe(i, meshX);
+ i++;
+ }
+
+ // Make a store object to return
+ final Md2DataStore store = new Md2DataStore(mesh, controller);
+
+ // store names
+ for (final Md2Frame frame : frames) {
+ store.getFrameNames().add(frame.name);
+ }
+
+ // store skin names
+ for (final String name : texNames) {
+ store.getSkinNames().add(name);
+ }
+
+ // Apply our texture
+ if (isLoadTextures()) {
+ Texture tex = null;
+ for (final String name : texNames) {
+ tex = loadTexture(name);
+ if (tex != null) {
+ break;
+ }
+ }
+
+ // try using model name
+ if (tex == null) {
+ tex = loadTexture(resource.getName());
+ }
+
+ if (tex != null) {
+ final TextureState ts = new TextureState();
+ ts.setTexture(tex);
+ mesh.setRenderState(ts);
+ }
+ }
+
+ return store;
+ } catch (final Exception e) {
+ throw new Error("Unable to load md2 resource from URL: " + resource, e);
+ }
+ }
+
+ private Texture loadTexture(final String name) {
+ Texture tex = null;
+ if (_textureLocator == null) {
+ tex = TextureManager.load(name, getMinificationFilter(),
+ isUseCompression() ? TextureStoreFormat.GuessCompressedFormat
+ : TextureStoreFormat.GuessNoCompressedFormat, isFlipTextureVertically());
+ } else {
+ final ResourceSource source = _textureLocator.locateResource(name);
+ if (source != null) {
+ tex = TextureManager.load(source, getMinificationFilter(),
+ isUseCompression() ? TextureStoreFormat.GuessCompressedFormat
+ : TextureStoreFormat.GuessNoCompressedFormat, isFlipTextureVertically());
+ }
+ }
+ return tex;
+ }
+
+ private void addNormal(final Md2GlCommand cmd, final Md2Frame frame, final int normalIndex,
+ final FloatBufferData norms) {
+ final int index = cmd.vertIndices[normalIndex];
+ final byte[] vertData = frame.vertData;
+ Md2Normals.getNormalVector(vertData[index * 4 + 3], calcVert);
+ norms.getBuffer().put(calcVert.getXf()).put(calcVert.getYf()).put(calcVert.getZf());
+ }
+
+ private void addVert(final Md2GlCommand cmd, final Md2Frame frame, final int vertIndex, final FloatBufferData verts) {
+ final int index = cmd.vertIndices[vertIndex];
+ final byte[] vertData = frame.vertData;
+ calcVert.set((vertData[index * 4 + 0] & 0xFF), (vertData[index * 4 + 1] & 0xFF),
+ (vertData[index * 4 + 2] & 0xFF));
+ calcVert.multiplyLocal(frame.scale).addLocal(frame.translate);
+ verts.getBuffer().put(calcVert.getXf()).put(calcVert.getYf()).put(calcVert.getZf());
+ }
+
+ /**
+ * Reads a MD2 file from the given resource
+ *
+ * @param resource
+ * the name of the resource to find.
+ * @return an ObjGeometryStore data object containing the scene and other useful elements.
+ */
+ public Md2DataStore load(final String resource) {
+ final ResourceSource source;
+ if (_modelLocator == null) {
+ source = ResourceLocatorTool.locateResource(ResourceLocatorTool.TYPE_MODEL, resource);
+ } else {
+ source = _modelLocator.locateResource(resource);
+ }
+
+ if (source == null) {
+ throw new Error("Unable to locate '" + resource + "'");
+ }
+
+ return load(source);
+ }
+
+ static class Md2GlCommand {
+ IndexMode mode;
+ float[] texCoords;
+ int[] vertIndices;
+
+ Md2GlCommand(final IndexMode indexMode, final int length) {
+ mode = indexMode;
+ texCoords = new float[length * 2];
+ vertIndices = new int[length];
+ }
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Normals.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Normals.java
new file mode 100644
index 0000000..20afdc2
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/md2/Md2Normals.java
@@ -0,0 +1,190 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.md2;
+
+import com.ardor3d.math.Vector3;
+
+final class Md2Normals {
+ /** Pre-generated md2 normals. See also http://tfc.duke.free.fr/coding/src/anorms.h */
+ final static float[] NORMALS = { //
+ -0.525731f, 0.000000f, 0.850651f, //
+ -0.442863f, 0.238856f, 0.864188f, //
+ -0.295242f, 0.000000f, 0.955423f, //
+ -0.309017f, 0.500000f, 0.809017f, //
+ -0.162460f, 0.262866f, 0.951056f, //
+ 0.000000f, 0.000000f, 1.000000f, //
+ 0.000000f, 0.850651f, 0.525731f, //
+ -0.147621f, 0.716567f, 0.681718f, //
+ 0.147621f, 0.716567f, 0.681718f, //
+ 0.000000f, 0.525731f, 0.850651f, //
+ 0.309017f, 0.500000f, 0.809017f, //
+ 0.525731f, 0.000000f, 0.850651f, //
+ 0.295242f, 0.000000f, 0.955423f, //
+ 0.442863f, 0.238856f, 0.864188f, //
+ 0.162460f, 0.262866f, 0.951056f, //
+ -0.681718f, 0.147621f, 0.716567f, //
+ -0.809017f, 0.309017f, 0.500000f, //
+ -0.587785f, 0.425325f, 0.688191f, //
+ -0.850651f, 0.525731f, 0.000000f, //
+ -0.864188f, 0.442863f, 0.238856f, //
+ -0.716567f, 0.681718f, 0.147621f, //
+ -0.688191f, 0.587785f, 0.425325f, //
+ -0.500000f, 0.809017f, 0.309017f, //
+ -0.238856f, 0.864188f, 0.442863f, //
+ -0.425325f, 0.688191f, 0.587785f, //
+ -0.716567f, 0.681718f, -0.147621f, //
+ -0.500000f, 0.809017f, -0.309017f, //
+ -0.525731f, 0.850651f, 0.000000f, //
+ 0.000000f, 0.850651f, -0.525731f, //
+ -0.238856f, 0.864188f, -0.442863f, //
+ 0.000000f, 0.955423f, -0.295242f, //
+ -0.262866f, 0.951056f, -0.162460f, //
+ 0.000000f, 1.000000f, 0.000000f, //
+ 0.000000f, 0.955423f, 0.295242f, //
+ -0.262866f, 0.951056f, 0.162460f, //
+ 0.238856f, 0.864188f, 0.442863f, //
+ 0.262866f, 0.951056f, 0.162460f, //
+ 0.500000f, 0.809017f, 0.309017f, //
+ 0.238856f, 0.864188f, -0.442863f, //
+ 0.262866f, 0.951056f, -0.162460f, //
+ 0.500000f, 0.809017f, -0.309017f, //
+ 0.850651f, 0.525731f, 0.000000f, //
+ 0.716567f, 0.681718f, 0.147621f, //
+ 0.716567f, 0.681718f, -0.147621f, //
+ 0.525731f, 0.850651f, 0.000000f, //
+ 0.425325f, 0.688191f, 0.587785f, //
+ 0.864188f, 0.442863f, 0.238856f, //
+ 0.688191f, 0.587785f, 0.425325f, //
+ 0.809017f, 0.309017f, 0.500000f, //
+ 0.681718f, 0.147621f, 0.716567f, //
+ 0.587785f, 0.425325f, 0.688191f, //
+ 0.955423f, 0.295242f, 0.000000f, //
+ 1.000000f, 0.000000f, 0.000000f, //
+ 0.951056f, 0.162460f, 0.262866f, //
+ 0.850651f, -0.525731f, 0.000000f, //
+ 0.955423f, -0.295242f, 0.000000f, //
+ 0.864188f, -0.442863f, 0.238856f, //
+ 0.951056f, -0.162460f, 0.262866f, //
+ 0.809017f, -0.309017f, 0.500000f, //
+ 0.681718f, -0.147621f, 0.716567f, //
+ 0.850651f, 0.000000f, 0.525731f, //
+ 0.864188f, 0.442863f, -0.238856f, //
+ 0.809017f, 0.309017f, -0.500000f, //
+ 0.951056f, 0.162460f, -0.262866f, //
+ 0.525731f, 0.000000f, -0.850651f, //
+ 0.681718f, 0.147621f, -0.716567f, //
+ 0.681718f, -0.147621f, -0.716567f, //
+ 0.850651f, 0.000000f, -0.525731f, //
+ 0.809017f, -0.309017f, -0.500000f, //
+ 0.864188f, -0.442863f, -0.238856f, //
+ 0.951056f, -0.162460f, -0.262866f, //
+ 0.147621f, 0.716567f, -0.681718f, //
+ 0.309017f, 0.500000f, -0.809017f, //
+ 0.425325f, 0.688191f, -0.587785f, //
+ 0.442863f, 0.238856f, -0.864188f, //
+ 0.587785f, 0.425325f, -0.688191f, //
+ 0.688191f, 0.587785f, -0.425325f, //
+ -0.147621f, 0.716567f, -0.681718f, //
+ -0.309017f, 0.500000f, -0.809017f, //
+ 0.000000f, 0.525731f, -0.850651f, //
+ -0.525731f, 0.000000f, -0.850651f, //
+ -0.442863f, 0.238856f, -0.864188f, //
+ -0.295242f, 0.000000f, -0.955423f, //
+ -0.162460f, 0.262866f, -0.951056f, //
+ 0.000000f, 0.000000f, -1.000000f, //
+ 0.295242f, 0.000000f, -0.955423f, //
+ 0.162460f, 0.262866f, -0.951056f, //
+ -0.442863f, -0.238856f, -0.864188f, //
+ -0.309017f, -0.500000f, -0.809017f, //
+ -0.162460f, -0.262866f, -0.951056f, //
+ 0.000000f, -0.850651f, -0.525731f, //
+ -0.147621f, -0.716567f, -0.681718f, //
+ 0.147621f, -0.716567f, -0.681718f, //
+ 0.000000f, -0.525731f, -0.850651f, //
+ 0.309017f, -0.500000f, -0.809017f, //
+ 0.442863f, -0.238856f, -0.864188f, //
+ 0.162460f, -0.262866f, -0.951056f, //
+ 0.238856f, -0.864188f, -0.442863f, //
+ 0.500000f, -0.809017f, -0.309017f, //
+ 0.425325f, -0.688191f, -0.587785f, //
+ 0.716567f, -0.681718f, -0.147621f, //
+ 0.688191f, -0.587785f, -0.425325f, //
+ 0.587785f, -0.425325f, -0.688191f, //
+ 0.000000f, -0.955423f, -0.295242f, //
+ 0.000000f, -1.000000f, 0.000000f, //
+ 0.262866f, -0.951056f, -0.162460f, //
+ 0.000000f, -0.850651f, 0.525731f, //
+ 0.000000f, -0.955423f, 0.295242f, //
+ 0.238856f, -0.864188f, 0.442863f, //
+ 0.262866f, -0.951056f, 0.162460f, //
+ 0.500000f, -0.809017f, 0.309017f, //
+ 0.716567f, -0.681718f, 0.147621f, //
+ 0.525731f, -0.850651f, 0.000000f, //
+ -0.238856f, -0.864188f, -0.442863f, //
+ -0.500000f, -0.809017f, -0.309017f, //
+ -0.262866f, -0.951056f, -0.162460f, //
+ -0.850651f, -0.525731f, 0.000000f, //
+ -0.716567f, -0.681718f, -0.147621f, //
+ -0.716567f, -0.681718f, 0.147621f, //
+ -0.525731f, -0.850651f, 0.000000f, //
+ -0.500000f, -0.809017f, 0.309017f, //
+ -0.238856f, -0.864188f, 0.442863f, //
+ -0.262866f, -0.951056f, 0.162460f, //
+ -0.864188f, -0.442863f, 0.238856f, //
+ -0.809017f, -0.309017f, 0.500000f, //
+ -0.688191f, -0.587785f, 0.425325f, //
+ -0.681718f, -0.147621f, 0.716567f, //
+ -0.442863f, -0.238856f, 0.864188f, //
+ -0.587785f, -0.425325f, 0.688191f, //
+ -0.309017f, -0.500000f, 0.809017f, //
+ -0.147621f, -0.716567f, 0.681718f, //
+ -0.425325f, -0.688191f, 0.587785f, //
+ -0.162460f, -0.262866f, 0.951056f, //
+ 0.442863f, -0.238856f, 0.864188f, //
+ 0.162460f, -0.262866f, 0.951056f, //
+ 0.309017f, -0.500000f, 0.809017f, //
+ 0.147621f, -0.716567f, 0.681718f, //
+ 0.000000f, -0.525731f, 0.850651f, //
+ 0.425325f, -0.688191f, 0.587785f, //
+ 0.587785f, -0.425325f, 0.688191f, //
+ 0.688191f, -0.587785f, 0.425325f, //
+ -0.955423f, 0.295242f, 0.000000f, //
+ -0.951056f, 0.162460f, 0.262866f, //
+ -1.000000f, 0.000000f, 0.000000f, //
+ -0.850651f, 0.000000f, 0.525731f, //
+ -0.955423f, -0.295242f, 0.000000f, //
+ -0.951056f, -0.162460f, 0.262866f, //
+ -0.864188f, 0.442863f, -0.238856f, //
+ -0.951056f, 0.162460f, -0.262866f, //
+ -0.809017f, 0.309017f, -0.500000f, //
+ -0.864188f, -0.442863f, -0.238856f, //
+ -0.951056f, -0.162460f, -0.262866f, //
+ -0.809017f, -0.309017f, -0.500000f, //
+ -0.681718f, 0.147621f, -0.716567f, //
+ -0.681718f, -0.147621f, -0.716567f, //
+ -0.850651f, 0.000000f, -0.525731f, //
+ -0.688191f, 0.587785f, -0.425325f, //
+ -0.587785f, 0.425325f, -0.688191f, //
+ -0.425325f, 0.688191f, -0.587785f, //
+ -0.425325f, -0.688191f, -0.587785f, //
+ -0.587785f, -0.425325f, -0.688191f, //
+ -0.688191f, -0.587785f, -0.425325f //
+ };
+
+ public static void getNormalVector(final byte i, final Vector3 store) {
+ final int index = 3 * (i & 0xff);
+ if (index < 0 || index > Md2Normals.NORMALS.length) {
+ store.set(0, 1, 0);
+ } else {
+ store.set(Md2Normals.NORMALS[index + 0], Md2Normals.NORMALS[index + 1], Md2Normals.NORMALS[index + 2]);
+ }
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjDataStore.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjDataStore.java
new file mode 100644
index 0000000..0c9fa91
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjDataStore.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.obj;
+
+import java.util.List;
+
+import com.ardor3d.math.Vector3;
+import com.google.common.collect.Lists;
+
+public class ObjDataStore {
+ private final List<Vector3> _vertices = Lists.newArrayList();
+ private final List<Vector3> _normals = Lists.newArrayList();
+ private final List<Vector3> _generatedNormals = Lists.newArrayList();
+ private final List<Vector3> _uvs = Lists.newArrayList();
+
+ public List<Vector3> getVertices() {
+ return _vertices;
+ }
+
+ public List<Vector3> getNormals() {
+ return _normals;
+ }
+
+ public List<Vector3> getGeneratedNormals() {
+ return _generatedNormals;
+ }
+
+ public List<Vector3> getUvs() {
+ return _uvs;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjGeometryStore.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjGeometryStore.java
new file mode 100644
index 0000000..e472bad
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjGeometryStore.java
@@ -0,0 +1,369 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.obj;
+
+import java.nio.Buffer;
+import java.nio.FloatBuffer;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+import com.ardor3d.math.Vector2;
+import com.ardor3d.math.Vector3;
+import com.ardor3d.renderer.queue.RenderBucketType;
+import com.ardor3d.renderer.state.BlendState;
+import com.ardor3d.renderer.state.MaterialState;
+import com.ardor3d.renderer.state.TextureState;
+import com.ardor3d.scenegraph.IndexBufferData;
+import com.ardor3d.scenegraph.Line;
+import com.ardor3d.scenegraph.Mesh;
+import com.ardor3d.scenegraph.Node;
+import com.ardor3d.scenegraph.Point;
+import com.ardor3d.scenegraph.Spatial;
+import com.ardor3d.scenegraph.hint.LightCombineMode;
+import com.ardor3d.util.geom.BufferUtils;
+import com.ardor3d.util.geom.GeometryTool;
+import com.ardor3d.util.geom.GeometryTool.MatchCondition;
+import com.ardor3d.util.geom.VertGroupData;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+public class ObjGeometryStore {
+ private static final String DEFAULT_GROUP = "_default_";
+
+ private final ObjDataStore _dataStore = new ObjDataStore();
+
+ private int _totalPoints = 0;
+ private int _totalLines = 0;
+ private int _totalMeshes = 0;
+ private final Node _root = new Node();
+ private final Map<String, Spatial> _groupMap = Maps.newHashMap();
+
+ private ObjMaterial _currentMaterial = new ObjMaterial("default");
+ private String _currentObjectName;
+ private String[] _currentGroupNames;
+
+ private ObjSetManager _meshManager;
+ private ObjSetManager _lineManager;
+ private ObjSetManager _pointManager;
+
+ private final Map<String, ObjMaterial> materialLibrary = Maps.newHashMap();
+ private final Map<Spatial, String> _materialMap = Maps.newHashMap();
+
+ public Map<String, ObjMaterial> getMaterialLibrary() {
+ return materialLibrary;
+ }
+
+ public ObjDataStore getDataStore() {
+ return _dataStore;
+ }
+
+ public Node getScene() {
+ return _root;
+ }
+
+ void addFace(final List<ObjIndexSet> indices) {
+ if (_meshManager == null) {
+ _meshManager = new ObjSetManager();
+ }
+
+ // Build a fan of triangles
+ final ObjIndexSet first = indices.get(0);
+ final int firstIndex = _meshManager.findSet(first);
+ ObjIndexSet second = indices.get(1);
+ int secondIndex = _meshManager.findSet(second);
+ for (int i = 2; i < indices.size(); i++) {
+ final ObjIndexSet third = indices.get(i);
+ final int thirdIndex = _meshManager.findSet(third);
+ _meshManager.addIndex(firstIndex);
+ _meshManager.addIndex(secondIndex);
+ _meshManager.addIndex(thirdIndex);
+ if (first.getVnIndex() == -1 || second.getVnIndex() == -1 || third.getVnIndex() == -1) {
+ // Generate flat face normal.
+ final Vector3 v = new Vector3(_dataStore.getVertices().get(second.getVIndex()));
+ final Vector3 w = new Vector3(_dataStore.getVertices().get(third.getVIndex()));
+ v.subtractLocal(_dataStore.getVertices().get(first.getVIndex()));
+ w.subtractLocal(_dataStore.getVertices().get(first.getVIndex()));
+ v.crossLocal(w);
+ v.normalizeLocal();
+ _dataStore.getGeneratedNormals().add(v);
+ final int genIndex = -1 * (_dataStore.getGeneratedNormals().size() - 1) - 2;
+ if (first.getVnIndex() == -1) {
+ first.setVnIndex(genIndex);
+ }
+ if (second.getVnIndex() == -1) {
+ second.setVnIndex(genIndex);
+ }
+ if (third.getVnIndex() == -1) {
+ third.setVnIndex(genIndex);
+ }
+ }
+ second = third;
+ secondIndex = thirdIndex;
+ }
+ }
+
+ void addLine(final List<ObjIndexSet> indices) {
+ if (_lineManager == null) {
+ _lineManager = new ObjSetManager();
+ }
+
+ // Build a single long line
+ for (int i = 0; i < indices.size(); i++) {
+ final ObjIndexSet point = indices.get(i);
+ final int index = _lineManager.findSet(point);
+ _lineManager.addIndex(index);
+ }
+
+ _lineManager.addLength(indices.size());
+ }
+
+ void addPoints(final List<ObjIndexSet> indices) {
+ if (_pointManager == null) {
+ _pointManager = new ObjSetManager();
+ }
+
+ // Add points
+ for (int i = 0; i < indices.size(); i++) {
+ final ObjIndexSet point = indices.get(i);
+ final int index = _pointManager.findSet(point);
+ _pointManager.addIndex(index);
+ }
+ }
+
+ void setCurrentGroupNames(final String[] names) {
+ commitObjects();
+ _currentGroupNames = names;
+ }
+
+ void setCurrentObjectName(final String name) {
+ commitObjects();
+ _currentObjectName = name;
+ }
+
+ void setCurrentMaterial(final ObjMaterial material) {
+ if (material != null) {
+ commitObjects();
+ _currentMaterial = material;
+ }
+ }
+
+ void cleanup() {
+ _currentGroupNames = null;
+ _currentMaterial = null;
+ _currentObjectName = null;
+
+ _meshManager = null;
+ _lineManager = null;
+ _pointManager = null;
+ }
+
+ void commitObjects() {
+ // go through each manager, if not null, turn into a scenegraph object and attach to root.
+ if (_pointManager != null) {
+ String name = _currentObjectName;
+ if (name == null) {
+ name = "obj_points" + _totalPoints;
+ }
+
+ final Vector3[] vertices = new Vector3[_pointManager.getStore().size()];
+ int i = 0;
+ for (final ObjIndexSet set : _pointManager.getStore().keySet()) {
+ vertices[i++] = _dataStore.getVertices().get(set.getVIndex());
+ }
+
+ final Point points = new Point(name, vertices, null, null, null);
+ final IndexBufferData<? extends Buffer> indexBuffer = BufferUtils.createIndexBufferData(_pointManager
+ .getIndices().size(), vertices.length - 1);
+ for (final int index : _pointManager.getIndices()) {
+ indexBuffer.put(index);
+ }
+ points.getMeshData().setIndices(indexBuffer);
+
+ GeometryTool.minimizeVerts(points, EnumSet.noneOf(MatchCondition.class));
+
+ applyCurrentMaterial(points);
+ mapToGroups(points);
+
+ points.updateModelBound();
+
+ _root.attachChild(points);
+ _pointManager = null;
+ _totalPoints++;
+ }
+
+ if (_lineManager != null) {
+ String name = _currentObjectName;
+ if (name == null) {
+ name = "obj_lines" + _totalLines;
+ }
+
+ final Vector3[] vertices = new Vector3[_lineManager.getStore().size()];
+ final Vector2[] uvs = new Vector2[vertices.length];
+ boolean hasUVs = false;
+ int i = 0;
+ for (final ObjIndexSet set : _lineManager.getStore().keySet()) {
+ vertices[i] = _dataStore.getVertices().get(set.getVIndex());
+ if (set.getVtIndex() >= 0) {
+ final Vector3 uv = _dataStore.getUvs().get(set.getVtIndex());
+ // our line only supports 2d uvs
+ uvs[i] = new Vector2(uv.getX(), uv.getY());
+ hasUVs = true;
+ }
+ i++;
+ }
+
+ final Line line = new Line(name, vertices, null, null, hasUVs ? uvs : null);
+ final IndexBufferData<? extends Buffer> indexBuffer = BufferUtils.createIndexBufferData(_lineManager
+ .getIndices().size(), vertices.length - 1);
+ for (final int index : _lineManager.getIndices()) {
+ indexBuffer.put(index);
+ }
+ line.getMeshData().setIndices(indexBuffer);
+ if (_lineManager.getLengths().size() > 1) {
+ final int[] lengths = new int[_lineManager.getLengths().size()];
+ i = 0;
+ for (final int l : _lineManager.getLengths()) {
+ lengths[i++] = l;
+ }
+ line.getMeshData().setIndexLengths(lengths);
+ }
+ GeometryTool.minimizeVerts(line, EnumSet.of(MatchCondition.UVs));
+
+ applyCurrentMaterial(line);
+ mapToGroups(line);
+
+ line.updateModelBound();
+
+ _root.attachChild(line);
+ _lineManager = null;
+ _totalLines++;
+ }
+
+ if (_meshManager != null) {
+ String name = _currentObjectName;
+ if (name == null) {
+ name = "obj_mesh" + _totalMeshes;
+ }
+
+ final Mesh mesh = new Mesh(name);
+
+ final FloatBuffer vertices = BufferUtils.createVector3Buffer(_meshManager.getStore().size());
+ final FloatBuffer normals = BufferUtils.createFloatBuffer(vertices.capacity());
+ final FloatBuffer uvs = BufferUtils.createFloatBuffer(vertices.capacity());
+ boolean hasNormals = false, hasUVs = false;
+
+ int j = 0;
+ final long[] vertGroups = new long[_meshManager.getStore().size()];
+ final List<Long> groups = Lists.newArrayList();
+ Vector3 vector;
+ for (final ObjIndexSet set : _meshManager.getStore().keySet()) {
+ vertGroups[j] = set.getSmoothGroup();
+ if (!groups.contains(set.getSmoothGroup())) {
+ groups.add(set.getSmoothGroup());
+ }
+ vector = _dataStore.getVertices().get(set.getVIndex());
+ vertices.put(vector.getXf()).put(vector.getYf()).put(vector.getZf());
+ if (set.getVnIndex() >= 0) {
+ vector = _dataStore.getNormals().get(set.getVnIndex());
+ normals.put(vector.getXf()).put(vector.getYf()).put(vector.getZf());
+ hasNormals = true;
+ } else if (set.getVnIndex() < -1) {
+ vector = _dataStore.getGeneratedNormals().get(-1 * set.getVnIndex() - 2);
+ normals.put(vector.getXf()).put(vector.getYf()).put(vector.getZf());
+ hasNormals = true;
+ }
+ if (set.getVtIndex() >= 0) {
+ vector = _dataStore.getUvs().get(set.getVtIndex());
+ // TODO: add 3d tex support?
+ uvs.put(vector.getXf()).put(vector.getYf());
+ hasUVs = true;
+ }
+ j++;
+ }
+
+ mesh.getMeshData().setVertexBuffer(vertices);
+ if (hasNormals) {
+ mesh.getMeshData().setNormalBuffer(normals);
+ }
+ if (hasUVs) {
+ mesh.getMeshData().setTextureBuffer(uvs, 0);
+ }
+
+ final IndexBufferData<? extends Buffer> indexBuffer = BufferUtils.createIndexBufferData(_meshManager
+ .getIndices().size(), _meshManager.getStore().size() - 1);
+ for (final int index : _meshManager.getIndices()) {
+ indexBuffer.put(index);
+ }
+ mesh.getMeshData().setIndices(indexBuffer);
+
+ final VertGroupData groupData = new VertGroupData();
+ // set all smooth groups to use "blend as long as UVs and SmoothGroup are same".
+ for (final long group : groups) {
+ groupData.setGroupConditions(group, EnumSet.of(MatchCondition.UVs));
+ }
+ // set the "no smooth" smooth group to use "blend only if vertex is same". (No color data in obj, so
+ // ignoring)
+ groupData.setVertGroups(vertGroups);
+ groupData.setGroupConditions(VertGroupData.DEFAULT_GROUP,
+ EnumSet.of(MatchCondition.Normal, MatchCondition.UVs));
+ GeometryTool.minimizeVerts(mesh, groupData);
+
+ applyCurrentMaterial(mesh);
+ mapToGroups(mesh);
+
+ mesh.updateModelBound();
+
+ _root.attachChild(mesh);
+ _meshManager = null;
+ _totalMeshes++;
+ }
+ }
+
+ private void applyCurrentMaterial(final Spatial target) {
+ final MaterialState material = _currentMaterial.getMaterialState();
+ if (material != null) {
+ target.setRenderState(material);
+ }
+
+ final TextureState tState = _currentMaterial.getTextureState();
+ if (tState != null) {
+ target.setRenderState(tState);
+ }
+
+ final BlendState blend = _currentMaterial.getBlendState();
+ if (blend != null) {
+ target.setRenderState(blend);
+ target.getSceneHints().setRenderBucketType(RenderBucketType.Transparent);
+ }
+
+ if (_currentMaterial.illumType == 0) {
+ target.getSceneHints().setLightCombineMode(LightCombineMode.Off);
+ }
+
+ _materialMap.put(target, _currentMaterial.getName());
+ }
+
+ private void mapToGroups(final Spatial target) {
+ if (_currentGroupNames != null) {
+ for (final String groupName : _currentGroupNames) {
+ _groupMap.put(groupName, target);
+ }
+ } else {
+ _groupMap.put(ObjGeometryStore.DEFAULT_GROUP, target);
+ }
+
+ }
+
+ public Map<Spatial, String> getMaterialMap() {
+ return _materialMap;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjImporter.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjImporter.java
new file mode 100644
index 0000000..0cfc59a
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjImporter.java
@@ -0,0 +1,515 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.obj;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import com.ardor3d.image.Texture.MinificationFilter;
+import com.ardor3d.image.TextureStoreFormat;
+import com.ardor3d.math.MathUtils;
+import com.ardor3d.math.Vector3;
+import com.ardor3d.util.TextureManager;
+import com.ardor3d.util.resource.ResourceLocator;
+import com.ardor3d.util.resource.ResourceLocatorTool;
+import com.ardor3d.util.resource.ResourceSource;
+import com.google.common.collect.Lists;
+
+/**
+ * Wavefront OBJ importer. See <a href="http://local.wasp.uwa.edu.au/~pbourke/dataformats/obj/">the format spec</a>
+ */
+public class ObjImporter {
+ private static final Logger logger = Logger.getLogger(ObjImporter.class.getName());
+
+ private boolean _loadTextures = true;
+ private ResourceLocator _textureLocator;
+ private ResourceLocator _modelLocator;
+ private ResourceLocator _materialLocator;
+
+ private float _specularMax = 200;
+
+ // texture defaults
+ private MinificationFilter _minificationFilter = MinificationFilter.Trilinear;
+ private boolean _useCompression = true;
+ private boolean _flipTextureVertically = true;
+
+ public boolean isLoadTextures() {
+ return _loadTextures;
+ }
+
+ public ObjImporter setLoadTextures(final boolean loadTextures) {
+ _loadTextures = loadTextures;
+ return this;
+ }
+
+ public ObjImporter setTextureLocator(final ResourceLocator locator) {
+ _textureLocator = locator;
+ return this;
+ }
+
+ public ObjImporter setModelLocator(final ResourceLocator locator) {
+ _modelLocator = locator;
+ return this;
+ }
+
+ public ObjImporter setMaterialLocator(final ResourceLocator locator) {
+ _materialLocator = locator;
+ return this;
+ }
+
+ public void setFlipTextureVertically(final boolean flipTextureVertically) {
+ _flipTextureVertically = flipTextureVertically;
+ }
+
+ public boolean isFlipTextureVertically() {
+ return _flipTextureVertically;
+ }
+
+ public void setUseCompression(final boolean useCompression) {
+ _useCompression = useCompression;
+ }
+
+ public boolean isUseCompression() {
+ return _useCompression;
+ }
+
+ public void setMinificationFilter(final MinificationFilter minificationFilter) {
+ _minificationFilter = minificationFilter;
+ }
+
+ public MinificationFilter getMinificationFilter() {
+ return _minificationFilter;
+ }
+
+ public float getObjSpecularMax() {
+ return _specularMax;
+ }
+
+ public void setObjSpecularMax(final float max) {
+ _specularMax = max;
+ }
+
+ /**
+ * Reads a Wavefront OBJ file from the given resource
+ *
+ * @param resource
+ * the name of the resource to find.
+ * @return an ObjGeometryStore data object containing the scene and other useful elements.
+ */
+ public ObjGeometryStore load(final String resource) {
+ final ResourceSource source;
+ if (_modelLocator == null) {
+ source = ResourceLocatorTool.locateResource(ResourceLocatorTool.TYPE_MODEL, resource);
+ } else {
+ source = _modelLocator.locateResource(resource);
+ }
+
+ if (source == null) {
+ throw new Error("Unable to locate '" + resource + "'");
+ }
+
+ return load(source);
+ }
+
+ /**
+ * Reads a Wavefront OBJ file from the given resource
+ *
+ * @param resource
+ * the name of the resource to find.
+ * @return an ObjGeometryStore data object containing the scene and other useful elements.
+ */
+ public ObjGeometryStore load(final ResourceSource resource) {
+ try {
+ final ObjGeometryStore store = new ObjGeometryStore();
+ long currentSmoothGroup = -1;
+
+ final BufferedReader reader = new BufferedReader(new InputStreamReader(resource.openStream()));
+ String line;
+ int lineNo = 0;
+ while ((line = reader.readLine()) != null) {
+ lineNo++;
+ line = line.trim();
+ // handle line continuation marker \
+ while (line.endsWith("\\")) {
+ line = line.substring(0, line.length() - 1);
+ final String s = reader.readLine();
+ if (s != null) {
+ line += s;
+ line = line.trim();
+ }
+ }
+
+ // ignore comments. goto next line
+ if (line.startsWith("#")) {
+ continue;
+ }
+
+ // tokenize line
+ final String[] tokens = line.split("\\s+");
+
+ // no tokens? must be an empty line. goto next line
+ if (tokens.length == 0) {
+ continue;
+ }
+
+ // grab our "keyword"
+ final String keyword = tokens[0];
+
+ // Act on our keyword...
+
+ // -------- VERTEX DATA KEYWORDS --------
+ // if vertex
+ if ("v".equals(keyword)) {
+ // XXX: support optional weight?
+ // final double w = tokens.length > 4 ? Double.valueOf(tokens[4]) : 1.0;
+
+ final Vector3 vertex = new Vector3(Double.valueOf(tokens[1]), Double.valueOf(tokens[2]),
+ Double.valueOf(tokens[3]));
+ store.getDataStore().getVertices().add(vertex);
+ }
+
+ // if texture coords
+ else if ("vt".equals(keyword)) {
+ final double v = tokens.length > 2 ? Double.valueOf(tokens[2]) : 0;
+ final double w = tokens.length > 3 ? Double.valueOf(tokens[3]) : 0;
+ final Vector3 coord = new Vector3(Double.valueOf(tokens[1]), v, w);
+ store.getDataStore().getUvs().add(coord);
+ }
+
+ // if normal vector
+ else if ("vn".equals(keyword)) {
+ final Vector3 normal = new Vector3(Double.valueOf(tokens[1]), Double.valueOf(tokens[2]),
+ Double.valueOf(tokens[3]));
+ store.getDataStore().getNormals().add(normal);
+ }
+
+ // if parameter space vertices
+ else if ("vp".equals(keyword)) {
+ // TODO: Add support for vp
+ ObjImporter.logger.warning("ObjModelImporter: vp not supported. (line " + lineNo + ") " + line);
+ }
+
+ // if curve/surface type
+ else if ("cstype".equals(keyword)) {
+ // TODO: Add support for cstype
+ ObjImporter.logger
+ .warning("ObjModelImporter: cstype not supported. (line " + lineNo + ") " + line);
+ }
+
+ // if degree
+ else if ("deg".equals(keyword)) {
+ // TODO: Add support for degree
+ ObjImporter.logger.warning("ObjModelImporter: deg not supported. (line " + lineNo + ") " + line);
+ }
+
+ // if basis matrix
+ else if ("bmat".equals(keyword)) {
+ // TODO: Add support for basis matrix
+ ObjImporter.logger.warning("ObjModelImporter: bmat not supported. (line " + lineNo + ") " + line);
+ }
+
+ // if step size
+ else if ("step".equals(keyword)) {
+ // TODO: Add support for step size
+ ObjImporter.logger.warning("ObjModelImporter: step not supported. (line " + lineNo + ") " + line);
+ }
+
+ // -------- GROUPING KEYWORDS --------
+
+ // if group name(s)
+ else if ("g".equals(keyword)) {
+ if (tokens.length < 2) {
+ store.setCurrentGroupNames(null);
+ continue;
+ // throw new Error("wrong number of args. g must have at least 1 argument. (line " + lineNo
+ // + ") " + line);
+ }
+
+ // Each token is a name
+ final String[] currentGroupNames = new String[tokens.length - 1];
+ store.setCurrentGroupNames(currentGroupNames);
+ System.arraycopy(tokens, 1, currentGroupNames, 0, tokens.length - 1);
+ }
+
+ // if smoothing group
+ else if ("s".equals(keyword)) {
+ if (tokens.length != 2) {
+ throw new Error("wrong number of args. s must have 1 argument. (line " + lineNo + ") " + line);
+ }
+
+ if ("off".equalsIgnoreCase(tokens[1])) {
+ currentSmoothGroup = 0;
+ } else {
+ currentSmoothGroup = Long.parseLong(tokens[1]);
+ }
+ }
+
+ // if merge group
+ else if ("mg".equals(keyword)) {
+ // TODO: Add support for merge groups
+ ObjImporter.logger.warning("ObjModelImporter: mg not supported. (line " + lineNo + ") " + line);
+ }
+
+ // if object name
+ else if ("o".equals(keyword)) {
+ if (tokens.length < 2) {
+ throw new Error("wrong number of args. o must have 1 argument. (line " + lineNo + ") " + line);
+ }
+ store.setCurrentObjectName(tokens[1]);
+ }
+
+ // -------- RENDER ATTRIBUTES KEYWORDS --------
+
+ // if material library(ies)
+ else if ("mtllib".equals(keyword)) {
+ if (tokens.length < 2) {
+ throw new Error("wrong number of args. mtllib must have at least 1 argument. (line " + lineNo
+ + ") " + line);
+ }
+
+ // load material libraries
+ for (int i = 1; i < tokens.length; i++) {
+ loadMaterialLibrary(tokens[i], resource, store.getMaterialLibrary());
+ }
+ }
+
+ // if use material command
+ else if ("usemtl".equals(keyword)) {
+ if (tokens.length != 2) {
+ throw new Error("wrong number of args. usemtl must have 1 argument. (line " + lineNo + ") "
+ + line);
+ }
+
+ // set new material
+ store.setCurrentMaterial(store.getMaterialLibrary().get(tokens[1]));
+ }
+
+ // -------- ELEMENTS KEYWORDS --------
+
+ // if point
+ else if ("p".equals(keyword) && tokens.length > 1) {
+ if (tokens.length < 2) {
+ throw new Error("wrong number of args. p must have at least 1 vertex. (line " + lineNo + ") "
+ + line);
+ }
+
+ // Each token corresponds to 1 vertex entry
+ final List<ObjIndexSet> indices = Lists.newArrayList();
+ for (int i = 1; i < tokens.length; i++) {
+ indices.add(new ObjIndexSet(tokens[i], store.getDataStore(), currentSmoothGroup));
+ }
+ store.addPoints(indices);
+ }
+
+ // if line
+ else if ("l".equals(keyword) && tokens.length > 1) {
+ if (tokens.length < 3) {
+ throw new Error("wrong number of args. l must have at least 2 vertices. (line " + lineNo
+ + ") " + line);
+ }
+
+ // Each token corresponds to 1 vertex entry and possibly one texture entry
+ final List<ObjIndexSet> indices = Lists.newArrayList();
+ for (int i = 1; i < tokens.length; i++) {
+ indices.add(new ObjIndexSet(tokens[i], store.getDataStore(), currentSmoothGroup));
+ }
+ store.addLine(indices);
+ }
+
+ // if face
+ else if (("f".equals(keyword) || "fo".equals(keyword)) && tokens.length > 1) {
+ if (tokens.length < 4) {
+ throw new Error("wrong number of args. f must have at least 3 vertices. (line " + lineNo
+ + ") " + line);
+ }
+
+ // Each token corresponds to 1 vertex entry and possibly one texture entry and normal entry.
+ final List<ObjIndexSet> indices = Lists.newArrayList();
+ for (int i = 1; i < tokens.length; i++) {
+ indices.add(new ObjIndexSet(tokens[i], store.getDataStore(), currentSmoothGroup));
+ }
+ store.addFace(indices);
+ }
+
+ // if curve
+ else if ("curv".equals(keyword)) {
+ // TODO: Add support for curves
+ ObjImporter.logger.warning("ObjModelImporter: curv not supported. (line " + lineNo + ") " + line);
+ }
+
+ // if 2d curve
+ else if ("curv2".equals(keyword)) {
+ // TODO: Add support for 2d curves
+ ObjImporter.logger.warning("ObjModelImporter: curv2 not supported. (line " + lineNo + ") " + line);
+ }
+
+ // if surface
+ else if ("surf".equals(keyword)) {
+ // TODO: Add support for surfaces
+ ObjImporter.logger.warning("ObjModelImporter: surf not supported. (line " + lineNo + ") " + line);
+ }
+ }
+
+ store.commitObjects();
+ store.cleanup();
+ return store;
+ } catch (final Exception e) {
+ throw new Error("Unable to load obj resource from URL: " + resource, e);
+ }
+ }
+
+ /**
+ * Load a .mtl resource
+ *
+ * @param fileName
+ * the name of the mtl resource to load.
+ * @param modelSource
+ * a source to pull the mtl relatively. Used only if a material locator was not set on this importer.
+ * @param store
+ * our material store to place the contents of the file in.
+ */
+ private void loadMaterialLibrary(final String fileName, final ResourceSource modelSource,
+ final Map<String, ObjMaterial> store) {
+ final ResourceSource source;
+ if (_materialLocator == null) {
+ source = modelSource.getRelativeSource(fileName);
+ } else {
+ source = _materialLocator.locateResource(fileName);
+ }
+
+ if (source == null) {
+ throw new Error("Unable to locate mtllib '" + fileName + "'");
+ }
+
+ loadMaterialLibrary(source, store);
+ }
+
+ /**
+ * Load a .mtl resource
+ *
+ * @param resource
+ * the mtl file to load, as a ResourceSource
+ * @param store
+ * our material store to place the contents of the file in.
+ */
+ private void loadMaterialLibrary(final ResourceSource resource, final Map<String, ObjMaterial> store) {
+ try {
+ final BufferedReader reader = new BufferedReader(new InputStreamReader(resource.openStream()));
+ String line;
+ ObjMaterial currentMaterial = null;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ // handle line continuation marker \
+ while (line.endsWith("\\")) {
+ line = line.substring(0, line.length() - 1);
+ final String s = reader.readLine();
+ if (s != null) {
+ line += s;
+ line = line.trim();
+ }
+ }
+
+ // ignore comments. goto next line
+ if (line.startsWith("#") || line.length() == 0) {
+ continue;
+ }
+
+ // tokenize line
+ final String[] tokens = line.split("\\s+");
+
+ // no tokens? must be an empty line. goto next line
+ if (tokens.length == 0) {
+ continue;
+ }
+
+ // grab our "keyword"
+ final String keyword = tokens[0];
+
+ // Act on our keyword...
+
+ // if newmtl
+ if ("newmtl".equals(keyword)) {
+ // start new material
+ currentMaterial = new ObjMaterial(tokens[1]);
+ store.put(tokens[1], currentMaterial);
+ continue;
+ }
+
+ if (currentMaterial == null) {
+ ObjImporter.logger.warning("No material is set");
+ return;
+ }
+
+ // if ambient value
+ if ("Ka".equals(keyword)) {
+ currentMaterial.Ka = new float[] { Float.parseFloat(tokens[1]), Float.parseFloat(tokens[2]),
+ Float.parseFloat(tokens[3]) };
+ }
+
+ // if diffuse value
+ else if ("Kd".equals(keyword)) {
+ currentMaterial.Kd = new float[] { Float.parseFloat(tokens[1]), Float.parseFloat(tokens[2]),
+ Float.parseFloat(tokens[3]) };
+ }
+
+ // if specular value
+ else if ("Ks".equals(keyword)) {
+ currentMaterial.Ks = new float[] { Float.parseFloat(tokens[1]), Float.parseFloat(tokens[2]),
+ Float.parseFloat(tokens[3]) };
+ }
+
+ // if illumination style
+ else if ("illum".equals(keyword)) {
+ currentMaterial.illumType = Integer.parseInt(tokens[1]);
+ }
+
+ // if "dissolve" (alpha) value
+ else if ("d".equals(keyword)) {
+ currentMaterial.d = Float.parseFloat(tokens[1]);
+ }
+
+ // if ambient value
+ else if ("Ns".equals(keyword)) {
+ final float Ns = Float.parseFloat(tokens[1]);
+ currentMaterial.Ns = 128 * MathUtils.clamp(Ns, 0, _specularMax) / _specularMax;
+ }
+
+ // if we mapped a texture to alpha
+ else if ("map_d".equals(keyword)) {
+ // force blending... probably also used texture in map_Kd, etc.
+ currentMaterial.forceBlend = true;
+ }
+
+ // if texture
+ else if (isLoadTextures() && "map_Kd".equals(keyword)) {
+ // TODO: it's possible for map_Kd to have arguments, then filename.
+ final String textureName = line.substring("map_Kd".length()).trim();
+ currentMaterial.textureName = textureName;
+ if (_textureLocator == null) {
+ currentMaterial.map_Kd = TextureManager.load(textureName, getMinificationFilter(),
+ isUseCompression() ? TextureStoreFormat.GuessCompressedFormat
+ : TextureStoreFormat.GuessNoCompressedFormat, isFlipTextureVertically());
+ } else {
+ final ResourceSource source = _textureLocator.locateResource(textureName);
+ currentMaterial.map_Kd = TextureManager.load(source, getMinificationFilter(),
+ isUseCompression() ? TextureStoreFormat.GuessCompressedFormat
+ : TextureStoreFormat.GuessNoCompressedFormat, isFlipTextureVertically());
+ }
+ }
+ }
+ } catch (final Exception e) {
+ throw new Error("Unable to load mtllib resource from URL: " + resource, e);
+ }
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjIndexSet.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjIndexSet.java
new file mode 100644
index 0000000..0e2bec1
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjIndexSet.java
@@ -0,0 +1,87 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.obj;
+
+public class ObjIndexSet {
+ private final int _vIndex, _vtIndex;
+ private final long _sGroup;
+ private int _vnIndex;
+
+ public ObjIndexSet(final String parts, final ObjDataStore store, final long smoothGroup) {
+ final String[] tokens = parts.split("/");
+ _vIndex = tokens.length < 1 ? -1 : parseValue(tokens[0], store.getVertices().size());
+ _vtIndex = tokens.length < 2 ? -1 : parseValue(tokens[1], store.getUvs().size());
+ _vnIndex = tokens.length < 3 ? -1 : parseValue(tokens[2], store.getNormals().size());
+ _sGroup = smoothGroup;
+ }
+
+ private int parseValue(final String token, final int currentPosition) {
+ if (token == null || "".equals(token)) {
+ return -1;
+ } else {
+ int value = Integer.parseInt(token);
+ if (value < 0) {
+ value += currentPosition;
+ } else {
+ // OBJ is 1 based, so drop 1.
+ value--;
+ }
+ return value;
+ }
+ }
+
+ public long getSmoothGroup() {
+ // normals override smoothing
+ if (_vnIndex >= 0) {
+ return 0;
+ }
+ return _sGroup;
+ }
+
+ public int getVIndex() {
+ return _vIndex;
+ }
+
+ public int getVtIndex() {
+ return _vtIndex;
+ }
+
+ public void setVnIndex(final int index) {
+ _vnIndex = index;
+ }
+
+ public int getVnIndex() {
+ return _vnIndex;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result += 31 * result + _vIndex;
+ result += 31 * result + _vtIndex;
+ result += 31 * result + _vnIndex;
+ result += 31 * result + _sGroup;
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ObjIndexSet)) {
+ return false;
+ }
+ final ObjIndexSet comp = (ObjIndexSet) o;
+ return _vIndex == comp._vIndex && _vnIndex == comp._vnIndex && _vtIndex == comp._vtIndex
+ && _sGroup == comp._sGroup;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjMaterial.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjMaterial.java
new file mode 100644
index 0000000..f060cec
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjMaterial.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.obj;
+
+import com.ardor3d.image.Texture;
+import com.ardor3d.math.ColorRGBA;
+import com.ardor3d.math.MathUtils;
+import com.ardor3d.renderer.state.BlendState;
+import com.ardor3d.renderer.state.MaterialState;
+import com.ardor3d.renderer.state.TextureState;
+
+public class ObjMaterial {
+ private final String name;
+
+ float[] Ka = null;
+ float[] Kd = null;
+ float[] Ks = null;
+ float Ns = -1;
+
+ String textureName;
+ Texture map_Kd = null;
+
+ int illumType = 2;
+
+ boolean forceBlend = false;
+ float d = -1;
+
+ public ObjMaterial(final String name) {
+ this.name = name;
+ }
+
+ public BlendState getBlendState() {
+ if (forceBlend || d != -1 && d < 1.0f) {
+ final BlendState blend = new BlendState();
+ blend.setBlendEnabled(true);
+ blend.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
+ blend.setDestinationFunction(BlendState.DestinationFunction.OneMinusSourceAlpha);
+ blend.setTestEnabled(true);
+ blend.setTestFunction(BlendState.TestFunction.GreaterThan);
+ blend.setReference(0);
+ return blend;
+ }
+ return null;
+ }
+
+ public TextureState getTextureState() {
+ if (map_Kd != null) {
+ final TextureState tState = new TextureState();
+ tState.setTexture(map_Kd, 0);
+ return tState;
+ }
+ return null;
+ }
+
+ public MaterialState getMaterialState() {
+ if (Ka != null || Kd != null || Ks != null || d != -1 || Ns != -1) {
+ final MaterialState material = new MaterialState();
+ final float alpha = d != -1 ? MathUtils.clamp(d, 0, 1) : 1;
+ if (Ka != null) {
+ material.setAmbient(new ColorRGBA(Ka[0], Ka[1], Ka[2], alpha));
+ }
+ if (Kd != null) {
+ material.setDiffuse(new ColorRGBA(Kd[0], Kd[1], Kd[2], alpha));
+ }
+ if (Ks != null) {
+ material.setSpecular(new ColorRGBA(Ks[0], Ks[1], Ks[2], alpha));
+ }
+
+ if (Ns != -1) {
+ material.setShininess(Ns);
+ }
+
+ return material;
+ }
+ return null;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getTextureName() {
+ return textureName;
+ }
+
+ public Texture getMap_Kd() {
+ return map_Kd;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjSetManager.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjSetManager.java
new file mode 100644
index 0000000..116371d
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/obj/ObjSetManager.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.obj;
+
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+public class ObjSetManager {
+ private final Map<ObjIndexSet, Integer> _store = Maps.newLinkedHashMap();
+ private final List<Integer> _indices = Lists.newArrayList();
+ private final List<Integer> _lengths = Lists.newArrayList();
+
+ public int findSet(final ObjIndexSet set) {
+ if (_store.containsKey(set)) {
+ return _store.get(set);
+ }
+
+ final int index = _store.size();
+ _store.put(set, index);
+ return index;
+ }
+
+ public void addIndex(final int index) {
+ _indices.add(index);
+ }
+
+ public void addLength(final int length) {
+ _lengths.add(length);
+ }
+
+ public Map<ObjIndexSet, Integer> getStore() {
+ return _store;
+ }
+
+ public List<Integer> getIndices() {
+ return _indices;
+ }
+
+ public List<Integer> getLengths() {
+ return _lengths;
+ }
+} \ No newline at end of file
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/KeyframeController.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/KeyframeController.java
new file mode 100644
index 0000000..887f415
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/KeyframeController.java
@@ -0,0 +1,762 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.util;
+
+import java.io.IOException;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.util.ArrayList;
+import java.util.logging.Logger;
+
+import com.ardor3d.scenegraph.FloatBufferData;
+import com.ardor3d.scenegraph.Mesh;
+import com.ardor3d.scenegraph.Spatial;
+import com.ardor3d.scenegraph.controller.ComplexSpatialController;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+import com.ardor3d.util.export.Savable;
+import com.ardor3d.util.geom.BufferUtils;
+
+/**
+ * TODO: Revisit for better Ardor3D integration.
+ *
+ * Started Date: Jun 12, 2004 <br>
+ *
+ *
+ * Class can do linear interpolation of a Mesh between units of time. Similar to VertexKeyframeController but
+ * interpolates float units of time instead of integer key frames.
+ *
+ * setSpeed(float) sets a speed relative to the defined speed. For example, the default is 1. A speed of 2 would run
+ * twice as fast and a speed of .5 would run half as fast
+ *
+ * setMinTime(float) and setMaxTime(float) both define the bounds that KeyframeController should follow. It is the
+ * programmer's responsibility to make sure that the MinTime and MaxTime are within the span of the defined setKeyframe
+ *
+ * Controller functions RepeatType and isActive are both defined in their default way for KeyframeController
+ *
+ * When this controller is saved/loaded to XML format, it assumes that the mesh it morphs is the Mesh it belongs to, so
+ * it is recommended to only attach this controller to the Mesh it animates.
+ *
+ * (Based on work by Jack Lindamood, kevglass (parts), hevee (blend time), Julien Gouesse (port to Ardor3D))
+ */
+
+public class KeyframeController<T extends Spatial> extends ComplexSpatialController<T> {
+
+ private static final Logger logger = Logger.getLogger(KeyframeController.class.getName());
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * An array of PointInTime s that defines the animation
+ */
+ transient public ArrayList<PointInTime> _keyframes;
+
+ /**
+ * A special array used with SmoothTransform to store temporary smooth transforms
+ */
+ transient private ArrayList<PointInTime> _prevKeyframes;
+
+ /**
+ * The mesh that is actually morphed
+ */
+ private Mesh _morphMesh;
+
+ /**
+ * The current time in the animation
+ */
+ transient private double _curTime;
+
+ /**
+ * The current frame of the animation
+ */
+ transient private int _curFrame;
+
+ /**
+ * The frame of animation we're heading towards
+ */
+ transient private int _nextFrame;
+
+ /**
+ * The PointInTime before curTime
+ */
+ transient private PointInTime _before;
+
+ /**
+ * The PointInTime after curTime
+ */
+ transient private PointInTime _after;
+
+ /**
+ * If true, the animation is moving forward, if false the animation is moving backwards
+ */
+ transient private boolean _movingForward = true;
+
+ /**
+ * Used with SmoothTransform to signal it is doing a smooth transform
+ */
+ transient private boolean _isSmooth;
+
+ /**
+ * Used with SmoothTransform to signal it is doing a smooth transform
+ */
+ transient private boolean _interpTex = true;
+
+ /**
+ * Used with SmoothTransform to hold the new beginning and ending time once the transform is complete
+ */
+ transient private double _tempNewBeginTime;
+
+ transient private double _tempNewEndTime;
+
+ /** If true, the model's bounding volume will update every frame. */
+ private boolean _updateBounding = true;
+
+ /**
+ * Default constructor. Speed is 1, MinTime is 0 MaxTime is 0. Both MinTime and MaxTime are automatically adjusted
+ * by setKeyframe if the setKeyframe time is less than MinTime or greater than MaxTime. Default RepeatType is WRAP.
+ */
+ public KeyframeController() {
+ setSpeed(1);
+ _keyframes = new ArrayList<PointInTime>();
+ _curFrame = 0;
+ setRepeatType(ComplexSpatialController.RepeatType.WRAP);
+ setMinTime(0);
+ setMaxTime(0);
+ }
+
+ public double getCurrentTime() {
+ return _curTime;
+ }
+
+ public int getCurrentFrame() {
+ return _curFrame;
+ }
+
+ /**
+ * Gets the current time in the animation
+ */
+ public double getCurTime() {
+ return _curTime;
+ }
+
+ /**
+ * Sets the current time in the animation
+ *
+ * @param time
+ * The time this Controller should continue at
+ */
+ public void setCurTime(final double time) {
+ _curTime = time;
+ }
+
+ /**
+ * Sets the Mesh that will be physically changed by this KeyframeController
+ *
+ * @param morph
+ * The new mesh to morph
+ */
+ public void setMorphingMesh(final Mesh morph) {
+ _morphMesh = morph;
+ _keyframes.clear();
+ _keyframes.add(new PointInTime(0, null));
+ }
+
+ public void shallowSetMorphMesh(final Mesh morph) {
+ _morphMesh = morph;
+ }
+
+ /**
+ * Tells the controller to change its morphMesh to shape at time seconds. Time must be >=0 and shape must be
+ * non-null and shape must have the same number of vertexes as the current shape. If not, then nothing happens. It
+ * is also required that setMorphingMesh(TriMesh) is called before setKeyframe. It is assumed that shape.indices ==
+ * morphMesh.indices, otherwise morphing may look funny
+ *
+ * @param time
+ * The time for the change
+ * @param shape
+ * The new shape at that time
+ */
+ public void setKeyframe(final double time, final Mesh shape) {
+ if (_morphMesh == null
+ || time < 0
+ || shape.getMeshData().getVertexBuffer().capacity() != _morphMesh.getMeshData().getVertexBuffer()
+ .capacity()) {
+ return;
+ }
+ for (int i = 0; i < _keyframes.size(); i++) {
+ final PointInTime lookingTime = _keyframes.get(i);
+ if (lookingTime._time == time) {
+ lookingTime._newShape = shape;
+ return;
+ }
+ if (lookingTime._time > time) {
+ _keyframes.add(i, new PointInTime(time, shape));
+ return;
+ }
+ }
+ _keyframes.add(new PointInTime(time, shape));
+ if (time > getMaxTime()) {
+ setMaxTime(time);
+ }
+ if (time < getMinTime()) {
+ setMinTime(time);
+ }
+ }
+
+ /**
+ * This function will do a smooth translation between a keframe's current look, to the look directly at
+ * newTimeToReach. It takes translationLen time (in sec) to do that translation, and once translated will animate
+ * like normal between newBeginTime and newEndTime <br>
+ * <br>
+ * This would be usefull for example when a figure stops running and tries to raise an arm. Instead of "teleporting"
+ * to the raise-arm animation begining, a smooth translation can occur.
+ *
+ * @param newTimeToReach
+ * The time to reach.
+ * @param translationLen
+ * How long it takes
+ * @param newBeginTime
+ * The new cycle begining time
+ * @param newEndTime
+ * The new cycle ending time.
+ */
+ public void setSmoothTranslation(final float newTimeToReach, final float translationLen, final float newBeginTime,
+ final float newEndTime) {
+ if (!isActive() || _isSmooth) {
+ return;
+ }
+ if (newBeginTime < 0 || newBeginTime > _keyframes.get(_keyframes.size() - 1)._time) {
+ KeyframeController.logger.warning("Attempt to set invalid begintime:" + newBeginTime);
+ return;
+ }
+ if (newEndTime < 0 || newEndTime > _keyframes.get(_keyframes.size() - 1)._time) {
+ KeyframeController.logger.warning("Attempt to set invalid endtime:" + newEndTime);
+ return;
+ }
+ Mesh begin = null, end = null;
+ if (_prevKeyframes == null) {
+ _prevKeyframes = new ArrayList<PointInTime>();
+ begin = new Mesh();
+ end = new Mesh();
+ } else {
+ begin = _prevKeyframes.get(0)._newShape;
+ end = _prevKeyframes.get(1)._newShape;
+ _prevKeyframes.clear();
+ }
+
+ getCurrent(begin);
+
+ _curTime = newTimeToReach;
+ _curFrame = 0;
+ setMinTime(0);
+ setMaxTime(_keyframes.get(_keyframes.size() - 1)._time);
+ update(0.0d, null);
+ getCurrent(end);
+
+ swapKeyframeSets();
+ _curTime = 0;
+ _curFrame = 0;
+ setMinTime(0);
+ setMaxTime(translationLen);
+ setKeyframe(0, begin);
+ setKeyframe(translationLen, end);
+ _isSmooth = true;
+ _tempNewBeginTime = newBeginTime;
+ _tempNewEndTime = newEndTime;
+ }
+
+ /**
+ * Swaps prevKeyframes and keyframes
+ */
+ private void swapKeyframeSets() {
+ final ArrayList<PointInTime> temp = _keyframes;
+ _keyframes = _prevKeyframes;
+ _prevKeyframes = temp;
+ }
+
+ /**
+ * Sets the new animation boundaries for this controller. This will start at newBeginTime and proceed in the
+ * direction of newEndTime (either forwards or backwards). If both are the same, then the animation is set to their
+ * time and turned off, otherwise the animation is turned on to start the animation acording to the repeat type. If
+ * either BeginTime or EndTime are invalid times (less than 0 or greater than the maximum set keyframe time) then a
+ * warning is set and nothing happens. <br>
+ * It is suggested that this function be called if new animation boundaries need to be set, instead of setMinTime
+ * and setMaxTime directly.
+ *
+ * @param newBeginTime
+ * The starting time
+ * @param newEndTime
+ * The ending time
+ */
+ public void setNewAnimationTimes(final double newBeginTime, final double newEndTime) {
+ if (_isSmooth) {
+ return;
+ }
+ if (newBeginTime < 0 || newBeginTime > _keyframes.get(_keyframes.size() - 1)._time) {
+ KeyframeController.logger.warning("Attempt to set invalid begintime:" + newBeginTime);
+ return;
+ }
+ if (newEndTime < 0 || newEndTime > _keyframes.get(_keyframes.size() - 1)._time) {
+ KeyframeController.logger.warning("Attempt to set invalid endtime:" + newEndTime);
+ return;
+ }
+ setMinTime(newBeginTime);
+ setMaxTime(newEndTime);
+ setActive(true);
+ if (newBeginTime <= newEndTime) { // Moving forward
+ _movingForward = true;
+ _curTime = newBeginTime;
+ if (newBeginTime == newEndTime) {
+ update(0.0d, null);
+ setActive(false);
+ }
+ } else { // Moving backwards
+ _movingForward = false;
+ _curTime = newEndTime;
+ }
+ }
+
+ /**
+ * Saves whatever the current morphMesh looks like into the dataCopy
+ *
+ * @param dataCopy
+ * The copy to save the current mesh into
+ */
+ private void getCurrent(final Mesh dataCopy) {
+ if (_morphMesh.getMeshData().getColorBuffer() != null) {
+ FloatBuffer dcColors = dataCopy.getMeshData().getColorBuffer();
+ if (dcColors != null) {
+ dcColors.clear();
+ }
+ final FloatBuffer mmColors = _morphMesh.getMeshData().getColorBuffer();
+ mmColors.clear();
+ if (dcColors == null || dcColors.capacity() != mmColors.capacity()) {
+ dcColors = BufferUtils.createFloatBuffer(mmColors.capacity());
+ dcColors.clear();
+ dataCopy.getMeshData().setColorBuffer(dcColors);
+ }
+
+ dcColors.put(mmColors);
+ dcColors.flip();
+ }
+ if (_morphMesh.getMeshData().getVertexBuffer() != null) {
+ FloatBuffer dcVerts = dataCopy.getMeshData().getVertexBuffer();
+ if (dcVerts != null) {
+ dcVerts.clear();
+ }
+ final FloatBuffer mmVerts = _morphMesh.getMeshData().getVertexBuffer();
+ mmVerts.clear();
+ if (dcVerts == null || dcVerts.capacity() != mmVerts.capacity()) {
+ dcVerts = BufferUtils.createFloatBuffer(mmVerts.capacity());
+ dcVerts.clear();
+ dataCopy.getMeshData().setVertexBuffer(dcVerts);
+ }
+
+ dcVerts.put(mmVerts);
+ dcVerts.flip();
+ }
+ if (_morphMesh.getMeshData().getNormalBuffer() != null) {
+ FloatBuffer dcNorms = dataCopy.getMeshData().getNormalBuffer();
+ if (dcNorms != null) {
+ dcNorms.clear();
+ }
+ final FloatBuffer mmNorms = _morphMesh.getMeshData().getNormalBuffer();
+ mmNorms.clear();
+ if (dcNorms == null || dcNorms.capacity() != mmNorms.capacity()) {
+ dcNorms = BufferUtils.createFloatBuffer(mmNorms.capacity());
+ dcNorms.clear();
+ dataCopy.getMeshData().setNormalBuffer(dcNorms);
+ }
+
+ dcNorms.put(mmNorms);
+ dcNorms.flip();
+ }
+ if (_morphMesh.getMeshData().getIndexBuffer() != null) {
+ IntBuffer dcInds = (IntBuffer) dataCopy.getMeshData().getIndexBuffer();
+ if (dcInds != null) {
+ dcInds.clear();
+ }
+ final IntBuffer mmInds = (IntBuffer) _morphMesh.getMeshData().getIndexBuffer();
+ mmInds.clear();
+ if (dcInds == null || dcInds.capacity() != mmInds.capacity()) {
+ dcInds = BufferUtils.createIntBuffer(mmInds.capacity());
+ dcInds.clear();
+ dataCopy.getMeshData().setIndexBuffer(dcInds);
+ }
+
+ dcInds.put(mmInds);
+ dcInds.flip();
+ }
+ if (_morphMesh.getMeshData().getTextureCoords(0) != null) {
+ FloatBuffer dcTexs = dataCopy.getMeshData().getTextureCoords(0).getBuffer();
+ if (dcTexs != null) {
+ dcTexs.clear();
+ }
+ final FloatBuffer mmTexs = _morphMesh.getMeshData().getTextureCoords(0).getBuffer();
+ mmTexs.clear();
+ if (dcTexs == null || dcTexs.capacity() != mmTexs.capacity()) {
+ dcTexs = BufferUtils.createFloatBuffer(mmTexs.capacity());
+ dcTexs.clear();
+ dataCopy.getMeshData().setTextureCoords(new FloatBufferData(dcTexs, 2), 0);
+ }
+
+ dcTexs.put(mmTexs);
+ dcTexs.flip();
+ }
+ }
+
+ /**
+ * As defined in Controller
+ *
+ * @param time
+ * as defined in Controller
+ */
+ @Override
+ public void update(final double time, final T caller) {
+ if (easyQuit()) {
+ return;
+ }
+ if (_movingForward) {
+ _curTime += time * getSpeed();
+ } else {
+ _curTime -= time * getSpeed();
+ }
+
+ findFrame();
+ _before = _keyframes.get(_curFrame);
+ // Change this bit so the next frame we're heading towards isn't always going
+ // to be one frame ahead since now we coule be animating from the last to first
+ // frames.
+ // after = keyframes.get(curFrame + 1));
+ _after = _keyframes.get(_nextFrame);
+
+ double delta = (_curTime - _before._time) / (_after._time - _before._time);
+
+ // If we doing that wrapping bit then delta should be caculated based
+ // on the time before the start of the animation we are.
+ if (_nextFrame < _curFrame) {
+ delta = blendTime - (getMinTime() - _curTime);
+ }
+
+ final Mesh oldShape = _before._newShape;
+ final Mesh newShape = _after._newShape;
+
+ final FloatBuffer verts = _morphMesh.getMeshData().getVertexBuffer();
+ final FloatBuffer norms = _morphMesh.getMeshData().getNormalBuffer();
+ final FloatBuffer texts = _interpTex ? _morphMesh.getMeshData().getTextureCoords(0) != null ? _morphMesh
+ .getMeshData().getTextureCoords(0).getBuffer() : null : null;
+ final FloatBuffer colors = _morphMesh.getMeshData().getColorBuffer();
+
+ final FloatBuffer oldverts = oldShape.getMeshData().getVertexBuffer();
+ final FloatBuffer oldnorms = oldShape.getMeshData().getNormalBuffer();
+ final FloatBuffer oldtexts = _interpTex ? oldShape.getMeshData().getTextureCoords(0) != null ? oldShape
+ .getMeshData().getTextureCoords(0).getBuffer() : null : null;
+ final FloatBuffer oldcolors = oldShape.getMeshData().getColorBuffer();
+
+ final FloatBuffer newverts = newShape.getMeshData().getVertexBuffer();
+ final FloatBuffer newnorms = newShape.getMeshData().getNormalBuffer();
+ final FloatBuffer newtexts = _interpTex ? newShape.getMeshData().getTextureCoords(0) != null ? newShape
+ .getMeshData().getTextureCoords(0).getBuffer() : null : null;
+ final FloatBuffer newcolors = newShape.getMeshData().getColorBuffer();
+ if (verts == null || oldverts == null || newverts == null) {
+ return;
+ }
+ final int vertQuantity = verts.capacity() / 3;
+ verts.rewind();
+ oldverts.rewind();
+ newverts.rewind();
+
+ if (norms != null) {
+ norms.rewind(); // reset to start
+ }
+ if (oldnorms != null) {
+ oldnorms.rewind(); // reset to start
+ }
+ if (newnorms != null) {
+ newnorms.rewind(); // reset to start
+ }
+
+ if (texts != null) {
+ texts.rewind(); // reset to start
+ }
+ if (oldtexts != null) {
+ oldtexts.rewind(); // reset to start
+ }
+ if (newtexts != null) {
+ newtexts.rewind(); // reset to start
+ }
+
+ if (colors != null) {
+ colors.rewind(); // reset to start
+ }
+ if (oldcolors != null) {
+ oldcolors.rewind(); // reset to start
+ }
+ if (newcolors != null) {
+ newcolors.rewind(); // reset to start
+ }
+
+ for (int i = 0; i < vertQuantity; i++) {
+ for (int x = 0; x < 3; x++) {
+ verts.put(i * 3 + x, (float) ((1f - delta) * oldverts.get(i * 3 + x) + delta * newverts.get(i * 3 + x)));
+ }
+
+ if (norms != null && oldnorms != null && newnorms != null) {
+ for (int x = 0; x < 3; x++) {
+ norms.put(i * 3 + x,
+ (float) ((1f - delta) * oldnorms.get(i * 3 + x) + delta * newnorms.get(i * 3 + x)));
+ }
+ }
+
+ if (_interpTex && texts != null && oldtexts != null && newtexts != null) {
+ for (int x = 0; x < 2; x++) {
+ texts.put(i * 2 + x,
+ (float) ((1f - delta) * oldtexts.get(i * 2 + x) + delta * newtexts.get(i * 2 + x)));
+ }
+ }
+
+ if (colors != null && oldcolors != null && newcolors != null) {
+ for (int x = 0; x < 4; x++) {
+ colors.put(i * 4 + x,
+ (float) ((1f - delta) * oldcolors.get(i * 4 + x) + delta * newcolors.get(i * 4 + x)));
+ }
+ }
+ }
+
+ if (_updateBounding) {
+ _morphMesh.updateModelBound();
+ }
+ }
+
+ /**
+ * If both min and max time are equal and the model is already updated, then it's an easy quit, or if it's on CLAMP
+ * and I've exceeded my time it's also an easy quit.
+ *
+ * @return true if update doesn't need to be called, false otherwise
+ */
+ private boolean easyQuit() {
+ if (getMaxTime() == getMinTime() && _curTime != getMinTime()) {
+ return true;
+ } else if (getRepeatType() == ComplexSpatialController.RepeatType.CLAMP
+ && (_curTime > getMaxTime() || _curTime < getMinTime())) {
+ return true;
+ } else if (_keyframes.size() < 2) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * If true, the model's bounding volume will be updated every frame. If false, it will not.
+ *
+ * @param update
+ * The new update model volume per frame value.
+ */
+ public void setUpdateBounding(final boolean update) {
+ _updateBounding = update;
+ }
+
+ /**
+ * Returns true if the model's bounding volume is being updated every frame.
+ *
+ * @return True if bounding volume is updating.
+ */
+ public boolean isUpdateBounding() {
+ return _updateBounding;
+ }
+
+ public void setInterpTex(final boolean interpTex) {
+ _interpTex = interpTex;
+ }
+
+ public boolean isInterpTex() {
+ return _interpTex;
+ }
+
+ private float blendTime = 0;
+
+ /**
+ * If repeat type RT_WRAP is set, after reaching the last frame of the currently set animation maxTime (see
+ * Controller.setMaxTime), there will be an additional blendTime seconds long phase inserted, morphing from the last
+ * frame to the first.
+ *
+ * @param blendTime
+ * The blend time to set
+ */
+ public void setBlendTime(final float blendTime) {
+ this.blendTime = blendTime;
+ }
+
+ /**
+ * Gets the currently set blending time for smooth animation transitions
+ *
+ * @return The current blend time
+ * @see #setBlendTime(float blendTime)
+ */
+ public float getBlendTime() {
+ return blendTime;
+ }
+
+ /**
+ * This is used by update(float). It calculates PointInTime before and after as well as makes adjustments on what to
+ * do when curTime is beyond the MinTime and MaxTime bounds
+ */
+ private void findFrame() {
+ // If we're in our special wrapping case then just ignore changing
+ // frames. Once we get back into the actual series we'll revert back
+ // to the normal process
+ if (_curTime < getMinTime() && _nextFrame < _curFrame) {
+ return;
+ }
+
+ // Update the rest to maintain our new nextFrame marker as one infront
+ // of the curFrame in all cases. The wrap case is where the real work
+ // is done.
+ if (_curTime > getMaxTime()) {
+ if (_isSmooth) {
+ swapKeyframeSets();
+ _isSmooth = false;
+ _curTime = _tempNewBeginTime;
+ _curFrame = 0;
+ _nextFrame = 1;
+ setNewAnimationTimes(_tempNewBeginTime, _tempNewEndTime);
+ return;
+ }
+ if (getRepeatType() == ComplexSpatialController.RepeatType.WRAP) {
+ final float delta = blendTime;
+ _curTime = getMinTime() - delta;
+ _curFrame = Math.min(_curFrame + 1, _keyframes.size() - 1);
+
+ for (_nextFrame = 0; _nextFrame < _keyframes.size() - 1; _nextFrame++) {
+ if (getMinTime() <= _keyframes.get(_nextFrame)._time) {
+ break;
+ }
+ }
+ return;
+ } else if (getRepeatType() == ComplexSpatialController.RepeatType.CLAMP) {
+ return;
+ } else { // Then assume it's RT_CYCLE
+ _movingForward = false;
+ _curTime = getMaxTime();
+ }
+ } else if (_curTime < getMinTime()) {
+ if (getRepeatType() == ComplexSpatialController.RepeatType.WRAP) {
+ _curTime = getMaxTime();
+ _curFrame = 0;
+ } else if (getRepeatType() == ComplexSpatialController.RepeatType.CLAMP) {
+ return;
+ } else { // Then assume it's RT_CYCLE
+ _movingForward = true;
+ _curTime = getMinTime();
+ }
+ }
+
+ _nextFrame = _curFrame + 1;
+
+ if (_curTime > _keyframes.get(_curFrame)._time) {
+ if (_curTime < _keyframes.get(_curFrame + 1)._time) {
+ _nextFrame = _curFrame + 1;
+ return;
+ }
+
+ for (; _curFrame < _keyframes.size() - 1; _curFrame++) {
+ if (_curTime <= _keyframes.get(_curFrame + 1)._time) {
+ _nextFrame = _curFrame + 1;
+ return;
+ }
+ }
+
+ // This -should- be unreachable because of the above
+ _curTime = getMinTime();
+ _curFrame = 0;
+ _nextFrame = _curFrame + 1;
+ return;
+ }
+
+ for (; _curFrame >= 0; _curFrame--) {
+ if (_curTime >= _keyframes.get(_curFrame)._time) {
+ _nextFrame = _curFrame + 1;
+ return;
+ }
+ }
+
+ // This should be unreachable because curTime>=0 and
+ // keyframes[0].time=0;
+ _curFrame = 0;
+ _nextFrame = _curFrame + 1;
+ }
+
+ /**
+ * This class defines a point in time that states _morphShape should look like _newShape at _time seconds
+ */
+ public static class PointInTime implements Savable {
+
+ public Mesh _newShape;
+
+ public double _time;
+
+ public PointInTime() {}
+
+ public PointInTime(final double time, final Mesh shape) {
+ this._time = time;
+ this._newShape = shape;
+ }
+
+ public void read(final InputCapsule capsule) throws IOException {
+ _time = capsule.readDouble("time", 0);
+ _newShape = (Mesh) capsule.readSavable("newShape", null);
+ }
+
+ public void write(final OutputCapsule capsule) throws IOException {
+ capsule.write(_time, "time", 0);
+ capsule.write(_newShape, "newShape", null);
+ }
+
+ public Class<?> getClassTag() {
+ return this.getClass();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void readObject(final java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+ _keyframes = (ArrayList<PointInTime>) in.readObject();
+ _movingForward = true;
+ }
+
+ public Mesh getMorphMesh() {
+ return _morphMesh;
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Class<? extends KeyframeController> getClassTag() {
+ return this.getClass();
+ }
+
+ @Override
+ public void read(final InputCapsule capsule) throws IOException {
+ super.read(capsule);
+ _updateBounding = capsule.readBoolean("updateBounding", false);
+ _morphMesh = (Mesh) capsule.readSavable("morphMesh", null);
+ _keyframes = (ArrayList<PointInTime>) capsule.readSavableList("keyframes", new ArrayList<PointInTime>());
+ _movingForward = true;
+ }
+
+ @Override
+ public void write(final OutputCapsule capsule) throws IOException {
+ super.write(capsule);
+ capsule.write(_updateBounding, "updateBounding", true);
+ capsule.write(_morphMesh, "morphMesh", null);
+ capsule.writeSavableList(_keyframes, "keyframes", new ArrayList<PointInTime>());
+ }
+
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvEdgeInfo.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvEdgeInfo.java
new file mode 100644
index 0000000..3ce40c2
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvEdgeInfo.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.util.nvtristrip;
+
+/**
+ * Ported from <a href="http://developer.nvidia.com/object/nvtristrip_library.html">NVIDIA's NvTriStrip Library</a>
+ */
+final class NvEdgeInfo {
+ long _refCount;
+ NvFaceInfo _face0, _face1;
+ int _v0, _v1;
+ NvEdgeInfo _nextV0, _nextV1;
+
+ // constructor puts 1 ref on us
+ NvEdgeInfo(final int v0, final int v1) {
+ _v0 = v0;
+ _v1 = v1;
+ _face0 = null;
+ _face1 = null;
+ _nextV0 = null;
+ _nextV1 = null;
+
+ // we will appear in 2 lists. this is a good
+ // way to make sure we delete it the second time
+ // we hit it in the edge infos
+ _refCount = 2;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvFaceInfo.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvFaceInfo.java
new file mode 100644
index 0000000..ae51a6a
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvFaceInfo.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.util.nvtristrip;
+
+/**
+ * Ported from <a href="http://developer.nvidia.com/object/nvtristrip_library.html">NVIDIA's NvTriStrip Library</a>
+ */
+final class NvFaceInfo {
+ int _v0, _v1, _v2;
+ int _stripId; // real strip Id
+ int _testStripId; // strip Id in an experiment
+ int _experimentId; // in what experiment was it given an experiment Id?
+ boolean _isFake; // if true, will be deleted when the strip it's in is deleted
+
+ NvFaceInfo(final int v0, final int v1, final int v2) {
+ this(v0, v1, v2, false);
+ }
+
+ NvFaceInfo(final int v0, final int v1, final int v2, final boolean bIsFake) {
+ _v0 = v0;
+ _v1 = v1;
+ _v2 = v2;
+ _stripId = -1;
+ _testStripId = -1;
+ _experimentId = -1;
+ _isFake = bIsFake;
+ }
+
+ /**
+ * Copies only v0, v1 and v2
+ *
+ * @param source
+ */
+ public NvFaceInfo(final NvFaceInfo source) {
+ this(source._v0, source._v1, source._v2);
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvStripInfo.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvStripInfo.java
new file mode 100644
index 0000000..5808e5d
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvStripInfo.java
@@ -0,0 +1,323 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.util.nvtristrip;
+
+import java.util.List;
+
+import com.google.common.collect.Lists;
+
+final class NvStripInfo {
+ NvStripStartInfo _startInfo;
+ List<NvFaceInfo> _faces = Lists.newArrayList();
+ int _stripId;
+ int _experimentId;
+
+ boolean _visited;
+
+ int _numDegenerates;
+
+ // A little information about the creation of the triangle strips
+ NvStripInfo(final NvStripStartInfo startInfo, final int stripId) {
+ this(startInfo, stripId, -1);
+ }
+
+ NvStripInfo(final NvStripStartInfo startInfo, final int stripId, final int experimentId) {
+ _startInfo = startInfo;
+ _stripId = stripId;
+ _experimentId = experimentId;
+ _visited = false;
+ _numDegenerates = 0;
+ }
+
+ /**
+ * @return true if the experiment id is >= 0
+ */
+ final boolean isExperiment() {
+ return _experimentId >= 0;
+ }
+
+ /**
+ * @param faceInfo
+ * @return
+ */
+ final boolean IsInStrip(final NvFaceInfo faceInfo) {
+ if (faceInfo == null) {
+ return false;
+ }
+
+ return _experimentId >= 0 ? faceInfo._testStripId == _stripId : faceInfo._stripId == _stripId;
+ }
+
+ /**
+ *
+ * @param faceInfo
+ * @param edgeInfos
+ * @return true if the input face and the current strip share an edge
+ */
+ boolean sharesEdge(final NvFaceInfo faceInfo, final List<NvEdgeInfo> edgeInfos) {
+ // check v0->v1 edge
+ NvEdgeInfo currEdge = NvStripifier.findEdgeInfo(edgeInfos, faceInfo._v0, faceInfo._v1);
+
+ if (IsInStrip(currEdge._face0) || IsInStrip(currEdge._face1)) {
+ return true;
+ }
+
+ // check v1->v2 edge
+ currEdge = NvStripifier.findEdgeInfo(edgeInfos, faceInfo._v1, faceInfo._v2);
+
+ if (IsInStrip(currEdge._face0) || IsInStrip(currEdge._face1)) {
+ return true;
+ }
+
+ // check v2->v0 edge
+ currEdge = NvStripifier.findEdgeInfo(edgeInfos, faceInfo._v2, faceInfo._v0);
+
+ if (IsInStrip(currEdge._face0) || IsInStrip(currEdge._face1)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * take the given forward and backward strips and combine them together
+ *
+ * @param forward
+ * @param backward
+ */
+ void combine(final List<NvFaceInfo> forward, final List<NvFaceInfo> backward) {
+ // add backward faces
+ int numFaces = backward.size();
+ for (int i = numFaces - 1; i >= 0; i--) {
+ _faces.add(backward.get(i));
+ }
+
+ // add forward faces
+ numFaces = forward.size();
+ for (int i = 0; i < numFaces; i++) {
+ _faces.add(forward.get(i));
+ }
+ }
+
+ /**
+ * @param faceVec
+ * @param face
+ * @return true if the face is "unique", i.e. has a vertex which doesn't exist in the faceVec
+ */
+ boolean unique(final List<NvFaceInfo> faceVec, final NvFaceInfo face) {
+ boolean bv0, bv1, bv2; // bools to indicate whether a vertex is in the faceVec or not
+ bv0 = bv1 = bv2 = false;
+
+ for (int i = 0; i < faceVec.size(); i++) {
+ if (!bv0) {
+ if (faceVec.get(i)._v0 == face._v0 || faceVec.get(i)._v1 == face._v0 || faceVec.get(i)._v2 == face._v0) {
+ bv0 = true;
+ }
+ }
+
+ if (!bv1) {
+ if (faceVec.get(i)._v0 == face._v1 || faceVec.get(i)._v1 == face._v1 || faceVec.get(i)._v2 == face._v1) {
+ bv1 = true;
+ }
+ }
+
+ if (!bv2) {
+ if (faceVec.get(i)._v0 == face._v2 || faceVec.get(i)._v1 == face._v2 || faceVec.get(i)._v2 == face._v2) {
+ bv2 = true;
+ }
+ }
+
+ // the face is not unique, all its vertices exist in the face vector
+ if (bv0 && bv1 && bv2) {
+ return false;
+ }
+ }
+
+ // if we get out here, it's unique
+ return true;
+ }
+
+ /**
+ * If either the faceInfo has a real strip index because it is already assign to a committed strip OR it is assigned
+ * in an experiment and the experiment index is the one we are building for, then it is marked and unavailable
+ */
+ boolean isMarked(final NvFaceInfo faceInfo) {
+ return faceInfo._stripId >= 0 || isExperiment() && faceInfo._experimentId == _experimentId;
+ }
+
+ /**
+ * Marks the face with the current strip ID
+ *
+ * @param faceInfo
+ */
+ void markTriangle(final NvFaceInfo faceInfo) {
+ assert !isMarked(faceInfo);
+ if (isExperiment()) {
+ faceInfo._experimentId = _experimentId;
+ faceInfo._testStripId = _stripId;
+ } else {
+ faceInfo._experimentId = -1;
+ faceInfo._stripId = _stripId;
+ }
+ }
+
+ /**
+ * Builds a strip forward as far as we can go, then builds backwards, and joins the two lists
+ *
+ * @param edgeInfos
+ * @param faceInfos
+ */
+ void build(final List<NvEdgeInfo> edgeInfos, final List<NvFaceInfo> faceInfos) {
+ // used in building the strips forward and backward
+ final List<Integer> scratchIndices = Lists.newArrayList();
+
+ // build forward... start with the initial face
+ final List<NvFaceInfo> forwardFaces = Lists.newArrayList();
+ final List<NvFaceInfo> backwardFaces = Lists.newArrayList();
+ forwardFaces.add(_startInfo._startFace);
+
+ markTriangle(_startInfo._startFace);
+
+ final int v0 = _startInfo._toV1 ? _startInfo._startEdge._v0 : _startInfo._startEdge._v1;
+ final int v1 = _startInfo._toV1 ? _startInfo._startEdge._v1 : _startInfo._startEdge._v0;
+
+ // easiest way to get v2 is to use this function which requires the
+ // other indices to already be in the list.
+ scratchIndices.add(v0);
+ scratchIndices.add(v1);
+ final int v2 = NvStripifier.getNextIndex(scratchIndices, _startInfo._startFace);
+ scratchIndices.add(v2);
+
+ //
+ // build the forward list
+ //
+ int nv0 = v1;
+ int nv1 = v2;
+
+ NvFaceInfo nextFace = NvStripifier.findOtherFace(edgeInfos, nv0, nv1, _startInfo._startFace);
+ while (nextFace != null && !isMarked(nextFace)) {
+ // check to see if this next face is going to cause us to die soon
+ int testnv0 = nv1;
+ final int testnv1 = NvStripifier.getNextIndex(scratchIndices, nextFace);
+
+ final NvFaceInfo nextNextFace = NvStripifier.findOtherFace(edgeInfos, testnv0, testnv1, nextFace);
+
+ if (nextNextFace == null || isMarked(nextNextFace)) {
+ // uh, oh, we're following a dead end, try swapping
+ final NvFaceInfo testNextFace = NvStripifier.findOtherFace(edgeInfos, nv0, testnv1, nextFace);
+
+ if (testNextFace != null && !isMarked(testNextFace)) {
+ // we only swap if it buys us something
+
+ // add a "fake" degenerate face
+ final NvFaceInfo tempFace = new NvFaceInfo(nv0, nv1, nv0, true);
+
+ forwardFaces.add(tempFace);
+ markTriangle(tempFace);
+
+ scratchIndices.add(nv0);
+ testnv0 = nv0;
+
+ ++_numDegenerates;
+ }
+
+ }
+
+ // add this to the strip
+ forwardFaces.add(nextFace);
+
+ markTriangle(nextFace);
+
+ // add the index
+ // nv0 = nv1;
+ // nv1 = NvStripifier.GetNextIndex(scratchIndices, nextFace);
+ scratchIndices.add(testnv1);
+
+ // and get the next face
+ nv0 = testnv0;
+ nv1 = testnv1;
+
+ nextFace = NvStripifier.findOtherFace(edgeInfos, nv0, nv1, nextFace);
+
+ }
+
+ // tempAllFaces is going to be forwardFaces + backwardFaces
+ // it's used for Unique()
+ final List<NvFaceInfo> tempAllFaces = Lists.newArrayList();
+ for (int i = 0; i < forwardFaces.size(); i++) {
+ tempAllFaces.add(forwardFaces.get(i));
+ }
+
+ //
+ // reset the indices for building the strip backwards and do so
+ //
+ scratchIndices.clear();
+ scratchIndices.add(v2);
+ scratchIndices.add(v1);
+ scratchIndices.add(v0);
+ nv0 = v1;
+ nv1 = v0;
+ nextFace = NvStripifier.findOtherFace(edgeInfos, nv0, nv1, _startInfo._startFace);
+ while (nextFace != null && !isMarked(nextFace)) {
+ // this tests to see if a face is "unique", meaning that its vertices aren't already in the list
+ // so, strips which "wrap-around" are not allowed
+ if (!unique(tempAllFaces, nextFace)) {
+ break;
+ }
+
+ // check to see if this next face is going to cause us to die soon
+ int testnv0 = nv1;
+ final int testnv1 = NvStripifier.getNextIndex(scratchIndices, nextFace);
+
+ final NvFaceInfo nextNextFace = NvStripifier.findOtherFace(edgeInfos, testnv0, testnv1, nextFace);
+
+ if (nextNextFace == null || isMarked(nextNextFace)) {
+ // uh, oh, we're following a dead end, try swapping
+ final NvFaceInfo testNextFace = NvStripifier.findOtherFace(edgeInfos, nv0, testnv1, nextFace);
+ if (testNextFace != null && !isMarked(testNextFace)) {
+ // we only swap if it buys us something
+
+ // add a "fake" degenerate face
+ final NvFaceInfo tempFace = new NvFaceInfo(nv0, nv1, nv0, true);
+
+ backwardFaces.add(tempFace);
+ markTriangle(tempFace);
+ scratchIndices.add(nv0);
+ testnv0 = nv0;
+
+ ++_numDegenerates;
+ }
+
+ }
+
+ // add this to the strip
+ backwardFaces.add(nextFace);
+
+ // this is just so Unique() will work
+ tempAllFaces.add(nextFace);
+
+ markTriangle(nextFace);
+
+ // add the index
+ // nv0 = nv1;
+ // nv1 = NvStripifier.GetNextIndex(scratchIndices, nextFace);
+ scratchIndices.add(testnv1);
+
+ // and get the next face
+ nv0 = testnv0;
+ nv1 = testnv1;
+ nextFace = NvStripifier.findOtherFace(edgeInfos, nv0, nv1, nextFace);
+ }
+
+ // Combine the forward and backwards stripification lists and put into our own face vector
+ combine(forwardFaces, backwardFaces);
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvStripStartInfo.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvStripStartInfo.java
new file mode 100644
index 0000000..4878058
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvStripStartInfo.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.util.nvtristrip;
+
+/**
+ * Ported from <a href="http://developer.nvidia.com/object/nvtristrip_library.html">NVIDIA's NvTriStrip Library</a>
+ */
+final class NvStripStartInfo {
+ NvFaceInfo _startFace;
+ NvEdgeInfo _startEdge;
+ boolean _toV1;
+
+ NvStripStartInfo(final NvFaceInfo startFace, final NvEdgeInfo startEdge, final boolean toV1) {
+ _startFace = startFace;
+ _startEdge = startEdge;
+ _toV1 = toV1;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvStripifier.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvStripifier.java
new file mode 100644
index 0000000..0bcca22
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvStripifier.java
@@ -0,0 +1,1297 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.util.nvtristrip;
+
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+/**
+ * Ported from <a href="http://developer.nvidia.com/object/nvtristrip_library.html">NVIDIA's NvTriStrip Library</a>
+ */
+final class NvStripifier {
+ private static final Logger logger = Logger.getLogger(NvStripifier.class.getName());
+
+ public static int CACHE_INEFFICIENCY = 6;
+
+ protected List<Integer> _indices = Lists.newArrayList();
+ protected int _cacheSize;
+ protected int _minStripLength;
+ protected float _meshJump;
+ protected boolean _firstTimeResetPoint;
+
+ /**
+ *
+ * @param in_indices
+ * the input indices of the mesh to stripify
+ * @param in_cacheSize
+ * the target cache size
+ * @param in_minStripLength
+ * @param maxIndex
+ * @param outStrips
+ * @param outFaceList
+ */
+ void stripify(final List<Integer> in_indices, final int in_cacheSize, final int in_minStripLength,
+ final int maxIndex, final List<NvStripInfo> outStrips, final List<NvFaceInfo> outFaceList) {
+ _meshJump = 0.0f;
+ _firstTimeResetPoint = true; // used in FindGoodResetPoint()
+
+ // the number of times to run the experiments
+ final int numSamples = 10;
+
+ // the cache size, clamped to one
+ _cacheSize = Math.max(1, in_cacheSize - NvStripifier.CACHE_INEFFICIENCY);
+
+ // this is the strip size threshold below which we dump the strip into a list
+ _minStripLength = in_minStripLength;
+
+ _indices = in_indices;
+
+ // build the stripification info
+ final List<NvFaceInfo> allFaceInfos = Lists.newArrayList();
+ final List<NvEdgeInfo> allEdgeInfos = Lists.newArrayList();
+
+ buildStripifyInfo(allFaceInfos, allEdgeInfos, maxIndex);
+
+ final List<NvStripInfo> allStrips = Lists.newArrayList();
+
+ // stripify
+ findAllStrips(allStrips, allFaceInfos, allEdgeInfos, numSamples);
+
+ // split up the strips into cache friendly pieces, optimize them, then dump these into outStrips
+ splitUpStripsAndOptimize(allStrips, outStrips, allEdgeInfos, outFaceList);
+ }
+
+ /**
+ * Generates actual strips from the list-in-strip-order.
+ *
+ * @param allStrips
+ * @param stripIndices
+ * @param bStitchStrips
+ * @param numSeparateStrips
+ * @param bRestart
+ * @param restartVal
+ */
+ int createStrips(final List<NvStripInfo> allStrips, final List<Integer> stripIndices, final boolean bStitchStrips,
+ final boolean bRestart, final int restartVal) {
+ int numSeparateStrips = 0;
+
+ NvFaceInfo tLastFace = new NvFaceInfo(0, 0, 0);
+ final int nStripCount = allStrips.size();
+ assert nStripCount > 0;
+
+ // we infer the cw/ccw ordering depending on the number of indices
+ // this is screwed up by the fact that we insert -1s to denote changing strips
+ // this is to account for that
+ int accountForNegatives = 0;
+
+ for (int i = 0; i < nStripCount; i++) {
+ final NvStripInfo strip = allStrips.get(i);
+ final int nStripFaceCount = strip._faces.size();
+ assert nStripFaceCount > 0;
+
+ // Handle the first face in the strip
+ {
+ final NvFaceInfo tFirstFace = new NvFaceInfo(strip._faces.get(0)._v0, strip._faces.get(0)._v1,
+ strip._faces.get(0)._v2);
+
+ // If there is a second face, reorder vertices such that the
+ // unique vertex is first
+ if (nStripFaceCount > 1) {
+ final int nUnique = NvStripifier.getUniqueVertexInB(strip._faces.get(1), tFirstFace);
+ if (nUnique == tFirstFace._v1) {
+ final int store = tFirstFace._v1;
+ tFirstFace._v1 = tFirstFace._v0;
+ tFirstFace._v0 = store;
+ } else if (nUnique == tFirstFace._v2) {
+ final int store = tFirstFace._v2;
+ tFirstFace._v2 = tFirstFace._v0;
+ tFirstFace._v0 = store;
+ }
+
+ // If there is a third face, reorder vertices such that the
+ // shared vertex is last
+ if (nStripFaceCount > 2) {
+ if (NvStripifier.isDegenerate(strip._faces.get(1))) {
+ final int pivot = strip._faces.get(1)._v1;
+ if (tFirstFace._v1 == pivot) {
+ final int store = tFirstFace._v2;
+ tFirstFace._v2 = tFirstFace._v1;
+ tFirstFace._v1 = store;
+ }
+ } else {
+ final int[] nShared = NvStripifier.getSharedVertices(strip._faces.get(2), tFirstFace);
+ if (nShared[0] == tFirstFace._v1 && nShared[1] == -1) {
+ final int store = tFirstFace._v2;
+ tFirstFace._v2 = tFirstFace._v1;
+ tFirstFace._v1 = store;
+ }
+ }
+ }
+ }
+
+ if (i == 0 || !bStitchStrips || bRestart) {
+ if (!NvStripifier.isCW(strip._faces.get(0), tFirstFace._v0, tFirstFace._v1)) {
+ stripIndices.add(tFirstFace._v0);
+ }
+ } else {
+ // Double tap the first in the new strip
+ stripIndices.add(tFirstFace._v0);
+
+ // Check CW/CCW ordering
+ if (NvStripifier.nextIsCW(stripIndices.size() - accountForNegatives) != NvStripifier.isCW(
+ strip._faces.get(0), tFirstFace._v0, tFirstFace._v1)) {
+ stripIndices.add(tFirstFace._v0);
+ }
+ }
+
+ stripIndices.add(tFirstFace._v0);
+ stripIndices.add(tFirstFace._v1);
+ stripIndices.add(tFirstFace._v2);
+
+ // Update last face info
+ tLastFace = tFirstFace;
+ }
+
+ for (int j = 1; j < nStripFaceCount; j++) {
+ final int nUnique = NvStripifier.getUniqueVertexInB(tLastFace, strip._faces.get(j));
+ if (nUnique != -1) {
+ stripIndices.add(nUnique);
+
+ // Update last face info
+ tLastFace._v0 = tLastFace._v1;
+ tLastFace._v1 = tLastFace._v2;
+ tLastFace._v2 = nUnique;
+ } else {
+ // we've hit a degenerate
+ stripIndices.add(strip._faces.get(j)._v2);
+ tLastFace._v0 = strip._faces.get(j)._v0;// tLastFace.m_v1;
+ tLastFace._v1 = strip._faces.get(j)._v1;// tLastFace.m_v2;
+ tLastFace._v2 = strip._faces.get(j)._v2;// tLastFace.m_v1;
+
+ }
+ }
+
+ // Double tap between strips.
+ if (bStitchStrips && !bRestart) {
+ if (i != nStripCount - 1) {
+ stripIndices.add(tLastFace._v2);
+ }
+ } else if (bRestart) {
+ stripIndices.add(restartVal);
+ } else {
+ // -1 index indicates next strip
+ stripIndices.add(-1);
+ accountForNegatives++;
+ numSeparateStrips++;
+ }
+
+ // Update last face info
+ tLastFace._v0 = tLastFace._v1;
+ tLastFace._v1 = tLastFace._v2;
+ // tLastFace._v2 = tLastFace._v2; // for info purposes.
+ }
+
+ if (bStitchStrips || bRestart) {
+ numSeparateStrips = 1;
+ }
+
+ return numSeparateStrips;
+ }
+
+ /**
+ * @param faceA
+ * @param faceB
+ * @return the first vertex unique to faceB
+ */
+ static int getUniqueVertexInB(final NvFaceInfo faceA, final NvFaceInfo faceB) {
+ final int facev0 = faceB._v0;
+ if (facev0 != faceA._v0 && facev0 != faceA._v1 && facev0 != faceA._v2) {
+ return facev0;
+ }
+
+ final int facev1 = faceB._v1;
+ if (facev1 != faceA._v0 && facev1 != faceA._v1 && facev1 != faceA._v2) {
+ return facev1;
+ }
+
+ final int facev2 = faceB._v2;
+ if (facev2 != faceA._v0 && facev2 != faceA._v1 && facev2 != faceA._v2) {
+ return facev2;
+ }
+
+ // nothing is different
+ return -1;
+ }
+
+ /**
+ * @param faceA
+ * @param faceB
+ * @return the (at most) two vertices shared between the two faces
+ */
+ static int[] getSharedVertices(final NvFaceInfo faceA, final NvFaceInfo faceB) {
+ final int[] vertexStore = new int[2];
+ vertexStore[0] = vertexStore[1] = -1;
+
+ final int facev0 = faceB._v0;
+ if (facev0 == faceA._v0 || facev0 == faceA._v1 || facev0 == faceA._v2) {
+ if (vertexStore[0] == -1) {
+ vertexStore[0] = facev0;
+ } else {
+ vertexStore[1] = facev0;
+ return vertexStore;
+ }
+ }
+
+ final int facev1 = faceB._v1;
+ if (facev1 == faceA._v0 || facev1 == faceA._v1 || facev1 == faceA._v2) {
+ if (vertexStore[0] == -1) {
+ vertexStore[0] = facev1;
+ } else {
+ vertexStore[1] = facev1;
+ return vertexStore;
+ }
+ }
+
+ final int facev2 = faceB._v2;
+ if (facev2 == faceA._v0 || facev2 == faceA._v1 || facev2 == faceA._v2) {
+ if (vertexStore[0] == -1) {
+ vertexStore[0] = facev2;
+ } else {
+ vertexStore[1] = facev2;
+ return vertexStore;
+ }
+ }
+
+ return vertexStore;
+ }
+
+ static boolean isDegenerate(final NvFaceInfo face) {
+ if (face._v0 == face._v1) {
+ return true;
+ } else if (face._v0 == face._v2) {
+ return true;
+ } else if (face._v1 == face._v2) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ static boolean isDegenerate(final int v0, final int v1, final int v2) {
+ if (v0 == v1) {
+ return true;
+ } else if (v0 == v2) {
+ return true;
+ } else if (v1 == v2) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////
+ //
+ // Big mess of functions called during stripification
+ // Note: I removed some that were orphans - JES
+ //
+ // ///////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * @param faceInfo
+ * @param v0
+ * @param v1
+ * @return true if the face is ordered in CW fashion
+ */
+ static boolean isCW(final NvFaceInfo faceInfo, final int v0, final int v1) {
+ if (faceInfo._v0 == v0) {
+ return faceInfo._v1 == v1;
+ } else if (faceInfo._v1 == v0) {
+ return faceInfo._v2 == v1;
+ } else {
+ return faceInfo._v0 == v1;
+ }
+ }
+
+ /**
+ *
+ * @param numIndices
+ * @return true if the next face should be ordered in CW fashion
+ */
+ static boolean nextIsCW(final int numIndices) {
+ return numIndices % 2 == 0;
+ }
+
+ /**
+ * @param indices
+ * @param face
+ * @return vertex of the input face which is "next" in the input index list
+ */
+ static int getNextIndex(final List<Integer> indices, final NvFaceInfo face) {
+ final int numIndices = indices.size();
+ assert numIndices >= 2;
+
+ final int v0 = indices.get(numIndices - 2);
+ final int v1 = indices.get(numIndices - 1);
+
+ final int fv0 = face._v0;
+ final int fv1 = face._v1;
+ final int fv2 = face._v2;
+
+ if (fv0 != v0 && fv0 != v1) {
+ if (fv1 != v0 && fv1 != v1 || fv2 != v0 && fv2 != v1) {
+ NvStripifier.logger.warning("getNextIndex: Triangle doesn't have all of its vertices\n");
+ NvStripifier.logger.warning("getNextIndex: Duplicate triangle probably got us derailed\n");
+ }
+ return fv0;
+ }
+ if (fv1 != v0 && fv1 != v1) {
+ if (fv0 != v0 && fv0 != v1 || fv2 != v0 && fv2 != v1) {
+ NvStripifier.logger.warning("getNextIndex: Triangle doesn't have all of its vertices\n");
+ NvStripifier.logger.warning("getNextIndex: Duplicate triangle probably got us derailed\n");
+ }
+ return fv1;
+ }
+ if (fv2 != v0 && fv2 != v1) {
+ if (fv0 != v0 && fv0 != v1 || fv1 != v0 && fv1 != v1) {
+ NvStripifier.logger.warning("getNextIndex: Triangle doesn't have all of its vertices\n");
+ NvStripifier.logger.warning("getNextIndex: Duplicate triangle probably got us derailed\n");
+ }
+ return fv2;
+ }
+
+ // shouldn't get here, but let's try and fail gracefully
+ if (fv0 == fv1 || fv0 == fv2) {
+ return fv0;
+ } else if (fv1 == fv0 || fv1 == fv2) {
+ return fv1;
+ } else if (fv2 == fv0 || fv2 == fv1) {
+ return fv2;
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * find the edge info for these two indices
+ *
+ * @param edgeInfos
+ * @param v0
+ * @param v1
+ * @return
+ */
+ static NvEdgeInfo findEdgeInfo(final List<NvEdgeInfo> edgeInfos, final int v0, final int v1) {
+ // we can get to it through either array because the edge infos have a v0 and v1 and there is no order except
+ // how it was first created.
+ NvEdgeInfo infoIter = edgeInfos.get(v0);
+ while (infoIter != null) {
+ if (infoIter._v0 == v0) {
+ if (infoIter._v1 == v1) {
+ return infoIter;
+ } else {
+ infoIter = infoIter._nextV0;
+ }
+ } else {
+ assert infoIter._v1 == v0;
+ if (infoIter._v0 == v1) {
+ return infoIter;
+ } else {
+ infoIter = infoIter._nextV1;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * find the other face sharing these vertices
+ *
+ * @param edgeInfos
+ * @param v0
+ * @param v1
+ * @param faceInfo
+ * @return
+ */
+ static NvFaceInfo findOtherFace(final List<NvEdgeInfo> edgeInfos, final int v0, final int v1,
+ final NvFaceInfo faceInfo) {
+ final NvEdgeInfo edgeInfo = NvStripifier.findEdgeInfo(edgeInfos, v0, v1);
+
+ if (edgeInfo == null && v0 == v1) {
+ // we've hit a degenerate
+ return null;
+ }
+
+ assert edgeInfo != null;
+ return edgeInfo._face0 == faceInfo ? edgeInfo._face1 : edgeInfo._face0;
+ }
+
+ /**
+ * A good reset point is one near other committed areas so that we know that when we've made the longest strips its
+ * because we're stripifying in the same general orientation.
+ *
+ * @param faceInfos
+ * @param edgeInfos
+ * @return
+ */
+ NvFaceInfo findGoodResetPoint(final List<NvFaceInfo> faceInfos, final List<NvEdgeInfo> edgeInfos) {
+ // we hop into different areas of the mesh to try to get other large open spans done. Areas of small strips can
+ // just be left to triangle lists added at the end.
+ NvFaceInfo result = null;
+ final int numFaces = faceInfos.size();
+ int startPoint;
+ if (_firstTimeResetPoint) {
+ // first time, find a face with few neighbors (look for an edge of the mesh)
+ startPoint = findStartPoint(faceInfos, edgeInfos);
+ _firstTimeResetPoint = false;
+ } else {
+ startPoint = (int) (((float) numFaces - 1) * _meshJump);
+ }
+
+ if (startPoint == -1) {
+ startPoint = (int) (((float) numFaces - 1) * _meshJump);
+
+ // meshJump += 0.1f;
+ // if (meshJump > 1.0f)
+ // meshJump = .05f;
+ }
+
+ int i = startPoint;
+ do {
+
+ // if this guy isn't visited, try him
+ if (faceInfos.get(i)._stripId < 0) {
+ result = faceInfos.get(i);
+ break;
+ }
+
+ // update the index and clamp to 0-(numFaces-1)
+ if (++i >= numFaces) {
+ i = 0;
+ }
+
+ } while (i != startPoint);
+
+ // update the meshJump
+ _meshJump += 0.1f;
+ if (_meshJump > 1.0f) {
+ _meshJump = .05f;
+ }
+
+ // return the best face we found
+ return result;
+ }
+
+ /**
+ * Does the stripification, puts output strips into vector allStrips
+ *
+ * Works by setting running a number of experiments in different areas of the mesh, and accepting the one which
+ * results in the longest strips. It then accepts this, and moves on to a different area of the mesh. We try to jump
+ * around the mesh some, to ensure that large open spans of strips get generated.
+ *
+ * @param allStrips
+ * @param allFaceInfos
+ * @param allEdgeInfos
+ * @param numSamples
+ */
+ @SuppressWarnings("unchecked")
+ void findAllStrips(final List<NvStripInfo> allStrips, final List<NvFaceInfo> allFaceInfos,
+ final List<NvEdgeInfo> allEdgeInfos, final int numSamples) {
+ // the experiments
+ int experimentId = 0;
+ int stripId = 0;
+ boolean done = false;
+
+ while (!done) {
+
+ //
+ // PHASE 1: Set up numSamples * numEdges experiments
+ //
+ final List<NvStripInfo>[] experiments = new List[numSamples * 6];
+ for (int i = 0; i < experiments.length; i++) {
+ experiments[i] = Lists.newArrayList();
+ }
+
+ int experimentIndex = 0;
+ final Set<NvFaceInfo> resetPoints = Sets.newHashSet();
+ for (int i = 0; i < numSamples; i++) {
+
+ // Try to find another good reset point.
+ // If there are none to be found, we are done
+ final NvFaceInfo nextFace = findGoodResetPoint(allFaceInfos, allEdgeInfos);
+ if (nextFace == null) {
+ done = true;
+ break;
+ }
+ // If we have already evaluated starting at this face in this slew
+ // of experiments, then skip going any further
+ else if (resetPoints.contains(nextFace)) {
+ continue;
+ }
+
+ // trying it now...
+ resetPoints.add(nextFace);
+
+ // otherwise, we shall now try experiments for starting on the 01,12, and 20 edges
+ assert nextFace._stripId < 0;
+
+ // build the strip off of this face's 0-1 edge
+ final NvEdgeInfo edge01 = NvStripifier.findEdgeInfo(allEdgeInfos, nextFace._v0, nextFace._v1);
+ final NvStripInfo strip01 = new NvStripInfo(new NvStripStartInfo(nextFace, edge01, true), stripId++,
+ experimentId++);
+ experiments[experimentIndex++].add(strip01);
+
+ // build the strip off of this face's 1-0 edge
+ final NvEdgeInfo edge10 = NvStripifier.findEdgeInfo(allEdgeInfos, nextFace._v0, nextFace._v1);
+ final NvStripInfo strip10 = new NvStripInfo(new NvStripStartInfo(nextFace, edge10, false), stripId++,
+ experimentId++);
+ experiments[experimentIndex++].add(strip10);
+
+ // build the strip off of this face's 1-2 edge
+ final NvEdgeInfo edge12 = NvStripifier.findEdgeInfo(allEdgeInfos, nextFace._v1, nextFace._v2);
+ final NvStripInfo strip12 = new NvStripInfo(new NvStripStartInfo(nextFace, edge12, true), stripId++,
+ experimentId++);
+ experiments[experimentIndex++].add(strip12);
+
+ // build the strip off of this face's 2-1 edge
+ final NvEdgeInfo edge21 = NvStripifier.findEdgeInfo(allEdgeInfos, nextFace._v1, nextFace._v2);
+ final NvStripInfo strip21 = new NvStripInfo(new NvStripStartInfo(nextFace, edge21, false), stripId++,
+ experimentId++);
+ experiments[experimentIndex++].add(strip21);
+
+ // build the strip off of this face's 2-0 edge
+ final NvEdgeInfo edge20 = NvStripifier.findEdgeInfo(allEdgeInfos, nextFace._v2, nextFace._v0);
+ final NvStripInfo strip20 = new NvStripInfo(new NvStripStartInfo(nextFace, edge20, true), stripId++,
+ experimentId++);
+ experiments[experimentIndex++].add(strip20);
+
+ // build the strip off of this face's 0-2 edge
+ final NvEdgeInfo edge02 = NvStripifier.findEdgeInfo(allEdgeInfos, nextFace._v2, nextFace._v0);
+ final NvStripInfo strip02 = new NvStripInfo(new NvStripStartInfo(nextFace, edge02, false), stripId++,
+ experimentId++);
+ experiments[experimentIndex++].add(strip02);
+ }
+
+ //
+ // PHASE 2: Iterate through that we setup in the last phase
+ // and really build each of the strips and strips that follow to see how
+ // far we get
+ //
+ final int numExperiments = experimentIndex;
+ for (int i = 0; i < numExperiments; i++) {
+ // get the strip set
+ NvStripInfo stripIter = experiments[i].get(0);
+
+ // build the first strip of the list
+ stripIter.build(allEdgeInfos, allFaceInfos);
+ final int currExperimentId = stripIter._experimentId;
+
+ final NvStripStartInfo startInfo = new NvStripStartInfo(null, null, false);
+ while (findTraversal(allFaceInfos, allEdgeInfos, stripIter, startInfo)) {
+
+ // create the new strip info
+ stripIter = new NvStripInfo(startInfo, stripId++, currExperimentId);
+
+ // build the next strip
+ stripIter.build(allEdgeInfos, allFaceInfos);
+
+ // add it to the list
+ experiments[i].add(stripIter);
+ }
+ }
+
+ //
+ // Phase 3: Find the experiment that has the most promise
+ //
+ int bestIndex = 0;
+ double bestValue = 0;
+ for (int i = 0; i < numExperiments; i++) {
+ final float avgStripSizeWeight = 1.0f;
+ // final float numTrisWeight = 0.0f; // unused
+ final float numStripsWeight = 0.0f;
+ final float avgStripSize = avgStripSize(experiments[i]);
+ final float numStrips = experiments[i].size();
+ final float value = avgStripSize * avgStripSizeWeight + numStrips * numStripsWeight;
+ // float value = 1.f / numStrips;
+ // float value = numStrips * avgStripSize;
+
+ if (value > bestValue) {
+ bestValue = value;
+ bestIndex = i;
+ }
+ }
+
+ //
+ // Phase 4: commit the best experiment of the bunch
+ //
+ commitStrips(allStrips, experiments[bestIndex]);
+ }
+ }
+
+ /**
+ * Splits the input vector of strips (allBigStrips) into smaller, cache friendly pieces, then reorders these pieces
+ * to maximize cache hits. The final strips are stored in outStrips
+ *
+ * @param allStrips
+ * @param outStrips
+ * @param edgeInfos
+ * @param outFaceList
+ */
+ void splitUpStripsAndOptimize(final List<NvStripInfo> allStrips, final List<NvStripInfo> outStrips,
+ final List<NvEdgeInfo> edgeInfos, final List<NvFaceInfo> outFaceList) {
+ final int threshold = _cacheSize;
+ final List<NvStripInfo> tempStrips = Lists.newArrayList();
+
+ // split up strips into threshold-sized pieces
+ for (int i = 0; i < allStrips.size(); i++) {
+ final NvStripInfo allStripI = allStrips.get(i);
+ NvStripInfo currentStrip;
+ final NvStripStartInfo startInfo = new NvStripStartInfo(null, null, false);
+
+ int actualStripSize = 0;
+ for (final NvFaceInfo face : allStripI._faces) {
+ if (!NvStripifier.isDegenerate(face)) {
+ actualStripSize++;
+ }
+ }
+
+ if (actualStripSize > threshold) {
+
+ final int numTimes = actualStripSize / threshold;
+ int numLeftover = actualStripSize % threshold;
+
+ int degenerateCount = 0, j = 0;
+ for (; j < numTimes; j++) {
+ currentStrip = new NvStripInfo(startInfo, 0, -1);
+
+ int faceCtr = j * threshold + degenerateCount;
+ boolean bFirstTime = true;
+ while (faceCtr < threshold + j * threshold + degenerateCount) {
+ if (NvStripifier.isDegenerate(allStripI._faces.get(faceCtr))) {
+ degenerateCount++;
+
+ // last time or first time through, no need for a degenerate
+ if ((faceCtr + 1 != threshold + j * threshold + degenerateCount || j == numTimes - 1
+ && numLeftover < 4 && numLeftover > 0)
+ && !bFirstTime) {
+ currentStrip._faces.add(allStripI._faces.get(faceCtr++));
+ } else {
+ ++faceCtr;
+ }
+ } else {
+ currentStrip._faces.add(allStripI._faces.get(faceCtr++));
+ bFirstTime = false;
+ }
+ }
+ if (j == numTimes - 1) // last time through
+ {
+ if (numLeftover < 4 && numLeftover > 0) // way too small
+ {
+ // just add to last strip
+ int ctr = 0;
+ while (ctr < numLeftover) {
+ if (NvStripifier.isDegenerate(allStripI._faces.get(faceCtr))) {
+ ++degenerateCount;
+ } else {
+ ++ctr;
+ }
+ currentStrip._faces.add(allStripI._faces.get(faceCtr++));
+ }
+ numLeftover = 0;
+ }
+ }
+ tempStrips.add(currentStrip);
+ }
+ int leftOff = j * threshold + degenerateCount;
+
+ if (numLeftover != 0) {
+ currentStrip = new NvStripInfo(startInfo, 0, -1);
+
+ int ctr = 0;
+ boolean bFirstTime = true;
+ while (ctr < numLeftover) {
+ if (!NvStripifier.isDegenerate(allStripI._faces.get(leftOff))) {
+ ctr++;
+ bFirstTime = false;
+ currentStrip._faces.add(allStripI._faces.get(leftOff++));
+ } else if (!bFirstTime) {
+ currentStrip._faces.add(allStripI._faces.get(leftOff++));
+ } else {
+ leftOff++;
+ }
+ }
+
+ tempStrips.add(currentStrip);
+ }
+ } else {
+ // we're not just doing a tempStrips.add(allBigStrips.get(i)) because
+ // this way we can delete allBigStrips later to free the memory
+ currentStrip = new NvStripInfo(startInfo, 0, -1);
+
+ for (int j = 0; j < allStripI._faces.size(); j++) {
+ currentStrip._faces.add(allStripI._faces.get(j));
+ }
+
+ tempStrips.add(currentStrip);
+ }
+ }
+
+ // add small strips to face list
+ final List<NvStripInfo> tempStrips2 = Lists.newArrayList();
+ removeSmallStrips(tempStrips, tempStrips2, outFaceList);
+
+ outStrips.clear();
+ // screw optimization for now
+ // for(i = 0; i < tempStrips.size(); ++i)
+ // outStrips.add(tempStrips.get(i));
+
+ if (tempStrips2.size() != 0) {
+ // Optimize for the vertex cache
+ final VertexCache vcache = new VertexCache(_cacheSize);
+
+ float bestNumHits = -1.0f;
+ float numHits;
+ int bestIndex = 0;
+ int firstIndex = 0;
+ float minCost = 10000.0f;
+
+ for (int i = 0; i < tempStrips2.size(); i++) {
+ final NvStripInfo tempStrips2I = tempStrips2.get(i);
+ int numNeighbors = 0;
+
+ // find strip with least number of neighbors per face
+ for (int j = 0; j < tempStrips2I._faces.size(); j++) {
+ numNeighbors += numNeighbors(tempStrips2I._faces.get(j), edgeInfos);
+ }
+
+ final float currCost = numNeighbors / (float) tempStrips2I._faces.size();
+ if (currCost < minCost) {
+ minCost = currCost;
+ firstIndex = i;
+ }
+ }
+
+ final NvStripInfo tempStrips2FirstIndex = tempStrips2.get(firstIndex);
+ updateCacheStrip(vcache, tempStrips2FirstIndex);
+ outStrips.add(tempStrips2FirstIndex);
+
+ tempStrips2FirstIndex._visited = true;
+
+ boolean bWantsCW = tempStrips2FirstIndex._faces.size() % 2 == 0;
+
+ // XXX: this n^2 algo is what slows down stripification so much.... needs to be improved
+ while (true) {
+ bestNumHits = -1.0f;
+
+ // find best strip to add next, given the current cache
+ for (int i = 0; i < tempStrips2.size(); i++) {
+ final NvStripInfo tempStrips2I = tempStrips2.get(i);
+ if (tempStrips2I._visited) {
+ continue;
+ }
+
+ numHits = calcNumHitsStrip(vcache, tempStrips2I);
+ if (numHits > bestNumHits) {
+ bestNumHits = numHits;
+ bestIndex = i;
+ } else if (numHits >= bestNumHits) {
+ // check previous strip to see if this one requires it to switch polarity
+ final NvStripInfo strip = tempStrips2I;
+ final int nStripFaceCount = strip._faces.size();
+
+ final NvFaceInfo tFirstFace = new NvFaceInfo(strip._faces.get(0));
+
+ // If there is a second face, reorder vertices such that the
+ // unique vertex is first
+ if (nStripFaceCount > 1) {
+ final int nUnique = NvStripifier.getUniqueVertexInB(strip._faces.get(1), tFirstFace);
+ if (nUnique == tFirstFace._v1) {
+ final int store = tFirstFace._v1;
+ tFirstFace._v1 = tFirstFace._v0;
+ tFirstFace._v0 = store;
+ } else if (nUnique == tFirstFace._v2) {
+ final int store = tFirstFace._v2;
+ tFirstFace._v2 = tFirstFace._v0;
+ tFirstFace._v0 = store;
+ }
+
+ // If there is a third face, reorder vertices such that the
+ // shared vertex is last
+ if (nStripFaceCount > 2) {
+ final int[] nShared = NvStripifier.getSharedVertices(strip._faces.get(2), tFirstFace);
+ if (nShared[0] == tFirstFace._v1 && nShared[1] == -1) {
+ final int store = tFirstFace._v2;
+ tFirstFace._v2 = tFirstFace._v1;
+ tFirstFace._v1 = store;
+ }
+ }
+ }
+
+ // Check CW/CCW ordering
+ if (bWantsCW == NvStripifier.isCW(strip._faces.get(0), tFirstFace._v0, tFirstFace._v1)) {
+ // I like this one!
+ bestIndex = i;
+ }
+ }
+ }
+
+ if (bestNumHits == -1.0f) {
+ break;
+ }
+ tempStrips2.get(bestIndex)._visited = true;
+ updateCacheStrip(vcache, tempStrips2.get(bestIndex));
+ outStrips.add(tempStrips2.get(bestIndex));
+ bWantsCW = tempStrips2.get(bestIndex)._faces.size() % 2 == 0 ? bWantsCW : !bWantsCW;
+ }
+ }
+ }
+
+ /**
+ * @param allStrips
+ * the whole strip vector...all small strips will be deleted from this list, to avoid leaking mem
+ * @param allBigStrips
+ * an out parameter which will contain all strips above minStripLength
+ * @param faceList
+ * an out parameter which will contain all faces which were removed from the striplist
+ */
+ void removeSmallStrips(final List<NvStripInfo> allStrips, final List<NvStripInfo> allBigStrips,
+ final List<NvFaceInfo> faceList) {
+ faceList.clear();
+ allBigStrips.clear(); // make sure these are empty
+ final List<NvFaceInfo> tempFaceList = Lists.newArrayList();
+
+ for (int i = 0; i < allStrips.size(); i++) {
+ final NvStripInfo allStripI = allStrips.get(i);
+ if (allStripI._faces.size() < _minStripLength) {
+ // strip is too small, add faces to faceList
+ for (int j = 0; j < allStripI._faces.size(); j++) {
+ tempFaceList.add(allStripI._faces.get(j));
+ }
+ } else {
+ allBigStrips.add(allStripI);
+ }
+ }
+
+ if (!tempFaceList.isEmpty()) {
+ final boolean[] bVisitedList = new boolean[tempFaceList.size()];
+ final VertexCache vcache = new VertexCache(_cacheSize);
+
+ int bestNumHits = -1;
+ int numHits = 0;
+ int bestIndex = 0;
+
+ while (true) {
+ bestNumHits = -1;
+
+ // find best face to add next, given the current cache
+ for (int i = 0; i < tempFaceList.size(); i++) {
+ if (bVisitedList[i]) {
+ continue;
+ }
+
+ numHits = calcNumHitsFace(vcache, tempFaceList.get(i));
+ if (numHits > bestNumHits) {
+ bestNumHits = numHits;
+ bestIndex = i;
+ }
+ }
+
+ if (bestNumHits == -1.0f) {
+ break;
+ }
+ bVisitedList[bestIndex] = true;
+ updateCacheFace(vcache, tempFaceList.get(bestIndex));
+ faceList.add(tempFaceList.get(bestIndex));
+ }
+ }
+ }
+
+ /**
+ * Finds the next face to start the next strip on.
+ *
+ * @param faceInfos
+ * @param edgeInfos
+ * @param strip
+ * @param startInfo
+ * @return
+ */
+ boolean findTraversal(final List<NvFaceInfo> faceInfos, final List<NvEdgeInfo> edgeInfos, final NvStripInfo strip,
+ final NvStripStartInfo startInfo) {
+ // if the strip was v0.v1 on the edge, then v1 will be a vertex in the next edge.
+ final int v = strip._startInfo._toV1 ? strip._startInfo._startEdge._v1 : strip._startInfo._startEdge._v0;
+
+ NvFaceInfo untouchedFace = null;
+ NvEdgeInfo edgeIter = edgeInfos.get(v);
+ while (edgeIter != null) {
+ final NvFaceInfo face0 = edgeIter._face0;
+ final NvFaceInfo face1 = edgeIter._face1;
+ if (face0 != null && !strip.IsInStrip(face0) && face1 != null && !strip.isMarked(face1)) {
+ untouchedFace = face1;
+ break;
+ }
+ if (face1 != null && !strip.IsInStrip(face1) && face0 != null && !strip.isMarked(face0)) {
+ untouchedFace = face0;
+ break;
+ }
+
+ // find the next edgeIter
+ edgeIter = edgeIter._v0 == v ? edgeIter._nextV0 : edgeIter._nextV1;
+ }
+
+ startInfo._startFace = untouchedFace;
+ startInfo._startEdge = edgeIter;
+ if (edgeIter != null) {
+ if (strip.sharesEdge(startInfo._startFace, edgeInfos)) {
+ startInfo._toV1 = edgeIter._v0 == v; // note! used to be m_v1
+ } else {
+ startInfo._toV1 = edgeIter._v1 == v;
+ }
+ }
+ return startInfo._startFace != null;
+ }
+
+ /**
+ * "Commits" the input strips by setting their m_experimentId to -1 and adding to the allStrips vector
+ *
+ * @param allStrips
+ * @param strips
+ */
+ void commitStrips(final List<NvStripInfo> allStrips, final List<NvStripInfo> strips) {
+ // Iterate through strips
+ for (final NvStripInfo strip : strips) {
+ // Tell the strip that it is now real
+ strip._experimentId = -1;
+
+ // add to the list of real strips
+ allStrips.add(strip);
+
+ // Iterate through the faces of the strip
+ // Tell the faces of the strip that they belong to a real strip now
+ final List<NvFaceInfo> faces = strip._faces;
+ for (final NvFaceInfo face : faces) {
+ strip.markTriangle(face);
+ }
+ }
+ }
+
+ /**
+ *
+ * @param strips
+ * @return the average strip size of the input vector of strips
+ */
+ float avgStripSize(final List<NvStripInfo> strips) {
+ int sizeAccum = 0;
+ for (final NvStripInfo strip : strips) {
+ sizeAccum += strip._faces.size();
+ sizeAccum -= strip._numDegenerates;
+ }
+ return (float) sizeAccum / (float) strips.size();
+ }
+
+ /**
+ * Finds a good starting point, namely one which has only one neighbor
+ *
+ * @param faceInfos
+ * @param edgeInfos
+ * @return
+ */
+ int findStartPoint(final List<NvFaceInfo> faceInfos, final List<NvEdgeInfo> edgeInfos) {
+ int bestCtr = -1;
+ int bestIndex = -1;
+
+ int i = 0;
+ for (final NvFaceInfo faceInfo : faceInfos) {
+ int ctr = 0;
+
+ if (NvStripifier.findOtherFace(edgeInfos, faceInfo._v0, faceInfo._v1, faceInfo) == null) {
+ ctr++;
+ }
+ if (NvStripifier.findOtherFace(edgeInfos, faceInfo._v1, faceInfo._v2, faceInfo) == null) {
+ ctr++;
+ }
+ if (NvStripifier.findOtherFace(edgeInfos, faceInfo._v2, faceInfo._v0, faceInfo) == null) {
+ ctr++;
+ }
+ if (ctr > bestCtr) {
+ bestCtr = ctr;
+ bestIndex = i;
+ }
+ i++;
+ }
+
+ if (bestCtr == 0) {
+ return -1;
+ } else {
+ return bestIndex;
+ }
+ }
+
+ /**
+ * Updates the input vertex cache with this strip's vertices
+ *
+ * @param vcache
+ * @param strip
+ */
+ void updateCacheStrip(final VertexCache vcache, final NvStripInfo strip) {
+ for (final NvFaceInfo face : strip._faces) {
+ updateCacheFace(vcache, face);
+ }
+ }
+
+ /**
+ * Updates the input vertex cache with this face's vertices
+ *
+ * @param vcache
+ * @param face
+ */
+ void updateCacheFace(final VertexCache vcache, final NvFaceInfo face) {
+ if (!vcache.inCache(face._v0)) {
+ vcache.addEntry(face._v0);
+ }
+
+ if (!vcache.inCache(face._v1)) {
+ vcache.addEntry(face._v1);
+ }
+
+ if (!vcache.inCache(face._v2)) {
+ vcache.addEntry(face._v2);
+ }
+ }
+
+ /**
+ * @param vcache
+ * @param strip
+ * @return the number of cache hits per face in the strip
+ */
+ float calcNumHitsStrip(final VertexCache vcache, final NvStripInfo strip) {
+ int numHits = 0;
+ int numFaces = 0;
+
+ for (final NvFaceInfo face : strip._faces) {
+ if (vcache.inCache(face._v0)) {
+ ++numHits;
+ }
+
+ if (vcache.inCache(face._v1)) {
+ ++numHits;
+ }
+
+ if (vcache.inCache(face._v2)) {
+ ++numHits;
+ }
+
+ numFaces++;
+ }
+
+ return (float) numHits / (float) numFaces;
+ }
+
+ /**
+ * @param vcache
+ * @param face
+ * @return the number of cache hits in the face
+ */
+ int calcNumHitsFace(final VertexCache vcache, final NvFaceInfo face) {
+ int numHits = 0;
+
+ if (vcache.inCache(face._v0)) {
+ numHits++;
+ }
+
+ if (vcache.inCache(face._v1)) {
+ numHits++;
+ }
+
+ if (vcache.inCache(face._v2)) {
+ numHits++;
+ }
+
+ return numHits;
+ }
+
+ /**
+ *
+ * @param face
+ * @param edgeInfoVec
+ * @return the number of neighbors that this face has
+ */
+ int numNeighbors(final NvFaceInfo face, final List<NvEdgeInfo> edgeInfoVec) {
+ int numNeighbors = 0;
+
+ if (NvStripifier.findOtherFace(edgeInfoVec, face._v0, face._v1, face) != null) {
+ numNeighbors++;
+ }
+
+ if (NvStripifier.findOtherFace(edgeInfoVec, face._v1, face._v2, face) != null) {
+ numNeighbors++;
+ }
+
+ if (NvStripifier.findOtherFace(edgeInfoVec, face._v2, face._v0, face) != null) {
+ numNeighbors++;
+ }
+
+ return numNeighbors;
+ }
+
+ /**
+ * Builds the list of all face and edge infos
+ *
+ * @param faceInfos
+ * @param edgeInfos
+ * @param maxIndex
+ */
+ void buildStripifyInfo(final List<NvFaceInfo> faceInfos, final List<NvEdgeInfo> edgeInfos, final int maxIndex) {
+ // reserve space for the face infos, but do not resize them.
+ final int numIndices = _indices.size();
+
+ // we actually resize the edge infos, so we must initialize to null
+ for (int i = 0; i <= maxIndex; i++) {
+ edgeInfos.add(null);
+ }
+
+ // iterate through the triangles of the triangle list
+ final int numTriangles = numIndices / 3;
+ int index = 0;
+ final boolean[] bFaceUpdated = new boolean[3];
+
+ for (int i = 0; i < numTriangles; i++) {
+ boolean bMightAlreadyExist = true;
+ bFaceUpdated[0] = false;
+ bFaceUpdated[1] = false;
+ bFaceUpdated[2] = false;
+
+ // grab the indices
+ final int v0 = _indices.get(index++);
+ final int v1 = _indices.get(index++);
+ final int v2 = _indices.get(index++);
+
+ // we disregard degenerates
+ if (NvStripifier.isDegenerate(v0, v1, v2)) {
+ continue;
+ }
+
+ // create the face info and add it to the list of faces, but only if this exact face doesn't already
+ // exist in the list
+ final NvFaceInfo faceInfo = new NvFaceInfo(v0, v1, v2);
+
+ // grab the edge infos, creating them if they do not already exist
+ NvEdgeInfo edgeInfo01 = NvStripifier.findEdgeInfo(edgeInfos, v0, v1);
+ if (edgeInfo01 == null) {
+ // since one of it's edges isn't in the edge data structure, it can't already exist in the face
+ // structure
+ bMightAlreadyExist = false;
+
+ // create the info
+ edgeInfo01 = new NvEdgeInfo(v0, v1);
+
+ // update the linked list on both
+ edgeInfo01._nextV0 = edgeInfos.get(v0);
+ edgeInfo01._nextV1 = edgeInfos.get(v1);
+ edgeInfos.set(v0, edgeInfo01);
+ edgeInfos.set(v1, edgeInfo01);
+
+ // set face 0
+ edgeInfo01._face0 = faceInfo;
+ } else {
+ if (edgeInfo01._face1 != null) {
+ NvStripifier.logger
+ .warning("BuildStripifyInfo: > 2 triangles on an edge... uncertain consequences\n");
+ } else {
+ edgeInfo01._face1 = faceInfo;
+ bFaceUpdated[0] = true;
+ }
+ }
+
+ // grab the edge infos, creating them if they do not already exist
+ NvEdgeInfo edgeInfo12 = NvStripifier.findEdgeInfo(edgeInfos, v1, v2);
+ if (edgeInfo12 == null) {
+ bMightAlreadyExist = false;
+
+ // create the info
+ edgeInfo12 = new NvEdgeInfo(v1, v2);
+
+ // update the linked list on both
+ edgeInfo12._nextV0 = edgeInfos.get(v1);
+ edgeInfo12._nextV1 = edgeInfos.get(v2);
+ edgeInfos.set(v1, edgeInfo12);
+ edgeInfos.set(v2, edgeInfo12);
+
+ // set face 0
+ edgeInfo12._face0 = faceInfo;
+ } else {
+ if (edgeInfo12._face1 != null) {
+ NvStripifier.logger
+ .warning("BuildStripifyInfo: > 2 triangles on an edge... uncertain consequences\n");
+ } else {
+ edgeInfo12._face1 = faceInfo;
+ bFaceUpdated[1] = true;
+ }
+ }
+
+ // grab the edge infos, creating them if they do not already exist
+ NvEdgeInfo edgeInfo20 = NvStripifier.findEdgeInfo(edgeInfos, v2, v0);
+ if (edgeInfo20 == null) {
+ bMightAlreadyExist = false;
+
+ // create the info
+ edgeInfo20 = new NvEdgeInfo(v2, v0);
+
+ // update the linked list on both
+ edgeInfo20._nextV0 = edgeInfos.get(v2);
+ edgeInfo20._nextV1 = edgeInfos.get(v0);
+ edgeInfos.set(v2, edgeInfo20);
+ edgeInfos.set(v0, edgeInfo20);
+
+ // set face 0
+ edgeInfo20._face0 = faceInfo;
+ } else {
+ if (edgeInfo20._face1 != null) {
+ NvStripifier.logger
+ .warning("BuildStripifyInfo: > 2 triangles on an edge... uncertain consequences\n");
+ } else {
+ edgeInfo20._face1 = faceInfo;
+ bFaceUpdated[2] = true;
+ }
+ }
+
+ if (bMightAlreadyExist) {
+ if (!alreadyExists(faceInfo, faceInfos)) {
+ faceInfos.add(faceInfo);
+ } else {
+ // cleanup pointers that point to this deleted face
+ if (bFaceUpdated[0]) {
+ edgeInfo01._face1 = null;
+ }
+ if (bFaceUpdated[1]) {
+ edgeInfo12._face1 = null;
+ }
+ if (bFaceUpdated[2]) {
+ edgeInfo20._face1 = null;
+ }
+ }
+ } else {
+ faceInfos.add(faceInfo);
+ }
+ }
+ }
+
+ boolean alreadyExists(final NvFaceInfo toFind, final List<NvFaceInfo> faceInfos) {
+ for (final NvFaceInfo face : faceInfos) {
+ if (face._v0 == toFind._v0 && face._v1 == toFind._v1 && face._v2 == toFind._v2) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvTriangleStripper.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvTriangleStripper.java
new file mode 100644
index 0000000..c16a631
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/NvTriangleStripper.java
@@ -0,0 +1,564 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.util.nvtristrip;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.ardor3d.renderer.IndexMode;
+import com.ardor3d.scenegraph.IndexBufferData;
+import com.ardor3d.scenegraph.IntBufferData;
+import com.ardor3d.scenegraph.Mesh;
+import com.ardor3d.scenegraph.MeshData;
+import com.ardor3d.scenegraph.Spatial;
+import com.ardor3d.scenegraph.visitor.Visitor;
+import com.ardor3d.util.geom.BufferUtils;
+import com.google.common.collect.Lists;
+
+/**
+ * Ported from <a href="http://developer.nvidia.com/object/nvtristrip_library.html">NVIDIA's NvTriStrip Library</a>
+ */
+public class NvTriangleStripper implements Visitor {
+ /** GeForce1 and 2 cache size */
+ public static final int CACHESIZE_GEFORCE1_2 = 16;
+
+ /** GeForce3 cache size */
+ public static final int CACHESIZE_GEFORCE3 = 24;
+
+ private int _cacheSize = NvTriangleStripper.CACHESIZE_GEFORCE3;
+ private int _minStripSize = 0;
+ private int _restartVal = 0;
+ private boolean _stitchStrips = true;
+ private boolean _listsOnly = false;
+ private boolean _restart = false;
+ private boolean _reorderVertices = false;
+
+ /**
+ * For GPUs that support primitive restart, this sets a value as the restart index
+ *
+ * Restart is meaningless if strips are not being stitched together, so enabling restart makes NvTriStrip forcing
+ * stitching. So, you'll get back one strip.
+ *
+ * @param restartVal
+ */
+ public void enableRestart(final int restartVal) {
+ _restart = true;
+ _restartVal = restartVal;
+ }
+
+ /**
+ * For GPUs that support primitive restart, this disables using primitive restart
+ */
+ public void disableRestart() {
+ _restart = false;
+ }
+
+ public boolean isRestart() {
+ return _restart;
+ }
+
+ /**
+ * Sets the cache size which the stripfier uses to optimize the data. Controls the length of the generated
+ * individual strips. This is the "actual" cache size, so 24 for GeForce3 and 16 for GeForce1/2 You may want to play
+ * around with this number to tweak performance.
+ *
+ * @param cacheSize
+ * (Default value: 24)
+ */
+ public void setCacheSize(final int cacheSize) {
+ _cacheSize = cacheSize;
+ }
+
+ public int getCacheSize() {
+ return _cacheSize;
+ }
+
+ /**
+ * boolean to indicate whether to stitch together strips into one huge strip or not. If set to true, you'll get back
+ * one huge strip stitched together using degenerate triangles. If set to false, you'll get back a large number of
+ * separate strips.
+ *
+ * @param bStitchStrips
+ * (Default value: true)
+ */
+ public void setStitchStrips(final boolean bStitchStrips) {
+ _stitchStrips = bStitchStrips;
+ }
+
+ public boolean isStitchStrips() {
+ return _stitchStrips;
+ }
+
+ /**
+ *
+ * Sets the minimum acceptable size for a strip, in triangles. All strips generated which are shorter than this will
+ * be thrown into one big, separate list.
+ *
+ * @param minSize
+ * (Default value: 0)
+ */
+ public void setMinStripSize(final int minSize) {
+ _minStripSize = minSize;
+ }
+
+ public int getMinStripSize() {
+ return _minStripSize;
+ }
+
+ /**
+ * If set to true, will return an optimized list, with no strips at all.
+ *
+ * @param bListsOnly
+ * (Default value: false)
+ */
+ public void setListsOnly(final boolean bListsOnly) {
+ _listsOnly = bListsOnly;
+ }
+
+ public boolean isListsOnly() {
+ return _listsOnly;
+ }
+
+ /**
+ * If set to true, will call remapIndices after generateStrips.
+ *
+ * @param reorder
+ * (Default value: false)
+ * @see #remapIndices(PrimitiveGroup[], AtomicReference, int)
+ */
+ public void setReorderVertices(final boolean reorder) {
+ _reorderVertices = reorder;
+ }
+
+ public boolean isReorderVertices() {
+ return _reorderVertices;
+ }
+
+ /**
+ *
+ Returns true if the two triangles defined by firstTri and secondTri are the same The "same" is defined in this
+ * case as having the same indices with the same winding order
+ *
+ * @param firstTri0
+ * @param firstTri1
+ * @param firstTri2
+ * @param secondTri0
+ * @param secondTri1
+ * @param secondTri2
+ * @return
+ */
+ boolean sameTriangle(final int firstTri0, final int firstTri1, final int firstTri2, final int secondTri0,
+ final int secondTri1, final int secondTri2) {
+ boolean isSame = false;
+
+ if (firstTri0 == secondTri0) {
+ if (firstTri1 == secondTri1) {
+ if (firstTri2 == secondTri2) {
+ isSame = true;
+ }
+ }
+ } else if (firstTri0 == secondTri1) {
+ if (firstTri1 == secondTri2) {
+ if (firstTri2 == secondTri0) {
+ isSame = true;
+ }
+ }
+ } else if (firstTri0 == secondTri2) {
+ if (firstTri1 == secondTri0) {
+ if (firstTri2 == secondTri1) {
+ isSame = true;
+ }
+ }
+ }
+
+ return isSame;
+ }
+
+ boolean testTriangle(final int v0, final int v1, final int v2, final List<NvFaceInfo> in_bins[], final int NUMBINS) {
+ // hash this triangle
+ boolean isLegit = false;
+ int ctr = v0 % NUMBINS;
+ NvFaceInfo face;
+ for (int k = 0; k < in_bins[ctr].size(); ++k) {
+ // check triangles in this bin
+ face = in_bins[ctr].get(k);
+ if (sameTriangle(face._v0, face._v1, face._v2, v0, v1, v2)) {
+ isLegit = true;
+ break;
+ }
+ }
+ if (!isLegit) {
+ ctr = v1 % NUMBINS;
+ for (int k = 0; k < in_bins[ctr].size(); ++k) {
+ face = in_bins[ctr].get(k);
+ // check triangles in this bin
+ if (sameTriangle(face._v0, face._v1, face._v2, v0, v1, v2)) {
+ isLegit = true;
+ break;
+ }
+ }
+
+ if (!isLegit) {
+ ctr = v2 % NUMBINS;
+ for (int k = 0; k < in_bins[ctr].size(); ++k) {
+ face = in_bins[ctr].get(k);
+ // check triangles in this bin
+ if (sameTriangle(face._v0, face._v1, face._v2, v0, v1, v2)) {
+ isLegit = true;
+ break;
+ }
+ }
+
+ }
+ }
+
+ return isLegit;
+ }
+
+ /**
+ *
+ * @param in_indices
+ * input index list, the indices you would use to render
+ * @param validate
+ * @return array of optimized/stripified PrimitiveGroups
+ */
+ @SuppressWarnings("unchecked")
+ public PrimitiveGroup[] generateStrips(final int[] in_indices, final boolean validate) {
+ if (in_indices == null || in_indices.length == 0) {
+ return new PrimitiveGroup[0];
+ }
+
+ int numGroups = 0;
+ PrimitiveGroup[] primGroups;
+
+ // put data in format that the stripifier likes
+ final List<Integer> tempIndices = Lists.newArrayList();
+ int maxIndex = 0;
+ for (int i = 0; i < in_indices.length; i++) {
+ tempIndices.add(in_indices[i]);
+ if (in_indices[i] > maxIndex) {
+ maxIndex = in_indices[i];
+ }
+ }
+ final List<NvStripInfo> tempStrips = Lists.newArrayList();
+ final List<NvFaceInfo> tempFaces = Lists.newArrayList();
+
+ final NvStripifier stripifier = new NvStripifier();
+
+ // do actual stripification
+ stripifier.stripify(tempIndices, _cacheSize, _minStripSize, maxIndex, tempStrips, tempFaces);
+
+ // stitch strips together
+ final List<Integer> stripIndices = Lists.newArrayList();
+ int numSeparateStrips = 0;
+
+ if (_listsOnly) {
+ // if we're outputting only lists, we're done
+ numGroups = 1;
+ primGroups = new PrimitiveGroup[numGroups];
+ primGroups[0] = new PrimitiveGroup();
+ final PrimitiveGroup[] primGroupArray = primGroups;
+
+ // count the total number of indices
+ int numIndices = 0;
+ for (int i = 0; i < tempStrips.size(); i++) {
+ numIndices += tempStrips.get(i)._faces.size() * 3;
+ }
+
+ // add in the list
+ numIndices += tempFaces.size() * 3;
+
+ primGroupArray[0].setType(IndexMode.Triangles);
+ primGroupArray[0].setIndices(new int[numIndices]);
+ primGroupArray[0].setNumIndices(numIndices);
+
+ // do strips
+ int indexCtr = 0;
+ for (final NvStripInfo strip : tempStrips) {
+ for (final NvFaceInfo face : strip._faces) {
+ // degenerates are of no use with lists
+ if (!NvStripifier.isDegenerate(face)) {
+ primGroupArray[0]._getIndices()[indexCtr++] = face._v0;
+ primGroupArray[0]._getIndices()[indexCtr++] = face._v1;
+ primGroupArray[0]._getIndices()[indexCtr++] = face._v2;
+ } else {
+ // we've removed a tri, reduce the number of indices
+ primGroupArray[0].setNumIndices(primGroupArray[0].getNumIndices() - 3);
+ }
+ }
+ }
+
+ // do lists
+ for (final NvFaceInfo face : tempFaces) {
+ primGroupArray[0]._getIndices()[indexCtr++] = face._v0;
+ primGroupArray[0]._getIndices()[indexCtr++] = face._v1;
+ primGroupArray[0]._getIndices()[indexCtr++] = face._v2;
+ }
+ } else {
+ numSeparateStrips = stripifier.createStrips(tempStrips, stripIndices, _stitchStrips, _restart, _restartVal);
+
+ // if we're stitching strips together, we better get back only one strip from createStrips()
+ assert _stitchStrips && numSeparateStrips == 1 || !_stitchStrips;
+
+ // convert to output format
+ numGroups = numSeparateStrips; // for the strips
+ if (tempFaces.size() != 0) {
+ numGroups++;
+ } // we've got a list as well, increment
+ primGroups = new PrimitiveGroup[numGroups];
+ for (int i = 0; i < primGroups.length; i++) {
+ primGroups[i] = new PrimitiveGroup();
+ }
+ final PrimitiveGroup[] primGroupArray = primGroups;
+
+ // first, the strips
+ int startingLoc = 0;
+ for (int stripCtr = 0; stripCtr < numSeparateStrips; stripCtr++) {
+ int stripLength = 0;
+
+ if (!_stitchStrips) {
+ int i = startingLoc;
+ // if we've got multiple strips, we need to figure out the correct length
+ for (; i < stripIndices.size(); i++) {
+ if (stripIndices.get(i) == -1) {
+ break;
+ }
+ }
+
+ stripLength = i - startingLoc;
+ } else {
+ stripLength = stripIndices.size();
+ }
+
+ primGroupArray[stripCtr].setType(IndexMode.TriangleStrip);
+ primGroupArray[stripCtr].setIndices(new int[stripLength]);
+ primGroupArray[stripCtr].setNumIndices(stripLength);
+
+ int indexCtr = 0;
+ for (int i = startingLoc; i < stripLength + startingLoc; i++) {
+ primGroupArray[stripCtr]._getIndices()[indexCtr++] = stripIndices.get(i);
+ }
+
+ // we add 1 to account for the -1 separating strips
+ // this doesn't break the stitched case since we'll exit the loop
+ startingLoc += stripLength + 1;
+ }
+
+ // next, the list
+ if (tempFaces.size() != 0) {
+ final int faceGroupLoc = numGroups - 1; // the face group is the last one
+ primGroupArray[faceGroupLoc].setType(IndexMode.Triangles);
+ primGroupArray[faceGroupLoc].setIndices(new int[tempFaces.size() * 3]);
+ primGroupArray[faceGroupLoc].setNumIndices(tempFaces.size() * 3);
+ int indexCtr = 0;
+ for (final NvFaceInfo face : tempFaces) {
+ primGroupArray[faceGroupLoc]._getIndices()[indexCtr++] = face._v0;
+ primGroupArray[faceGroupLoc]._getIndices()[indexCtr++] = face._v1;
+ primGroupArray[faceGroupLoc]._getIndices()[indexCtr++] = face._v2;
+ }
+ }
+ }
+
+ // validate generated data against input
+ if (validate) {
+ final int NUMBINS = 100;
+
+ final List<NvFaceInfo> in_bins[] = new List[NUMBINS];
+ for (int i = 0; i < NUMBINS; i++) {
+ in_bins[i] = Lists.newArrayList();
+ }
+
+ // hash input indices on first index
+ for (int i = 0; i < in_indices.length; i += 3) {
+ final NvFaceInfo faceInfo = new NvFaceInfo(in_indices[i], in_indices[i + 1], in_indices[i + 2]);
+ in_bins[in_indices[i] % NUMBINS].add(faceInfo);
+ }
+
+ for (int i = 0; i < numGroups; ++i) {
+ switch (primGroups[i].getType()) {
+ case Triangles: {
+ for (int j = 0; j < primGroups[i].getNumIndices(); j += 3) {
+ final int v0 = primGroups[i]._getIndices()[j];
+ final int v1 = primGroups[i]._getIndices()[j + 1];
+ final int v2 = primGroups[i]._getIndices()[j + 2];
+
+ // ignore degenerates
+ if (NvStripifier.isDegenerate(v0, v1, v2)) {
+ continue;
+ }
+
+ if (!testTriangle(v0, v1, v2, in_bins, NUMBINS)) {
+ throw new IllegalStateException("failed validation");
+ }
+ }
+ break;
+ }
+
+ case TriangleStrip: {
+ boolean flip = false;
+ for (int j = 2; j < primGroups[i].getNumIndices(); ++j) {
+ final int v0 = primGroups[i]._getIndices()[j - 2];
+ int v1 = primGroups[i]._getIndices()[j - 1];
+ int v2 = primGroups[i]._getIndices()[j];
+
+ if (flip) {
+ // swap v1 and v2
+ final int swap = v1;
+ v1 = v2;
+ v2 = swap;
+ }
+
+ // ignore degenerates
+ if (NvStripifier.isDegenerate(v0, v1, v2)) {
+ flip = !flip;
+ continue;
+ }
+
+ if (!testTriangle(v0, v1, v2, in_bins, NUMBINS)) {
+ throw new IllegalStateException("failed validation");
+ }
+
+ flip = !flip;
+ }
+ break;
+ }
+
+ case TriangleFan:
+ default:
+ break;
+ }
+ }
+
+ }
+
+ return primGroups;
+ }
+
+ /**
+ * Function to remap your indices to improve spatial locality in your vertex buffer.
+ *
+ * Note that you must reorder your vertex buffer according to the remapping handed back to you.
+ *
+ * Credit goes to the MS Xbox crew for the idea for this interface.
+ *
+ * @param in_primGroups
+ * array of PrimitiveGroups you want remapped
+ * @param numVerts
+ * number of vertices in your vertex buffer, also can be thought of as the range of acceptable values for
+ * indices in your primitive groups.
+ * @return index remap. old index is key into array, value there is the old location for the vertex. -1 means vertex
+ * was never referenced
+ */
+ public PrimitiveGroup[] remapIndices(final PrimitiveGroup[] in_primGroups,
+ final AtomicReference<int[]> remappedVertices, final int numVerts) {
+ final PrimitiveGroup[] remappedGroups = new PrimitiveGroup[in_primGroups.length];
+
+ // caches oldIndex --> newIndex conversion
+ final int[] indexCache = new int[numVerts];
+ Arrays.fill(indexCache, -1);
+
+ // loop over primitive groups
+ int indexCtr = 0;
+ for (int i = 0; i < in_primGroups.length; i++) {
+ final int numIndices = in_primGroups[i].getNumIndices();
+
+ // init remapped group
+ remappedGroups[i] = new PrimitiveGroup();
+ remappedGroups[i].setType(in_primGroups[i].getType());
+ remappedGroups[i].setNumIndices(numIndices);
+ remappedGroups[i].setIndices(new int[numIndices]);
+
+ for (int j = 0; j < numIndices; j++) {
+ final int cachedIndex = indexCache[in_primGroups[i]._getIndices()[j]];
+ if (cachedIndex == -1) // we haven't seen this index before
+ {
+ // point to "last" vertex in VB
+ remappedGroups[i]._getIndices()[j] = indexCtr;
+
+ // add to index cache, increment
+ indexCache[in_primGroups[i]._getIndices()[j]] = indexCtr++;
+ } else {
+ // we've seen this index before
+ remappedGroups[i]._getIndices()[j] = cachedIndex;
+ }
+ }
+ }
+ if (remappedVertices != null) {
+ remappedVertices.set(indexCache);
+ }
+
+ return remappedGroups;
+ }
+
+ @Override
+ public void visit(final Spatial spatial) {
+ if (spatial instanceof Mesh) {
+ final Mesh mesh = (Mesh) spatial;
+ final MeshData md = mesh.getMeshData();
+ if (md.getTotalPrimitiveCount() < 1 || md.getVertexCount() < 3) {
+ return;
+ }
+ for (final IndexMode mode : md.getIndexModes()) {
+ if (mode != IndexMode.Triangles) {
+ return;
+ }
+ }
+
+ final int[] indices;
+ if (md.getIndices() == null) {
+ indices = new int[md.getVertexCount()];
+ for (int i = 0; i < indices.length; i++) {
+ indices[i] = i;
+ }
+ } else {
+ indices = BufferUtils.getIntArray(md.getIndices());
+ }
+ PrimitiveGroup[] strips = generateStrips(indices, false);
+
+ if (_reorderVertices) {
+ final AtomicReference<int[]> newOrder = new AtomicReference<int[]>();
+ strips = remapIndices(strips, newOrder, md.getVertexCount());
+
+ // ask mesh to apply new vertex order
+ mesh.reorderVertexData(newOrder.get());
+ }
+
+ // construct our new index buffer, modes and counts
+ int indexCount = 0, j = 0, count = 0;
+ for (final PrimitiveGroup group : strips) {
+ if (group.getIndices().length > 0) {
+ count++;
+ }
+ }
+ final int[] counts = new int[count];
+ final IndexMode[] modes = new IndexMode[count];
+ for (final PrimitiveGroup group : strips) {
+ indexCount += group.getIndices().length;
+ if (group.getIndices().length > 0) {
+ modes[j] = group.getType();
+ counts[j++] = group.getIndices().length;
+ }
+ }
+ final IndexBufferData<?> newIndices = BufferUtils.createIndexBufferData(indexCount, md.getVertexCount());
+ for (final PrimitiveGroup group : strips) {
+ final IntBufferData data = new IntBufferData(group.getIndices().length);
+ data.getBuffer().put(group.getIndices());
+ data.rewind();
+ newIndices.put(data);
+ }
+ newIndices.rewind();
+
+ // ask mesh to apply new index data
+ mesh.reorderIndices(newIndices, modes, counts);
+ }
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/PrimitiveGroup.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/PrimitiveGroup.java
new file mode 100644
index 0000000..01eaff8
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/PrimitiveGroup.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.util.nvtristrip;
+
+import com.ardor3d.renderer.IndexMode;
+
+public class PrimitiveGroup {
+
+ private IndexMode _type;
+ private int[] _indices;
+ private int _numIndices;
+
+ PrimitiveGroup() {
+ setType(IndexMode.Triangles);
+ setIndices(null);
+ setNumIndices(0);
+ }
+
+ int[] _getIndices() {
+ return _indices;
+ }
+
+ public int[] getIndices() {
+ if (_indices.length == _numIndices) {
+ return _indices;
+ }
+
+ // crop it down to actual size...
+ final int[] realIndices = new int[_numIndices];
+ System.arraycopy(_indices, 0, realIndices, 0, _numIndices);
+ _indices = realIndices;
+ return _indices;
+ }
+
+ int getNumIndices() {
+ return _numIndices;
+ }
+
+ public IndexMode getType() {
+ return _type;
+ }
+
+ void setType(final IndexMode type) {
+ _type = type;
+ }
+
+ void setIndices(final int[] indices) {
+ _indices = indices;
+ }
+
+ void setNumIndices(final int numIndices) {
+ this._numIndices = numIndices;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/VertexCache.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/VertexCache.java
new file mode 100644
index 0000000..cf90e5f
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/util/nvtristrip/VertexCache.java
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.model.util.nvtristrip;
+
+import java.util.Arrays;
+
+/**
+ * Ported from <a href="http://developer.nvidia.com/object/nvtristrip_library.html">NVIDIA's NvTriStrip Library</a>
+ */
+final class VertexCache {
+
+ private final int[] _entries;
+ private final int _numEntries;
+
+ public VertexCache(final int size) {
+ _numEntries = size;
+
+ _entries = new int[_numEntries];
+
+ for (int i = 0; i < _numEntries; i++) {
+ _entries[i] = -1;
+ }
+ }
+
+ public VertexCache() {
+ this(16);
+ }
+
+ public boolean inCache(final int entry) {
+ boolean returnVal = false;
+ for (int i = 0; i < _numEntries; i++) {
+ if (_entries[i] == entry) {
+ returnVal = true;
+ break;
+ }
+ }
+
+ return returnVal;
+ }
+
+ public int addEntry(final int entry) {
+ int removed;
+
+ removed = _entries[_numEntries - 1];
+
+ // push everything right one
+ for (int i = _numEntries - 2; i >= 0; i--) {
+ _entries[i + 1] = _entries[i];
+ }
+
+ _entries[0] = entry;
+
+ return removed;
+ }
+
+ public void clear() {
+ Arrays.fill(_entries, -1);
+ }
+
+ public void copy(final VertexCache inVcache) {
+ for (int i = 0; i < _numEntries; i++) {
+ inVcache.set(i, _entries[i]);
+ }
+ }
+
+ public int at(final int index) {
+ return _entries[index];
+ }
+
+ public void set(final int index, final int value) {
+ _entries[index] = value;
+ }
+}
diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/useful/TrailMesh.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/useful/TrailMesh.java
new file mode 100644
index 0000000..8a9f483
--- /dev/null
+++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/useful/TrailMesh.java
@@ -0,0 +1,358 @@
+/**
+ * Copyright (c) 2008-2012 Ardor Labs, Inc.
+ *
+ * This file is part of Ardor3D.
+ *
+ * Ardor3D is free software: you can redistribute it and/or modify it
+ * under the terms of its license which may be found in the accompanying
+ * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
+ */
+
+package com.ardor3d.extension.useful;
+
+import java.io.IOException;
+import java.nio.FloatBuffer;
+import java.util.LinkedList;
+
+import com.ardor3d.math.ColorRGBA;
+import com.ardor3d.math.Vector3;
+import com.ardor3d.math.type.ReadOnlyVector3;
+import com.ardor3d.renderer.IndexMode;
+import com.ardor3d.scenegraph.FloatBufferData;
+import com.ardor3d.scenegraph.IndexBufferData;
+import com.ardor3d.scenegraph.Mesh;
+import com.ardor3d.util.export.InputCapsule;
+import com.ardor3d.util.export.OutputCapsule;
+import com.ardor3d.util.geom.BufferUtils;
+
+/**
+ * <code>TrailMesh</code>
+ */
+public class TrailMesh extends Mesh {
+
+ private int nrTrailSections;
+ private int trailVertices;
+
+ public enum UpdateMode {
+ Step, Interpolate
+ }
+
+ /**
+ * Update mode defines how the trailmesh vertices are to be updated. Shifted position by position or interpolated
+ * for smoother movement.
+ */
+ private UpdateMode updateMode = UpdateMode.Step;
+
+ public enum FacingMode {
+ Tangent, Billboard
+ }
+
+ /*
+ * Facing mode defines the orientation of the trailmesh. Tangent uses the tangent value specified when calling
+ * setTrailFront and Billboard automatically orients the trailmesh to always face the camera.
+ */
+ private FacingMode facingMode = FacingMode.Billboard;
+
+ /**
+ * Storage for each section in the trailmesh.
+ */
+ public class TrailData {
+ public Vector3 position = new Vector3();
+ public Vector3 tangent;
+ public double width;
+ public Vector3 interpolatedPosition;
+ }
+
+ private final LinkedList<TrailData> trailVectors;
+
+ private float throttle = Float.MAX_VALUE;
+
+ // How often to update the trail front (controlling section spacing)
+ private float updateSpeed = 20.0f;
+
+ // Whether the TrailData is updated or not
+ private boolean invalid;
+
+ // Temporary vectors
+ private final Vector3 trailCamVec = new Vector3();
+ private final Vector3 trailDirection = new Vector3();
+
+ /**
+ * Creates a new TrailMesh.
+ *
+ * @param name
+ * Name of Spatial
+ * @param nrTrailSections
+ * Number of sections the TrailMesh should consist of. Number of vertices in the mesh will be
+ * nrTrailSections * 2.
+ */
+ public TrailMesh(final String name, final int nrTrailSections) {
+ super(name);
+ this.nrTrailSections = nrTrailSections;
+ trailVertices = nrTrailSections * 2;
+
+ trailVectors = new LinkedList<TrailData>();
+ for (int i = 0; i < nrTrailSections; i++) {
+ trailVectors.add(new TrailData());
+ }
+
+ setData();
+ }
+
+ /**
+ * Update the front position of the trail.
+ *
+ * @param position
+ * New position of the trail front
+ * @param width
+ * Width of the trail
+ * @param tpf
+ * Current time per frame
+ */
+ public void setTrailFront(final Vector3 position, final float width, final float tpf) {
+ setTrailFront(position, null, width, tpf);
+ }
+
+ /**
+ * Update the front position of the trail.
+ *
+ * @param position
+ * New position of the trail front
+ * @param tangent
+ * Specifies the gradient of the trail (if facingmode is set to tangent)
+ * @param width
+ * Width of the trail
+ * @param tpf
+ * Current time per frame
+ */
+ public void setTrailFront(final ReadOnlyVector3 position, final ReadOnlyVector3 tangent, final double width,
+ final double tpf) {
+
+ TrailData trail = null;
+
+ // Check if time to add or wrap the trail sections
+ throttle += tpf * updateSpeed;
+ if (throttle > 1.0f) {
+ throttle %= 1.0f;
+
+ trail = trailVectors.removeLast();
+ trailVectors.addFirst(trail);
+ } else {
+ trail = trailVectors.getFirst();
+ }
+
+ if (trail == null) {
+ return;
+ }
+
+ // Always update the front section
+ trail.position.set(position);
+ if (tangent != null) {
+ if (trail.tangent == null) {
+ trail.tangent = new Vector3();
+ }
+ trail.tangent.set(tangent);
+ }
+ trail.width = width;
+ invalid = true;
+ }
+
+ /**
+ * Update the vertices of the trail.
+ *
+ * @param camPos
+ * Camera position used for billboarding.
+ */
+ public void update(final ReadOnlyVector3 camPos) {
+ if (trailVectors.size() < 2) {
+ return;
+ }
+
+ if (invalid || facingMode == FacingMode.Billboard) {
+ if (updateMode == UpdateMode.Step) {
+ updateStep(camPos);
+ } else {
+ updateInterpolate(camPos);
+ }
+ invalid = false;
+ }
+ }
+
+ public void invalidate() {
+ invalid = true;
+ }
+
+ private void updateStep(final ReadOnlyVector3 camPos) {
+ final FloatBuffer vertBuf = getMeshData().getVertexBuffer();
+ vertBuf.rewind();
+
+ for (int i = 0; i < nrTrailSections; i++) {
+ final TrailData trailData = trailVectors.get(i);
+ final Vector3 trailVector = trailData.position;
+
+ if (facingMode == FacingMode.Billboard) {
+ if (i == 0) {
+ trailDirection.set(trailVectors.get(i + 1).position).subtractLocal(trailVector);
+ } else if (i == nrTrailSections - 1) {
+ trailDirection.set(trailVector).subtractLocal(trailVectors.get(i - 1).position);
+ } else {
+ trailDirection.set(trailVectors.get(i + 1).position)
+ .subtractLocal(trailVectors.get(i - 1).position);
+ }
+
+ trailCamVec.set(trailVector).subtractLocal(camPos);
+ trailDirection.crossLocal(trailCamVec);
+ trailDirection.normalizeLocal().multiplyLocal(trailData.width * 0.5);
+ } else if (trailData.tangent != null) {
+ trailDirection.set(trailData.tangent).multiplyLocal(trailData.width * 0.5);
+ } else {
+ trailDirection.set(trailData.width * 0.5f, 0, 0);
+ }
+
+ vertBuf.put(trailVector.getXf() - trailDirection.getXf());
+ vertBuf.put(trailVector.getYf() - trailDirection.getYf());
+ vertBuf.put(trailVector.getZf() - trailDirection.getZf());
+
+ vertBuf.put(trailVector.getXf() + trailDirection.getXf());
+ vertBuf.put(trailVector.getYf() + trailDirection.getYf());
+ vertBuf.put(trailVector.getZf() + trailDirection.getZf());
+ }
+ }
+
+ private void updateInterpolate(final ReadOnlyVector3 camPos) {
+ final FloatBuffer vertBuf = getMeshData().getVertexBuffer();
+ vertBuf.rewind();
+
+ for (int i = 0; i < nrTrailSections; i++) {
+ final TrailData trailData = trailVectors.get(i);
+
+ Vector3 interpolationVector = trailData.interpolatedPosition;
+ if (trailData.interpolatedPosition == null) {
+ trailData.interpolatedPosition = new Vector3();
+ interpolationVector = trailData.interpolatedPosition;
+ }
+
+ interpolationVector.set(trailData.position);
+
+ if (i > 0) {
+ interpolationVector.lerpLocal(trailVectors.get(i - 1).position, throttle);
+ }
+ }
+
+ for (int i = 0; i < nrTrailSections; i++) {
+ final TrailData trailData = trailVectors.get(i);
+ final Vector3 trailVector = trailData.interpolatedPosition;
+
+ if (facingMode == FacingMode.Billboard) {
+ if (i == 0) {
+ trailDirection.set(trailVectors.get(i + 1).interpolatedPosition).subtractLocal(trailVector);
+ } else if (i == nrTrailSections - 1) {
+ trailDirection.set(trailVector).subtractLocal(trailVectors.get(i - 1).interpolatedPosition);
+ } else {
+ trailDirection.set(trailVectors.get(i + 1).interpolatedPosition).subtractLocal(
+ trailVectors.get(i - 1).interpolatedPosition);
+ }
+
+ trailCamVec.set(trailVector).subtractLocal(camPos);
+ trailDirection.crossLocal(trailCamVec);
+ trailDirection.normalizeLocal().multiplyLocal(trailData.width * 0.5);
+ } else if (trailData.tangent != null) {
+ trailDirection.set(trailData.tangent).multiplyLocal(trailData.width * 0.5);
+ } else {
+ trailDirection.set(trailData.width * 0.5f, 0, 0);
+ }
+
+ vertBuf.put(trailVector.getXf() - trailDirection.getXf());
+ vertBuf.put(trailVector.getYf() - trailDirection.getYf());
+ vertBuf.put(trailVector.getZf() - trailDirection.getZf());
+
+ vertBuf.put(trailVector.getXf() + trailDirection.getXf());
+ vertBuf.put(trailVector.getYf() + trailDirection.getYf());
+ vertBuf.put(trailVector.getZf() + trailDirection.getZf());
+ }
+ }
+
+ public void resetPosition(final ReadOnlyVector3 position) {
+ for (int i = 0; i < nrTrailSections; i++) {
+ trailVectors.get(i).position.set(position);
+ }
+ }
+
+ private void setData() {
+ getMeshData().setVertexBuffer(BufferUtils.createVector3Buffer(getMeshData().getVertexBuffer(), trailVertices));
+ getMeshData().setNormalBuffer(BufferUtils.createVector3Buffer(getMeshData().getNormalBuffer(), trailVertices));
+ setDefaultColor(new ColorRGBA(ColorRGBA.WHITE));
+ setTextureData();
+ setIndexData();
+ }
+
+ private void setTextureData() {
+ if (getMeshData().getTextureCoords(0) == null) {
+ final FloatBuffer tex = BufferUtils.createVector2Buffer(trailVertices);
+ getMeshData().setTextureCoords(new FloatBufferData(tex, 2), 0);
+ for (int i = 0; i < nrTrailSections; i++) {
+ tex.put((float) i / nrTrailSections).put(0);
+ tex.put((float) i / nrTrailSections).put(1);
+ }
+ }
+ }
+
+ private void setIndexData() {
+ getMeshData().setIndexMode(IndexMode.TriangleStrip);
+ if (getMeshData().getIndexBuffer() == null) {
+ getMeshData().setIndexBuffer(BufferUtils.createIntBuffer(trailVertices));
+ final IndexBufferData<?> indexBuf = getMeshData().getIndices();
+ for (int i = 0; i < trailVertices; i++) {
+ indexBuf.put(i);
+ }
+ }
+ }
+
+ @Override
+ public void write(final OutputCapsule capsule) throws IOException {
+ super.write(capsule);
+ capsule.write(nrTrailSections, "nrTrailSections", 0);
+ capsule.write(trailVertices, "trailVertices", 0);
+ }
+
+ @Override
+ public void read(final InputCapsule capsule) throws IOException {
+ super.read(capsule);
+ nrTrailSections = capsule.readInt("nrTrailSections", 0);
+ trailVertices = capsule.readInt("trailVertices", 0);
+ }
+
+ public void setUpdateSpeed(final float updateSpeed) {
+ this.updateSpeed = updateSpeed;
+ }
+
+ public float getUpdateSpeed() {
+ return updateSpeed;
+ }
+
+ public void setUpdateMode(final UpdateMode updateMode) {
+ this.updateMode = updateMode;
+ }
+
+ public UpdateMode getUpdateMode() {
+ return updateMode;
+ }
+
+ public void setFacingMode(final FacingMode facingMode) {
+ this.facingMode = facingMode;
+ }
+
+ public FacingMode getFacingMode() {
+ return facingMode;
+ }
+
+ /**
+ * Get the mesh data to modify it manually. If data is modified, invalidate() method call is required.
+ *
+ * @return
+ */
+ public LinkedList<TrailData> getTrailData() {
+ return trailVectors;
+ }
+
+} \ No newline at end of file