コード例 #1
0
        // -----------------------------------------
        // DumpAnims
        // -----------------------------------------

        void DumpAnims(GameObject _animPrefab, GLTF _gltf, BufferInfo _bufInfo)
        {
            // get animations
            GameObject           prefabInst = PrefabUtility.InstantiatePrefab(_animPrefab) as GameObject;
            List <AnimationClip> clips      = Utils.GetAnimationClips(prefabInst);

            // get joints
            List <GameObject> joints = new List <GameObject>();

            Utils.RecurseNode(prefabInst, _go => {
                // this is not a joint
                if (_go.GetComponent <SkinnedMeshRenderer>() != null)
                {
                    return(false);
                }

                joints.Add(_go);
                return(true);
            });

            //
            if (clips != null)
            {
                // process AnimationClip(s)
                foreach (AnimationClip clip in clips)
                {
                    int accOffset = _bufInfo.GetAccessorCount();

                    AnimData animData = DumpAnimData(prefabInst, clip);
                    DumpBufferInfoFromAnimData(animData, _bufInfo);

                    GLTF_AnimationEx gltfAnim = DumpGltfAnimationEx(animData, joints, accOffset);
                    _gltf.animations.Add(gltfAnim);
                }
            }

            Object.DestroyImmediate(prefabInst);
        }
