466 lines
15 KiB
QML
466 lines
15 KiB
QML
/*
|
|
* Komplex Wallpaper Engine
|
|
* Copyright (C) 2025 @DigitalArtifex | github.com/DigitalArtifex
|
|
*
|
|
* ShaderChannel.qml
|
|
*
|
|
* This component provides a configurable channel type to more closely resemble how ShaderToys
|
|
* works. It can be configured to be an Image, Video, Shader, Audio, or CubeMap that can be
|
|
* used as the input channel of another ShaderChannel, allowing for more complex shader compositions.
|
|
*
|
|
* It would also appear that in order to support the audio channel, we will need to
|
|
* implement a way to capture the desktop audio in C++, as the MediaPlayer component does not support this.
|
|
* This will have to come later.
|
|
*
|
|
* Volumetric shaders do not seem to be widely used in ShaderToys, so it has not been implemented here.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>
|
|
*/
|
|
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtMultimedia
|
|
import QtQuick3D
|
|
|
|
import com.github.digitalartifex.komplex 1.0 as Komplex
|
|
|
|
Item
|
|
{
|
|
enum Type
|
|
{
|
|
ImageChannel,
|
|
VideoChannel,
|
|
ShaderChannel,
|
|
CubeMapChannel,
|
|
AudioChannel
|
|
}
|
|
|
|
property int type: ShaderChannel.Type.ImageChannel
|
|
property string source: ""
|
|
|
|
property int fillMode: Image.PreserveAspectCrop
|
|
|
|
property var iChannel0: null
|
|
property var iChannel1: null
|
|
property var iChannel2: null
|
|
property var iChannel3: null
|
|
|
|
property vector3d iResolution
|
|
property real iResolutionScale: 1 // This is used to scale the resolution of the shader, allowing for performance optimizations
|
|
property real iTime: 0 //used by most motion shaders
|
|
property real iTimeDelta: 0
|
|
property var iChannelTime: [data.iTime, data.iTime, data.iTime, data.iTime] //individual channel time values
|
|
property real iSampleRate: 44100 //used by audio shaders
|
|
property int iFrame: 0
|
|
property var iFrameRate: 60
|
|
property vector4d iMouse
|
|
property real mouseBias: 1
|
|
property var iDate
|
|
property real iTimeScale: 1 // This is used to scale the time for the shader, allowing for slow motion or fast forward effects per channel
|
|
property int frameBufferChannel: -1
|
|
|
|
property bool invert
|
|
|
|
property var iChannelResolution: [Qt.vector3d(channel.width * iResolutionScale, channel.height * iResolutionScale, 1), Qt.vector3d(channel.width * iResolutionScale, channel.height * iResolutionScale, 1), Qt.vector3d(channel.width * iResolutionScale, channel.height * iResolutionScale, 1)]
|
|
|
|
QtObject
|
|
{
|
|
id: data
|
|
|
|
property vector4d iMouse: Qt.vector4d(channel.iMouse.x * channel.mouseBias, channel.iMouse.y * channel.mouseBias, channel.iMouse.z, channel.iMouse.w)
|
|
property real iTime: 0
|
|
property real lastITime: 0
|
|
property real angle: channel.invert ? 180 : 0
|
|
}
|
|
|
|
onITimeChanged:
|
|
{
|
|
iTimeDelta = iTime - data.lastITime
|
|
data.lastITime = iTime
|
|
data.iTime += iTimeDelta * iTimeScale
|
|
}
|
|
|
|
id: channel
|
|
visible: false // Set to false by default, main shader needs be set to true in MainWindow.qml
|
|
|
|
// This is used to dynamically load the appropriate channel type based on the `type` property of the channel
|
|
Loader
|
|
{
|
|
id: loader
|
|
anchors.fill: parent
|
|
|
|
sourceComponent: channelImage
|
|
|
|
states:[
|
|
State
|
|
{
|
|
when: channel.type === ShaderChannel.Type.ImageChannel
|
|
PropertyChanges
|
|
{
|
|
loader.sourceComponent: channelImage
|
|
}
|
|
},
|
|
State
|
|
{
|
|
when: channel.type === ShaderChannel.Type.VideoChannel
|
|
PropertyChanges
|
|
{
|
|
loader.sourceComponent: channelVideo
|
|
}
|
|
},
|
|
State
|
|
{
|
|
when: channel.type === ShaderChannel.Type.CubeMapChannel
|
|
PropertyChanges
|
|
{
|
|
loader.sourceComponent: channelCubeMap
|
|
}
|
|
},
|
|
State
|
|
{
|
|
when: channel.type === ShaderChannel.Type.ShaderChannel
|
|
PropertyChanges
|
|
{
|
|
loader.sourceComponent: channelShader
|
|
}
|
|
},
|
|
State
|
|
{
|
|
when: channel.type === ShaderChannel.Type.AudioChannel
|
|
PropertyChanges
|
|
{
|
|
loader.sourceComponent: channelAudio
|
|
|
|
loader.width: 512
|
|
loader.height: 2
|
|
}
|
|
}
|
|
]
|
|
|
|
transform: Rotation
|
|
{
|
|
id: channelRotation
|
|
origin.x: channel.width / 2
|
|
origin.y: channel.height / 2
|
|
|
|
// For vertical flipping, we need to transform the x axis
|
|
axis
|
|
{
|
|
x: 1
|
|
y: 0
|
|
z: 0
|
|
}
|
|
|
|
angle: data.angle
|
|
}
|
|
}
|
|
|
|
//The image channel will be the default channel type for backwards compatability
|
|
Component
|
|
{
|
|
id: channelImage
|
|
|
|
Image
|
|
{
|
|
id: image
|
|
anchors.fill: parent
|
|
source: Qt.resolvedUrl(channel.source)
|
|
fillMode: channel.fillMode
|
|
}
|
|
}
|
|
|
|
//Video channel
|
|
Component
|
|
{
|
|
id: channelVideo
|
|
|
|
VideoOutput
|
|
{
|
|
property alias duration: mediaPlayer.duration
|
|
property alias mediaSource: mediaPlayer.source
|
|
property alias metaData: mediaPlayer.metaData
|
|
property alias playbackRate: mediaPlayer.playbackRate
|
|
property alias position: mediaPlayer.position
|
|
property alias seekable: mediaPlayer.seekable
|
|
property alias volume: audioOutput.volume
|
|
|
|
signal sizeChanged
|
|
signal fatalError
|
|
|
|
id: videoOutput
|
|
|
|
visible: true
|
|
anchors.fill: parent
|
|
fillMode: VideoOutput.PreserveAspectCrop
|
|
smooth: true
|
|
|
|
onHeightChanged: this.sizeChanged()
|
|
|
|
MediaPlayer
|
|
{
|
|
id: mediaPlayer
|
|
videoOutput: videoOutput
|
|
loops: MediaPlayer.Infinite
|
|
source: Qt.resolvedUrl(channel.source)
|
|
|
|
audioOutput: AudioOutput
|
|
{
|
|
id: audioOutput
|
|
volume: 0
|
|
}
|
|
|
|
onErrorOccurred: function(error, errorString)
|
|
{
|
|
if (MediaPlayer.NoError !== error)
|
|
{
|
|
console.log("[qmlvideo] VideoItem.onError error " + error + " errorString " + errorString)
|
|
videoOutput.fatalError()
|
|
}
|
|
}
|
|
|
|
onSourceChanged:
|
|
{
|
|
if(mediaPlayer.source != "")
|
|
mediaPlayer.play()
|
|
else
|
|
mediaPlayer.stop()
|
|
}
|
|
}
|
|
|
|
function start() { mediaPlayer.play() }
|
|
function stop() { mediaPlayer.stop() }
|
|
function seek(position) { mediaPlayer.setPosition(position); }
|
|
}
|
|
}
|
|
|
|
// A shader channel can be a shader
|
|
Component
|
|
{
|
|
id: channelShader
|
|
|
|
Item
|
|
{
|
|
property var frameBufferData: undefined
|
|
id: channelShaderContent
|
|
anchors.fill: parent
|
|
|
|
// Setup the shader effect sources for each channel
|
|
// These are needed to provide the uniform data to the shader channel buffers
|
|
ShaderEffectSource
|
|
{
|
|
id: channelSource0
|
|
sourceItem: channel.iChannel0 ? channel.iChannel0 : null
|
|
live: true
|
|
smooth: true
|
|
sourceRect: Qt.rect(0,0, sourceItem ? sourceItem.width : 0, sourceItem ? sourceItem.height : 0)
|
|
wrapMode: ShaderEffectSource.Repeat
|
|
textureSize: Qt.size(channelShaderContent.width, channelShaderContent.height)
|
|
textureMirroring: ShaderEffectSource.NoMirroring
|
|
recursive: true
|
|
mipmap: true
|
|
}
|
|
|
|
ShaderEffectSource
|
|
{
|
|
id: channelSource1
|
|
sourceItem: channel.iChannel1 ? channel.iChannel1 : null
|
|
live: true
|
|
smooth: true
|
|
sourceRect: Qt.rect(0,0, sourceItem ? sourceItem.width : 0, sourceItem ? sourceItem.height : 0)
|
|
wrapMode: ShaderEffectSource.Repeat
|
|
textureSize: Qt.size(channelShaderContent.width, channelShaderContent.height)
|
|
textureMirroring: ShaderEffectSource.NoMirroring
|
|
recursive: true
|
|
mipmap: true
|
|
}
|
|
|
|
ShaderEffectSource
|
|
{
|
|
id: channelSource2
|
|
sourceItem: channel.iChannel2 ? channel.iChannel2 : null
|
|
live: true
|
|
smooth: true
|
|
sourceRect: Qt.rect(0,0, sourceItem ? sourceItem.width : 0, sourceItem ? sourceItem.height : 0)
|
|
wrapMode: ShaderEffectSource.Repeat
|
|
textureSize: Qt.size(channelShaderContent.width, channelShaderContent.height)
|
|
textureMirroring: ShaderEffectSource.NoMirroring
|
|
recursive: true
|
|
mipmap: true
|
|
}
|
|
|
|
ShaderEffectSource
|
|
{
|
|
id: channelSource3
|
|
sourceItem: channel.iChannel3 ? channel.iChannel3 : null
|
|
live: true
|
|
smooth: true
|
|
sourceRect: Qt.rect(0,0, sourceItem ? sourceItem.width : 0, sourceItem ? sourceItem.height : 0)
|
|
wrapMode: ShaderEffectSource.Repeat
|
|
textureSize: Qt.size(channelShaderContent.width, channelShaderContent.height)
|
|
textureMirroring: ShaderEffectSource.NoMirroring
|
|
recursive: true
|
|
mipmap: true
|
|
}
|
|
|
|
// recursive frame buffer
|
|
ShaderEffectSource
|
|
{
|
|
id: frameBufferSource
|
|
sourceItem: channelShaderContent
|
|
live: true
|
|
sourceRect: Qt.rect(0,0, channelShaderContent.width, channelShaderContent.height)
|
|
wrapMode: ShaderEffectSource.Repeat
|
|
recursive: true
|
|
mipmap: true
|
|
textureSize: Qt.size(channelShaderContent.width, channelShaderContent.height)
|
|
anchors.fill: parent
|
|
visible: false
|
|
textureMirroring: ShaderEffectSource.NoMirroring
|
|
}
|
|
|
|
// The shader effect that will be used to render the shader
|
|
ShaderEffect
|
|
{
|
|
property var iChannel0: channel.frameBufferChannel === 0 ? frameBufferSource : channelSource0
|
|
property var iChannel1: channel.frameBufferChannel === 1 ? frameBufferSource : channelSource1
|
|
property var iChannel2: channel.frameBufferChannel === 2 ? frameBufferSource : channelSource2
|
|
property var iChannel3: channel.frameBufferChannel === 3 ? frameBufferSource : channelSource3
|
|
|
|
property var iResolution: channel.iResolution
|
|
property var iTime: data.iTime
|
|
property var iTimeDelta: channel.iTimeDelta
|
|
property var iChannelTime: channel.iChannelTime
|
|
property var iSampleRate: channel.iSampleRate
|
|
property var iFrame: channel.iFrame
|
|
property var iFrameRate: channel.iFrameRate
|
|
property var iMouse: data.iMouse
|
|
property var iDate: channel.iDate
|
|
|
|
property var iChannelResolution: channel.iResolution
|
|
|
|
fragmentShader: Qt.resolvedUrl(channel.source)
|
|
anchors.fill: parent
|
|
|
|
id: channelShaderOutput
|
|
}
|
|
}
|
|
}
|
|
|
|
//In order to support CubeMaps, we will need to select a folder that contains a series of JPGs, named as described
|
|
//in the CubeMapTexture documentation ie "posx.jpg;negx.jpg;posy.jpg;negy.jpg;posz.jpg;negz.jpg"
|
|
Component
|
|
{
|
|
id: channelCubeMap
|
|
|
|
View3D
|
|
{
|
|
id: view3d
|
|
anchors.fill: parent
|
|
|
|
property real lastX: 0
|
|
property real lastY: 0
|
|
property bool mousePressed: false
|
|
property real yaw: 0
|
|
property real pitch: 0
|
|
|
|
environment: SceneEnvironment
|
|
{
|
|
backgroundMode: SceneEnvironment.SkyBoxCubeMap
|
|
skyBoxCubeMap: CubeMapTexture
|
|
{
|
|
source: channel.source !== "" ? Qt.resolvedUrl(channel.source) + "/%p.jpg" : ""
|
|
}
|
|
}
|
|
|
|
camera: PerspectiveCamera
|
|
{
|
|
id: camera
|
|
position: Qt.vector3d(0, 0, 10)
|
|
}
|
|
|
|
function updateCamera()
|
|
{
|
|
yaw -= (data.iMouse.x - lastX) * 0.5
|
|
pitch -= (data.iMouse.y - lastY) * -0.5
|
|
pitch = Math.max(-89, Math.min(89, pitch))
|
|
lastX = data.iMouse.x
|
|
lastY = data.iMouse.y
|
|
|
|
var radYaw = yaw * Math.PI / 180
|
|
var radPitch = pitch * Math.PI / 180
|
|
var x = Math.cos(radPitch) * Math.sin(radYaw)
|
|
var y = Math.sin(radPitch)
|
|
var z = Math.cos(radPitch) * Math.cos(radYaw)
|
|
|
|
camera.eulerRotation = Qt.vector3d((radPitch * 180 / Math.PI), (radYaw * 180 / Math.PI), 0)
|
|
}
|
|
|
|
Connections
|
|
{
|
|
target: channel
|
|
function onIMouseChanged()
|
|
{
|
|
view3d.updateCamera()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Connect the audio channel to the cpp backend
|
|
Component
|
|
{
|
|
id: channelAudio
|
|
|
|
Rectangle
|
|
{
|
|
anchors.fill: parent
|
|
color: "black"
|
|
Image
|
|
{
|
|
width: 512
|
|
height: 2
|
|
anchors.top: channel.invert ? undefined : parent.top
|
|
anchors.bottom: channel.invert ? parent.bottom : undefined
|
|
id: textureImage
|
|
source: "image://audiotexture/frame"
|
|
fillMode: Image.PreserveAspectFit
|
|
}
|
|
|
|
Timer
|
|
{
|
|
property int frame: 0
|
|
interval: 16
|
|
repeat: true
|
|
triggeredOnStart: true
|
|
running: true
|
|
|
|
onTriggered:
|
|
{
|
|
frame++
|
|
textureImage.source = "image://audiotexture/frame" + frame
|
|
}
|
|
}
|
|
|
|
Component.onCompleted:
|
|
{
|
|
Komplex.AudioModel.startCapture()
|
|
}
|
|
|
|
Component.onDestruction:
|
|
{
|
|
Komplex.AudioModel.stopCapture()
|
|
}
|
|
}
|
|
}
|
|
} |