How do I draw the outline of an object in Qt3D? - qt

How can I draw the outline of an object on top of any other object in Qt3D? For instance to highlight a selected object in a 3D editor?

If you want to draw the outline of an entity at all times, even if the entity is behind other entities, one solution is to do it in two steps:
Draw everything as normal.
Draw only the outline of the selected object.
When drawing the outline, you need to use an outline effect, which can be implemented in two render passes:
Render the geometry to a texture using a simple color shader.
Render to screen using a shader that takes each pixel in the texture and compares the surrounding pixels. If they are equal, we are inside the object and the fragment can be discarded. If they differ, we are on the edge of the object and we should draw the color.
Here is a simple implementation of the above-mentioned shader:
#version 150
uniform sampler2D color;
uniform vec2 winSize;
out vec4 fragColor;
void main()
{
int lineWidth = 5;
vec2 texCoord = gl_FragCoord.xy / winSize;
vec2 texCoordUp = (gl_FragCoord.xy + vec2(0, lineWidth)) / winSize;
vec2 texCoordDown = (gl_FragCoord.xy + vec2(0, -lineWidth)) / winSize;
vec2 texCoordRight = (gl_FragCoord.xy + vec2(lineWidth, 0)) / winSize;
vec2 texCoordLeft = (gl_FragCoord.xy + vec2(-lineWidth, 0)) / winSize;
vec4 col = texture(color, texCoord);
vec4 colUp = texture(color, texCoordUp);
vec4 colDown = texture(color, texCoordDown);
vec4 colRight = texture(color, texCoordRight);
vec4 colLeft = texture(color, texCoordLeft);
if ((colUp == colDown && colRight == colLeft) || col.a == 0.0)
discard;
fragColor = col;
}
Note: It might be a better idea to take the difference between the values instead of using an equality.
With this method, you don't have to worry about depth testing and the order in which the objects are drawn: The second time you draw, you will always draw on top of everything else.
You could do this by adding a single effect with two techniques with different filter keys. Alternatively, if you want to use materials from Qt3D.Extras, you can add another entity with the same transform and mesh and a material that uses the outline technique.
Here is an example that draws the outline on top of everything else using two render passes:
import QtQuick 2.2 as QQ2
import Qt3D.Core 2.0
import Qt3D.Render 2.0
import Qt3D.Input 2.0
import Qt3D.Extras 2.0
Entity {
Camera {
id: camera
projectionType: CameraLens.PerspectiveProjection
fieldOfView: 45
aspectRatio: 16/9
nearPlane : 0.1
farPlane : 1000.0
position: Qt.vector3d( 0.0, 0.0, -40.0 )
upVector: Qt.vector3d( 0.0, 1.0, 0.0 )
viewCenter: Qt.vector3d( 0.0, 0.0, 0.0 )
}
OrbitCameraController {
camera: camera
}
components: [
RenderSettings {
activeFrameGraph: RenderSurfaceSelector {
id: surfaceSelector
Viewport {
CameraSelector {
camera: camera
FrustumCulling {
TechniqueFilter {
matchAll: [
FilterKey { name: "renderingStyle"; value: "forward" }
]
ClearBuffers {
clearColor: Qt.rgba(0.1, 0.2, 0.3)
buffers: ClearBuffers.ColorDepthStencilBuffer
}
}
TechniqueFilter {
matchAll: [
FilterKey { name: "renderingStyle"; value: "outline" }
]
RenderPassFilter {
matchAny: [
FilterKey {
name: "pass"; value: "geometry"
}
]
ClearBuffers {
buffers: ClearBuffers.ColorDepthStencilBuffer
RenderTargetSelector {
target: RenderTarget {
attachments : [
RenderTargetOutput {
objectName : "color"
attachmentPoint : RenderTargetOutput.Color0
texture : Texture2D {
id : colorAttachment
width : surfaceSelector.surface.width
height : surfaceSelector.surface.height
format : Texture.RGBA32F
}
}
]
}
}
}
}
RenderPassFilter {
parameters: [
Parameter { name: "color"; value: colorAttachment },
Parameter { name: "winSize"; value : Qt.size(surfaceSelector.surface.width, surfaceSelector.surface.height) }
]
matchAny: [
FilterKey {
name: "pass"; value: "outline"
}
]
}
}
}
}
}
}
},
InputSettings { }
]
PhongMaterial {
id: material
}
Material {
id: outlineMaterial
effect: Effect {
techniques: [
Technique {
graphicsApiFilter {
api: GraphicsApiFilter.OpenGL
majorVersion: 3
minorVersion: 1
profile: GraphicsApiFilter.CoreProfile
}
filterKeys: [
FilterKey { name: "renderingStyle"; value: "outline" }
]
renderPasses: [
RenderPass {
filterKeys: [
FilterKey { name: "pass"; value: "geometry" }
]
shaderProgram: ShaderProgram {
vertexShaderCode: "
#version 150 core
in vec3 vertexPosition;
uniform mat4 modelViewProjection;
void main()
{
gl_Position = modelViewProjection * vec4( vertexPosition, 1.0 );
}
"
fragmentShaderCode: "
#version 150 core
out vec4 fragColor;
void main()
{
fragColor = vec4( 1.0, 0.0, 0.0, 1.0 );
}
"
}
}
]
}
]
}
}
SphereMesh {
id: sphereMesh
radius: 3
}
Transform {
id: sphereTransform
}
Transform {
id: sphereTransform2
// TODO workaround because the transform cannot be shared
matrix: sphereTransform.matrix
}
Entity {
id: sphereEntity
components: [ sphereMesh, material, sphereTransform ]
}
Entity {
id: sphereOutlineEntity
components: [ sphereMesh, outlineMaterial, sphereTransform2 ]
}
Entity {
id: outlineQuad
components: [
PlaneMesh {
width: 2.0
height: 2.0
meshResolution: Qt.size(2, 2)
},
Transform {
rotation: fromAxisAndAngle(Qt.vector3d(1, 0, 0), 90)
},
Material {
effect: Effect {
techniques: [
Technique {
filterKeys: [
FilterKey { name: "renderingStyle"; value: "outline" }
]
graphicsApiFilter {
api: GraphicsApiFilter.OpenGL
profile: GraphicsApiFilter.CoreProfile
majorVersion: 3
minorVersion: 1
}
renderPasses : RenderPass {
filterKeys : FilterKey { name : "pass"; value : "outline" }
shaderProgram : ShaderProgram {
vertexShaderCode: "
#version 150
in vec4 vertexPosition;
uniform mat4 modelMatrix;
void main()
{
gl_Position = modelMatrix * vertexPosition;
}
"
fragmentShaderCode: "
#version 150
uniform sampler2D color;
uniform vec2 winSize;
out vec4 fragColor;
void main()
{
int lineWidth = 5;
vec2 texCoord = gl_FragCoord.xy / winSize;
vec2 texCoordUp = (gl_FragCoord.xy + vec2(0, lineWidth)) / winSize;
vec2 texCoordDown = (gl_FragCoord.xy + vec2(0, -lineWidth)) / winSize;
vec2 texCoordRight = (gl_FragCoord.xy + vec2(lineWidth, 0)) / winSize;
vec2 texCoordLeft = (gl_FragCoord.xy + vec2(-lineWidth, 0)) / winSize;
vec4 col = texture(color, texCoord);
vec4 colUp = texture(color, texCoordUp);
vec4 colDown = texture(color, texCoordDown);
vec4 colRight = texture(color, texCoordRight);
vec4 colLeft = texture(color, texCoordLeft);
if ((colUp == colDown && colRight == colLeft) || col.a == 0.0)
discard;
fragColor = col;
}
"
}
}
}]
}
}
]
}
}
The result:

Related

Classifying cubic bezier curves according to Loop & Blinn 2005

I am trying flatten a bezier path (remove all intersections and replace with end points) as part of the implementation of the rendering algorithm described here and found the algorithm described by Loop and Blinn in GPU Gems 3 Ch. 25, with correction for $a3$'s cross product to detect curve self-intersection (loops).
The algorithm calls for the evaluation of the expression discr=d₁²(3d₂²-4d₁d₃) such that the curve is self-intersecting iff the discr < 0.
where
d₁=a₁-2a₂+3a₃,
d₂=-a₂+3a₃,
d₃=3a₃
a₁=b₀·(b₃⨯b₂),
a₂=b₁·(b₀⨯b₃),
a₃=b₂·(b₁⨯b₀)
I have implemented the algorithm in the sample code below, as well as that of a few other algorithms I have been able to find. None of them agree, and all of them are wrong. Specifically, I have implemented
The algorithm described in GPU Gems 3
The same algorithm as described in the original paper.
The same algorithm as implemented in paper.js (issue)(sketch).
The curve canonicalization algorithm described in Pomax' Bezier Primer.
That all three algorithms disagree suggests a major misunderstanding. What is it, and (more importantly) how might I identify such an issue myself?
I referred to the questions here and here, as well as the paper in which Loop & Blinn describe their approach, to no effect.
#[derive(Clone, Copy, Debug, PartialEq)]
enum Kind {
Serpentine,
Cusp,
Loop,
Quadratic,
Line,
}
#[derive(Clone, Copy)]
pub struct Point {
x: f32,
y: f32,
}
#[derive(Clone, Copy)]
struct Vec3 {
x: f32,
y: f32,
z: f32,
}
impl Vec3 {
fn from_point(p: Point) -> Vec3 {
Vec3 {
x: p.x,
y: p.y,
z: 1.0,
}
}
fn dot(&self, other: Vec3) -> f32 {
self.x * other.x + self.y * other.y + self.z * other.z
}
fn cross(&self, other: Vec3) -> Vec3 {
Vec3 {
x: self.y * other.z - self.z * other.y,
y: self.z * other.x - self.x * other.z,
z: self.x * other.y - self.y * other.x,
}
}
}
fn is_zero(f: f32) -> bool {
f.abs() < 0.000001
}
fn approx_eq(a: f32, b: f32) -> bool {
is_zero((a - b).abs())
}
fn normalize(a: f32, b: f32, c: f32) -> (f32, f32, f32) {
let len = (a * a + b * b + c * c).sqrt();
if is_zero(len) {
(0.0, 0.0, 0.0)
} else {
(a / len, b / len, c / len)
}
}
pub fn classify(curve: &[Point; 4]) {
let p0 = Vec3::from_point(curve[0]);
let p1 = Vec3::from_point(curve[1]);
let p2 = Vec3::from_point(curve[2]);
let p3 = Vec3::from_point(curve[3]);
let loop_blinn = {
let det1 = -p3.dot(p2.cross(p0));
let det2 = p3.dot(p2.cross(p0));
let det3 = -p2.dot(p1.cross(p0));
let (det1, det2, det3) = normalize(det1, det2, det3);
let discr = det2 * det2 * (3.0 * det2 * det2 - 4.0 * det3 * det1);
if is_zero(det1) {
if !is_zero(det2) {
Kind::Cusp
} else if is_zero(det3) {
Kind::Line
} else {
Kind::Quadratic
}
} else if is_zero(discr) {
Kind::Cusp
} else if discr > 0.0 {
Kind::Serpentine
} else {
Kind::Loop
}
};
let gpu_gems = {
let a1 = p0.dot(p3.cross(p2));
let a2 = p1.dot(p0.cross(p3));
let a3 = p2.dot(p1.cross(p0));
let d1 = a1 - 2.0 * a2 + 3.0 * a3;
let d2 = -a2 + 3.0 * a3;
let d3 = 3.0 * a3;
let discr = d2 * d2 * (3.0 * d2 * d2 - 4.0 * d1 * d3);
if is_zero(d1) {
if is_zero(d2) && is_zero(d3) {
Kind::Line
} else {
Kind::Quadratic
}
} else if is_zero(discr) {
Kind::Cusp
} else if discr < 0.0 {
Kind::Serpentine
} else {
Kind::Loop
}
};
let paper_js = {
let x1 = p0.x;
let y1 = p0.y;
let x2 = p1.x;
let y2 = p1.y;
let x3 = p2.x;
let y3 = p2.y;
let x4 = p3.x;
let y4 = p3.y;
let a1 = x1 * (y4 - y3) + y1 * (x3 - x4) + x4 * y3 - x3 * y4;
let a2 = x2 * (y1 - y4) + y2 * (x4 - x1) + x1 * y4 - y1 * x4;
let a3 = x3 * (y2 - y1) + y3 * (x1 - x2) + x2 * y1 - y2 * x1;
let d3 = 3.0 * a3;
let d2 = d3 - a2;
let d1 = d2 - a2 + a1;
let (d1, d2, d3) = normalize(d1, d2, d3);
let d = 3.0 * d2 * d2 - 4.0 * d1 * d3;
if is_zero(d1) {
if is_zero(d2) {
if is_zero(d3) {
Kind::Line
} else {
Kind::Quadratic
}
} else {
Kind::Serpentine
}
} else if is_zero(d) {
Kind::Cusp
} else if d > 0.0 {
Kind::Serpentine
} else {
Kind::Loop
}
};
let pomax_primer = {
let y31 = p3.y / p1.y;
let y21 = p2.y / p1.y;
let x32 = (p3.x - p2.x * y31) / (p2.x - p1.x * y21);
let x = x32;
let y = y31 + x32 * (1.0 - y21);
let cusp_line = (-(x * x) + 2.0 * x + 3.0) / 4.0;
let loop_at_1 = ((3.0 * 4.0 * x - x * x).sqrt() - x) / 2.0;
let loop_at_0 = (-(x * x) + 3.0 * x) / 3.0;
if x > 1.0 || y > 1.0 {
Kind::Serpentine
} else if (0.0..1.0).contains(&x) {
if approx_eq(loop_at_1, y) {
Kind::Loop
} else if approx_eq(cusp_line, y) {
Kind::Cusp
} else if y < cusp_line || y > loop_at_1 {
Kind::Serpentine
} else {
Kind::Loop
}
} else if approx_eq(loop_at_0, y) {
Kind::Loop
} else if approx_eq(cusp_line, y) {
Kind::Cusp
} else if y < cusp_line || y > loop_at_0 {
Kind::Loop
} else {
Kind::Serpentine
}
};
println!("\tMethod 1 (Loop Blinn 2005): {:?}", loop_blinn);
println!("\tMethod 2 (GPU Gems 3.25): {:?}", gpu_gems);
println!("\tMethod 3 (Paper.js): {:?}", paper_js);
println!("\tMethod 4 (Pomax Primer): {:?}", pomax_primer);
}
pub fn main() {
let point = [
Point { x: 1.0, y: 1.0 },
Point { x: 1.0, y: 1.0 },
Point { x: 1.0, y: 1.0 },
Point { x: 1.0, y: 1.0 },
];
println!("Expecting: Kind::Line");
classify(&point);
let line = [
Point { x: 1.0, y: 1.0 },
Point { x: 2.0, y: 1.0 },
Point { x: 3.0, y: 1.0 },
Point { x: 4.0, y: 1.0 },
];
println!("Expecting: Kind::Line");
classify(&line);
// loop
let normal_loop = [
Point { x: 75.0, y: 98.0 },
Point { x: 195.0, y: 201.0 },
Point { x: 63.0, y: 198.0 },
Point { x: 135.0, y: 103.0 },
];
println!("Expecting: Kind::Loop");
classify(&normal_loop);
let end_loop = [
Point { x: 120.0, y: 331.0 },
Point { x: 246.0, y: 261.0 },
Point { x: 187.0, y: 242.0 },
Point { x: 148.0, y: 314.0 },
];
println!("Expecting: Kind::Loop");
classify(&end_loop);
// cusp
let cusp = [
Point { x: 272.0, y: 305.0 },
Point { x: 93.0, y: 223.0 },
Point { x: 221.0, y: 223.0 },
Point { x: 148.0, y: 314.0 },
];
println!("Expecting: Kind::Cusp");
classify(&cusp);
// serpentine
let serpentine = [
Point { x: 148.0, y: 314.0 },
Point { x: 187.0, y: 242.0 },
Point { x: 246.0, y: 261.0 },
Point { x: 272.0, y: 305.0 },
];
println!("Expecting: Kind::Serpentine");
classify(&serpentine);
let serpentine2 = [
Point { x: 110.0, y: 150.0 },
Point { x: 25.0, y: 190.0 },
Point { x: 210.0, y: 250.0 },
Point { x: 210.0, y: 30.0 },
];
println!("Expecting: Kind::Serpentine");
classify(&serpentine2);
}