コード例 #2
0
ファイル: Exporter.cs プロジェクト: JoneLau/mahjong-unity
        Dictionary <string, JSON_Asset> saveAssets(
            string dest,
            List <Object> prefabs,
            List <Object> modelPrefabs,
            List <Mesh> meshes,
            List <Texture> textures,
            List <Texture> spriteTextures,
            List <Material> materials,
            List <Font> fonts
            )
        {
            Dictionary <string, JSON_Asset> assetsJson = new Dictionary <string, JSON_Asset>();

            // DELME {
            // // save meshes
            // var destMeshes = Path.Combine(dest, "meshes");
            // foreach (Mesh mesh in meshes) {
            //   string id = Utils.AssetID(mesh);
            //   GLTF gltf = new GLTF();
            //   gltf.asset = new GLTF_Asset
            //   {
            //     version = "1.0.0",
            //     generator = "u3d-exporter"
            //   };
            //   BufferInfo bufInfo = new BufferInfo
            //   {
            //     id = id,
            //     name = mesh.name
            //   };

            //   DumpMesh(mesh, gltf, bufInfo, 0);
            //   DumpBuffer(bufInfo, gltf);

            //   Save(
            //     destMeshes,
            //     id,
            //     gltf,
            //     new List<BufferInfo> { bufInfo }
            //   );

            //   // add asset to table
            //   assetsJson.Add(id, new JSON_Asset {
            //     type = "mesh",
            //     urls = new Dictionary<string, string> {
            //       { "gltf", "meshes/" + id + ".gltf" },
            //       { "bin", "meshes/" + id + ".bin" }
            //     }
            //   });
            // }
            // } DELME

            // ========================================
            // save animations
            // ========================================

            var destAnims = Path.Combine(dest, "anims");

            foreach (GameObject prefab in prefabs)
            {
                // skip ModelPrefab
                if (PrefabUtility.GetPrefabType(prefab) == PrefabType.ModelPrefab)
                {
                    Debug.LogWarning("Can not export model prefab " + prefab.name + " in the scene");
                    continue;
                }

                // skip non-animation prefab
                bool isAnimPrefab = Utils.IsAnimPrefab(prefab);
                if (isAnimPrefab == false)
                {
                    continue;
                }

                // get animations
                GameObject           prefabInst = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
                List <AnimationClip> clips      = Utils.GetAnimationClips(prefabInst);

                // get joints
                List <GameObject> joints = new List <GameObject>();
                Utils.RecurseNode(prefabInst, _go => {
                    // this is not a joint
                    if (_go.GetComponent <SkinnedMeshRenderer>() != null)
                    {
                        return(false);
                    }

                    joints.Add(_go);
                    return(true);
                });

                // dump animation clips
                if (clips != null)
                {
                    // process AnimationClip(s)
                    foreach (AnimationClip clip in clips)
                    {
                        string id   = Utils.AssetID(clip);
                        GLTF   gltf = new GLTF();
                        gltf.asset = new GLTF_Asset {
                            version   = "1.0.0",
                            generator = "u3d-exporter"
                        };
                        BufferInfo bufInfo = new BufferInfo {
                            id   = id,
                            name = prefab.name
                        };

                        AnimData animData = DumpAnimData(prefabInst, clip);
                        DumpBufferInfoFromAnimData(animData, bufInfo);

                        GLTF_AnimationEx gltfAnim = DumpGltfAnimationEx(animData, joints, 0);
                        gltf.animations.Add(gltfAnim);

                        DumpBuffer(bufInfo, gltf);

                        Save(
                            destAnims,
                            id + ".anim",
                            gltf,
                            new List <BufferInfo> {
                            bufInfo
                        }
                            );

                        // add asset to table
                        try {
                            if (!assetsJson.ContainsKey(id))
                            {
                                assetsJson.Add(id, new JSON_Asset {
                                    type = "animation",
                                    urls = new Dictionary <string, string> {
                                        { "anim", "anims/" + id + ".anim" },
                                        { "bin", "anims/" + id + ".bin" }
                                    }
                                });
                            }
                        } catch (System.SystemException e) {
                            Debug.LogError("Failed to add " + id + " to assets: " + e);
                        }
                    }
                }

                Object.DestroyImmediate(prefabInst);
            }

            // ========================================
            // save prefabs
            // ========================================

            var destMeshes  = Path.Combine(dest, "meshes");
            var destPrefabs = Path.Combine(dest, "prefabs");

            // create dest directory
            if (!Directory.Exists(destMeshes))
            {
                Directory.CreateDirectory(destMeshes);
            }
            if (!Directory.Exists(destPrefabs))
            {
                Directory.CreateDirectory(destPrefabs);
            }

            foreach (GameObject prefab in prefabs)
            {
                string id = Utils.AssetID(prefab);

                // save prefabs
                if (PrefabUtility.GetPrefabType(prefab) == PrefabType.ModelPrefab)
                {
                    Debug.LogWarning("Can not export model prefab " + prefab.name + " in the scene");
                    continue;
                }
                var    prefabJson = DumpPrefab(prefab);
                string path;
                string json = JsonConvert.SerializeObject(prefabJson, Formatting.Indented);

                path = Path.Combine(destPrefabs, id + ".json");
                StreamWriter writer = new StreamWriter(path);
                writer.Write(json);
                writer.Close();

                // Debug.Log(Path.GetFileName(path) + " saved.");

                // add asset to table
                if (!assetsJson.ContainsKey(id))
                {
                    assetsJson.Add(id, new JSON_Asset {
                        type = "prefab",
                        urls = new Dictionary <string, string> {
                            { "json", "prefabs/" + id + ".json" },
                        }
                    });
                }
            }

            // save model prefab (as gltf)
            foreach (GameObject modelPrefab in modelPrefabs)
            {
                string id = Utils.AssetID(modelPrefab);
                // save model prefabs
                GLTF gltf = new GLTF();
                gltf.asset = new GLTF_Asset {
                    version   = "1.0.0",
                    generator = "u3d-exporter"
                };
                BufferInfo bufInfo = new BufferInfo {
                    id   = id,
                    name = modelPrefab.name
                };

                bool isAnimPrefab = Utils.IsAnimPrefab(modelPrefab);
                if (isAnimPrefab)
                {
                    DumpSkinningModel(modelPrefab, gltf, bufInfo);
                    DumpBuffer(bufInfo, gltf);
                }
                else
                {
                    DumpModel(modelPrefab, gltf, bufInfo);
                    DumpBuffer(bufInfo, gltf);
                }

                Save(
                    destMeshes,
                    id + ".gltf",
                    gltf,
                    new List <BufferInfo> {
                    bufInfo
                }
                    );

                // add asset to table
                if (!assetsJson.ContainsKey(id))
                {
                    assetsJson.Add(id, new JSON_Asset {
                        type = "gltf",
                        urls = new Dictionary <string, string> {
                            { "gltf", "meshes/" + id + ".gltf" },
                            { "bin", "meshes/" + id + ".bin" }
                        }
                    });
                }
            }

            // save meshes (as gltf)
            foreach (Mesh mesh in meshes)
            {
                string id = Utils.AssetID(mesh);
                // save model prefabs
                GLTF gltf = new GLTF();
                gltf.asset = new GLTF_Asset {
                    version   = "1.0.0",
                    generator = "u3d-exporter"
                };
                BufferInfo bufInfo = new BufferInfo {
                    id   = id,
                    name = mesh.name
                };

                DumpMesh(mesh, gltf, bufInfo, 0);
                DumpBuffer(bufInfo, gltf);

                Save(
                    destMeshes,
                    id + ".mesh",
                    gltf,
                    new List <BufferInfo> {
                    bufInfo
                }
                    );

                // add asset to table
                if (!assetsJson.ContainsKey(id))
                {
                    assetsJson.Add(id, new JSON_Asset {
                        type = "mesh",
                        urls = new Dictionary <string, string> {
                            { "mesh", "meshes/" + id + ".mesh" },
                            { "bin", "meshes/" + id + ".bin" }
                        }
                    });
                }
            }

            // ========================================
            // save textures
            // ========================================

            var destTextures = Path.Combine(dest, "textures");

            // create dest directory
            if (!Directory.Exists(destTextures))
            {
                Directory.CreateDirectory(destTextures);
            }
            foreach (Texture tex in textures)
            {
                var    textureJson = DumpTexture(tex);
                string path;
                string json = JsonConvert.SerializeObject(textureJson, Formatting.Indented);
                string id   = Utils.AssetID(tex);

                // json
                path = Path.Combine(destTextures, id + ".json");
                StreamWriter writer = new StreamWriter(path);
                writer.Write(json);
                writer.Close();

                // image
                string assetPath = AssetDatabase.GetAssetPath(tex);
                path = Path.Combine(destTextures, id + Utils.AssetExt(tex));
                File.Copy(assetPath, path, true);

                // Debug.Log(Path.GetFileName(path) + " saved.");

                // add asset to table
                if (!assetsJson.ContainsKey(id))
                {
                    assetsJson.Add(id, new JSON_Asset {
                        type = "texture",
                        urls = new Dictionary <string, string> {
                            { "json", "textures/" + id + ".json" },
                            { "image", "textures/" + id + Utils.AssetExt(tex) },
                        }
                    });
                }
            }

            // ========================================
            // save sprite textures
            // ========================================

            var destSprites = Path.Combine(dest, "sprites");

            // create dest directory
            if (!Directory.Exists(destSprites))
            {
                Directory.CreateDirectory(destSprites);
            }
            foreach (Texture spriteTex in spriteTextures)
            {
                var    spriteTextureJson = DumpSpriteTexture(spriteTex);
                string path;
                string json = JsonConvert.SerializeObject(spriteTextureJson, Formatting.Indented);
                string id   = Utils.AssetID(spriteTex);

                // json
                path = Path.Combine(destSprites, id + ".json");
                StreamWriter writer = new StreamWriter(path);
                writer.Write(json);
                writer.Close();

                // image
                string assetPath = AssetDatabase.GetAssetPath(spriteTex);
                path = Path.Combine(destSprites, id + Utils.AssetExt(spriteTex));
                File.Copy(assetPath, path);

                // add asset to table
                if (!assetsJson.ContainsKey(id))
                {
                    assetsJson.Add(id, new JSON_Asset {
                        type = "texture",
                        urls = new Dictionary <string, string> {
                            { "json", "sprites/" + id + ".json" },
                            { "image", "sprites/" + id + Utils.AssetExt(spriteTex) },
                        }
                    });
                }
            }

            // ========================================
            // save fonts
            // ========================================

            var destFonts = Path.Combine(dest, "fonts");

            if (!Directory.Exists(destFonts))
            {
                Directory.CreateDirectory(destFonts);
            }

            foreach (Font font in fonts)
            {
                if (font.dynamic)
                {
                    if (font.fontNames.Length > 1) // system font
                    // do nothing
                    {
                    }
                    else // opentype font
                    {
                        JSON_OpenTypeFont fontJson = DumpOpenTypeFont(font);

                        // save font json
                        string path;
                        string json = JsonConvert.SerializeObject(fontJson, Formatting.Indented);
                        string id   = Utils.AssetID(font);

                        path = Path.Combine(destFonts, id + ".json");
                        StreamWriter writer = new StreamWriter(path);
                        writer.Write(json);
                        writer.Close();

                        // save font file (ttf)
                        Texture tex       = font.material.mainTexture;
                        string  assetPath = AssetDatabase.GetAssetPath(font);
                        path = Path.Combine(destFonts, id + Utils.AssetExt(font));
                        File.Copy(assetPath, path, true);

                        if (!assetsJson.ContainsKey(id))
                        {
                            assetsJson.Add(id, new JSON_Asset {
                                type = "otfont",
                                urls = new Dictionary <string, string> {
                                    { "json", "fonts/" + id + ".json" },
                                    { "bin", "fonts/" + id + Utils.AssetExt(font) },
                                }
                            });
                        }
                    }
                }
                else // bitmapFont
                {
                    JSON_BitmapFont fontJson = DumpBitmapFont(font);

                    string path;
                    string json = JsonConvert.SerializeObject(fontJson, Formatting.Indented);
                    string id   = Utils.AssetID(font);

                    path = Path.Combine(destFonts, id + ".json");
                    StreamWriter writer = new StreamWriter(path);
                    writer.Write(json);
                    writer.Close();

                    if (!assetsJson.ContainsKey(id))
                    {
                        assetsJson.Add(id, new JSON_Asset {
                            type = "bmfont",
                            urls = new Dictionary <string, string> {
                                { "json", "fonts/" + id + ".json" },
                            }
                        });
                    }
                }
            }

            // ========================================
            // save materials
            // ========================================

            var destMaterials = Path.Combine(dest, "materials");

            // create dest directory
            if (!Directory.Exists(destMaterials))
            {
                Directory.CreateDirectory(destMaterials);
            }
            foreach (Material mat in materials)
            {
                var materialJson = DumpMaterial(mat);
                if (materialJson == null)
                {
                    continue;
                }

                string path;
                string json = JsonConvert.SerializeObject(materialJson, Formatting.Indented);
                string id   = Utils.AssetID(mat);

                // json
                path = Path.Combine(destMaterials, id + ".json");
                StreamWriter writer = new StreamWriter(path);
                writer.Write(json);
                writer.Close();

                // Debug.Log(Path.GetFileName(path) + " saved.");

                // add asset to table
                if (!assetsJson.ContainsKey(id))
                {
                    assetsJson.Add(id, new JSON_Asset {
                        type = "material",
                        urls = new Dictionary <string, string> {
                            { "json", "materials/" + id + ".json" },
                        }
                    });
                }
            }

            // ========================================
            // save assets
            // ========================================

            {
                string path = Path.Combine(dest, "assets.json");
                string json = JsonConvert.SerializeObject(assetsJson, Formatting.Indented);

                StreamWriter writer = new StreamWriter(path);
                writer.Write(json);
                writer.Close();
            }

            return(assetsJson);
        }
