Merge pull request #7441 from Ultimaker/xray_in_solid_sidequest

CURA-7262 X-Ray in solid-view (colors instead)
This commit is contained in:
Nino van Hooff 2020-04-08 15:45:00 +02:00 committed by GitHub
commit 74253e420a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 304 additions and 34 deletions

View File

@ -3,6 +3,7 @@
import os.path
from UM.Resources import Resources
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
@ -23,7 +24,7 @@ class XRayPass(RenderPass):
def render(self):
if not self._shader:
self._shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("XRayView"), "xray.shader"))
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "xray.shader"))
batch = RenderBatch(self._shader, type = RenderBatch.RenderType.NoType, backface_cull = False, blend_mode = RenderBatch.BlendMode.Additive)
for node in DepthFirstIterator(self._scene.getRoot()):

View File

@ -1,22 +1,43 @@
# Copyright (c) 2019 Ultimaker B.V.
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
from UM.View.View import View
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.Selection import Selection
from UM.Resources import Resources
from PyQt5.QtGui import QOpenGLContext, QImage
from PyQt5.QtCore import QSize
import numpy as np
import time
from UM.Application import Application
from UM.View.RenderBatch import RenderBatch
from UM.Logger import Logger
from UM.Message import Message
from UM.Math.Color import Color
from UM.PluginRegistry import PluginRegistry
from UM.Platform import Platform
from UM.Event import Event
from UM.View.RenderBatch import RenderBatch
from UM.View.GL.OpenGL import OpenGL
from UM.i18n import i18nCatalog
from cura.Settings.ExtruderManager import ExtruderManager
from cura import XRayPass
import math
catalog = i18nCatalog("cura")
## Standard view for mesh models.
class SolidView(View):
_show_xray_warning_preference = "view/show_xray_warning"
def __init__(self):
super().__init__()
application = Application.getInstance()
@ -27,13 +48,31 @@ class SolidView(View):
self._non_printing_shader = None
self._support_mesh_shader = None
self._xray_shader = None
self._xray_pass = None
self._xray_composite_shader = None
self._composite_pass = None
self._extruders_model = None
self._theme = None
self._support_angle = 90
self._global_stack = None
Application.getInstance().engineCreatedSignal.connect(self._onGlobalContainerChanged)
self._old_composite_shader = None
self._old_layer_bindings = None
self._next_xray_checking_time = time.time()
self._xray_checking_update_time = 1.0 # seconds
self._xray_warning_cooldown = 60 * 10 # reshow Model error message every 10 minutes
self._xray_warning_message = Message(
catalog.i18nc("@info:status", "Your model is not manifold. The highlighted areas indicate either missing or extraneous surfaces."),
lifetime = 60 * 5, # leave message for 5 minutes
title = catalog.i18nc("@info:title", "Model errors"),
)
application.getPreferences().addPreference(self._show_xray_warning_preference, True)
application.engineCreatedSignal.connect(self._onGlobalContainerChanged)
def _onGlobalContainerChanged(self) -> None:
if self._global_stack:
@ -92,6 +131,41 @@ class SolidView(View):
self._support_mesh_shader.setUniformValue("u_vertical_stripes", True)
self._support_mesh_shader.setUniformValue("u_width", 5.0)
if not Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference):
self._xray_shader = None
self._xray_composite_shader = None
if self._composite_pass and 'xray' in self._composite_pass.getLayerBindings():
self._composite_pass.setLayerBindings(self._old_layer_bindings)
self._composite_pass.setCompositeShader(self._old_composite_shader)
self._old_layer_bindings = None
self._old_composite_shader = None
self._xray_warning_message.hide()
else:
if not self._xray_shader:
self._xray_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "xray.shader"))
if not self._xray_composite_shader:
self._xray_composite_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "xray_composite.shader"))
theme = Application.getInstance().getTheme()
self._xray_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb()))
self._xray_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb()))
renderer = self.getRenderer()
if not self._composite_pass or not 'xray' in self._composite_pass.getLayerBindings():
# Currently the RenderPass constructor requires a size > 0
# This should be fixed in RenderPass's constructor.
self._xray_pass = XRayPass.XRayPass(1, 1)
renderer.addRenderPass(self._xray_pass)
if not self._composite_pass:
self._composite_pass = self.getRenderer().getRenderPass("composite")
self._old_layer_bindings = self._composite_pass.getLayerBindings()
self._composite_pass.setLayerBindings(["default", "selection", "xray"])
self._old_composite_shader = self._composite_pass.getCompositeShader()
self._composite_pass.setCompositeShader(self._xray_composite_shader)
def beginRendering(self):
scene = self.getController().getScene()
renderer = self.getRenderer()
@ -175,4 +249,65 @@ class SolidView(View):
renderer.queueNode(scene.getRoot(), mesh = node.getBoundingBoxMesh(), mode = RenderBatch.RenderMode.LineLoop)
def endRendering(self):
pass
# check whether the xray overlay is showing badness
if time.time() > self._next_xray_checking_time\
and Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference):
self._next_xray_checking_time = time.time() + self._xray_checking_update_time
xray_img = self._xray_pass.getOutput()
xray_img = xray_img.convertToFormat(QImage.Format_RGB888)
# We can't just read the image since the pixels are aligned to internal memory positions.
# xray_img.byteCount() != xray_img.width() * xray_img.height() * 3
# The byte count is a little higher sometimes. We need to check the data per line, but fast using Numpy.
# See https://stackoverflow.com/questions/5810970/get-raw-data-from-qimage for a description of the problem.
# We can't use that solution though, since it doesn't perform well in Python.
class QImageArrayView:
"""
Class that ducktypes to be a Numpy ndarray.
"""
def __init__(self, qimage):
self.__array_interface__ = {
"shape": (qimage.height(), qimage.width()),
"typestr": "|u4", # Use 4 bytes per pixel rather than 3, since Numpy doesn't support 3.
"data": (int(qimage.bits()), False),
"strides": (qimage.bytesPerLine(), 3), # This does the magic: For each line, skip the correct number of bytes. Bytes per pixel is always 3 due to QImage.Format.Format_RGB888.
"version": 3
}
array = np.asarray(QImageArrayView(xray_img)).view(np.dtype({
"r": (np.uint8, 0, "red"),
"g": (np.uint8, 1, "green"),
"b": (np.uint8, 2, "blue"),
"a": (np.uint8, 3, "alpha") # Never filled since QImage was reformatted to RGB888.
}), np.recarray)
if np.any(np.mod(array.r, 2)):
self._next_xray_checking_time = time.time() + self._xray_warning_cooldown
self._xray_warning_message.show()
Logger.log("i", "X-Ray overlay found non-manifold pixels.")
def event(self, event):
if event.type == Event.ViewActivateEvent:
# FIX: on Max OS X, somehow QOpenGLContext.currentContext() can become None during View switching.
# This can happen when you do the following steps:
# 1. Start Cura
# 2. Load a model
# 3. Switch to Custom mode
# 4. Select the model and click on the per-object tool icon
# 5. Switch view to Layer view or X-Ray
# 6. Cura will very likely crash
# It seems to be a timing issue that the currentContext can somehow be empty, but I have no clue why.
# This fix tries to reschedule the view changing event call on the Qt thread again if the current OpenGL
# context is None.
if Platform.isOSX():
if QOpenGLContext.currentContext() is None:
Logger.log("d", "current context of OpenGL is empty on Mac OS X, will try to create shaders later")
Application.getInstance().callLater(lambda e = event: self.event(e))
return
if event.type == Event.ViewDeactivateEvent:
if self._composite_pass and 'xray' in self._composite_pass.getLayerBindings():
self.getRenderer().removeRenderPass(self._xray_pass)
self._composite_pass.setLayerBindings(self._old_layer_bindings)
self._composite_pass.setCompositeShader(self._old_composite_shader)
self._xray_warning_message.hide()