how to add PathLine to ShapePath with a Repeater?

UPDATE 1 - i can get it to work using Javascript - but that seems to be a little bit unoptimized (from 2-3% to 30% load with qmlscene.exe when activated) - complete recreation when the model changes (its not ever growing), is that the only way or is there a more "declarative" style available?
How can i add a PathLine to a ShapePath based on a Model?
i know how to use a Repeater from outside of Items but not how to implode them inside of Items
do i need some sort of Repeater-Delegate on pathElements?
and i don't want to use the LineSeries component
import QtQuick.Shapes 1.15
import QtQuick 2.15
Shape {
ListModel {
id: myPositions
ListElement { x: 0; y:38 }
ListElement { x: 10; y: 28 }
ListElement { x: 20; y: 30 }
ListElement { x: 30; y: 14 }
}
ShapePath {
strokeColor: "black"
strokeWidth: 1
fillColor: "transparent"
startX: 0
startY: 0
PathLine { x: 0; y: 38 }
PathLine { x: 10; y: 28 }
PathLine { x: 20; y: 30 }
PathLine { x: 30; y: 14 }
// Repeater {
// model: myPositions
// PathLine { x: model.x; y: model.y }
// }
}
}
UPDATE 1
import QtQuick.Shapes 1.15
import QtQuick 2.15
Shape {
ListModel {
id: myPositions
}
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
Timer
{
interval: 100
running: true
repeat: true
property real myX: 0
onTriggered: {
myPositions.append({"x":myX, "y":getRandomInt(0,30)})
myX = myX + 10
}
}
function createPathLineElements(positionsModel, shapePath)
{
var pathElements = []
for (var i = 0; i < positionsModel.count; i++)
{
var pos = myPositions.get(i)
var pathLine = Qt.createQmlObject('import QtQuick 2.15; PathLine {}',
shapePath);
pathLine.x = pos.x
pathLine.y = pos.y
pathElements.push(pathLine)
}
return pathElements
}
ShapePath {
id: myPath
strokeColor: "black"
strokeWidth: 1
fillColor: "transparent"
startX: 0
startY: 0
pathElements: createPathLineElements(myPositions, myPath)
}
}
Use an Instantiator:
Shape {
ListModel {
id: myPositions
}
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
Timer {
interval: 100
running: true
repeat: true
property real myX: 0
onTriggered: {
myPositions.append({"x":myX, "y":getRandomInt(0,30)})
myX = myX + 10
}
}
ShapePath {
id: myPath
fillColor: "transparent"
strokeColor: "red"
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
strokeWidth: 3
strokeStyle: ShapePath.SolidLine
}
Instantiator {
model: myPositions
onObjectAdded: myPath.pathElements.push(object)
PathLine {
x: model.x
y: model.y
}
}
}
You can't use repeater in that element.
The most performant way is to use QQuickItem in order to create a custom Item which draws incremental path.
Yet another simpler ways are:
1- Use PathSvg element and set its path runtime like below:
ShapePath {
fillColor: "transparent"
strokeColor: "red"
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
strokeWidth: 3
strokeStyle: ShapePath.SolidLine
PathSvg { id: ps; path: parent.p } //<== fill path property using js
property string p: ""
Component.onCompleted: {
for ( var i = 0; i < myModel.count; i++) {
p += "L %1 %2".arg(myModel.get(i).x).arg(myModel.get(i).y);
}
ps.path = p;
}
}
2- If you know steps, so you can pre-declare all PathLines and then set their values runtime. Just like a heart rate line on a health care monitor.