コード例 #3
0
        // -----------------------------------------
        // DumpGltfAnimationEx
        // -----------------------------------------

        GLTF_AnimationEx DumpGltfAnimationEx(AnimData _animData, List <GameObject> _joints, int _accOffset)
        {
            GLTF_AnimationEx result = new GLTF_AnimationEx();

            result.name = _animData.name;

            List <GLTF_AnimChannelEx> channels = new List <GLTF_AnimChannelEx>();

            int offset = 2; // acc offset start from 2 (0 is time0, 1 is times)

            foreach (var entry in _animData.nameToFrames)
            {
                NodeFrames frames = entry.Value;

                // T
                if (frames.tlist.Count > 0)
                {
                    // NOTE: index 0 = "time0", index 1 = "time1"
                    GLTF_AnimChannelEx channel = new GLTF_AnimChannelEx {
                        input  = (frames.tlist.Count == 1 ? 0 : 1) + _accOffset,
                        output = offset + _accOffset,
                        node   = _joints.IndexOf(frames.node),
                        path   = "translation",
                    };
                    channels.Add(channel);
                    offset += 1;
                }

                // S
                if (frames.slist.Count > 0)
                {
                    // NOTE: index 0 = "time0", index 1 = "time1"
                    GLTF_AnimChannelEx channel = new GLTF_AnimChannelEx {
                        input  = (frames.slist.Count == 1 ? 0 : 1) + _accOffset,
                        output = offset + _accOffset,
                        node   = _joints.IndexOf(frames.node),
                        path   = "scale",
                    };
                    channels.Add(channel);
                    offset += 1;
                }

                // R
                if (frames.rlist.Count > 0)
                {
                    // NOTE: index 0 = "time0", index 1 = "time1"
                    GLTF_AnimChannelEx channel = new GLTF_AnimChannelEx {
                        input  = (frames.rlist.Count == 1 ? 0 : 1) + _accOffset,
                        output = offset + _accOffset,
                        node   = _joints.IndexOf(frames.node),
                        path   = "rotation",
                    };
                    channels.Add(channel);
                    offset += 1;
                }
            }

            result.channels = channels;

            return(result);
        }
