-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathmain.qml
379 lines (341 loc) · 12.5 KB
/
main.qml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
import QtQuick 2.7
import QtWebSockets 1.0
import Painter 1.0
import "."
/* Allows to link Substance 3D Painter with an external program by using a persistent connection between both programs
*
* The connection is based on a WebSocket connection with a simple command protocol: "[COMMAND KEY] [JSON DATA]"
* List of commands to which this plugin is able to answer:
* - CREATE_PROJECT: Create a project with specified mesh and link it with the integration
* - OPEN_PROJECT: Open an existing project and link it with the integration
* - SEND_PROJECT_INFO: Send back info on the current project (project url and integration link identifier)
*
* For CREATE_PROJECT and OPEN_PROJECT, we receive a bag of information containing all the configuration
* allowing to link a project:
* {
* applicationName: "", // External application name
* workspacePath: "", // Workspace path (the workspace of the external application)
* exportPath: "", // Relative path in the workspace in which we want to export textures
* materials: { // List of linked materials
* "my_application_material_name": {
* assetPath: "", // External application material ID
* exportPreset: "", // Name or url of the export preset to use
* resourceShader: "", // Name or url of the Substance 3D Painter shader to render the material
* spToUnityProperties: {
* // Association between Substance 3D Painter exported textures and application textures
* // "texture name as define in the SP template" => "external application ID"
* // Example:
* "$mesh_$textureSet_baseColor": "basecolor_texture_sampler_in_my_shader",
* "...": "..."
* }
* },
* "...", {...}
* }
* project:
* {
* meshUrl: "", // Mesh to use for the new project
* normal: "", // Normal format to use (OpenGL/DirectX)
* template: "", // Template url or name to use
* url: "" // Substance 3D Painter project location
* },
* linkIdentifier: "" // Identifier that will be serialized with the project to allow reconnection
* }
*/
PainterPlugin {
id: root
property alias linkQuickInterval : settings.linkQuickInterval
property alias linkDegradedResolution : settings.linkDegradedResolution
property alias linkHQTreshold : settings.linkHQTreshold
property alias linkHQInterval : settings.linkHQInterval
property alias initDelayOnProjectCreation : settings.initDelayOnProjectCreation
property bool isLinked: false
property var liveLinkConfig: null
property var sendMapsButton: null
readonly property string linkIdentifierKey: "live_link_identifier"
readonly property bool enableAutoLink: settings.enableAutoLink
state: "disconnected"
states: [
State {
name: "disconnected"
PropertyChanges { target: root; isLinked: false }
},
State {
name: "connected"
PropertyChanges { target: root; isLinked: true }
},
State {
name: "exporting"
extend: "connected"
}
]
Component.onCompleted: {
sendMapsButton = alg.ui.addWidgetToPluginToolBar("ButtonSendMaps.qml");
sendMapsButton.clicked.connect(sendMaps);
sendMapsButton.enabled = Qt.binding(function() { return root.isLinked; });
}
onConfigure:
{
settings.open();
}
onEnableAutoLinkChanged: {
if (enableAutoLink) {
autoLink();
}
if (sendMapsButton) {
sendMapsButton.enableAutoLink = enableAutoLink
}
}
function disconnect() {
if (root.liveLinkConfig) {
alg.log.info(root.liveLinkConfig.applicationName + " client disconnected");
}
lqTimer.stop();
hqTimer.stop();
root.liveLinkConfig = null;
root.state = "disconnected"
}
function sendMaps(materialsToSend, mapExportConfig) {
function sendMaterialsMaps() {
var fullExportPath = "%1/%2".arg(root.liveLinkConfig.workspacePath).arg(root.liveLinkConfig.exportPath);
function sendMaterialMaps(materialLink, mapsInfos) {
// Ask the loading of each maps
var data = {
material: materialLink.assetPath,
params: {}
};
for (var mapName in mapsInfos) {
// Convert absolute map path as a workspace relative path
var mapPath = mapsInfos[mapName];
if (mapPath.length == 0) continue;
if (mapName in materialLink.spToLiveLinkProperties) {
// Convert absolute map path as a workspace relative path
var relativeMapPath = mapPath.replace(root.liveLinkConfig.workspacePath + "/", '');
data.params[materialLink.spToLiveLinkProperties[mapName]] = relativeMapPath;
}
else {
alg.log.warn("No defined association with the exported '%1' map".arg(mapName));
}
}
server.sendCommand("SET_MATERIAL_PARAMS", data);
}
// Export map from preset
var materialsName = alg.mapexport.documentStructure().materials.map(
function(m) { return m.name; }
).sort();
// Filter materials if needed
if (materialsToSend !== undefined) {
materialsName = materialsName.filter(
function(m) { return materialsToSend.indexOf(m) !== -1; }
);
}
for (var i in materialsName) {
var materialName = materialsName[i];
if (!(materialName in root.liveLinkConfig.materials)) {
alg.log.warn("Material %1 is not correctly linked with %2"
.arg(materialName)
.arg(root.liveLinkConfig.applicationName));
continue;
}
var materialLink = root.liveLinkConfig.materials[materialName];
root.state = "exporting"
var exportData = alg.mapexport.exportDocumentMaps(
materialLink.exportPreset,
fullExportPath,
"tga",
mapExportConfig,
[materialName]
);
root.state = "connected"
if (root.sendMapsButton) {
root.sendMapsButton.clientName = root.liveLinkConfig.applicationName;
}
for (var stackPath in exportData) {
sendMaterialMaps(materialLink, exportData[stackPath]);
}
}
}
if (root.state == "disconnected") return;
try {
sendMaterialsMaps();
}
catch(err) {alg.log.exception(err);}
}
function getCurrentTextureSet() {
return alg.mapexport.documentStructure().materials
.filter(function(m){return m.selected})[0].name;
}
function autoLink() {
if (root.state == "disconnected") return;
var textureSet = getCurrentTextureSet();
var resolution = alg.mapexport.textureSetResolution(textureSet);
// Enable deferred high quality only if resolution > treshold
if (resolution[0] * resolution[1] <=
root.linkHQTreshold * root.linkHQTreshold) {
sendMaps([textureSet]);
}
else {
sendMaps([textureSet], {
resolution: [
root.linkDegradedResolution,
root.linkDegradedResolution * (resolution[1] / resolution[0])
]
});
hqTimer.start();
}
}
Timer {
id: lqTimer
repeat: false
interval: root.linkQuickInterval
onTriggered: autoLink()
}
Timer {
id: hqTimer
repeat: false
interval: root.linkHQInterval
onTriggered: sendMaps([getCurrentTextureSet()])
}
onComputationStatusChanged: {
// When the engine status becomes non busy; we send the current texture set.
// If resolution is too high; we send a first degraded version to
// quickly visualize results; then we send the high quality version after
// few seconds.
// If paint engine status change during this time, we stop all timers.
lqTimer.stop();
hqTimer.stop();
if (root.state === "connected" && !isComputing && enableAutoLink) {
lqTimer.start();
}
}
function linkToClient(data) {
root.liveLinkConfig = {
applicationName: data.applicationName,
exportPath: data.exportPath.replace("\\", "/"),
workspacePath: data.workspacePath.replace("\\", "/"),
linkIdentifier: data.linkIdentifier, // Identifier to allow reconnection
materials: data.materials, // Materials info (path, export preset, shader, association)
project: data.project // Project configuration (mesh, normal, template, url)
}
alg.log.info(root.liveLinkConfig.applicationName + " client connected");
}
function applyResourceShaders() {
var shaderInstances = {
shaders: {},
texturesets: {}
};
// Create one shader instance per material
for (var materialName in root.liveLinkConfig.materials) {
var materialLink = root.liveLinkConfig.materials[materialName];
var shaderInstanceName = materialName;
shaderInstances.shaders[shaderInstanceName] = {
shader: materialLink.resourceShader,
shaderInstance: shaderInstanceName
};
shaderInstances.texturesets[materialName] = {
shader: shaderInstanceName
}
}
try {
alg.shaders.shaderInstancesFromObject(shaderInstances);
}
catch(err) {
alg.log.warn("Error while creating shader instances: %1".arg(err.message));
}
}
function initSynchronization(mapsNeeded) {
// If there is only one material, auto associate one
// with SP one even if name doesn't match
{
var spMaterials = alg.mapexport.documentStructure().materials;
var integrationMaterials = root.liveLinkConfig.materials;
var integrationMaterialsNames = Object.keys(integrationMaterials);
if (spMaterials.length === 1 && integrationMaterialsNames.length === 1) {
var matName = integrationMaterialsNames[0];
var spMatName = spMaterials[0].name;
var material = root.liveLinkConfig.materials[matName];
root.liveLinkConfig.materials = {};
root.liveLinkConfig.materials[spMatName] = material;
}
}
root.state = "connected";
alg.project.settings.setValue(linkIdentifierKey, root.liveLinkConfig.linkIdentifier);
if (mapsNeeded) {
sendMaps();
}
applyResourceShaders();
}
function createProject(data) {
linkToClient(data);
if (alg.project.isOpen()) {
// TODO: Ask the user if he wants to save its current opened project
alg.project.close();
}
alg.project.create(root.liveLinkConfig.project.meshUrl, null, root.liveLinkConfig.project.template, {
normalMapFormat: root.liveLinkConfig.project.normal
});
// HACK: Substance 3D Painter is not synchronous when creating a project
setTimeout(function(projectUrl) {
return function() {
initSynchronization();
alg.project.save(projectUrl);
};
}(data.project.url), root.initDelayOnProjectCreation);
}
function openProject(data) {
linkToClient(data);
var projectOpened = alg.project.isOpen();
var isAlreadyOpen = false;
try {
function cleanUrl(url) {
return alg.fileIO.localFileToUrl(alg.fileIO.urlToLocalFile(url));
}
isAlreadyOpen =
cleanUrl(alg.project.url()) == cleanUrl(data.project.url) ||
data.linkIdentifier == alg.project.settings.value(linkIdentifierKey);
}
catch (err) {}
// If the project is already opened, keep it
try {
if (!isAlreadyOpen) {
if (projectOpened) {
// TODO: Ask the user if he wants to save its current opened project
alg.project.close();
}
alg.project.open(data.project.url);
}
var mapsNeeded = !isAlreadyOpen;
initSynchronization(mapsNeeded);
}
catch (err) {
alg.log.exception(err)
disconnect()
}
}
function sendProjectInfo() {
try {
if (alg.project.settings.contains(linkIdentifierKey)) {
server.sendCommand("OPENED_PROJECT_INFO", {
linkIdentifier: alg.project.settings.value(linkIdentifierKey),
projectUrl: alg.project.url()
});
}
}
catch(err) {}
}
CommandServer {
id: server
Component.onCompleted: {
registerCallback("CREATE_PROJECT", createProject);
registerCallback("OPEN_PROJECT", openProject);
registerCallback("SEND_PROJECT_INFO", sendProjectInfo);
}
onConnectedChanged: {
if (!connected) {
disconnect();
}
}
}
Settings {
id: settings
}
}