invert parent qt3d entity transform (doesn't work for scale3D)

For reasons that are more complex than this minimal testcase, I need to have a entity (childEntity, the magenta box) child of another entity (parentEntity, the cyan box), but childEntity should be independent of parentEntity's transform.
Therefore I add this handler:
QtQuick.Connections {
target: parentTransform
onMatrixChanged: {
// cancel parent's transform
var m = parentTransform.matrix
var i = m.inverted()
childTransform.matrix = i
// debug:
console.log(parentTransform.matrix.times(i))
}
}
which works well for cancelling out parent's translation and rotation, but not for scale.
When parent's scale3D is not [1,1,1] and rotation is also set, then childEntity appears distorted, despite the product of parentTransform.matrix times childTransform.matrix gives the 4x4 identity. Why?
Minimal testcase: (load into a QQuickView)
import QtQml 2.12 as QtQml
import QtQuick 2.12 as QtQuick
import QtQuick.Controls 2.12 as QtQuickControls
import QtQuick.Scene3D 2.0
import Qt3D.Core 2.0
import Qt3D.Render 2.0
import Qt3D.Input 2.0
import Qt3D.Extras 2.0
Scene3D {
function change_translation_and_rotation() {
parentTransform.translation.x = 0.1
parentTransform.translation.y = 0.5
parentTransform.translation.z = 2
parentTransform.rotationX = 30
parentTransform.rotationY = 60
parentTransform.rotationZ = 10
}
function change_rotation_and_scale() {
parentTransform.rotationX = 30
parentTransform.rotationY = 60
parentTransform.rotationZ = 10
parentTransform.scale3D.x = 0.1
parentTransform.scale3D.y = 0.5
parentTransform.scale3D.z = 2
}
function reset_transform() {
parentTransform.translation.x = -0.5
parentTransform.translation.y = 0
parentTransform.translation.z = 0.5
parentTransform.rotationX = 0
parentTransform.rotationY = 0
parentTransform.rotationZ = 0
parentTransform.scale3D.x = 1
parentTransform.scale3D.y = 1
parentTransform.scale3D.z = 1
}
data: [
QtQml.Connections {
target: parentTransform
onMatrixChanged: {
// cancel parent's transform
var m = parentTransform.matrix
var i = m.inverted()
childTransform.matrix = i
// debug:
console.log(parentTransform.matrix.times(i))
}
},
QtQuick.Column {
spacing: 5
QtQuick.Repeater {
id: buttons
model: ["change_translation_and_rotation", "change_rotation_and_scale", "reset_transform"]
delegate: QtQuickControls.Button {
text: modelData.replace(/_/g, ' ')
font.bold: focus
onClicked: {focus = true; scene3d[modelData]()}
}
}
}
]
id: scene3d
anchors.fill: parent
aspects: ["render", "logic", "input"]
Entity {
id: root
components: [RenderSettings {activeFrameGraph: ForwardRenderer {camera: mainCamera}}, InputSettings {}]
Camera {
id: mainCamera
projectionType: CameraLens.PerspectiveProjection
fieldOfView: 45
aspectRatio: 16/9
nearPlane : 0.1
farPlane : 1000.0
position: Qt.vector3d(-3.46902, 4.49373, -3.78577)
upVector: Qt.vector3d(0.41477, 0.789346, 0.452641)
viewCenter: Qt.vector3d(0.0, 0.5, 0.0)
}
OrbitCameraController {
camera: mainCamera
}
Entity {
id: parentEntity
components: [
CuboidMesh {
xExtent: 1
yExtent: 1
zExtent: 1
},
PhongMaterial {
ambient: "#6cc"
},
Transform {
id: parentTransform
translation: Qt.vector3d(-0.5, 0, 0.5)
}
]
Entity {
id: childEntity
components: [
CuboidMesh {
xExtent: 0.5
yExtent: 0.5
zExtent: 0.5
},
PhongMaterial {
ambient: "#c6c"
},
Transform {
id: childTransform
translation: Qt.vector3d(-0.5, 0, 0.5)
}
]
}
}
QtQuick.Component.onCompleted: reset_transform()
}
}
The problem is that the QTransform node does not store the transformation as a general 4x4 matrix. Rather is decomposes the matrix into a 3 transformations that are applied in fixed order:
S - a diagonal scaling matrix
R - the rotation matrix
T - translation
and then applies it in the order T * R * S * X to a point X.
The documentation for the matrix property writes about this decomposition step https://doc.qt.io/qt-5/qt3dcore-qtransform.html#matrix-prop
So when the transformation on the parent is M = T * R * S, then the inverse on the child will be M^-1 = S^-1 * R^-1 * T^-1. Setting the inverse on the child QTransform will attempt to decompose it in the same way:
M^-1 = T_i * R_i * S_i = S^-1 * R^-1 * T^-1
That doesn't work, because particularly S and R don't commute like this.
You can test this in your code by comparing the value of childTransform.matrix and i after you set the childTransform.matrix.
I think the only solution is to rather have 3 QTransforms on entities nested above the child to implement the correct order of the inverses as S^-1 * R^-1 * T^-1.
It is pretty simple to compute the inverse S^-1, R^-1 T^-1 from the corresponding parameters in the parent QTransform.

"cant add two pointers" error C2110, using a GLfloat array.cannot

So i think this is the part of my code that has the error. I think im not sending my array correctly or its something different when trying to add my vertex vectors.
void tetrahedron(GLfloat vertice[6][3])
{
glTranslatef(0.3f, 0.0f, 0.0f);
glRotatef(45.0f, 0.0, 1.0, 0.0);
glRotatef(45.0f, 0.0, 0.0, 1.0);
//Cara1
glBegin(GL_QUADS);
glColor3f(0.1, 0.0, 0.9);
GLfloat temp[3];
temp = vertice[2] + vertice[5] + vertice[0];
glVertex3fv(temp);
glVertex3fv(temp);
glVertex3fv(temp);
glEnd();
}
The error is C2110: '+': cannot add two pointers
in: temp = vertice[2] + vertice[5] + vertice[0];
void octahedron(GLfloat vertice[6][3])
{
glTranslatef(0.0f, 0.0f, -5.0f);
glRotatef(45.0f, 0.0, 1.0, 0.0);
glRotatef(45.0f, 0.0, 0.0, 1.0);
}
nothing special there
In the next function is where i create my array that im trying to send to octahedron and tetrahedron:
void display(void) // Creamos la funcion donde se dibuja
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Limiamos pantalla y Depth Buffer
glMatrixMode(GL_MODELVIEW);
glLoadIdentity(); // Reinicializamos la actual matriz Modelview
GLfloat vertice[6][3] = {
{0.0, 0.5, 0.0}, //arriba
{ 0.0,-0.5,0.0 }, //abajo
{ 0.5,0.0,0.0 }, //izq
{ -0.5,0.0,0.0 }, //der
{ 0.0,0.0,0.5 }, //frontal
{ 0.0,0.0,-0.5 }, //trasero
};
octahedron(vertice);
tetrahedron(vertice);
glFlush();
}
Found my problem, it was that i forgot how to add arrays in general. Solved with a for and stuff:
GLfloat temp[3];
int i;
//cara 1
for ( i = 0; i < 3; i++); {
temp[i]= (vertice[2][i]) + (vertice[5][i]) + (vertice[0][i]);
}