コード例 #4
0
        // -----------------------------------------
        // DumpBufferInfoFromAnimData
        // -----------------------------------------

        void DumpBufferInfoFromAnimData(AnimData _animData, BufferInfo _bufInfo)
        {
            List <AccessorInfo> accessors = new List <AccessorInfo>();

            // calculate total length of animation-data
            int          length = 0;
            AccessorInfo acc;

            // time0
            acc = new AccessorInfo
            {
                name     = "time0@" + _animData.name,
                offset   = length,
                count    = 1,
                compType = ComponentType.FLOAT32,
                attrType = AttrType.SCALAR
            };

            accessors.Add(acc);
            length += 4;

            // times
            acc = new AccessorInfo
            {
                name     = "times@" + _animData.name,
                offset   = length,
                count    = _animData.times.Count,
                compType = ComponentType.FLOAT32,
                attrType = AttrType.SCALAR
            };

            accessors.Add(acc);
            length += _animData.times.Count * 4;

            // frames
            foreach (var entry in _animData.nameToFrames)
            {
                NodeFrames frames = entry.Value;

                if (frames.tlist.Count > 0)
                {
                    acc = new AccessorInfo
                    {
                        name     = frames.node.name + "_T@" + _animData.name,
                        offset   = length,
                        count    = frames.tlist.Count,
                        compType = ComponentType.FLOAT32,
                        attrType = AttrType.VEC3
                    };

                    accessors.Add(acc);
                    length += frames.tlist.Count * 12;
                }

                if (frames.slist.Count > 0)
                {
                    acc = new AccessorInfo
                    {
                        name     = frames.node.name + "_S@" + _animData.name,
                        offset   = length,
                        count    = frames.slist.Count,
                        compType = ComponentType.FLOAT32,
                        attrType = AttrType.VEC3
                    };

                    accessors.Add(acc);
                    length += frames.slist.Count * 12;
                }

                if (frames.rlist.Count > 0)
                {
                    acc = new AccessorInfo
                    {
                        name     = frames.node.name + "_R@" + _animData.name,
                        offset   = length,
                        count    = frames.rlist.Count,
                        compType = ComponentType.FLOAT32,
                        attrType = AttrType.VEC4
                    };

                    accessors.Add(acc);
                    length += frames.rlist.Count * 16;
                }
            }

            // write data
            byte[] clipData;
            using (MemoryStream stream = new MemoryStream(length)) {
                using (BinaryWriter writer = new BinaryWriter(stream)) {
                    // time0
                    writer.Write(0.0f);

                    // times
                    for (int i = 0; i < _animData.times.Count; ++i)
                    {
                        writer.Write(_animData.times[i]);
                    }

                    foreach (var entry in _animData.nameToFrames)
                    {
                        NodeFrames frames = entry.Value;

                        if (frames.tlist.Count > 0)
                        {
                            for (int i = 0; i < frames.tlist.Count; ++i)
                            {
                                // NOTE: convert LH to RH
                                writer.Write(frames.tlist[i].x);
                                writer.Write(frames.tlist[i].y);
                                writer.Write(-frames.tlist[i].z);
                            }
                        }

                        if (frames.slist.Count > 0)
                        {
                            for (int i = 0; i < frames.slist.Count; ++i)
                            {
                                writer.Write(frames.slist[i].x);
                                writer.Write(frames.slist[i].y);
                                writer.Write(frames.slist[i].z);
                            }
                        }

                        if (frames.rlist.Count > 0)
                        {
                            for (int i = 0; i < frames.rlist.Count; ++i)
                            {
                                // NOTE: convert LH to RH
                                writer.Write(-frames.rlist[i].x);
                                writer.Write(-frames.rlist[i].y);
                                writer.Write(frames.rlist[i].z);
                                writer.Write(frames.rlist[i].w);
                            }
                        }
                    }
                }
                clipData = stream.ToArray();
            }

            // buffer view
            BufferViewInfo bufView = new BufferViewInfo
            {
                name      = _animData.name,
                offset    = _bufInfo.data.Length,
                length    = clipData.Length,
                type      = BufferType.NONE,
                accessors = accessors
            };

            //
            byte[] data   = new byte[_bufInfo.data.Length + clipData.Length];
            int    offset = 0;

            System.Buffer.BlockCopy(_bufInfo.data, 0, data, offset, _bufInfo.data.Length);
            offset += _bufInfo.data.Length;

            System.Buffer.BlockCopy(clipData, 0, data, offset, clipData.Length);
            offset += clipData.Length;

            //
            _bufInfo.data = data;
            _bufInfo.bufferViews.Add(bufView);
        }