View File

@ -0,0 +1,50 @@
[shaders]
vertex =
uniform highp mat4 u_modelViewProjectionMatrix;
attribute highp vec4 a_vertex;
void main()
{
gl_Position = u_modelViewProjectionMatrix * a_vertex;
}
fragment =
uniform lowp vec4 u_xray_error;
void main()
{
gl_FragColor = u_xray_error;
}
vertex41core =
#version 410
uniform highp mat4 u_modelViewProjectionMatrix;
in highp vec4 a_vertex;
void main()
{
gl_Position = u_modelViewProjectionMatrix * a_vertex;
}
fragment41core =
#version 410
uniform lowp vec4 u_xray_error;
out vec4 frag_color;
void main()
{
frag_color = u_xray_error;
}
[defaults]
u_xray_error = [1.0, 1.0, 1.0, 1.0]
[bindings]
u_modelViewProjectionMatrix = model_view_projection_matrix
[attributes]
a_vertex = vertex

View File

@ -1,13 +1,14 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
from PyQt5.QtGui import QOpenGLContext
from PyQt5.QtGui import QOpenGLContext, QImage
from UM.Application import Application
from UM.Logger import Logger
from UM.Math.Color import Color
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from UM.Platform import Platform
from UM.Event import Event
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
@ -19,7 +20,8 @@ from cura.CuraApplication import CuraApplication
from cura.CuraView import CuraView
from cura.Scene.ConvexHullNode import ConvexHullNode
from . import XRayPass
from cura import XRayPass
## View used to display a see-through version of objects with errors highlighted.
class XRayView(CuraView):
@ -38,7 +40,7 @@ class XRayView(CuraView):
renderer = self.getRenderer()
if not self._xray_shader:
self._xray_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("XRayView"), "xray.shader"))
self._xray_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "xray.shader"))
self._xray_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("xray").getRgb()))
for node in BreadthFirstIterator(scene.getRoot()):
@ -87,10 +89,8 @@ class XRayView(CuraView):
self.getRenderer().addRenderPass(self._xray_pass)
if not self._xray_composite_shader:
self._xray_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("XRayView"), "xray_composite.shader"))
theme = Application.getInstance().getTheme()
self._xray_composite_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "xray_composite.shader"))
self._xray_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb()))
self._xray_composite_shader.setUniformValue("u_error_color", Color(*theme.getColor("xray_error").getRgb()))
self._xray_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb()))
if not self._composite_pass:

View File

@ -86,6 +86,8 @@ UM.PreferencesPage
prefixJobNameCheckbox.checked = boolCheck(UM.Preferences.getValue("cura/jobname_prefix"))
UM.Preferences.resetPreference("view/show_overhang");
showOverhangCheckbox.checked = boolCheck(UM.Preferences.getValue("view/show_overhang"))
UM.Preferences.resetPreference("view/show_xray_warning");
showXrayErrorCheckbox.checked = boolCheck(UM.Preferences.getValue("view/show_warning"))
UM.Preferences.resetPreference("view/center_on_select");
centerOnSelectCheckbox.checked = boolCheck(UM.Preferences.getValue("view/center_on_select"))
UM.Preferences.resetPreference("view/invert_zoom");
@ -337,6 +339,25 @@ UM.PreferencesPage
}
}
UM.TooltipArea
{
width: childrenRect.width;
height: childrenRect.height;
text: catalog.i18nc("@info:tooltip", "Highlight missing or extraneous surfaces of the model using warning signs. The toolpaths will often be missing parts of the intended geometry.")
CheckBox
{
id: showXrayErrorCheckbox
checked: boolCheck(UM.Preferences.getValue("view/show_xray_warning"))
onClicked: UM.Preferences.setValue("view/show_xray_warning", checked)
text: catalog.i18nc("@option:check", "Display model errors");
}
}
UM.TooltipArea
{
width: childrenRect.width;

View File

@ -38,9 +38,13 @@ fragment =
varying highp vec3 f_vertex;
varying highp vec3 f_normal;
float round(float f)
{
return sign(f) * floor(abs(f) + 0.5);
}
void main()
{
mediump vec4 finalColor = vec4(0.0);
// Ambient Component
@ -62,8 +66,10 @@ fragment =
finalColor = (-normal.y > u_overhangAngle) ? u_overhangColor : finalColor;
vec3 grid = vec3(f_vertex.x - round(f_vertex.x), f_vertex.y - round(f_vertex.y), f_vertex.z - round(f_vertex.z));
finalColor.a = dot(grid, grid) < 0.245 ? 0.667 : 1.0;
gl_FragColor = finalColor;
gl_FragColor.a = 1.0;
}
vertex41core =
@ -111,7 +117,6 @@ fragment41core =
void main()
{
mediump vec4 finalColor = vec4(0.0);
// Ambient Component
@ -134,7 +139,9 @@ fragment41core =
finalColor = (u_faceId != gl_PrimitiveID) ? ((-normal.y > u_overhangAngle) ? u_overhangColor : finalColor) : u_faceColor;
frag_color = finalColor;
frag_color.a = 1.0;
vec3 grid = f_vertex - round(f_vertex);
frag_color.a = dot(grid, grid) < 0.245 ? 0.667 : 1.0;
}
[defaults]

View File

@ -12,7 +12,14 @@ vertex =
}
fragment =
uniform lowp vec4 u_color;
#ifdef GL_ES
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif // GL_FRAGMENT_PRECISION_HIGH
#endif // GL_ES
uniform vec4 u_color;
void main()
{
@ -34,7 +41,8 @@ vertex41core =
fragment41core =
#version 410
uniform lowp vec4 u_color;
uniform vec4 u_color;
out vec4 frag_color;

View File

@ -28,8 +28,8 @@ fragment =
uniform float u_outline_strength;
uniform vec4 u_outline_color;
uniform vec4 u_error_color;
uniform vec4 u_background_color;
uniform float u_xray_error_strength;
const vec3 x_axis = vec3(1.0, 0.0, 0.0);
const vec3 y_axis = vec3(0.0, 1.0, 0.0);
@ -39,6 +39,20 @@ fragment =
float kernel[9];
vec3 shiftHue(vec3 color, float hue)
{
// Make sure colors are distinct when grey-scale is used too:
if ((max(max(color.r, color.g), color.b) - min(min(color.r, color.g), color.b)) < 0.1)
{
color = vec3(1.0, 0.0, 0.0);
}
// The actual hue shift:
const vec3 k = vec3(0.57735, 0.57735, 0.57735);
float cosAngle = cos(hue);
return vec3(color * cosAngle + cross(k, color) * sin(hue) + k * dot(k, color) * (1.0 - cosAngle));
}
void main()
{
kernel[0] = 0.0; kernel[1] = 1.0; kernel[2] = 0.0;
@ -48,12 +62,18 @@ fragment =
vec4 result = u_background_color;
vec4 layer0 = texture2D(u_layer0, v_uvs);
result = layer0 * layer0.a + result * (1.0 - layer0.a);
float intersection_count = (texture2D(u_layer2, v_uvs).r * 255.0) / 5.0;
if(mod(intersection_count, 2.0) == 1.0)
float hue_shift = (layer0.a - 0.333) * 6.2831853;
if (layer0.a > 0.5)
{
result = u_error_color;
layer0.a = 1.0;
}
result = mix(result, layer0, layer0.a);
float intersection_count = texture2D(u_layer2, v_uvs).r * 51.0; // (1 / .02) + 1 (+1 magically fixes issues with high intersection count models)
float rest = mod(intersection_count + .01, 2.0);
if (rest > 1.0 && rest < 1.5 && intersection_count < 49.0)
{
result = vec4(shiftHue(layer0.rgb, hue_shift), result.a);
}
vec4 sum = vec4(0.0);
@ -70,8 +90,10 @@ fragment =
}
else
{
gl_FragColor = mix(result, vec4(abs(sum.a)) * u_outline_color, abs(sum.a));
gl_FragColor = mix(result, u_outline_color, abs(sum.a));
}
gl_FragColor.a = gl_FragColor.a > 0.5 ? 1.0 : 0.0;
}
vertex41core =
@ -98,8 +120,8 @@ fragment41core =
uniform float u_outline_strength;
uniform vec4 u_outline_color;
uniform vec4 u_error_color;
uniform vec4 u_background_color;
uniform float u_xray_error_strength;
const vec3 x_axis = vec3(1.0, 0.0, 0.0);
const vec3 y_axis = vec3(0.0, 1.0, 0.0);
@ -110,6 +132,20 @@ fragment41core =
float kernel[9];
vec3 shiftHue(vec3 color, float hue)
{
// Make sure colors are distinct when grey-scale is used too:
if ((max(max(color.r, color.g), color.b) - min(min(color.r, color.g), color.b)) < 0.1)
{
color = vec3(1.0, 0.0, 0.0);
}
// The actual hue shift:
const vec3 k = vec3(0.57735, 0.57735, 0.57735);
float cosAngle = cos(hue);
return vec3(color * cosAngle + cross(k, color) * sin(hue) + k * dot(k, color) * (1.0 - cosAngle));
}
void main()
{
kernel[0] = 0.0; kernel[1] = 1.0; kernel[2] = 0.0;
@ -119,12 +155,18 @@ fragment41core =
vec4 result = u_background_color;
vec4 layer0 = texture(u_layer0, v_uvs);
result = layer0 * layer0.a + result * (1.0 - layer0.a);
float intersection_count = (texture(u_layer2, v_uvs).r * 255.0) / 5.0;
if(mod(intersection_count, 2.0) == 1.0)
float hue_shift = (layer0.a - 0.333) * 6.2831853;
if (layer0.a > 0.5)
{
result = u_error_color;
layer0.a = 1.0;
}
result = mix(result, layer0, layer0.a);
float intersection_count = texture(u_layer2, v_uvs).r * 51; // (1 / .02) + 1 (+1 magically fixes issues with high intersection count models)
float rest = mod(intersection_count + .01, 2.0);
if (rest > 1.0 && rest < 1.5 && intersection_count < 49)
{
result = vec4(shiftHue(layer0.rgb, hue_shift), result.a);
}
vec4 sum = vec4(0.0);
@ -141,8 +183,10 @@ fragment41core =
}
else
{
frag_color = mix(result, vec4(abs(sum.a)) * u_outline_color, abs(sum.a));
frag_color = mix(result, u_outline_color, abs(sum.a));
}
frag_color.a = frag_color.a > 0.5 ? 1.0 : 0.0;
}
[defaults]
@ -152,7 +196,6 @@ u_layer2 = 2
u_background_color = [0.965, 0.965, 0.965, 1.0]
u_outline_strength = 1.0
u_outline_color = [0.05, 0.66, 0.89, 1.0]
u_error_color = [1.0, 0.0, 0.0, 1.0]
[bindings]

View File

@ -12,6 +12,8 @@
"model_overhang": [200, 0, 255, 255],
"xray_error_dark": [255, 0, 0, 255],
"xray_error_light": [255, 255, 0, 255],
"xray": [26, 26, 62, 255],
"xray_error": [255, 0, 0, 255],

View File

@ -10,9 +10,10 @@
"y_axis": [64, 64, 255, 255],
"model_default": [156, 201, 36, 255],
"model_overhang": [200, 0, 255, 255],
"model_selection_outline": [12, 169, 227, 255],
"xray_error_dark": [255, 0, 0, 255],
"xray_error_light": [255, 255, 0, 255],
"xray": [26, 26, 62, 255],
"xray_error": [255, 0, 0, 255],

View File

@ -369,6 +369,8 @@
"model_selection_outline": [50, 130, 255, 255],
"model_non_printing": [122, 122, 122, 255],
"xray_error_dark": [255, 0, 0, 255],
"xray_error_light": [255, 255, 0, 255],
"xray": [26, 26, 62, 255],
"xray_error": [255, 0, 0, 255],