Qt Quick Controls 2 on-screen Number pad on spinbox

In a touchpanel application, is it possible to edit the integer value of a qml SpinBox, from QtQuickControls 2.0, in such a way that a numeric keypad appears on-screen and one can enter the exact value?
Or do you have any idea on how to embed this predefined number pad in a customized spinbox, that should pop-up when the user taps on the integer number?
The numpad can be set to be invisible and put on top of everything, then you can have a function to enable its visibility and set it's target. When you are done with typing the number, you set the target value to the numpad value, and hide it again. This way you can target different spinboxes.
As of the actual method to request the keypad, you can put a MouseArea to fill the spinbox on top of it. Or you can make the mouse area narrower, so that the plus/minus buttons of the spinbox are still clickable.
Also keep in mind that numpad you have linked is not actually functional.
Thanks for your suggestion. I came up with a first quick solution based on your idea and the number pad from the example section. I post it here, just in case it helps someone else as starting point. I am a QML beginner, so happy to get any improvements or corrections.
See attached screenshot of the numeric virtual touchpad appearing when clicking on the spinbox number
SpinBox {
id: boxCommPos
x: 50
y: 50
z: 0
width: 200
height: 50
to: 360
editable: false
contentItem: TextInput {
z: 1
text: parent.textFromValue(parent.value, parent.locale)
font: parent.font
//color: "#21be2b"
//selectionColor: "#21be2b"
//selectedTextColor: "#ffffff"
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
readOnly: !parent.editable
validator: parent.validator
inputMethodHints: Qt.ImhFormattedNumbersOnly
}
Label {
id: commUnits
x: 125
y: 0
z: 3
text: qsTr("º")
font.pointSize: 16
anchors.right: parent.right
anchors.rightMargin: 45
}
MouseArea {
id: padCommPos
x: 40
y: 0
z: 2
width: parent.width-x-x
height: 50
onClicked: touchpad.visible=true
}
}
NumberTouchpad {
id: touchpad
x: 470
y: -20
z: 1
scale: 0.90
visible: false
Rectangle {
id: backrect
x: -parent.x/parent.scale
z: -1
width: parent.parent.width/parent.scale
height: parent.parent.height/parent.scale
color: "#eceeea"
opacity: 0.5
MouseArea {
anchors.fill: parent
}
}
}
The file NumberPad.qml
import QtQuick 2.0
Grid {
columns: 3
columnSpacing: 32
rowSpacing: 16
signal buttonPressed
Button { text: "7" }
Button { text: "8" }
Button { text: "9" }
Button { text: "4" }
Button { text: "5" }
Button { text: "6" }
Button { text: "1" }
Button { text: "2" }
Button { text: "3" }
Button { text: "0" }
Button { text: "."; dimmable: true }
//Button { text: " " }
Button { text: "±"; color: "#6da43d"; operator: true; dimmable: true }
//Button { text: "−"; color: "#6da43d"; operator: true; dimmable: true }
//Button { text: "+"; color: "#6da43d"; operator: true; dimmable: true }
//Button { text: "√"; color: "#6da43d"; operator: true; dimmable: true }
//Button { text: "÷"; color: "#6da43d"; operator: true; dimmable: true }
//Button { text: "×"; color: "#6da43d"; operator: true; dimmable: true }
Button { text: "C"; color: "#6da43d"; operator: true }
Button { text: "✔"; color: "#6da43d"; operator: true; dimmable: true }
Button { text: "X"; color: "#6da43d"; operator: true }
}
Display.qml
// Copyright (C) 2015 The Qt Company Ltd.
import QtQuick 2.0
import QtQuick.Window 2.0
Item {
id: display
property real fontSize: Math.floor(Screen.pixelDensity * 10.0)
property string fontColor: "#000000"
property bool enteringDigits: false
property int maxDigits: (width / fontSize) + 1
property string displayedOperand
property string errorString: qsTr("ERROR")
property bool isError: displayedOperand === errorString
function displayOperator(operator)
{
listView.model.append({ "operator": operator, "operand": "" })
enteringDigits = true
listView.positionViewAtEnd()
//console.log("display",operator);
}
function newLine(operator, operand)
{
displayedOperand = displayNumber(operand)
listView.model.append({ "operator": operator, "operand": displayedOperand })
enteringDigits = false
listView.positionViewAtEnd()
//console.log("newLine",operator);
}
function appendDigit(digit)
{
if (!enteringDigits)
listView.model.append({ "operator": "", "operand": "" })
var i = listView.model.count - 1;
listView.model.get(i).operand = listView.model.get(i).operand + digit;
enteringDigits = true
listView.positionViewAtEnd()
//console.log("num is ", digit);
}
function setDigit(digit)
{
var i = listView.model.count - 1;
listView.model.get(i).operand = digit;
listView.positionViewAtEnd()
//console.log("setDigit",digit);
}
function clear()
{
displayedOperand = ""
if (enteringDigits) {
var i = listView.model.count - 1
if (i >= 0)
listView.model.remove(i)
enteringDigits = false
}
//console.log("clearing");
}
// Returns a string representation of a number that fits in
// display.maxDigits characters, trying to keep as much precision
// as possible. If the number cannot be displayed, returns an
// error string.
function displayNumber(num) {
if (typeof(num) != "number")
return errorString;
var intNum = parseInt(num);
var intLen = intNum.toString().length;
// Do not count the minus sign as a digit
var maxLen = num < 0 ? maxDigits + 1 : maxDigits;
if (num.toString().length <= maxLen) {
if (isFinite(num))
return num.toString();
return errorString;
}
// Integer part of the number is too long - try
// an exponential notation
if (intNum == num || intLen > maxLen - 3) {
var expVal = num.toExponential(maxDigits - 6).toString();
if (expVal.length <= maxLen)
return expVal;
}
// Try a float presentation with fixed number of digits
var floatStr = parseFloat(num).toFixed(maxDigits - intLen - 1).toString();
if (floatStr.length <= maxLen)
return floatStr;
return errorString;
}
Item {
id: theItem
width: parent.width
height: parent.height
Rectangle {
id: rect
x: 0
color: "#eceeea"
height: parent.height
width: display.width
}
/*Image {
anchors.right: rect.left
source: "images/paper-edge-left.png"
height: parent.height
fillMode: Image.TileVertically
}
Image {
anchors.left: rect.right
source: "images/paper-edge-right.png"
height: parent.height
fillMode: Image.TileVertically
}
Image {
id: grip
source: "images/paper-grip.png"
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 20
}*/
ListView {
id: listView
width: display.width
height: display.height
delegate: Item {
height: display.fontSize * 1.1
width: parent.width
Text {
id: operator
font.pixelSize: display.fontSize
color: "#6da43d"
text: model.operator
}
Text {
id: operand
y:5
font.pixelSize: display.fontSize
color: display.fontColor
anchors.right: parent.right
anchors.rightMargin: 22
text: model.operand
}
}
model: ListModel { }
}
}
}
calculator.qs
// Copyright (C) 2015 The Qt Company Ltd.
var curVal = 0
var memory = 0
var lastOp = ""
var previousOperator = ""
var digits = ""
function disabled(op) {
if (op == "✔")
display.fontColor="#000000"
if (op=="X")
return false
else if (op == "✔" && (digits.toString().search(/\./) != -1 || digits.toString().search(/-/)!= -1 || parseInt(digits)>359)) {
display.fontColor="#ff0000"
return true
}
else if (digits == "" && !((op >= "0" && op <= "9") || op == "."))
return true
else if (op == '=' && previousOperator.length != 1)
return true
else if (op == "." && digits.toString().search(/\./) != -1) {
return true
} else if (op == "√" && digits.toString().search(/-/) != -1) {
return true
} else {
return false
}
}
function digitPressed(op)
{
if (disabled(op))
return
if (digits.toString().length >= display.maxDigits)
return
if (lastOp.toString().length == 1 && ((lastOp >= "0" && lastOp <= "9") || lastOp == ".") ) {
digits = digits + op.toString()
display.appendDigit(op.toString())
} else {
digits = op
display.appendDigit(op.toString())
}
lastOp = op
}
function operatorPressed(op)
{
if (disabled(op))
return
lastOp = op
if (op == "±") {
digits = Number(digits.valueOf() * -1)
display.setDigit(display.displayNumber(digits))
return
}
if (previousOperator == "+") {
digits = Number(digits.valueOf()) + Number(curVal.valueOf())
} else if (previousOperator == "−") {
digits = Number(curVal.valueOf()) - Number(digits.valueOf())
} else if (previousOperator == "×") {
digits = Number(curVal) * Number(digits.valueOf())
} else if (previousOperator == "÷") {
digits = Number(curVal) / Number(digits.valueOf())
}
if (op == "+" || op == "−" || op == "×" || op == "÷") {
previousOperator = op
curVal = digits.valueOf()
digits = ""
display.displayOperator(previousOperator)
return
}
if (op == "=") {
display.newLine("=", digits.valueOf())
}
curVal = 0
previousOperator = ""
if (op == "1/x") {
digits = (1 / digits.valueOf()).toString()
} else if (op == "x^2") {
digits = (digits.valueOf() * digits.valueOf()).toString()
} else if (op == "Abs") {
digits = (Math.abs(digits.valueOf())).toString()
} else if (op == "Int") {
digits = (Math.floor(digits.valueOf())).toString()
} else if (op == "√") {
digits = Number(Math.sqrt(digits.valueOf()))
display.newLine("√", digits.valueOf())
} else if (op == "mc") {
memory = 0;
} else if (op == "m+") {
memory += digits.valueOf()
} else if (op == "mr") {
digits = memory.toString()
} else if (op == "m-") {
memory = digits.valueOf()
} else if (op == "backspace") {
digits = digits.toString().slice(0, -1)
display.clear()
display.appendDigit(digits)
} else if (op == "✔") {
window.visible=false
boxCommPos.value=parseInt(digits)
display.clear()
curVal = 0
memory = 0
lastOp = ""
digits = ""
} else if (op == "X") {
window.visible=false
display.clear()
curVal = 0
memory = 0
lastOp = ""
digits = ""
}
// Reset the state on 'C' operator or after
// an error occurred
if (op == "C" || display.isError) {
display.clear()
curVal = 0
memory = 0
lastOp = ""
digits = ""
}
}
Button.qml
// Copyright (C) 2015 The Qt Company Ltd.
import QtQuick 2.0
Item {
id: button
property alias text: textItem.text
property color color: "#eceeea"
property bool operator: false
property bool dimmable: false
property bool dimmed: false
width: 30
height: 50
Text {
id: textItem
font.pixelSize: 48
wrapMode: Text.WordWrap
lineHeight: 0.75
color: (dimmable && dimmed) ? Qt.darker(button.color) : button.color
Behavior on color { ColorAnimation { duration: 120; easing.type: Easing.OutElastic} }
states: [
State {
name: "pressed"
when: mouse.pressed && !dimmed
PropertyChanges {
target: textItem
color: Qt.lighter(button.color)
}
}
]
}
MouseArea {
id: mouse
anchors.fill: parent
anchors.margins: -5
onClicked: {
if (operator)
window.operatorPressed(parent.text)
else
window.digitPressed(parent.text)
}
}
function updateDimmed() {
dimmed = window.isButtonDisabled(button.text)
}
Component.onCompleted: {
numPad.buttonPressed.connect(updateDimmed)
updateDimmed()
}
}

Resources