コード例 #5
0
        // -----------------------------------------
        // DumpAnimData
        // -----------------------------------------

        AnimData DumpAnimData(GameObject _prefabInst, AnimationClip _clip)
        {
            AnimData animData = new AnimData();

            // name
            animData.name = _clip.name;

            // get frames
            float step = 1.0f / _clip.frameRate;

            for (float t = 0.0f; t < _clip.length; t += step)
            {
                animData.times.Add(t);
            }
            animData.times.Add(_clip.length);

            // sample frames
            for (int i = 0; i < animData.times.Count; ++i)
            {
                float t = animData.times[i];
                _clip.SampleAnimation(_prefabInst, t);

                Utils.RecurseNode(_prefabInst, _go => {
                    // this is not a joint
                    if (_go.GetComponent <SkinnedMeshRenderer>() != null)
                    {
                        return(false);
                    }

                    // dump translation, scale and rotation in current frame
                    string name = _go.name;
                    NodeFrames frames;

                    if (animData.nameToFrames.TryGetValue(name, out frames) == false)
                    {
                        frames      = new NodeFrames();
                        frames.node = _go;

                        animData.nameToFrames.Add(name, frames);
                    }

                    frames.tlist.Add(_go.transform.localPosition);
                    frames.slist.Add(_go.transform.localScale);
                    frames.rlist.Add(_go.transform.localRotation);

                    return(true);
                });
            }

            // strip empty frames (keep 1 frame for it)
            foreach (var entry in animData.nameToFrames)
            {
                NodeFrames frames = entry.Value;

                // T
                bool hasFrameT = false;
                for (int i = 0; i < frames.tlist.Count; ++i)
                {
                    if (frames.tlist[i] != frames.tlist[0])
                    {
                        hasFrameT = true;
                        break;
                    }
                }
                if (hasFrameT == false)
                {
                    if (frames.tlist[0] == Vector3.zero)
                    {
                        frames.tlist.Clear();
                    }
                    else
                    {
                        frames.tlist.RemoveRange(1, frames.tlist.Count - 1);
                        hasFrameT = true;
                    }
                }

                // S
                bool hasFrameS = false;
                for (int i = 0; i < frames.slist.Count; ++i)
                {
                    if (frames.slist[i] != frames.slist[0])
                    {
                        hasFrameS = true;
                        break;
                    }
                }
                if (hasFrameS == false)
                {
                    if (frames.slist[0] == Vector3.one)
                    {
                        frames.slist.Clear();
                    }
                    else
                    {
                        frames.slist.RemoveRange(1, frames.slist.Count - 1);
                        hasFrameS = true;
                    }
                }

                // R
                bool hasFrameR = false;
                for (int i = 0; i < frames.rlist.Count; ++i)
                {
                    if (frames.rlist[i] != frames.rlist[0])
                    {
                        hasFrameR = true;
                        break;
                    }
                }
                if (hasFrameR == false)
                {
                    if (frames.rlist[0] == Quaternion.identity)
                    {
                        frames.rlist.Clear();
                    }
                    else
                    {
                        frames.rlist.RemoveRange(1, frames.rlist.Count - 1);
                        hasFrameR = true;
                    }
                }

                //
                if (!hasFrameT && !hasFrameS && !hasFrameR)
                {
                    animData.nameToFrames.Remove(name);
                }
            }

            return(animData);
        }