// Urho3D material editor Window@ materialWindow; Material@ editMaterial; XMLFile@ oldMaterialState; bool inMaterialRefresh = true; View3D@ materialPreview; Scene@ previewScene; Node@ previewCameraNode; Node@ previewLightNode; Light@ previewLight; Node@ previewModelNode; StaticModel@ previewModel; void CreateMaterialEditor() { if (materialWindow !is null) return; materialWindow = ui.LoadLayout(cache.GetResource("XMLFile", "UI/EditorMaterialWindow.xml")); ui.root.AddChild(materialWindow); materialWindow.opacity = uiMaxOpacity; InitMaterialPreview(); RefreshMaterialEditor(); int height = Min(ui.root.height - 60, 500); materialWindow.SetSize(300, height); CenterDialog(materialWindow); HideMaterialEditor(); SubscribeToEvent(materialWindow.GetChild("NewButton", true), "Released", "NewMaterial"); SubscribeToEvent(materialWindow.GetChild("RevertButton", true), "Released", "RevertMaterial"); SubscribeToEvent(materialWindow.GetChild("SaveButton", true), "Released", "SaveMaterial"); SubscribeToEvent(materialWindow.GetChild("SaveAsButton", true), "Released", "SaveMaterialAs"); SubscribeToEvent(materialWindow.GetChild("CloseButton", true), "Released", "HideMaterialEditor"); SubscribeToEvent(materialWindow.GetChild("NewParameterDropDown", true), "ItemSelected", "CreateShaderParameter"); SubscribeToEvent(materialWindow.GetChild("DeleteParameterButton", true), "Released", "DeleteShaderParameter"); SubscribeToEvent(materialWindow.GetChild("NewTechniqueButton", true), "Released", "NewTechnique"); SubscribeToEvent(materialWindow.GetChild("DeleteTechniqueButton", true), "Released", "DeleteTechnique"); SubscribeToEvent(materialWindow.GetChild("SortTechniquesButton", true), "Released", "SortTechniques"); SubscribeToEvent(materialWindow.GetChild("ConstantBiasEdit", true), "TextChanged", "EditConstantBias"); SubscribeToEvent(materialWindow.GetChild("ConstantBiasEdit", true), "TextFinished", "EditConstantBias"); SubscribeToEvent(materialWindow.GetChild("SlopeBiasEdit", true), "TextChanged", "EditSlopeBias"); SubscribeToEvent(materialWindow.GetChild("SlopeBiasEdit", true), "TextFinished", "EditSlopeBias"); SubscribeToEvent(materialWindow.GetChild("CullModeEdit", true), "ItemSelected", "EditCullMode"); SubscribeToEvent(materialWindow.GetChild("ShadowCullModeEdit", true), "ItemSelected", "EditShadowCullMode"); } bool ShowMaterialEditor() { RefreshMaterialEditor(); materialWindow.visible = true; materialWindow.BringToFront(); return true; } void HideMaterialEditor() { materialWindow.visible = false; } void InitMaterialPreview() { previewScene = Scene("PreviewScene"); previewScene.CreateComponent("Octree"); Node@ zoneNode = previewScene.CreateChild("Zone"); Zone@ zone = zoneNode.CreateComponent("Zone"); zone.boundingBox = BoundingBox(-1000, 1000); zone.ambientColor = Color(0.15, 0.15, 0.15); zone.fogColor = Color(0, 0, 0); zone.fogStart = 10.0; zone.fogEnd = 100.0; previewCameraNode = previewScene.CreateChild("PreviewCamera"); previewCameraNode.position = Vector3(0, 0, -1.5); Camera@ camera = previewCameraNode.CreateComponent("Camera"); camera.nearClip = 0.1f; camera.farClip = 100.0f; previewLightNode = previewScene.CreateChild("PreviewLight"); previewLightNode.direction = Vector3(0.5, -0.5, 0.5); previewLight = previewLightNode.CreateComponent("Light"); previewLight.lightType = LIGHT_DIRECTIONAL; previewLight.specularIntensity = 0.5; previewModelNode = previewScene.CreateChild("PreviewModel"); previewModelNode.rotation = Quaternion(0, 0, 0); previewModel = previewModelNode.CreateComponent("StaticModel"); previewModel.model = cache.GetResource("Model", "Models/Sphere.mdl"); materialPreview = materialWindow.GetChild("MaterialPreview", true); materialPreview.SetFixedHeight(100); materialPreview.SetView(previewScene, camera); materialPreview.autoUpdate = false; SubscribeToEvent(materialPreview, "DragMove", "RotateMaterialPreview"); } void EditMaterial(Material@ mat) { if (editMaterial !is null) UnsubscribeFromEvent(editMaterial, "ReloadFinished"); editMaterial = mat; if (editMaterial !is null) SubscribeToEvent(editMaterial, "ReloadFinished", "RefreshMaterialEditor"); ShowMaterialEditor(); } void RefreshMaterialEditor() { RefreshMaterialPreview(); RefreshMaterialName(); RefreshMaterialTechniques(); RefreshMaterialTextures(); RefreshMaterialShaderParameters(); RefreshMaterialMiscParameters(); } void RefreshMaterialPreview() { previewModel.material = editMaterial; materialPreview.QueueUpdate(); } void RefreshMaterialName() { UIElement@ container = materialWindow.GetChild("NameContainer", true); container.RemoveAllChildren(); LineEdit@ nameEdit = CreateAttributeLineEdit(container, null, 0, 0); if (editMaterial !is null) nameEdit.text = editMaterial.name; SubscribeToEvent(nameEdit, "TextFinished", "EditMaterialName"); Button@ pickButton = CreateResourcePickerButton(container, null, 0, 0, "Pick"); SubscribeToEvent(pickButton, "Released", "PickEditMaterial"); } void RefreshMaterialTechniques(bool fullUpdate = true) { ListView@ list = materialWindow.GetChild("TechniqueList", true); if (editMaterial is null) return; if (fullUpdate == true) { list.RemoveAllItems(); for (uint i = 0; i < editMaterial.numTechniques; ++i) { TechniqueEntry entry = editMaterial.techniqueEntries[i]; UIElement@ container = UIElement(); container.SetLayout(LM_HORIZONTAL, 4); container.SetFixedHeight(ATTR_HEIGHT); list.AddItem(container); LineEdit@ nameEdit = CreateAttributeLineEdit(container, null, i, 0); nameEdit.name = "TechniqueNameEdit" + String(i); Button@ pickButton = CreateResourcePickerButton(container, null, i, 0, "Pick"); SubscribeToEvent(pickButton, "Released", "PickMaterialTechnique"); Button@ openButton = CreateResourcePickerButton(container, null, i, 0, "Open"); SubscribeToEvent(openButton, "Released", "OpenResource"); if (entry.technique !is null) nameEdit.text = entry.technique.name; SubscribeToEvent(nameEdit, "TextFinished", "EditMaterialTechnique"); UIElement@ container2 = UIElement(); container2.SetLayout(LM_HORIZONTAL, 4); container2.SetFixedHeight(ATTR_HEIGHT); list.AddItem(container2); Text@ text = container2.CreateChild("Text"); text.style = "EditorAttributeText"; text.text = "Quality"; LineEdit@ attrEdit = CreateAttributeLineEdit(container2, null, i, 0); attrEdit.text = String(entry.qualityLevel); SubscribeToEvent(attrEdit, "TextChanged", "EditTechniqueQuality"); SubscribeToEvent(attrEdit, "TextFinished", "EditTechniqueQuality"); text = container2.CreateChild("Text"); text.style = "EditorAttributeText"; text.text = "LOD Distance"; attrEdit = CreateAttributeLineEdit(container2, null, i, 0); attrEdit.text = String(entry.lodDistance); SubscribeToEvent(attrEdit, "TextChanged", "EditTechniqueLodDistance"); SubscribeToEvent(attrEdit, "TextFinished", "EditTechniqueLodDistance"); } } else { for (uint i = 0; i < editMaterial.numTechniques; ++i) { TechniqueEntry entry = editMaterial.techniqueEntries[i]; LineEdit@ nameEdit = materialWindow.GetChild("TechniqueNameEdit" + String(i), true); if (nameEdit is null) continue; nameEdit.text = entry.technique !is null ? entry.technique.name : ""; } } } void RefreshMaterialTextures(bool fullUpdate = true) { if (fullUpdate) { ListView@ list = materialWindow.GetChild("TextureList", true); list.RemoveAllItems(); for (uint i = 0; i < MAX_MATERIAL_TEXTURE_UNITS; ++i) { String tuName = GetTextureUnitName(TextureUnit(i)); tuName[0] = ToUpper(tuName[0]); UIElement@ parent = CreateAttributeEditorParentWithSeparatedLabel(list, "Unit " + i + " " + tuName, i, 0, false); UIElement@ container = UIElement(); container.SetLayout(LM_HORIZONTAL, 4, IntRect(10, 0, 4, 0)); container.SetFixedHeight(ATTR_HEIGHT); parent.AddChild(container); LineEdit@ nameEdit = CreateAttributeLineEdit(container, null, i, 0); nameEdit.name = "TextureNameEdit" + String(i); Button@ pickButton = CreateResourcePickerButton(container, null, i, 0, "Pick"); SubscribeToEvent(pickButton, "Released", "PickMaterialTexture"); Button@ openButton = CreateResourcePickerButton(container, null, i, 0, "Open"); SubscribeToEvent(openButton, "Released", "OpenResource"); if (editMaterial !is null) { Texture@ texture = editMaterial.textures[i]; if (texture !is null) nameEdit.text = texture.name; } SubscribeToEvent(nameEdit, "TextFinished", "EditMaterialTexture"); } } else { for (uint i = 0; i < MAX_MATERIAL_TEXTURE_UNITS; ++i) { LineEdit@ nameEdit = materialWindow.GetChild("TextureNameEdit" + String(i), true); if (nameEdit is null) continue; String textureName; if (editMaterial !is null) { Texture@ texture = editMaterial.textures[i]; if (texture !is null) textureName = texture.name; } nameEdit.text = textureName; } } } void RefreshMaterialShaderParameters() { ListView@ list = materialWindow.GetChild("ShaderParameterList", true); list.RemoveAllItems(); if (editMaterial is null) return; Array@ parameterNames = editMaterial.shaderParameterNames; for (uint i = 0; i < parameterNames.length; ++i) { VariantType type = editMaterial.shaderParameters[parameterNames[i]].type; Variant value = editMaterial.shaderParameters[parameterNames[i]]; UIElement@ parent = CreateAttributeEditorParent(list, parameterNames[i], 0, 0); uint numCoords = type - VAR_FLOAT + 1; Array coordValues = value.ToString().Split(' '); for (uint j = 0; j < numCoords; ++j) { LineEdit@ attrEdit = CreateAttributeLineEdit(parent, null, 0, 0); attrEdit.vars["Coordinate"] = j; attrEdit.vars["Name"] = parameterNames[i]; attrEdit.text = coordValues[j]; CreateDragSlider(attrEdit); SubscribeToEvent(attrEdit, "TextChanged", "EditShaderParameter"); SubscribeToEvent(attrEdit, "TextFinished", "EditShaderParameter"); } } } void RefreshMaterialMiscParameters() { if (editMaterial is null) return; BiasParameters bias = editMaterial.depthBias; inMaterialRefresh = true; LineEdit@ attrEdit = materialWindow.GetChild("ConstantBiasEdit", true); attrEdit.text = String(bias.constantBias); attrEdit = materialWindow.GetChild("SlopeBiasEdit", true); attrEdit.text = String(bias.slopeScaledBias); DropDownList@ attrList = materialWindow.GetChild("CullModeEdit", true); attrList.selection = editMaterial.cullMode; attrList = materialWindow.GetChild("ShadowCullModeEdit", true); attrList.selection = editMaterial.shadowCullMode; inMaterialRefresh = false; } void RotateMaterialPreview(StringHash eventType, VariantMap& eventData) { int elemX = eventData["ElementX"].GetInt(); int elemY = eventData["ElementY"].GetInt(); if (materialPreview.height > 0 && materialPreview.width > 0) { float yaw = ((materialPreview.height / 2) - elemY) * (90.0 / materialPreview.height); float pitch = ((materialPreview.width / 2) - elemX) * (90.0 / materialPreview.width); previewModelNode.rotation = previewModelNode.rotation.Slerp(Quaternion(yaw, pitch, 0), 0.1); materialPreview.QueueUpdate(); } } void EditMaterialName(StringHash eventType, VariantMap& eventData) { LineEdit@ nameEdit = eventData["Element"].GetPtr(); String newMaterialName = nameEdit.text.Trimmed(); if (!newMaterialName.empty) { Material@ newMaterial = cache.GetResource("Material", newMaterialName); if (newMaterial !is null) EditMaterial(newMaterial); } } void PickEditMaterial() { @resourcePicker = GetResourcePicker(StringHash("Material")); if (resourcePicker is null) return; String lastPath = resourcePicker.lastPath; if (lastPath.empty) lastPath = sceneResourcePath; CreateFileSelector("Pick " + resourcePicker.typeName, "OK", "Cancel", lastPath, resourcePicker.filters, resourcePicker.lastFilter); SubscribeToEvent(uiFileSelector, "FileSelected", "PickEditMaterialDone"); } void PickEditMaterialDone(StringHash eventType, VariantMap& eventData) { StoreResourcePickerPath(); CloseFileSelector(); if (!eventData["OK"].GetBool()) { @resourcePicker = null; return; } String resourceName = eventData["FileName"].GetString(); Resource@ res = GetPickedResource(resourceName); if (res !is null) EditMaterial(cast(res)); @resourcePicker = null; } void NewMaterial() { EditMaterial(Material()); } void RevertMaterial() { if (editMaterial is null) return; BeginMaterialEdit(); cache.ReloadResource(editMaterial); EndMaterialEdit(); RefreshMaterialEditor(); } void SaveMaterial() { if (editMaterial is null || editMaterial.name.empty) return; String fullName = cache.GetResourceFileName(editMaterial.name); if (fullName.empty) return; MakeBackup(fullName); File saveFile(fullName, FILE_WRITE); bool success = editMaterial.Save(saveFile); RemoveBackup(success, fullName); } void SaveMaterialAs() { if (editMaterial is null) return; @resourcePicker = GetResourcePicker(StringHash("Material")); if (resourcePicker is null) return; String lastPath = resourcePicker.lastPath; if (lastPath.empty) lastPath = sceneResourcePath; CreateFileSelector("Save material as", "Save", "Cancel", lastPath, resourcePicker.filters, resourcePicker.lastFilter); SubscribeToEvent(uiFileSelector, "FileSelected", "SaveMaterialAsDone"); } void SaveMaterialAsDone(StringHash eventType, VariantMap& eventData) { StoreResourcePickerPath(); CloseFileSelector(); @resourcePicker = null; if (editMaterial is null) return; if (!eventData["OK"].GetBool()) { @resourcePicker = null; return; } String fullName = eventData["FileName"].GetString(); // Add default extension for saving if not specified String filter = eventData["Filter"].GetString(); if (GetExtension(fullName).empty && filter != "*.*") fullName = fullName + filter.Substring(1); MakeBackup(fullName); File saveFile(fullName, FILE_WRITE); if (editMaterial.Save(saveFile)) { saveFile.Close(); RemoveBackup(true, fullName); // Load the new resource to update the name in the editor Material@ newMat = cache.GetResource("Material", GetResourceNameFromFullName(fullName)); if (newMat !is null) EditMaterial(newMat); } } void EditShaderParameter(StringHash eventType, VariantMap& eventData) { if (editMaterial is null) return; LineEdit@ attrEdit = eventData["Element"].GetPtr(); uint coordinate = attrEdit.vars["Coordinate"].GetUInt(); String name = attrEdit.vars["Name"].GetString(); Variant oldValue = editMaterial.shaderParameters[name]; Array coordValues = oldValue.ToString().Split(' '); coordValues[coordinate] = String(attrEdit.text.ToFloat()); String valueString; for (uint i = 0; i < coordValues.length; ++i) { valueString += coordValues[i]; valueString += " "; } Variant newValue; newValue.FromString(oldValue.type, valueString); BeginMaterialEdit(); editMaterial.shaderParameters[name] = newValue; EndMaterialEdit(); } void CreateShaderParameter(StringHash eventType, VariantMap& eventData) { if (editMaterial is null) return; LineEdit@ nameEdit = materialWindow.GetChild("ParameterNameEdit", true); String newName = nameEdit.text.Trimmed(); if (newName.empty) return; DropDownList@ dropDown = eventData["Element"].GetPtr(); Variant newValue; switch (dropDown.selection) { case 0: newValue = float(0); break; case 1: newValue = Vector2(0, 0); break; case 2: newValue = Vector3(0, 0, 0); break; case 3: newValue = Vector4(0, 0, 0, 0); break; } BeginMaterialEdit(); editMaterial.shaderParameters[newName] = newValue; EndMaterialEdit(); RefreshMaterialShaderParameters(); } void DeleteShaderParameter() { if (editMaterial is null) return; LineEdit@ nameEdit = materialWindow.GetChild("ParameterNameEdit", true); String name = nameEdit.text.Trimmed(); if (name.empty) return; BeginMaterialEdit(); editMaterial.RemoveShaderParameter(name); EndMaterialEdit(); RefreshMaterialShaderParameters(); } void PickMaterialTexture(StringHash eventType, VariantMap& eventData) { if (editMaterial is null) return; UIElement@ button = eventData["Element"].GetPtr(); resourcePickIndex = button.vars["Index"].GetUInt(); @resourcePicker = GetResourcePicker(StringHash("Texture2D")); if (resourcePicker is null) return; String lastPath = resourcePicker.lastPath; if (lastPath.empty) lastPath = sceneResourcePath; CreateFileSelector("Pick " + resourcePicker.typeName, "OK", "Cancel", lastPath, resourcePicker.filters, resourcePicker.lastFilter); SubscribeToEvent(uiFileSelector, "FileSelected", "PickMaterialTextureDone"); } void PickMaterialTextureDone(StringHash eventType, VariantMap& eventData) { StoreResourcePickerPath(); CloseFileSelector(); if (!eventData["OK"].GetBool()) { @resourcePicker = null; return; } String resourceName = eventData["FileName"].GetString(); Resource@ res = GetPickedResource(resourceName); if (res !is null && editMaterial !is null) { BeginMaterialEdit(); editMaterial.textures[resourcePickIndex] = res; EndMaterialEdit(); RefreshMaterialTextures(false); } @resourcePicker = null; } void EditMaterialTexture(StringHash eventType, VariantMap& eventData) { if (editMaterial is null) return; LineEdit@ attrEdit = eventData["Element"].GetPtr(); String textureName = attrEdit.text.Trimmed(); uint index = attrEdit.vars["Index"].GetUInt(); BeginMaterialEdit(); if (!textureName.empty) { Texture@ texture = cache.GetResource(GetExtension(textureName) == ".xml" ? "TextureCube" : "Texture2D", textureName); editMaterial.textures[index] = texture; } else editMaterial.textures[index] = null; EndMaterialEdit(); } void NewTechnique() { if (editMaterial is null) return; BeginMaterialEdit(); editMaterial.numTechniques = editMaterial.numTechniques + 1; EndMaterialEdit(); RefreshMaterialTechniques(); } void DeleteTechnique() { if (editMaterial is null || editMaterial.numTechniques < 2) return; BeginMaterialEdit(); editMaterial.numTechniques = editMaterial.numTechniques - 1; EndMaterialEdit(); RefreshMaterialTechniques(); } void PickMaterialTechnique(StringHash eventType, VariantMap& eventData) { if (editMaterial is null) return; UIElement@ button = eventData["Element"].GetPtr(); resourcePickIndex = button.vars["Index"].GetUInt(); @resourcePicker = GetResourcePicker(StringHash("Technique")); if (resourcePicker is null) return; String lastPath = resourcePicker.lastPath; if (lastPath.empty) lastPath = sceneResourcePath; CreateFileSelector("Pick " + resourcePicker.typeName, "OK", "Cancel", lastPath, resourcePicker.filters, resourcePicker.lastFilter); SubscribeToEvent(uiFileSelector, "FileSelected", "PickMaterialTechniqueDone"); } void PickMaterialTechniqueDone(StringHash eventType, VariantMap& eventData) { StoreResourcePickerPath(); CloseFileSelector(); if (!eventData["OK"].GetBool()) { @resourcePicker = null; return; } String resourceName = eventData["FileName"].GetString(); Resource@ res = GetPickedResource(resourceName); if (res !is null && editMaterial !is null) { BeginMaterialEdit(); TechniqueEntry entry = editMaterial.techniqueEntries[resourcePickIndex]; editMaterial.SetTechnique(resourcePickIndex, res, entry.qualityLevel, entry.lodDistance); EndMaterialEdit(); RefreshMaterialTechniques(false); } @resourcePicker = null; } void EditMaterialTechnique(StringHash eventType, VariantMap& eventData) { if (editMaterial is null) return; LineEdit@ attrEdit = eventData["Element"].GetPtr(); String techniqueName = attrEdit.text.Trimmed(); uint index = attrEdit.vars["Index"].GetUInt(); BeginMaterialEdit(); Technique@ newTech; if (!techniqueName.empty) newTech = cache.GetResource("Technique", techniqueName); TechniqueEntry entry = editMaterial.techniqueEntries[index]; editMaterial.SetTechnique(index, newTech, entry.qualityLevel, entry.lodDistance); EndMaterialEdit(); } void EditTechniqueQuality(StringHash eventType, VariantMap& eventData) { if (editMaterial is null) return; LineEdit@ attrEdit = eventData["Element"].GetPtr(); uint newQualityLevel = attrEdit.text.ToUInt(); uint index = attrEdit.vars["Index"].GetUInt(); BeginMaterialEdit(); TechniqueEntry entry = editMaterial.techniqueEntries[index]; editMaterial.SetTechnique(index, entry.technique, newQualityLevel, entry.lodDistance); EndMaterialEdit(); } void EditTechniqueLodDistance(StringHash eventType, VariantMap& eventData) { if (editMaterial is null) return; LineEdit@ attrEdit = eventData["Element"].GetPtr(); float newLodDistance = attrEdit.text.ToFloat(); uint index = attrEdit.vars["Index"].GetUInt(); BeginMaterialEdit(); TechniqueEntry entry = editMaterial.techniqueEntries[index]; editMaterial.SetTechnique(index, entry.technique, entry.qualityLevel, newLodDistance); EndMaterialEdit(); } void SortTechniques() { if (editMaterial is null) return; BeginMaterialEdit(); editMaterial.SortTechniques(); EndMaterialEdit(); RefreshMaterialTechniques(); } void EditConstantBias(StringHash eventType, VariantMap& eventData) { if (editMaterial is null || inMaterialRefresh) return; BeginMaterialEdit(); LineEdit@ attrEdit = eventData["Element"].GetPtr(); BiasParameters bias = editMaterial.depthBias; bias.constantBias = attrEdit.text.ToFloat(); editMaterial.depthBias = bias; EndMaterialEdit(); } void EditSlopeBias(StringHash eventType, VariantMap& eventData) { if (editMaterial is null || inMaterialRefresh) return; BeginMaterialEdit(); LineEdit@ attrEdit = eventData["Element"].GetPtr(); BiasParameters bias = editMaterial.depthBias; bias.slopeScaledBias = attrEdit.text.ToFloat(); editMaterial.depthBias = bias; EndMaterialEdit(); } void EditCullMode(StringHash eventType, VariantMap& eventData) { if (editMaterial is null || inMaterialRefresh) return; BeginMaterialEdit(); DropDownList@ attrEdit = eventData["Element"].GetPtr(); editMaterial.cullMode = CullMode(attrEdit.selection); EndMaterialEdit(); } void EditShadowCullMode(StringHash eventType, VariantMap& eventData) { if (editMaterial is null || inMaterialRefresh) return; BeginMaterialEdit(); DropDownList@ attrEdit = eventData["Element"].GetPtr(); editMaterial.shadowCullMode = CullMode(attrEdit.selection); EndMaterialEdit(); } void BeginMaterialEdit() { if (editMaterial is null) return; oldMaterialState = XMLFile(); XMLElement materialElem = oldMaterialState.CreateRoot("material"); editMaterial.Save(materialElem); } void EndMaterialEdit() { if (editMaterial is null) return; if (!dragEditAttribute) { EditMaterialAction@ action = EditMaterialAction(); action.Define(editMaterial, oldMaterialState); SaveEditAction(action); } materialPreview.QueueUpdate(); }