Initial release of the Unity plugin.
PiperOrigin-RevId: 425947505
This commit is contained in:
committed by
Saran Tunyasuvunakool
parent
8ec3dd97a4
commit
6de9eafa13
@@ -0,0 +1,24 @@
|
||||
// Copyright 2019 DeepMind Technologies Limited
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mujoco {
|
||||
|
||||
// Use to annotate a float field that should only assume absolute values.
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class AbsoluteValueAttribute : PropertyAttribute {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: deb86daf07f8f4e4a97fabbb0f9f6c2e
|
||||
timeCreated: 1538126279
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,361 @@
|
||||
// Copyright 2019 DeepMind Technologies Limited
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mujoco {
|
||||
|
||||
// Mujoco engine helper methods.
|
||||
public static class MjEngineTool {
|
||||
private const int _elementsPerPosition = 3;
|
||||
private const int _elementsPerRotation = 4;
|
||||
private const int _elementsPerTransform = 7;
|
||||
private static double[] _mjQuat = new double[4];
|
||||
private static double[] _mjMat = new double[9];
|
||||
private const int _MjSuccess = 1;
|
||||
|
||||
public static string Sanitize(string name) {
|
||||
return name.Replace('/', '_');
|
||||
}
|
||||
|
||||
// Loads a model from the specified file.
|
||||
// `filename` should contain an absolute path to the file.
|
||||
public static unsafe MujocoLib.mjModel_* LoadModelFromFile(string fileName) {
|
||||
var errorBuf = new StringBuilder(1024);
|
||||
MujocoLib.mjModel_* model = MujocoLib.mj_loadXML(fileName, null, errorBuf, 1024);
|
||||
if (model == null || errorBuf.Length > 0) {
|
||||
throw new IOException(string.Format("Error loading the model: {0}", errorBuf));
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
// Saves a model to the specified file.
|
||||
// `filename` should contain an absolute path to the file.
|
||||
public static unsafe void SaveModelToFile(string fileName, MujocoLib.mjModel_* model) {
|
||||
var errorBuf = new StringBuilder(1024);
|
||||
MujocoLib.mj_saveLastXML(fileName, model, errorBuf, 1024);
|
||||
if (errorBuf.Length > 0) {
|
||||
throw new IOException(string.Format("Error saving the model: {0}", errorBuf));
|
||||
}
|
||||
}
|
||||
|
||||
// Loads a model from a string.
|
||||
// It allows to perform diskless instantiation of Mujoco models.
|
||||
public static unsafe MujocoLib.mjModel_* LoadModelFromString(string contents) {
|
||||
using (var vfs = new MjVfs()) {
|
||||
var filename = "filename";
|
||||
vfs.AddFile(filename, contents);
|
||||
return vfs.LoadXML(filename);
|
||||
}
|
||||
}
|
||||
|
||||
// Stores a unity vector in a Mujoco target buffer.
|
||||
// The vector coordinates will be stored at the 'entryIndex * 3' of the 'mjTarget' array.
|
||||
public static unsafe void SetMjVector3(double* mjTarget, Vector3 unityVec, int entryIndex) {
|
||||
var startOffset = entryIndex * _elementsPerPosition;
|
||||
var mjVec = MjVector3(unityVec);
|
||||
mjTarget[startOffset] = mjVec[0];
|
||||
mjTarget[startOffset + 1] = mjVec[1];
|
||||
mjTarget[startOffset + 2] = mjVec[2];
|
||||
}
|
||||
|
||||
// Stores a unity quaternion in a Mujoco target buffer.
|
||||
// The vector coordinates will be stored at the 'entryIndex * 4' of the 'mjTarget' array.
|
||||
public static unsafe void SetMjQuaternion(
|
||||
double* mjTarget, Quaternion unityQuat, int entryIndex) {
|
||||
var startOffset = entryIndex * _elementsPerRotation;
|
||||
var mjQuat = MjQuaternion(unityQuat);
|
||||
mjTarget[startOffset] = mjQuat.w;
|
||||
mjTarget[startOffset + 1] = mjQuat.x;
|
||||
mjTarget[startOffset + 2] = mjQuat.y;
|
||||
mjTarget[startOffset + 3] = mjQuat.z;
|
||||
}
|
||||
|
||||
// Stores a unity transform (position+rotation) in a Mujoco target buffer.
|
||||
// The vector coordinates will be stored at the 'entryIndex * 7' of the 'mjTarget' array.
|
||||
public static unsafe void SetMjTransform(
|
||||
double* mjTarget, Vector3 unityVec, Quaternion unityQuat, int entryIndex) {
|
||||
var startOffset = entryIndex * _elementsPerTransform;
|
||||
var mjVec = MjVector3(unityVec);
|
||||
var mjQuat = MjQuaternion(unityQuat);
|
||||
mjTarget[startOffset] = mjVec[0];
|
||||
mjTarget[startOffset + 1] = mjVec[1];
|
||||
mjTarget[startOffset + 2] = mjVec[2];
|
||||
mjTarget[startOffset + 3] = mjQuat.w;
|
||||
mjTarget[startOffset + 4] = mjQuat.x;
|
||||
mjTarget[startOffset + 5] = mjQuat.y;
|
||||
mjTarget[startOffset + 6] = mjQuat.z;
|
||||
}
|
||||
|
||||
// Converts a Unity Vector3 to a Mujoco vector.
|
||||
public static Vector3 MjVector3(Vector3 unityVec) {
|
||||
return new Vector3(unityVec.x, unityVec.z, unityVec.y);
|
||||
}
|
||||
|
||||
// Converts a Mujoco vector to a Unity vector.
|
||||
public static unsafe Vector3 UnityVector3(double* coords, int entryIndex) {
|
||||
var startOffset = entryIndex * _elementsPerPosition;
|
||||
return new Vector3(
|
||||
(float)coords[startOffset], (float)coords[startOffset + 2], (float)coords[startOffset + 1]);
|
||||
}
|
||||
|
||||
// Converts a Mujoco vector to a Unity vector.
|
||||
// The vector coordinates are expected to be stored at the 'entryIndex * 3' of the 'coords'
|
||||
// array.
|
||||
public static unsafe Vector3 UnityVector3(float[] coords, int entryIndex) {
|
||||
var startOffset = entryIndex * _elementsPerPosition;
|
||||
return new Vector3(coords[startOffset], coords[startOffset + 2], coords[startOffset + 1]);
|
||||
}
|
||||
|
||||
// Converts a Mujoco vector to a Unity vector.
|
||||
public static Vector3 UnityVector3(Vector3 mjVector) {
|
||||
return new Vector3(mjVector.x, mjVector.z, mjVector.y);
|
||||
}
|
||||
|
||||
// Converts a Unity Quaternion to a Mujoco Quaternion
|
||||
public static Quaternion MjQuaternion(Quaternion unityQuat) {
|
||||
return new Quaternion(w:-unityQuat.w, x:unityQuat.x, y:unityQuat.z, z:unityQuat.y);
|
||||
}
|
||||
|
||||
// Converts a Mujoco quaternion to a Unity quaternion.
|
||||
// The quaternions is read starting at 'entryIndex * 4' of the 'coords' array.
|
||||
public static Quaternion UnityQuaternion(double[] coords, int entryIndex) {
|
||||
var startOffset = entryIndex * _elementsPerRotation;
|
||||
return new Quaternion(
|
||||
x:(float)coords[startOffset + 1], y:(float)coords[startOffset + 3],
|
||||
z:(float)coords[startOffset + 2], w:(float)-coords[startOffset]);
|
||||
}
|
||||
|
||||
// Converts a Mujoco quaternion to a Unity quaternion.
|
||||
// The quaternion is read starting at 'entryIndex * 4' of the 'coords' array.
|
||||
public static unsafe Quaternion UnityQuaternion(double* coords, int entryIndex) {
|
||||
var startOffset = entryIndex * _elementsPerRotation;
|
||||
return new Quaternion(
|
||||
x:(float)coords[startOffset + 1], y:(float)coords[startOffset + 3],
|
||||
z:(float)coords[startOffset + 2], w:(float)-coords[startOffset]);
|
||||
}
|
||||
|
||||
// Converts a Mujoco quaternion to a Unity quaternion.
|
||||
public static Quaternion UnityQuaternion(Quaternion mjQuat) {
|
||||
// This is simply the inverse permutation of the one in MjQuaternion
|
||||
return new Quaternion(x:mjQuat.x, y:mjQuat.z, z:mjQuat.y, w:-mjQuat.w);
|
||||
}
|
||||
|
||||
// Converts a Mujoco matrix to a Unity quaternion.
|
||||
// The matrix coordinates are at the 'entryIndex * 9' of the 'coords' array.
|
||||
public static unsafe Quaternion UnityQuaternionFromMatrix(double* coords, int entryIndex) {
|
||||
var startOffset = entryIndex * _mjMat.Length;
|
||||
for (var j = 0; j < _mjMat.Length; ++j) {
|
||||
_mjMat[j] = coords[startOffset + j];
|
||||
}
|
||||
fixed (double* a = _mjQuat)
|
||||
fixed (double* b = _mjMat) {
|
||||
MujocoLib.mju_mat2Quat(a, b);
|
||||
}
|
||||
return UnityQuaternion(_mjQuat, 0);
|
||||
}
|
||||
|
||||
// Converts a Unity extents Vector3 to a Mujoco extents vector.
|
||||
// Use to convert size vectors that should contain only absolute values.
|
||||
public static Vector3 MjExtents(Vector3 unityExtents) {
|
||||
return new Vector3(unityExtents.x, unityExtents.z, unityExtents.y);
|
||||
}
|
||||
|
||||
// Converts a Mujoco extents vector to a Unity extents Vector3.
|
||||
// Use to convert size vectors that should contain only absolute values.
|
||||
public static Vector3 UnityExtents(Vector3 mjExtents) {
|
||||
return new Vector3(mjExtents.x, mjExtents.z, mjExtents.y);
|
||||
}
|
||||
|
||||
// Converts a vector containing Mujoco Euler angles to a Unity representation.
|
||||
public static Vector3 UnityEuler(Vector3 mjEuler) {
|
||||
return new Vector3(mjEuler.x, mjEuler.z, -mjEuler.y);
|
||||
}
|
||||
|
||||
public static string Vector3ToMjcf(Vector3 vec) {
|
||||
return $"{vec.x} {vec.y} {vec.z}";
|
||||
}
|
||||
|
||||
public static string QuaternionToMjcf(Quaternion quat) {
|
||||
return $"{quat.w} {quat.x} {quat.y} {quat.z}";
|
||||
}
|
||||
|
||||
// We can't use transform.localPosition because we allow arbitrary deep gameObject hierarchies
|
||||
// between MuJoCo parent and child entities. Therefore, the method takes into account extra
|
||||
// transforms that may be present in the chain separating the component from its parent body.
|
||||
public static MjTransformation LocalTransformInParentBody(MjComponent component) {
|
||||
var MjParent = MjHierarchyTool.FindParentComponent<MjBaseBody>(component);
|
||||
var parentGlobalPosition = Vector3.zero;
|
||||
var parentGlobalRotation = Quaternion.identity;
|
||||
if (MjParent != null) {
|
||||
parentGlobalPosition = MjParent.transform.position;
|
||||
parentGlobalRotation = MjParent.transform.rotation;
|
||||
}
|
||||
|
||||
var invParentGlobalRotation = Quaternion.Inverse(parentGlobalRotation);
|
||||
|
||||
var localPosition =
|
||||
invParentGlobalRotation * (component.transform.position - parentGlobalPosition);
|
||||
var localRotation = invParentGlobalRotation * component.transform.rotation;
|
||||
return new MjTransformation(localPosition, localRotation);
|
||||
}
|
||||
|
||||
// Identity quaternion in MujocoLib.
|
||||
public static readonly Quaternion MjQuaternionIdentity = new Quaternion(w:-1, x:0, y:0, z:0);
|
||||
|
||||
// Mujoco's Z-axis corresponds to Unity's up (Y) axis.
|
||||
public static readonly Vector3 MjVector3Up = new Vector3(0, 0, 1);
|
||||
|
||||
// Sorts the components of a Vector2. Doesn't modify the original vector.
|
||||
public static Vector2 GetSorted(Vector2 vec) {
|
||||
return new Vector2(Math.Min(vec.x, vec.y), Math.Max(vec.x, vec.y));
|
||||
}
|
||||
|
||||
// Converts an array of floats to an Mjcf.
|
||||
public static string ArrayToMjcf(float[] array) {
|
||||
return String.Join(" ", array);
|
||||
}
|
||||
|
||||
// Converts a list of floats to an Mjcf.
|
||||
public static string ListToMjcf(List<float> list) {
|
||||
return String.Join(" ", list);
|
||||
}
|
||||
|
||||
// Generates an Mjcf of the specified component's transform.
|
||||
//
|
||||
// It will use the component's position (Vector3) and rotation (Quaternion), relative to its
|
||||
// MjComponent parent, and save them in "pos" and "quat" attributes.
|
||||
//
|
||||
// The method takes into account extra transforms that may be present in the chain separating
|
||||
// the component from its MjComponent parent.
|
||||
public static void PositionRotationToMjcf(XmlElement mjcf, MjComponent component) {
|
||||
var localTransform = LocalTransformInParentBody(component);
|
||||
mjcf.SetAttribute(
|
||||
"pos",
|
||||
MjEngineTool.Vector3ToMjcf(MjEngineTool.MjVector3(localTransform.Translation)));
|
||||
mjcf.SetAttribute(
|
||||
"quat",
|
||||
MjEngineTool.QuaternionToMjcf(MjEngineTool.MjQuaternion(localTransform.Rotation)));
|
||||
}
|
||||
|
||||
// Generates an Mjcf of the specified component's transform.
|
||||
//
|
||||
// It will use the component's position (Vector3) and forward direction (Vector3), relative to
|
||||
// its MjComponent parent, and save them in "pos" and "axis" attributes. Designed to work
|
||||
// with Joints, it will set the "ref" attribute to 0, indicating that the joint is in its
|
||||
// reference pose frame.
|
||||
//
|
||||
// The method takes into account extra transforms that may be present in the chain separating
|
||||
// the component from its MjComponent parent.
|
||||
public static void PositionAxisToMjcf(XmlElement mjcf, MjComponent component) {
|
||||
var localTransform = LocalTransformInParentBody(component);
|
||||
var axis = (localTransform.Rotation * Vector3.right).normalized;
|
||||
|
||||
mjcf.SetAttribute(
|
||||
"pos",
|
||||
MjEngineTool.Vector3ToMjcf(MjEngineTool.MjVector3(localTransform.Translation)));
|
||||
mjcf.SetAttribute("axis", MjEngineTool.Vector3ToMjcf(MjEngineTool.MjVector3(axis)));
|
||||
mjcf.SetAttribute("ref", "0");
|
||||
}
|
||||
|
||||
public static void PositionToMjcf(XmlElement mjcf, MjComponent component) {
|
||||
var localTransform = LocalTransformInParentBody(component);
|
||||
mjcf.SetAttribute(
|
||||
"pos",
|
||||
MjEngineTool.Vector3ToMjcf(MjEngineTool.MjVector3(localTransform.Translation)));
|
||||
}
|
||||
|
||||
private const int _fromOffset = 0;
|
||||
private const int _toOffset = 1;
|
||||
private const float _fromToValidityTolerance = 1e-3f;
|
||||
|
||||
public static bool ParseFromToMjcf(XmlElement mjcf, out Vector3 fromPoint, out Vector3 toPoint) {
|
||||
var fromto = mjcf.GetFloatArrayAttribute("fromto", defaultValue: null);
|
||||
if (fromto != null) {
|
||||
fromPoint = MjEngineTool.UnityVector3(fromto, _fromOffset);
|
||||
toPoint = MjEngineTool.UnityVector3(fromto, _toOffset);
|
||||
var nodeName = mjcf.GetStringAttribute("name", mjcf.Name);
|
||||
if ((toPoint - fromPoint).magnitude < _fromToValidityTolerance) {
|
||||
throw new ArgumentException(
|
||||
$"{nodeName}: 'fromto' produces a vector that's too short. {fromto} has magnitude " +
|
||||
"{fromto.magnitude} lower than the tolerance {_fromToValidityTolerance}");
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
fromPoint = Vector3.zero;
|
||||
toPoint = Vector3.zero;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Parses the transform-shape specifications, shared by the Geom and Site Mjcfs.
|
||||
public static void ParseTransformMjcf(XmlElement mjcf, Transform transform) {
|
||||
Vector3 fromPoint, toPoint;
|
||||
if (ParseFromToMjcf(mjcf, out fromPoint, out toPoint)) {
|
||||
transform.localPosition = Vector3.Lerp(toPoint, fromPoint, 0.5f);
|
||||
transform.localRotation = Quaternion.FromToRotation(
|
||||
Vector3.up, (toPoint - fromPoint).normalized);
|
||||
} else {
|
||||
// Parse the regular transform.
|
||||
transform.localPosition = MjEngineTool.UnityVector3(
|
||||
mjcf.GetVector3Attribute("pos", defaultValue: Vector3.zero));
|
||||
transform.localRotation = Quaternion.identity;
|
||||
|
||||
if (mjcf.HasAttribute("quat")) {
|
||||
transform.localRotation = MjEngineTool.UnityQuaternion(
|
||||
mjcf.GetQuaternionAttribute("quat",
|
||||
defaultValue: MjEngineTool.MjQuaternionIdentity));
|
||||
} else if (mjcf.HasAttribute("zaxis")) {
|
||||
var upAxis = MjEngineTool.UnityVector3(
|
||||
mjcf.GetVector3Attribute("zaxis", defaultValue: MjEngineTool.MjVector3Up));
|
||||
transform.localRotation = Quaternion.FromToRotation(
|
||||
Vector3.up, upAxis);
|
||||
} else if (mjcf.HasAttribute("xyaxes")) {
|
||||
var xyaxes = mjcf.GetFloatArrayAttribute(
|
||||
"xyaxes", defaultValue: new float[] { 1, 0, 0, 0, 1, 0 });
|
||||
var xAxis = MjEngineTool.UnityVector3(xyaxes, 0);
|
||||
var yAxis = MjEngineTool.UnityVector3(xyaxes, 1);
|
||||
var zAxis = Vector3.Cross(xAxis, yAxis);
|
||||
transform.localRotation = Quaternion.LookRotation(zAxis, yAxis);
|
||||
} else if (mjcf.HasAttribute("axisangle")) {
|
||||
var axisAngle = mjcf.GetFloatArrayAttribute(
|
||||
"axisangle", defaultValue: new float[] { 0, 0, 1, 0 });
|
||||
var axis = MjEngineTool.UnityVector3(axisAngle, 0);
|
||||
var angle = axisAngle[3];
|
||||
if (MjSceneImportSettings.AnglesInDegrees == false) {
|
||||
angle *= Mathf.Rad2Deg;
|
||||
}
|
||||
transform.localRotation = Quaternion.AngleAxis(angle, axis);
|
||||
} else if (mjcf.HasAttribute("euler")) {
|
||||
var euler = MjEngineTool.UnityEuler(
|
||||
mjcf.GetVector3Attribute("euler", defaultValue: Vector3.zero));
|
||||
if (MjSceneImportSettings.AnglesInDegrees == false) {
|
||||
euler = euler * Mathf.Rad2Deg;
|
||||
}
|
||||
transform.localRotation = Quaternion.Euler(euler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MjSceneImportSettings {
|
||||
public static bool AnglesInDegrees = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 957bf8e1dc4af40c2a42dd1a33fe10d2
|
||||
timeCreated: 1536920023
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2019 DeepMind Technologies Limited
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mujoco {
|
||||
|
||||
// A set of helper methods for working with hierarchies of components.
|
||||
public static class MjHierarchyTool {
|
||||
|
||||
public static IEnumerable<MjComponent> LinearizeHierarchyBFS(Transform start) {
|
||||
var open = new Queue<Transform>();
|
||||
open.Enqueue(start);
|
||||
while (open.Count > 0) {
|
||||
var transform = open.Dequeue();
|
||||
|
||||
var components = transform.GetComponents<MjComponent>();
|
||||
foreach (var component in components) {
|
||||
if (component.isActiveAndEnabled) {
|
||||
yield return component;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Transform child in transform) {
|
||||
open.Enqueue(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recurses up the tree (excluding self) and returns the first instance of a MjComponent.
|
||||
public static MjComponent FindParentComponent(MonoBehaviour start) {
|
||||
return FindParentComponent<MjComponent>(start);
|
||||
}
|
||||
|
||||
// This is not equivalent to Unity's GetComponentInParent as it excludes self from the search.
|
||||
public static T FindParentComponent<T>(MonoBehaviour start) where T : MonoBehaviour {
|
||||
if (start.transform.parent == null) {
|
||||
return null;
|
||||
}
|
||||
var loopTransform = start.transform.parent;
|
||||
T result = loopTransform.GetComponent<T>();
|
||||
while (result == null && loopTransform.parent != null) {
|
||||
loopTransform = loopTransform.parent;
|
||||
result = loopTransform.GetComponent<T>();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<T> GetComponentsInImmediateChildren<T>(Transform parent)
|
||||
where T : MonoBehaviour {
|
||||
var components = new List<T>();
|
||||
foreach (Transform child in parent) {
|
||||
components.AddRange(child.gameObject.GetComponents<T>());
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
// Finds a single component that matches both the type and the name criteria.
|
||||
public static T FindComponentOfTypeAndName<T>(string name) where T : MjComponent {
|
||||
var components = UnityEngine.Object.FindObjectsOfType<T>().Where(
|
||||
component => component.gameObject.name == name);
|
||||
var numComponents = components.Count();
|
||||
if (components == null || numComponents == 0) {
|
||||
throw new ArgumentException($"No components named '{name}' were found.");
|
||||
}
|
||||
if (numComponents > 1) {
|
||||
throw new ArgumentException($"More than one component named '{name}' was found.");
|
||||
}
|
||||
return components.FirstOrDefault() as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e246736c791384d918a7781a04225eee
|
||||
timeCreated: 1537463659
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2019 DeepMind Technologies Limited
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mujoco {
|
||||
|
||||
// Utility class that represents a translation and rotation.
|
||||
// Used for converting between MuJoCo and Unity spaces.
|
||||
public struct MjTransformation {
|
||||
public Vector3 Translation;
|
||||
public Quaternion Rotation;
|
||||
|
||||
public MjTransformation(Vector3 translation, Quaternion rotation) {
|
||||
Translation = translation;
|
||||
Rotation = rotation;
|
||||
}
|
||||
|
||||
public void Set(Vector3 translation, Quaternion rotation) {
|
||||
Translation = translation;
|
||||
Rotation = rotation;
|
||||
}
|
||||
|
||||
// Initializes MjTransformation from the specified Transform's local coordinates.
|
||||
public static MjTransformation LoadGlobal(Transform transform) {
|
||||
if (transform != null) {
|
||||
return new MjTransformation(transform.position, transform.rotation);
|
||||
} else {
|
||||
return new MjTransformation(Vector3.zero, Quaternion.identity);
|
||||
}
|
||||
}
|
||||
|
||||
// Initializes MjTransformation from the specified Transform's global coordinates.
|
||||
public static MjTransformation LoadLocal(Transform transform) {
|
||||
if (transform != null) {
|
||||
return new MjTransformation(transform.localPosition, transform.localRotation);
|
||||
} else {
|
||||
return new MjTransformation(Vector3.zero, Quaternion.identity);
|
||||
}
|
||||
}
|
||||
|
||||
// Stores the transform coordinates in the specified Transform's global coordinates.
|
||||
public void StoreGlobal(Transform transform) {
|
||||
transform.position = Translation;
|
||||
transform.rotation = Rotation;
|
||||
}
|
||||
|
||||
// Stores the transform coordinates in the specified Transform's local coordinates.
|
||||
public void StoreLocal(Transform transform) {
|
||||
transform.localPosition = Translation;
|
||||
transform.localRotation = Rotation;
|
||||
}
|
||||
|
||||
// Concatenates two transforms.
|
||||
// Equivalent of a matrix multiplication. This operation is not transitive.
|
||||
public static MjTransformation operator *(
|
||||
MjTransformation lhs, MjTransformation rhs) {
|
||||
return new MjTransformation(
|
||||
lhs.Translation + lhs.Rotation * rhs.Translation, lhs.Rotation * rhs.Rotation);
|
||||
}
|
||||
|
||||
// Returns an inverse of this transform.
|
||||
public MjTransformation Inverse() {
|
||||
var inverseRotation = Quaternion.Inverse(Rotation);
|
||||
return new MjTransformation(inverseRotation * Translation * -1.0f, inverseRotation);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return $"[{Translation}, {Rotation}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 69065be4c455640e8ab0a9d346646be5
|
||||
timeCreated: 1551107971
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright 2019 DeepMind Technologies Limited
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mujoco {
|
||||
|
||||
using static MujocoLib;
|
||||
|
||||
// A wrapper around Mujoco's native _mjVFS that simplifies the process of creating memory files.
|
||||
public sealed class MjVfs : IDisposable {
|
||||
|
||||
private _mjVFS _managedVfs;
|
||||
private IntPtr _unmanagedVfs;
|
||||
|
||||
// Managed version of the underlying _mjVFS structure.
|
||||
public _mjVFS Data {
|
||||
get {
|
||||
_managedVfs = (_mjVFS)Marshal.PtrToStructure(_unmanagedVfs, typeof(_mjVFS));
|
||||
return _managedVfs;
|
||||
}
|
||||
set {
|
||||
_managedVfs = value;
|
||||
Marshal.StructureToPtr(_managedVfs, _unmanagedVfs, fDeleteOld: false);
|
||||
}
|
||||
}
|
||||
|
||||
// Number of files added to the filesystem.
|
||||
public int FilesCount {
|
||||
get { return Data.nfile; }
|
||||
}
|
||||
|
||||
// Adds a new file to the virtual filesystem.
|
||||
public unsafe void AddFile(string filename, string contents) {
|
||||
var result = mj_makeEmptyFileVFS(_unmanagedVfs.ToPointer(), filename, contents.Length);
|
||||
if (result != 0) {
|
||||
throw new Exception(
|
||||
"VFS error (" + result + ") encountered while creating an empty file");
|
||||
}
|
||||
var fileIndex = mj_findFileVFS(_unmanagedVfs.ToPointer(), filename);
|
||||
if (fileIndex < 0) {
|
||||
throw new IndexOutOfRangeException("VFS didn't properly create the empty file.");
|
||||
}
|
||||
SetFileContents(fileIndex, contents);
|
||||
}
|
||||
|
||||
// Searches the VFS for the specified file and returns its index.
|
||||
// The index then can be used to retrieve the file contents from Data.filedata array.
|
||||
public unsafe int FindFile(string filename) {
|
||||
return mj_findFileVFS(_unmanagedVfs.ToPointer(), filename);
|
||||
}
|
||||
|
||||
// Loads a model from the specified file.
|
||||
// The file is assumed to be located in the filesystem. If it's not found, the method will throw
|
||||
// an ArgumentException.
|
||||
public unsafe MujocoLib.mjModel_* LoadXML(string filename) {
|
||||
if (FindFile(filename) < 0) {
|
||||
throw new ArgumentException($"File {filename} was not added to the VFS.");
|
||||
}
|
||||
|
||||
var errorBuf = new StringBuilder(1024);
|
||||
MujocoLib.mjModel_* model = MujocoLib.mj_loadXML(
|
||||
filename, _unmanagedVfs.ToPointer(), errorBuf, errorBuf.Capacity);
|
||||
if (model == null || errorBuf.Length > 0) {
|
||||
throw new IOException($"Error loading the model: {errorBuf}");
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
private unsafe void SetFileContents(int fileIndex, string contents) {
|
||||
var data = Data;
|
||||
Marshal.FreeHGlobal(data.filedata[fileIndex]);
|
||||
data.filedata[fileIndex] = Marshal.StringToHGlobalAnsi(contents);
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public MjVfs() {
|
||||
_managedVfs = new _mjVFS();
|
||||
_unmanagedVfs = Marshal.AllocHGlobal(Marshal.SizeOf(_managedVfs));
|
||||
Marshal.StructureToPtr(_managedVfs, _unmanagedVfs, true);
|
||||
}
|
||||
|
||||
~MjVfs() {
|
||||
ReleaseUnmanagedMemory();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
GC.SuppressFinalize(this);
|
||||
ReleaseUnmanagedMemory();
|
||||
}
|
||||
|
||||
private void ReleaseUnmanagedMemory() {
|
||||
if (_unmanagedVfs != IntPtr.Zero) {
|
||||
var data = Data;
|
||||
for (var fileIndex = 0; fileIndex < data.nfile; ++fileIndex) {
|
||||
Marshal.FreeHGlobal(data.filedata[fileIndex]);
|
||||
}
|
||||
|
||||
Marshal.FreeHGlobal(_unmanagedVfs);
|
||||
_unmanagedVfs = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3cf049acadd3a4c8eb75f2a5a72ba589
|
||||
timeCreated: 1535566910
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2019 DeepMind Technologies Limited
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mujoco {
|
||||
|
||||
// An instance of this class will be used to collect dynamic dependencies produced by a Mujoco model
|
||||
// during scene generation.
|
||||
public class MjcfGenerationContext {
|
||||
|
||||
public int NUserSensor {
|
||||
get { return _nuserSensor; }
|
||||
set { _nuserSensor = Math.Max(_nuserSensor, value); }
|
||||
}
|
||||
|
||||
private int _nuserSensor;
|
||||
private int _numGeneratedNames = 0;
|
||||
private Dictionary<Mesh, string> _meshAssets = new Dictionary<Mesh, string>();
|
||||
|
||||
public void GenerateMjcf(XmlElement mjcf) {
|
||||
GenerateConfigurationMjcf(mjcf);
|
||||
GenerateAssetsMjcf(mjcf);
|
||||
}
|
||||
|
||||
// Adds a mesh to the list of assets that will be added to the generated MJCF.
|
||||
// Returns the unique name that identifies this new asset.
|
||||
public string AddMeshAsset(Mesh mesh) {
|
||||
if (!_meshAssets.ContainsKey(mesh)) {
|
||||
var uniqueName = $"mesh_{_numGeneratedNames}";
|
||||
_numGeneratedNames++;
|
||||
_meshAssets.Add(mesh, uniqueName);
|
||||
}
|
||||
return _meshAssets[mesh];
|
||||
}
|
||||
|
||||
// Generates a Unique name for the specified component.
|
||||
// Uniqueness of names is required for every type independently.
|
||||
// The per-type uniqueness will be guaranteed by the combination of information used:
|
||||
// - name of the host GameObject.
|
||||
// - number of names that have been generated to date.
|
||||
public string GenerateName(Component comp) {
|
||||
var name = $"{comp.gameObject.name}_{_numGeneratedNames}";
|
||||
_numGeneratedNames++;
|
||||
return name;
|
||||
}
|
||||
|
||||
private void GenerateConfigurationMjcf(XmlElement mjcf) {
|
||||
var doc = mjcf.OwnerDocument;
|
||||
var compilerMjcf = (XmlElement)mjcf.AppendChild(doc.CreateElement("compiler"));
|
||||
compilerMjcf.SetAttribute("coordinate", "local");
|
||||
|
||||
var optionMjcf = (XmlElement)mjcf.AppendChild(doc.CreateElement("option"));
|
||||
optionMjcf.SetAttribute(
|
||||
"gravity", MjEngineTool.Vector3ToMjcf(MjEngineTool.MjVector3(Physics.gravity)));
|
||||
optionMjcf.SetAttribute("timestep", $"{Time.fixedDeltaTime}");
|
||||
|
||||
var sizeMjcf = (XmlElement)mjcf.AppendChild(doc.CreateElement("size"));
|
||||
sizeMjcf.SetAttribute("nuser_sensor", $"{_nuserSensor}");
|
||||
|
||||
var settings = MjGlobalSettings.Instance;
|
||||
if (settings) {
|
||||
settings.OptionSizeToMjcf(optionMjcf, sizeMjcf);
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateAssetsMjcf(XmlElement mjcf) {
|
||||
var doc = mjcf.OwnerDocument;
|
||||
var assetMjcf = (XmlElement)mjcf.AppendChild(doc.CreateElement("asset"));
|
||||
foreach (var asset in _meshAssets) {
|
||||
var meshMjcf = (XmlElement)assetMjcf.AppendChild(doc.CreateElement("mesh"));
|
||||
meshMjcf.SetAttribute("name", asset.Value);
|
||||
GenerateMeshMjcf(asset.Key, meshMjcf);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateMeshMjcf(Mesh mesh, XmlElement mjcf) {
|
||||
var vertexPositionsStr = new StringBuilder();
|
||||
foreach (var unityVertex in mesh.vertices) {
|
||||
var mjVertex = MjEngineTool.MjVector3(unityVertex);
|
||||
vertexPositionsStr.Append(MjEngineTool.Vector3ToMjcf(mjVertex));
|
||||
vertexPositionsStr.Append(" ");
|
||||
}
|
||||
mjcf.SetAttribute("vertex", vertexPositionsStr.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6b8eb0cef9a04e2d92bb573828f1be8
|
||||
timeCreated: 1550078178
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright 2019 DeepMind Technologies Limited
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Mujoco {
|
||||
// This class helps us manipulate MuJoCo-bound XML files. It is only intended to be used
|
||||
// within the context of MuJoCo - elsewhere the preferred format is JSON.
|
||||
public static class XmlElementExtensions {
|
||||
|
||||
public static bool GetBoolAttribute(
|
||||
this XmlElement element, string name, bool defaultValue = false) {
|
||||
if (!element.HasAttribute(name)) {
|
||||
return defaultValue;
|
||||
}
|
||||
var strValue = element.GetAttribute(name);
|
||||
bool parsedValue;
|
||||
if (bool.TryParse(strValue, out parsedValue)) {
|
||||
return parsedValue;
|
||||
} else {
|
||||
throw new ArgumentException($"'{strValue}' is not a bool.");
|
||||
}
|
||||
}
|
||||
|
||||
public static float GetFloatAttribute(
|
||||
this XmlElement element, string name, float defaultValue = 0.0f) {
|
||||
if (!element.HasAttribute(name)) {
|
||||
return defaultValue;
|
||||
}
|
||||
var strValue = element.GetAttribute(name);
|
||||
float parsedValue;
|
||||
if (float.TryParse(strValue, out parsedValue)) {
|
||||
return parsedValue;
|
||||
} else {
|
||||
throw new ArgumentException($"'{strValue}' is not a float.");
|
||||
}
|
||||
}
|
||||
|
||||
// The MuJoCo parser is case-sensitive.
|
||||
public static T GetEnumAttribute<T>(
|
||||
this XmlElement element, string name, T defaultValue,
|
||||
bool ignoreCase = false) where T : struct, IConvertible {
|
||||
if (!typeof(T).IsEnum) {
|
||||
throw new ArgumentException("T must be an enumerated type.");
|
||||
}
|
||||
if (!element.HasAttribute(name)) {
|
||||
return defaultValue;
|
||||
}
|
||||
var strValue = element.GetAttribute(name);
|
||||
T parsedValue;
|
||||
if (Enum.TryParse<T>(strValue, ignoreCase, out parsedValue)) {
|
||||
return parsedValue;
|
||||
} else {
|
||||
throw new ArgumentException(
|
||||
$"Reading attribute {name}: '{strValue}' is not a value of enum {typeof(T)}.");
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetStringAttribute(
|
||||
this XmlElement element, string name, string defaultValue = null) {
|
||||
if (!element.HasAttribute(name)) {
|
||||
return defaultValue;
|
||||
}
|
||||
return element.GetAttribute(name);
|
||||
}
|
||||
|
||||
public static T GetObjectReferenceAttribute<T>(
|
||||
this XmlElement mjcf, string attributeName) where T : MjComponent {
|
||||
var objectName = mjcf.GetStringAttribute(attributeName);
|
||||
if (string.IsNullOrEmpty(objectName)) {
|
||||
return null;
|
||||
}
|
||||
T foundObject = MjHierarchyTool.FindComponentOfTypeAndName<T>(objectName);
|
||||
if (foundObject == null) {
|
||||
throw new ArgumentException(
|
||||
$"Object {objectName} of type {typeof(T).ToString()} referred by {attributeName} " +
|
||||
"doesn't exist.");
|
||||
}
|
||||
return foundObject;
|
||||
}
|
||||
|
||||
public static Vector2 GetVector2Attribute(
|
||||
this XmlElement element, string name, Vector2 defaultValue) {
|
||||
var defaultValues = new float[] {defaultValue.x, defaultValue.y};
|
||||
var components = element.GetFloatArrayAttribute(name, defaultValues, fillMissingValues: false);
|
||||
if (components.Length != 2) {
|
||||
throw new ArgumentException("Invalid Vector2 string representation.");
|
||||
}
|
||||
return new Vector2(components[0], components[1]);
|
||||
}
|
||||
|
||||
public static Vector3 GetVector3Attribute(
|
||||
this XmlElement element, string name, Vector3 defaultValue) {
|
||||
var defaultValues = new float[] {
|
||||
defaultValue.x, defaultValue.y, defaultValue.z};
|
||||
var components = element.GetFloatArrayAttribute(name, defaultValues, fillMissingValues: false);
|
||||
if (components.Length != 3) {
|
||||
throw new ArgumentException("Invalid Vector3 string representation.");
|
||||
}
|
||||
return new Vector3(components[0], components[1], components[2]);
|
||||
}
|
||||
|
||||
public static Quaternion GetQuaternionAttribute(
|
||||
this XmlElement element, string name, Quaternion defaultValue) {
|
||||
var mjDefault = new float[] {
|
||||
defaultValue.w, defaultValue.x, defaultValue.y, defaultValue.z };
|
||||
var components = element.GetFloatArrayAttribute(name, mjDefault, fillMissingValues: false);
|
||||
if (components.Length != 4) {
|
||||
throw new ArgumentException("Invalid Quaternion string representation.");
|
||||
}
|
||||
return new Quaternion(w:components[0], x:components[1], y:components[2], z:components[3]);
|
||||
}
|
||||
|
||||
// Parses an array of whitespace separated floating points.
|
||||
//
|
||||
// Args:
|
||||
// . element: XmlElement that contains the attribute to be parsed.
|
||||
// . name: Name of the attribute to be parsed.
|
||||
// . defaultValue: An array of floats, or null. A default value to be returned in case the
|
||||
// the attribute is missing.
|
||||
// . fillMissingValues: If a default value was provided, and it has more components than the value
|
||||
// parsed from the attribute, the missing components will be copied from the defaultValue.
|
||||
public static float[] GetFloatArrayAttribute(
|
||||
this XmlElement element, string name, float[] defaultValue, bool fillMissingValues = true) {
|
||||
if (!element.HasAttribute(name)) {
|
||||
return defaultValue;
|
||||
}
|
||||
var strValue = element.GetAttribute(name);
|
||||
var components = strValue.Split(new char[] {' '}, StringSplitOptions.RemoveEmptyEntries);
|
||||
var resultLength = components.Length;
|
||||
if (fillMissingValues && defaultValue != null) {
|
||||
// If filling of the missing values was enabled, and a default value was provided,
|
||||
// allocate an array large enough to store a value of this length, in case when the parsed
|
||||
// value has fewer components.
|
||||
resultLength = Math.Max(resultLength, defaultValue.Length);
|
||||
}
|
||||
var result = new float[resultLength];
|
||||
for (var i = 0; i < components.Length; ++i) {
|
||||
float componentValue;
|
||||
if (float.TryParse(components[i], out componentValue)) {
|
||||
result[i] = componentValue;
|
||||
} else {
|
||||
throw new ArgumentException($"'{components[i]}' is not a float.");
|
||||
}
|
||||
}
|
||||
for (var i = components.Length; i < resultLength; ++i) {
|
||||
// Fill the missing values with defaults.
|
||||
result[i] = defaultValue[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 783a359f292c1477caf7e242023ed32c
|
||||
timeCreated: 1538658945
|
||||
licenseType: Pro
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user