// DAZ|Studio version 1.4.16.0 filetype DAZ|Script // Daz|Studio -> SecondLife BVH Export addin // (C) 2006-2007 RobbyRacoon Olmstead and Player // All rights reserved under US CopyRight Laws and International treaties // // License: Free for personal or commercial use // const RAD_TO_DEG : DzVec3 = new DzVec3(180.0 / Math.PI, 180.0 / Math.PI, 180.0 / Math.PI); const DEG_TO_RAD : DzVec3 = new DzVec3( Math.PI / 180, Math.PI / 180, Math.PI / 180 ); const SIG_DIGITS : Number = 6; var info : exportInfo; var file; class exportInfo { var destFile : String; var hipPosScaleX : Number; var hipPosScaleY : Number; var hipPosScaleZ : Number; var hipPosAbsolute : Boolean; var figure : DzSkeleton; } class exportDialog { const nMARGIN = 5; const nSPACING = 5; const nMIN_BTN_WIDTH = 80; const nMAX_BTN_HEIGHT = 20; // Support for Help and What's This var oHelpMgr; var oActionMgr; var oHelpAction; // widgets used during validation var dlg : DzDialog; var chkAbsolutePos : DzCheckBox; var chkHipPosEqual : DzCheckBox; var cboSkeleton : DzComboBox; var grpProgress : DzHGroupBox; var btnAccept : DzPushButton; var txtMessages : DzTextEdit; var txtDestination : DzLineEdit; var numHipScaleX : DzFloatSlider; var numHipScaleY : DzFloatSlider; var numHipScaleZ : DzFloatSlider; var numFrameTime : DzIntSlider; var lblFrameTime : DzLabel; var btnBrowseForFile: DzPushButton; function isValid() { if( !cboSkeleton.currentText || cboSkeleton.currentText == "" ) return false; if( !txtDestination.text || txtDestination.text == "" ) return false; var fi = new DzFileInfo(txtDestination.text); if( !fi.path() || fi.path() == "" ) return false; var pi = new DzFileInfo( fi.path() ); if( !pi.exists() ) return false; return true; } function validate() { try { btnAccept.enabled = isValid(); } catch( err ) { // Stub } } function hipPosSliderValueChangedX( newValue ) { if( chkHipPosEqual.checked ) { numHipScaleY.value = numHipScaleX.value; numHipScaleZ.value = numHipScaleX.value; } } function hipPosSliderValueChangedY( newValue ) { if( chkHipPosEqual.checked ) { numHipScaleX.value = numHipScaleY.value; numHipScaleZ.value = numHipScaleY.value; } } function hipPosSliderValueChangedZ( newValue ) { if( chkHipPosEqual.checked ) { numHipScaleY.value = numHipScaleZ.value; numHipScaleX.value = numHipScaleZ.value; } } function hipPosLockChecked( newValue ) { if( chkHipPosEqual.checked ) { numHipScaleY.value = numHipScaleX.value; numHipScaleZ.value = numHipScaleX.value; } } function fpsChanged( newValue ) { //var totalTime = (Scene.getAnimRange().end / 4800); //var frameCount : Number = Math.floor( Scene.getAnimRange().end / Scene.getTimeStep() ); lblFrameTime.text = "Animation Playback Speed (FPS) Frame Time: " + num2string(1 / numFrameTime.value, 3) + " FPS"; } function createFrameTimeWidgets() { var totalTime = Scene.getAnimRange().end / 4800; var frameCount : Number = Math.floor( Scene.getAnimRange().end / Scene.getTimeStep() ); var grpFrameTime = new DzVGroupBox( dlg ); grpFrameTime.title = "Animation Speed"; // Create a layout for the dialog buttons var layout = new DzGridLayout( grpFrameTime ); layout.margin = nMARGIN; layout.spacing = nSPACING; lblFrameTime = new DzLabel( grpFrameTime ); lblFrameTime.text = "Animation Playback Speed (FPS)"; layout.addWidget( lblFrameTime, 0, 0 ); numFrameTime = new DzIntSlider( grpFrameTime ); numFrameTime.min = 1; numFrameTime.max = 100; numFrameTime.clamped = true; numFrameTime.sensitivity = 5; numFrameTime.value = frameCount / totalTime; numFrameTime.textEditable = true; numFrameTime.textVisible = true; numFrameTime.displayAsPercent = false; numFrameTime.toolTip = "Allows you to override the animation playback speed"; connect( numFrameTime, "valueChanged(int)", fpsChanged ); layout.addWidget( numFrameTime, 1, 0 ); fpsChanged( numFrameTime.value ); } function createHipPosWidgets() { var grpHipPosScale = new DzVGroupBox( dlg ); grpHipPosScale.title = "Hip Position"; // Create a check box chkAbsolutePos = new DzCheckBox( grpHipPosScale ); chkAbsolutePos.text = "Use absolute values for hip position (instead of relative)"; chkAbsolutePos.toolTip = "When checked, will export absolute values for hip position rather than the default relative values"; var grp = new DzGroupBox( grpHipPosScale ); grp.flat = true; //grp.title = "Scaling"; // Create a layout for the dialog buttons var layout = new DzGridLayout( grp ); layout.margin = nMARGIN; layout.spacing = nSPACING; chkHipPosEqual = new DzCheckBox( grp ); chkHipPosEqual.text = "Scale all axes equally"; chkHipPosEqual.checked = true; chkHipPosEqual.toolTip = "When checked, will make sure that hip position scaling is applied equally to all three axes"; connect( chkHipPosEqual, "toggled(bool)", hipPosLockChecked ); layout.addMultiCellWidget( chkHipPosEqual, 0, 0, 0, 1 ); var lbl = new DzLabel( grp ); lbl.text = "X Axis"; lbl.maxWidth = 75; layout.addWidget( lbl, 1, 0 ); // Create a float slider numHipScaleX = new DzFloatSlider( grp ); numHipScaleX.min = 0; numHipScaleX.max = 1; numHipScaleX.clamped = true; numHipScaleX.sensitivity = 0.05; numHipScaleX.value = 0.53; numHipScaleX.textEditable = true; numHipScaleX.textVisible = true; numHipScaleX.displayAsPercent = true; numHipScaleX.toolTip = "Sets the amount of scaling applied to the hip position on the X Axis"; //numHipScaleX.whatsThis = "Sets the amount of scaling applied to the hip position on the X Axis"; connect( numHipScaleX, "valueChanged(float)", hipPosSliderValueChangedX ); layout.addWidget( numHipScaleX, 1, 1 ); var lbl = new DzLabel( grp ); lbl.text = "Y Axis"; lbl.maxWidth = 75; layout.addWidget( lbl, 2, 0 ); // Create a float slider numHipScaleY = new DzFloatSlider( grp ); numHipScaleY.min = 0; numHipScaleY.max = 1; numHipScaleY.clamped = true; numHipScaleY.sensitivity = 0.05; numHipScaleY.value = 0.53; numHipScaleY.textEditable = true; numHipScaleY.textVisible = true; numHipScaleY.displayAsPercent = true; numHipScaleY.toolTip = "Sets the amount of scaling applied to the hip position on the Y Axis"; //numHipScaleY.whatsThis = "Sets the amount of scaling applied to the hip position on the Y Axis"; connect( numHipScaleY, "valueChanged(float)", hipPosSliderValueChangedY ); layout.addWidget( numHipScaleY, 2, 1 ); var lbl = new DzLabel( grp ); lbl.text = "Z Axis"; lbl.maxWidth = 75; layout.addWidget( lbl, 3, 0 ); // Create a float slider numHipScaleZ = new DzFloatSlider( grp ); numHipScaleZ.min = 0; numHipScaleZ.max = 1; numHipScaleZ.clamped = true; numHipScaleZ.sensitivity = 0.01; numHipScaleZ.value = 0.53; numHipScaleZ.textEditable = true; numHipScaleZ.textVisible = true; numHipScaleZ.displayAsPercent = true; numHipScaleZ.toolTip = "Sets the amount of scaling applied to the hip position on the Z Axis"; //numHipScaleZ.whatsThis = "Sets the amount of scaling applied to the hip position on the Z Axis"; connect( numHipScaleZ, "valueChanged(float)", hipPosSliderValueChangedZ ); layout.addWidget( numHipScaleZ, 3, 1 ); } function createSkeletonSelectWidgets() { var wskeleton = new DzHGroupBox( dlg ); wskeleton.title = "Select the avatar to export"; // Create a combo box cboSkeleton = new DzComboBox( wskeleton ); cboSkeleton.toolTip = "Select the character you wish to export. Useful for 'couples' animations, etc."; var skeletons : Array = Scene.getSkeletonList(); for( var i = 0; i < skeletons.length; i++ ) { if( skeletons[i].isVisible() ) { cboSkeleton.insertItem(skeletons[i].getLabel()) } } if( cboSkeleton.count > 0 ) cboSkeleton.currentItem = 0; } function createDialogButtons() { // Create a group box for the dialog buttons var dlgBtnsGB = new DzGroupBox( dlg ); dlgBtnsGB.flat = true; // Create a layout for the dialog buttons var dlgBtnsLyt = new DzGridLayout( dlgBtnsGB ); dlgBtnsLyt.margin = nMARGIN; dlgBtnsLyt.spacing = nSPACING; // Create the accept push button btnAccept = new DzPushButton( dlgBtnsGB ); btnAccept.enabled = false; btnAccept.text = "&Accept"; btnAccept.minWidth = nMIN_BTN_WIDTH; btnAccept.maxHeight = nMAX_BTN_HEIGHT; dlg.setAcceptButton( btnAccept ); btnAccept.toolTip = oHelpMgr.getToolTip( "AcceptDialog" ); btnAccept.whatsThis = oHelpMgr.getHelpString( "AcceptDialog" ); dlgBtnsLyt.addWidget( btnAccept, 0, 2 ); // Create the cancel push button var wCancelBtn = new DzPushButton( dlgBtnsGB ); wCancelBtn.text = "&Cancel"; wCancelBtn.minWidth = nMIN_BTN_WIDTH; wCancelBtn.maxHeight = nMAX_BTN_HEIGHT; dlg.setRejectButton( wCancelBtn ); wCancelBtn.toolTip = oHelpMgr.getToolTip( "CancelDialog" ); wCancelBtn.whatsThis = oHelpMgr.getHelpString( "CancelDialog" ); dlgBtnsLyt.addWidget( wCancelBtn, 0, 3 ); } function browseForDestinationFile() { if( !cboSkeleton.currentText || cboSkeleton.currentText == "" ) { MessageBox.information( "You must select a figure to export first", "No figure selected", "&OK" ); return; } var filename = FileDialog.doFileDialog( false, "Export BVH for " + cboSkeleton.currentText, txtDestination.text, "Biovision (*.bvh); All Files (*.*)" ); if( !filename ) return; txtDestination.text = filename; } function onDestFileChanged( value ) { validate(); } function createDestinationWidgets() { var grpDestination = new DzGroupBox( dlg ); //grpDestination.title = "Export destination"; grpDestination.toolTip = "The destination .BVH file"; // Create a layout for the dialog buttons var layout = new DzGridLayout( grpDestination ); layout.margin = nMARGIN; layout.spacing = nSPACING; var spacer = new DzLabel( grpDestination ); spacer.text = "Select the destination BVH file"; layout.addWidget( spacer, 0, 0 ); txtDestination = new DzLineEdit( grpDestination ); txtDestination.text = "untitled.bvh"; txtDestination.width = 300; txtDestination.height = 24; txtDestination.maxHeight = 24; layout.addWidget( txtDestination, 1, 0 ); connect( txtDestination, "textChanged(const QString&)", onDestFileChanged ); btnBrowseForFile = new DzPushButton( grpDestination ); btnBrowseForFile.text = "&Browse"; layout.addWidget( btnBrowseForFile, 1, 1 ); connect( btnBrowseForFile, "pressed()", browseForDestinationFile ); var bvhFilename = "untitled.bvh"; var dazFilename = Scene.getFilename(); if (dazFilename) { var dazFile = new DzFileInfo(dazFilename); bvhFilename = dazFile.path() + "/" + dazFile.baseName() + ".bvh"; } txtDestination.text = bvhFilename; } function exportDialog() { oHelpMgr = App.getHelpMgr(); oActionMgr = MainWindow.getActionMgr(); oHelpAction = oActionMgr ? oActionMgr.findAction( "DzWhatsThisAction" ) : undefined; dlg = new DzDialog; dlg.caption = "DAZ|Studio -> SecondLife BVH Exporter"; //dlg.sizeGripEnabled = true; // Main Layout var dlgLyt = new DzVBoxLayout( dlg ); dlgLyt.autoAdd = true; dlgLyt.margin = nMARGIN; dlgLyt.spacing = nSPACING; // Creation of all user interface widgets createSkeletonSelectWidgets(); createDestinationWidgets(); createFrameTimeWidgets(); createHipPosWidgets(); createDialogButtons(); // Polish dlg.width = dlg.minWidth > 400 ? dlg.minWidth : 400; dlg.height = dlg.minHeight; } function run() { if( cboSkeleton.count == 0 ) { MessageBox.information( "This scene does not contain any exportable figures", "DAZ|Studio -> SecondLife Export", "&OK" ); return null; } validate(); if( !dlg.exec() ) return null; if( isValid() ) { var info = new exportInfo(); info.hipPosAbsolute = chkAbsolutePos.checked; info.hipPosScaleX = numHipScaleX.value; info.hipPosScaleY = numHipScaleY.value; info.hipPosScaleZ = numHipScaleZ.value; info.destFile = txtDestination.text; var fi = new DzFileInfo( info.destFile ); if( !fi.extension() || fi.extension() == "" ) info.destFile += ".bvh"; info.figure = Scene.findSkeleton( cboSkeleton.currentText ); if( !info.figure ) info.figure = Scene.findSkeletonByLabel( cboSkeleton.currentText ); if( !info.figure ) { MessageBox.error( "Could not find the indicated figure.", "FATAL ERROR", "&OK" ); return null; } return info; } return null; } } function num2string(number : Number, sig_digits : Number) { var s : String = number.toString(); if (s.toUpperCase().indexOf("E") != -1) { s = "0.000000"; } else { var dp_index : Number = s.indexOf("."); if (dp_index != -1) { if (s.length - dp_index > sig_digits) { s = s.left(dp_index) + s.mid(dp_index, sig_digits + 1); } else { while( s.length < sig_digits + 2 ) s += "0"; } } else { s += ".0"; while( s.length < sig_digits + 2 ) s += "0"; } } return s; } function vec2string( vec : DzVec3 ) { return num2string(vec.x, SIG_DIGITS) + " " + num2string(vec.y, SIG_DIGITS) + " " + num2string(vec.z, SIG_DIGITS); } function vec2string2( vec : DzVec3, firstAxis : Number, secondAxis : Number, thirdAxis : Number ) { var values = [ vec.x, vec.y, vec.z ]; return num2string( values[firstAxis], SIG_DIGITS) + " " + num2string(values[secondAxis], SIG_DIGITS) + " " + num2string(values[thirdAxis], SIG_DIGITS); } class jsBone { var boneRef : DzBone; var depth : Number = 0; var parent : jsBone; var boneName : String; var childBones : Array = new Array(); var axisOrder : Array = new Array(); function getOffset() { if( !parent ) return new DzVec3(); return boneRef.getOrigin().subtract( parent.boneRef.getOrigin() ); } function jsBone( name : String, rotationOrder : Array ) { if( name ) { init( name ); if( !rotationOrder ) throw "No rotation order specified for '" + name + "'"; axisOrder = rotationOrder; } } function init( name : String ) { boneName = name.left(1).toLowerCase() + name.right(name.length - 1); boneRef = info.figure.findBone( name ); if( !boneRef ) throw "'" + name + "' was not found in the bone hierarchy"; } function appendChild( name : String, rotationOrder : Array ) { var retVal = new jsBone( name, rotationOrder ); retVal.parent = this; retVal.depth = depth + 1; childBones.push( retVal ); return retVal; } function getIndent( count : Number ) { var retVal : String = ""; count += depth; while( count-- ) retVal += "\t"; return retVal; } function writeJointDefinition() { if( !boneRef.getNodeParent().isA("DzBone") ) file.write( "ROOT " ); else file.write( getIndent(0) + "JOINT " ); file.writeLine( boneName ); file.writeLine( getIndent(0) + "{" ); writeOffset(); writeChannels(); if( childBones.length == 0 ) { writeEndSite(); } else { for( var i = 0; i < childBones.length; i++ ) { var nextChild = childBones[i]; nextChild.writeJointDefinition(); } } file.writeLine( getIndent(0) + "}" ); } function writeOffset() { var offset : DzVec3 = getOffset(); //boneRef.getOrigin().subtract(boneRef.getNodeParent().getOrigin()); file.writeLine( getIndent(1) + "OFFSET " + vec2string( offset ) ); } function writeEndSite() { // Borrowed from Escort DeFarge, and I am unsure if it is correct. var offset = boneRef.getEndPoint().subtract( boneRef.getOrigin() ); // ?? file.writeLine( getIndent(1) + "End Site" ); file.writeLine( getIndent(1) + "{" ); file.writeLine( getIndent(2) + "OFFSET " + vec2string( offset ) ); file.writeLine( getIndent(1) + "}" ); } function writeChannels() { const rotAxis = [ "Xrotation", "Yrotation", "Zrotation" ]; var order = boneRef.getRotationOrder(); var suffix = rotAxis[ axisOrder[0] ] + " " + rotAxis[ axisOrder[1] ] + " " + rotAxis[ axisOrder[2] ]; if( boneRef.getNodeParent().isA("DzBone") ) { file.writeLine( getIndent(1) + "CHANNELS 3 " + suffix ); } else { file.writeLine( getIndent(1) + "CHANNELS 6 Xposition Yposition Zposition " + suffix ); } } function writeFrame( time : Number ) { if( !parent ) { var pos = boneRef.getLocalPos( time ) ; if( info.hipPosAbsolute ) pos = boneRef.getWSPos( time ); pos.x *= info.hipPosScaleX; pos.y *= info.hipPosScaleY; pos.z *= info.hipPosScaleZ; pos.y += 2.5; file.write( vec2string( pos ) + " " ); } var order = boneRef.getRotationOrder(); var rot = boneRef.getLocalRot(time); // Pull the values out in reverse axis order. var r2 = rot.getValue( axisOrder[2], axisOrder[1], axisOrder[0] ).multiply( RAD_TO_DEG ); // Write the rotation to the BVH file in the axis order specified by the SL readme.txt // file that was in the SL_AVATAR zip file. var v = [ r2.x, r2.y, r2.z ]; file.write( v[axisOrder[0]] + " " + v[axisOrder[1]] + " " + v[axisOrder[2]] + " " ); for( var i = 0; i < childBones.length; i++ ) { var nextChild = childBones[i]; nextChild.writeFrame( time ); } if( !parent ) file.writeLine( "" ); } } function main() { try { var dlg = new exportDialog(); info = dlg.run(); if( !info ) return; setBusyCursor(); file = new DzFile( info.destFile ); if (file.exists()) { if (MessageBox.warning( "The file \"" + info.destFile + "\" already exists. Do you wish to overwrite it?", "Daz|Studio -> SecondLife", "&OK", "&Cancel") == 0) { file.remove(); } else { return; } } if( !file.open(file.WriteOnly) ) throw "An error occurred opening the file"; var root : jsBone = new jsBone( "hip", [ 0, 2, 1 ] ); var abdomen : jsBone = root.appendChild( "abdomen", [ 0, 2, 1 ] ); var chest : jsBone = abdomen.appendChild( "chest", [ 0, 2, 1 ] ); var neck : jsBone = chest.appendChild( "neck", [ 0, 2, 1 ] ); var head : jsBone = neck.appendChild( "head", [ 0, 2, 1 ] ); var lCollar : jsBone = chest.appendChild( "lCollar", [ 1, 2, 0 ] ); var lShldr : jsBone = lCollar.appendChild( "lShldr", [ 2, 1, 0 ] ); var lForeArm : jsBone = lShldr.appendChild( "lForeArm", [ 1, 2, 0 ] ); var lHand : jsBone = lForeArm.appendChild( "lHand", [ 2, 1, 0 ] ); var rCollar : jsBone = chest.appendChild( "rCollar", [ 1, 2, 0 ] ); var rShldr : jsBone = rCollar.appendChild( "rShldr", [ 2, 1, 0 ] ); var rForeArm : jsBone = rShldr.appendChild( "rForeArm", [ 1, 2, 0 ] ); var rHand : jsBone = rForeArm.appendChild( "rHand", [ 2, 1, 0 ] ); var lThigh : jsBone = root.appendChild( "lThigh", [ 0, 2, 1 ] ); var lShin : jsBone = lThigh.appendChild( "lShin", [ 0, 2, 1 ] ); var lFoot : jsBone = lShin.appendChild( "lFoot", [ 0, 1, 2 ] ); var rThigh : jsBone = root.appendChild( "rThigh", [ 0, 2, 1 ] ); var rShin : jsBone = rThigh.appendChild( "rShin", [ 0, 2, 1 ] ); var rFoot : jsBone = rShin.appendChild( "rFoot", [ 0, 1, 2 ] ); // UI Sweetener if (Scene.isPlaying()) { Scene.pause(); } var origTime = Scene.getTime(); var origLoop = Scene.isLoopingEnabled(); Scene.loopPlayback(false); file.writeLine( "HIERARCHY" ); root.writeJointDefinition(); var totalTime = Scene.getAnimRange().end / 4800; var frameCount : Number = Math.floor( Scene.getAnimRange().end / Scene.getTimeStep() ); var start : Number = Scene.getAnimRange().start; file.writeLine( "MOTION" ); file.writeLine( "Frames:\t" + frameCount ); file.writeLine( "Frame Time:\t" + num2string( 1 / dlg.numFrameTime.value, 3 ) ); startProgress( "Exporting to file '" + file.baseName() + "." + file.extension() + "'", frameCount, false ); var time : Number = 0; for( var i = 0; i < frameCount; i++ ) { root.writeFrame( start + i * Scene.getTimeStep() ); updateProgress( i ); } MessageBox.information( "Export operation complete", "DAZ|Studio -> SecondLife", "&OK"); } catch( err ) { MessageBox.information( String(err), "Export Animation as BVH", "&OK"); } finally { if( file ) file.close(); clearBusyCursor(); finishProgress(); } } main();