workshop rework

This commit is contained in:
Vysyr committed 2026-10-31 17:03:41 +01:00
1 parent a3a80dfa16
commit fb39e290c0
115 files changed
+1865 -497

No files matched your search

+6
View File
@@ -249,3 +249,9 @@ Plugins/**/Intermediate/*
# Cache files for the editor to use
DerivedDataCache/*
# Workshop output and machine-local tools
renders/
Content/Workshop/Generated/
*.blend1
*.blend2
+3 -6
View File
@@ -1,6 +1,3 @@
[/Script/AdvancedPreviewScene.SharedProfiles]
+Profiles=(ProfileName="Epic Headquarters",bSharedProfile=True,bIsEngineDefaultProfile=True,bUseSkyLighting=True,DirectionalLightIntensity=1.000000,DirectionalLightColor=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),SkyLightIntensity=1.000000,bRotateLightingRig=False,bShowEnvironment=True,bShowFloor=True,bShowGrid=False,EnvironmentColor=(R=0.200000,G=0.200000,B=0.200000,A=1.000000),EnvironmentIntensity=1.000000,EnvironmentCubeMapPath="/Engine/EditorMaterials/AssetViewer/EpicQuadPanorama_CC+EV1.EpicQuadPanorama_CC+EV1",EnvironmentFloorMeshPath="/Engine/EditorMeshes/AssetViewer/Floor_Mesh.Floor_Mesh",EnvironmentFloorRotation=(Pitch=0.000000,Yaw=0.000000,Roll=0.000000),EnvironmentFloorScale=(X=4.000000,Y=4.000000,Z=1.000000),EnvironmentFloorMaterialPath="/Engine/EngineMaterials/M_Grid.M_Grid",bPostProcessingEnabled=True,PostProcessingSettings=(bOverride_TemperatureType=False,bOverride_WhiteTemp=False,bOverride_WhiteTint=False,bOverride_ColorSaturation=False,bOverride_ColorContrast=False,bOverride_ColorGamma=False,bOverride_ColorGain=False,bOverride_ColorOffset=False,bOverride_ColorSaturationShadows=False,bOverride_ColorContrastShadows=False,bOverride_ColorGammaShadows=False,bOverride_ColorGainShadows=False,bOverride_ColorOffsetShadows=False,bOverride_ColorSaturationMidtones=False,bOverride_ColorContrastMidtones=False,bOverride_ColorGammaMidtones=False,bOverride_ColorGainMidtones=False,bOverride_ColorOffsetMidtones=False,bOverride_ColorSaturationHighlights=False,bOverride_ColorContrastHighlights=False,bOverride_ColorGammaHighlights=False,bOverride_ColorGainHighlights=False,bOverride_ColorOffsetHighlights=False,bOverride_ColorCorrectionShadowsMax=False,bOverride_ColorCorrectionHighlightsMin=False,bOverride_ColorCorrectionHighlightsMax=False,bOverride_BlueCorrection=False,bOverride_ExpandGamut=False,bOverride_ToneCurveAmount=False,bOverride_TonemappingMethod=False,bOverride_FilmSlope=False,bOverride_FilmToe=False,bOverride_FilmShoulder=False,bOverride_FilmBlackClip=False,bOverride_FilmWhiteClip=False,bOverride_SceneColorTint=False,bOverride_SceneFringeIntensity=False,bOverride_ChromaticAberrationStartOffset=False,bOverride_bMegaLights=False,bOverride_MegaLightsFrontLayerTranslucency=False,bOverride_AmbientCubemapTint=False,bOverride_AmbientCubemapIntensity=False,bOverride_BloomMethod=False,bOverride_BloomIntensity=False,bOverride_BloomGaussianIntensity=False,bOverride_BloomThreshold=False,bOverride_Bloom1Tint=False,bOverride_Bloom1Size=False,bOverride_Bloom2Size=False,bOverride_Bloom2Tint=False,bOverride_Bloom3Tint=False,bOverride_Bloom3Size=False,bOverride_Bloom4Tint=False,bOverride_Bloom4Size=False,bOverride_Bloom5Tint=False,bOverride_Bloom5Size=False,bOverride_Bloom6Tint=False,bOverride_Bloom6Size=False,bOverride_BloomSizeScale=False,bOverride_BloomConvolutionIntensity=False,bOverride_BloomConvolutionTexture=False,bOverride_BloomConvolutionScatterDispersion=False,bOverride_BloomConvolutionSize=False,bOverride_BloomConvolutionCenterUV=False,bOverride_BloomConvolutionPreFilterMin=False,bOverride_BloomConvolutionPreFilterMax=False,bOverride_BloomConvolutionPreFilterMult=False,bOverride_BloomConvolutionBufferScale=False,bOverride_BloomDirtMaskIntensity=False,bOverride_BloomDirtMaskTint=False,bOverride_BloomDirtMask=False,bOverride_CameraShutterSpeed=False,bOverride_CameraISO=False,bOverride_AutoExposureMethod=False,bOverride_AutoExposureLowPercent=False,bOverride_AutoExposureHighPercent=False,bOverride_AutoExposureMinBrightness=True,bOverride_AutoExposureMaxBrightness=True,bOverride_AutoExposureSpeedUp=False,bOverride_AutoExposureSpeedDown=False,bOverride_AutoExposureBias=False,bOverride_AutoExposureBiasCurve=False,bOverride_AutoExposureMeterMask=False,bOverride_AutoExposureApplyPhysicalCameraExposure=False,bOverride_HistogramLogMin=False,bOverride_HistogramLogMax=False,bOverride_LocalExposureMethod=False,bOverride_LocalExposureHighlightContrastScale=False,bOverride_LocalExposureShadowContrastScale=False,bOverride_LocalExposureHighlightContrastCurve=False,bOverride_LocalExposureShadowContrastCurve=False,bOverride_LocalExposureHighlightThreshold=False,bOverride_LocalExposureShadowThreshold=False,bOverride_LocalExposureDetailStrength=False,bOverride_LocalExposureBlurredLuminanceBlend=False,bOverride_LocalExposureBlurredLuminanceKernelSizePercent=False,bOverride_LocalExposureHighlightThresholdStrength=False,bOverride_LocalExposureShadowThresholdStrength=False,bOverride_LocalExposureMiddleGreyBias=False,bOverride_LensFlareIntensity=False,bOverride_LensFlareTint=False,bOverride_LensFlareTints=False,bOverride_LensFlareBokehSize=False,bOverride_LensFlareBokehShape=False,bOverride_LensFlareThreshold=False,bOverride_VignetteIntensity=False,bOverride_VignetteType=False,bOverride_VignetteCenter=False,bOverride_VignetteColor=False,bOverride_VignetteSize=False,bOverride_VignetteSoftness=False,bOverride_VignetteTexture=False,bOverride_Sharpen=False,bOverride_FilmGrainIntensity=False,bOverride_FilmGrainIntensityShadows=False,bOLine truncated
+Profiles=(ProfileName="Grey Wireframe",bSharedProfile=True,bIsEngineDefaultProfile=True,bUseSkyLighting=True,DirectionalLightIntensity=1.000000,DirectionalLightColor=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),SkyLightIntensity=1.000000,bRotateLightingRig=False,bShowEnvironment=False,bShowFloor=False,bShowGrid=False,EnvironmentColor=(R=0.039216,G=0.039216,B=0.039216,A=1.000000),EnvironmentIntensity=1.000000,EnvironmentCubeMapPath="/Engine/EditorMaterials/AssetViewer/EpicQuadPanorama_CC+EV1.EpicQuadPanorama_CC+EV1",EnvironmentFloorMeshPath="/Engine/EditorMeshes/AssetViewer/Floor_Mesh.Floor_Mesh",EnvironmentFloorRotation=(Pitch=0.000000,Yaw=0.000000,Roll=0.000000),EnvironmentFloorScale=(X=4.000000,Y=4.000000,Z=1.000000),EnvironmentFloorMaterialPath="/Engine/EngineMaterials/M_Grid.M_Grid",bPostProcessingEnabled=False,PostProcessingSettings=(bOverride_TemperatureType=False,bOverride_WhiteTemp=False,bOverride_WhiteTint=False,bOverride_ColorSaturation=False,bOverride_ColorContrast=False,bOverride_ColorGamma=False,bOverride_ColorGain=False,bOverride_ColorOffset=False,bOverride_ColorSaturationShadows=False,bOverride_ColorContrastShadows=False,bOverride_ColorGammaShadows=False,bOverride_ColorGainShadows=False,bOverride_ColorOffsetShadows=False,bOverride_ColorSaturationMidtones=False,bOverride_ColorContrastMidtones=False,bOverride_ColorGammaMidtones=False,bOverride_ColorGainMidtones=False,bOverride_ColorOffsetMidtones=False,bOverride_ColorSaturationHighlights=False,bOverride_ColorContrastHighlights=False,bOverride_ColorGammaHighlights=False,bOverride_ColorGainHighlights=False,bOverride_ColorOffsetHighlights=False,bOverride_ColorCorrectionShadowsMax=False,bOverride_ColorCorrectionHighlightsMin=False,bOverride_ColorCorrectionHighlightsMax=False,bOverride_BlueCorrection=False,bOverride_ExpandGamut=False,bOverride_ToneCurveAmount=False,bOverride_TonemappingMethod=False,bOverride_FilmSlope=False,bOverride_FilmToe=False,bOverride_FilmShoulder=False,bOverride_FilmBlackClip=False,bOverride_FilmWhiteClip=False,bOverride_SceneColorTint=False,bOverride_SceneFringeIntensity=False,bOverride_ChromaticAberrationStartOffset=False,bOverride_bMegaLights=False,bOverride_MegaLightsFrontLayerTranslucency=False,bOverride_AmbientCubemapTint=False,bOverride_AmbientCubemapIntensity=False,bOverride_BloomMethod=False,bOverride_BloomIntensity=False,bOverride_BloomGaussianIntensity=False,bOverride_BloomThreshold=False,bOverride_Bloom1Tint=False,bOverride_Bloom1Size=False,bOverride_Bloom2Size=False,bOverride_Bloom2Tint=False,bOverride_Bloom3Tint=False,bOverride_Bloom3Size=False,bOverride_Bloom4Tint=False,bOverride_Bloom4Size=False,bOverride_Bloom5Tint=False,bOverride_Bloom5Size=False,bOverride_Bloom6Tint=False,bOverride_Bloom6Size=False,bOverride_BloomSizeScale=False,bOverride_BloomConvolutionIntensity=False,bOverride_BloomConvolutionTexture=False,bOverride_BloomConvolutionScatterDispersion=False,bOverride_BloomConvolutionSize=False,bOverride_BloomConvolutionCenterUV=False,bOverride_BloomConvolutionPreFilterMin=False,bOverride_BloomConvolutionPreFilterMax=False,bOverride_BloomConvolutionPreFilterMult=False,bOverride_BloomConvolutionBufferScale=False,bOverride_BloomDirtMaskIntensity=False,bOverride_BloomDirtMaskTint=False,bOverride_BloomDirtMask=False,bOverride_CameraShutterSpeed=False,bOverride_CameraISO=False,bOverride_AutoExposureMethod=False,bOverride_AutoExposureLowPercent=False,bOverride_AutoExposureHighPercent=False,bOverride_AutoExposureMinBrightness=True,bOverride_AutoExposureMaxBrightness=True,bOverride_AutoExposureSpeedUp=False,bOverride_AutoExposureSpeedDown=False,bOverride_AutoExposureBias=False,bOverride_AutoExposureBiasCurve=False,bOverride_AutoExposureMeterMask=False,bOverride_AutoExposureApplyPhysicalCameraExposure=False,bOverride_HistogramLogMin=False,bOverride_HistogramLogMax=False,bOverride_LocalExposureMethod=False,bOverride_LocalExposureHighlightContrastScale=False,bOverride_LocalExposureShadowContrastScale=False,bOverride_LocalExposureHighlightContrastCurve=False,bOverride_LocalExposureShadowContrastCurve=False,bOverride_LocalExposureHighlightThreshold=False,bOverride_LocalExposureShadowThreshold=False,bOverride_LocalExposureDetailStrength=False,bOverride_LocalExposureBlurredLuminanceBlend=False,bOverride_LocalExposureBlurredLuminanceKernelSizePercent=False,bOverride_LocalExposureHighlightThresholdStrength=False,bOverride_LocalExposureShadowThresholdStrength=False,bOverride_LocalExposureMiddleGreyBias=False,bOverride_LensFlareIntensity=False,bOverride_LensFlareTint=False,bOverride_LensFlareTints=False,bOverride_LensFlareBokehSize=False,bOverride_LensFlareBokehShape=False,bOverride_LensFlareThreshold=False,bOverride_VignetteIntensity=False,bOverride_VignetteType=False,bOverride_VignetteCenter=False,bOverride_VignetteColor=False,bOverride_VignetteSize=False,bOverride_VignetteSoftness=False,bOverride_VignetteTexture=False,bOverride_Sharpen=False,bOverride_FilmGrainIntensity=False,bOverride_FilmGrainIntensityShadows=False,bOLine truncated
+Profiles=(ProfileName="Grey Ambient",bSharedProfile=True,bIsEngineDefaultProfile=True,bUseSkyLighting=True,DirectionalLightIntensity=4.000000,DirectionalLightColor=(R=1.000000,G=1.000000,B=1.000000,A=1.000000),SkyLightIntensity=2.000000,bRotateLightingRig=False,bShowEnvironment=True,bShowFloor=True,bShowGrid=False,EnvironmentColor=(R=0.200000,G=0.200000,B=0.200000,A=1.000000),EnvironmentIntensity=1.000000,EnvironmentCubeMapPath="/Engine/EditorMaterials/AssetViewer/T_GreyAmbient",EnvironmentFloorMeshPath="/Engine/EditorMeshes/AssetViewer/Floor_Mesh.Floor_Mesh",EnvironmentFloorRotation=(Pitch=0.000000,Yaw=0.000000,Roll=0.000000),EnvironmentFloorScale=(X=4.000000,Y=4.000000,Z=1.000000),EnvironmentFloorMaterialPath="/Engine/EngineMaterials/M_Grid.M_Grid",bPostProcessingEnabled=False,PostProcessingSettings=(bOverride_TemperatureType=False,bOverride_WhiteTemp=False,bOverride_WhiteTint=False,bOverride_ColorSaturation=False,bOverride_ColorContrast=False,bOverride_ColorGamma=False,bOverride_ColorGain=False,bOverride_ColorOffset=False,bOverride_ColorSaturationShadows=False,bOverride_ColorContrastShadows=False,bOverride_ColorGammaShadows=False,bOverride_ColorGainShadows=False,bOverride_ColorOffsetShadows=False,bOverride_ColorSaturationMidtones=False,bOverride_ColorContrastMidtones=False,bOverride_ColorGammaMidtones=False,bOverride_ColorGainMidtones=False,bOverride_ColorOffsetMidtones=False,bOverride_ColorSaturationHighlights=False,bOverride_ColorContrastHighlights=False,bOverride_ColorGammaHighlights=False,bOverride_ColorGainHighlights=False,bOverride_ColorOffsetHighlights=False,bOverride_ColorCorrectionShadowsMax=False,bOverride_ColorCorrectionHighlightsMin=False,bOverride_ColorCorrectionHighlightsMax=False,bOverride_BlueCorrection=False,bOverride_ExpandGamut=False,bOverride_ToneCurveAmount=False,bOverride_TonemappingMethod=False,bOverride_FilmSlope=False,bOverride_FilmToe=False,bOverride_FilmShoulder=False,bOverride_FilmBlackClip=False,bOverride_FilmWhiteClip=False,bOverride_SceneColorTint=False,bOverride_SceneFringeIntensity=False,bOverride_ChromaticAberrationStartOffset=False,bOverride_bMegaLights=False,bOverride_MegaLightsFrontLayerTranslucency=False,bOverride_AmbientCubemapTint=False,bOverride_AmbientCubemapIntensity=False,bOverride_BloomMethod=False,bOverride_BloomIntensity=False,bOverride_BloomGaussianIntensity=False,bOverride_BloomThreshold=False,bOverride_Bloom1Tint=False,bOverride_Bloom1Size=False,bOverride_Bloom2Size=False,bOverride_Bloom2Tint=False,bOverride_Bloom3Tint=False,bOverride_Bloom3Size=False,bOverride_Bloom4Tint=False,bOverride_Bloom4Size=False,bOverride_Bloom5Tint=False,bOverride_Bloom5Size=False,bOverride_Bloom6Tint=False,bOverride_Bloom6Size=False,bOverride_BloomSizeScale=False,bOverride_BloomConvolutionIntensity=False,bOverride_BloomConvolutionTexture=False,bOverride_BloomConvolutionScatterDispersion=False,bOverride_BloomConvolutionSize=False,bOverride_BloomConvolutionCenterUV=False,bOverride_BloomConvolutionPreFilterMin=False,bOverride_BloomConvolutionPreFilterMax=False,bOverride_BloomConvolutionPreFilterMult=False,bOverride_BloomConvolutionBufferScale=False,bOverride_BloomDirtMaskIntensity=False,bOverride_BloomDirtMaskTint=False,bOverride_BloomDirtMask=False,bOverride_CameraShutterSpeed=False,bOverride_CameraISO=False,bOverride_AutoExposureMethod=False,bOverride_AutoExposureLowPercent=False,bOverride_AutoExposureHighPercent=False,bOverride_AutoExposureMinBrightness=True,bOverride_AutoExposureMaxBrightness=True,bOverride_AutoExposureSpeedUp=False,bOverride_AutoExposureSpeedDown=False,bOverride_AutoExposureBias=False,bOverride_AutoExposureBiasCurve=False,bOverride_AutoExposureMeterMask=False,bOverride_AutoExposureApplyPhysicalCameraExposure=False,bOverride_HistogramLogMin=False,bOverride_HistogramLogMax=False,bOverride_LocalExposureMethod=False,bOverride_LocalExposureHighlightContrastScale=False,bOverride_LocalExposureShadowContrastScale=False,bOverride_LocalExposureHighlightContrastCurve=False,bOverride_LocalExposureShadowContrastCurve=False,bOverride_LocalExposureHighlightThreshold=False,bOverride_LocalExposureShadowThreshold=False,bOverride_LocalExposureDetailStrength=False,bOverride_LocalExposureBlurredLuminanceBlend=False,bOverride_LocalExposureBlurredLuminanceKernelSizePercent=False,bOverride_LocalExposureHighlightThresholdStrength=False,bOverride_LocalExposureShadowThresholdStrength=False,bOverride_LocalExposureMiddleGreyBias=False,bOverride_LensFlareIntensity=False,bOverride_LensFlareTint=False,bOverride_LensFlareTints=False,bOverride_LensFlareBokehSize=False,bOverride_LensFlareBokehShape=False,bOverride_LensFlareThreshold=False,bOverride_VignetteIntensity=False,bOverride_VignetteType=False,bOverride_VignetteCenter=False,bOverride_VignetteColor=False,bOverride_VignetteSize=False,bOverride_VignetteSoftness=False,bOverride_VignetteTexture=False,bOverride_Sharpen=False,bOverride_FilmGrainIntensity=False,bOverride_FilmGrainIntensityShadows=False,bOverride_FilmGrainIntensityMidtones=FalLine truncated
Version=ApplyNewGridMaterial
[/Script/PythonScriptPlugin.PythonScriptPluginSettings]
bDeveloperMode=True
bRemoteExecution=False
+5 -72
View File
@@ -1,94 +1,27 @@
[/Script/EngineSettings.GameMapsSettings]
GameDefaultMap=/Engine/Maps/Templates/OpenWorld
EditorStartupMap=/Engine/Maps/Entry
GameDefaultMap=/Engine/Maps/Entry
[/Script/Engine.RendererSettings]
r.AllowStaticLighting=False
r.Shadow.Virtual.Enable=1
r.GenerateMeshDistanceFields=True
r.DynamicGlobalIlluminationMethod=1
r.ReflectionMethod=1
r.SkinCache.CompileShaders=True
r.RayTracing=True
r.RayTracing.RayTracingProxies.ProjectEnabled=True
r.Substrate=True
r.Substrate.ProjectGBufferFormat=0
r.Shadow.Virtual.Enable=1
r.RayTracing=False
r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange=True
r.DefaultFeature.LocalExposure.HighlightContrastScale=0.8
r.DefaultFeature.LocalExposure.ShadowContrastScale=0.8
[/Script/WindowsTargetPlatform.WindowsTargetSettings]
DefaultGraphicsRHI=DefaultGraphicsRHI_DX12
DefaultGraphicsRHI=DefaultGraphicsRHI_DX12
-D3D12TargetedShaderFormats=PCD3D_SM5
+D3D12TargetedShaderFormats=PCD3D_SM6
-D3D11TargetedShaderFormats=PCD3D_SM5
+D3D11TargetedShaderFormats=PCD3D_SM5
Compiler=Default
AudioSampleRate=48000
AudioCallbackBufferFrameSize=1024
AudioNumBuffersToEnqueue=1
AudioMaxChannels=0
AudioNumSourceWorkers=4
SpatializationPlugin=
SourceDataOverridePlugin=
ReverbPlugin=
OcclusionPlugin=
CompressionOverrides=(bOverrideCompressionTimes=False,DurationThreshold=5.000000,MaxNumRandomBranches=0,SoundCueQualityIndex=0)
CacheSizeKB=65536
MaxChunkSizeOverrideKB=0
bResampleForDevice=False
MaxSampleRate=48000.000000
HighSampleRate=32000.000000
MedSampleRate=24000.000000
LowSampleRate=12000.000000
MinSampleRate=8000.000000
CompressionQualityModifier=1.000000
AutoStreamingThreshold=0.000000
SoundCueCookQualityIndex=-1
[/Script/LinuxTargetPlatform.LinuxTargetSettings]
-TargetedRHIs=SF_VULKAN_SM5
+TargetedRHIs=SF_VULKAN_SM6
[/Script/MacTargetPlatform.MacTargetSettings]
-TargetedRHIs=SF_METAL_SM5
+TargetedRHIs=SF_METAL_SM6
[/Script/HardwareTargeting.HardwareTargetingSettings]
TargetedHardwareClass=Desktop
AppliedTargetedHardwareClass=Desktop
DefaultGraphicsPerformance=Maximum
AppliedDefaultGraphicsPerformance=Maximum
[/Script/WorldPartitionEditor.WorldPartitionEditorSettings]
CommandletClass=Class'/Script/UnrealEd.WorldPartitionConvertCommandlet'
[/Script/Engine.UserInterfaceSettings]
bAuthorizeAutomaticWidgetVariableCreation=False
FontDPIPreset=Standard
FontDPI=72
[/Script/Engine.Engine]
+ActiveGameNameRedirects=(OldGameName="TP_BlankBP",NewGameName="/Script/ExampleProject")
+ActiveGameNameRedirects=(OldGameName="/Script/TP_BlankBP",NewGameName="/Script/ExampleProject")
[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings]
bEnablePlugin=True
bAllowNetworkConnection=True
SecurityToken=72FA219A42EF2331665DBAA277560BD5
SecurityToken=83CD9B53427AF31BB12CAE94F92BE8C6
bIncludeInShipping=False
bAllowExternalStartInShipping=False
bCompileAFSProject=False
+2 -10
View File
@@ -1,12 +1,4 @@
[/Script/CommonUI.CommonUISettings]
CommonButtonAcceptKeyHandling=TriggerClick
[ConsoleVariables]
CommonUI.CheckKeyboardFocusAndParentage=1
CommonUI.DisallowUserFocusedWidgetForPendingFocusRecipient=1
CommonUI.FallbackToDesiredOnAutoRestoreFailure=1
[/Script/EngineSettings.GeneralProjectSettings]
ProjectID=30537DB4496937438003FCA5BA29D5EF
ProjectName=Python Workshop
Description=Godzinny warsztat renderowania z Pythonem w UE 5.7
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+15
View File
@@ -0,0 +1,15 @@
"""Python rendering workshop for Unreal Engine 5.7."""
import importlib
__all__ = ["Scene", "Sequence", "Renderer", "camera", "lighting", "materials", "assets", "blender", "render", "video"]
def __getattr__(name):
# Plain Python launchers can import paths/video/blender without importing unreal.
if name in {"Scene", "Sequence"}:
return getattr(importlib.import_module("." + name.lower(), __name__), name)
if name == "Renderer":
return importlib.import_module(".render", __name__).Renderer
if name in __all__:
return importlib.import_module("." + name, __name__)
raise AttributeError(name)
+105
View File
@@ -0,0 +1,105 @@
"""Synchronize basic glTF PBR properties preserved by UE mesh reimports."""
import json
import base64
import re
import struct
import unreal
from .paths import PROJECT
def link_textures(source, mesh, destination):
if source.suffix.lower() == ".glb":
data = source.read_bytes()
size, kind = struct.unpack_from("<II", data, 12)
if kind != 0x4E4F534A:
raise ValueError("GLB nie zawiera poczatkowego chunka JSON.")
document = json.loads(data[20:20 + size])
binary = data[28 + size:]
else:
document = json.loads(source.read_text(encoding="utf-8"))
binary = b""
def sanitized(value):
return re.sub(r"[^A-Za-z0-9_]", "_", value)
textures = {}
def load_texture(info, parameter):
index = document["textures"][info["index"]]["source"]
role = "Normal" if parameter == "NormalTexture" else (
"Color" if parameter in {"BaseColorTexture", "EmissiveTexture"} else "Linear")
key = (index, role)
if key in textures:
return textures[key]
image = document["images"][index]
name = f"T_{index:03d}_{sanitized(image.get('name', 'Image'))}_{role}"
uri = image.get("uri", "")
if "bufferView" in image:
view = document["bufferViews"][image["bufferView"]]
offset = view.get("byteOffset", 0)
payload = binary[offset:offset + view["byteLength"]]
elif uri.startswith("data:"):
payload = base64.b64decode(uri.split(",", 1)[1])
else:
from urllib.parse import unquote
payload = (source.parent / unquote(uri)).read_bytes()
extension = ".png" if payload.startswith(b"\x89PNG") else ".jpg" if payload.startswith(b"\xff\xd8") else None
if extension is None:
raise ValueError("Tekstury warsztatowe musza byc zapisane jako PNG lub JPEG.")
file = PROJECT / "build/GltfTextures" / source.stem / (name + extension)
file.parent.mkdir(parents=True, exist_ok=True)
file.write_bytes(payload)
task = unreal.AssetImportTask()
task.filename = str(file)
task.destination_path = destination + "/WorkshopTextures"
task.destination_name = name
task.automated = True
task.replace_existing = True
task.save = True
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
imported = [obj for obj in task.get_objects() if isinstance(obj, unreal.Texture2D)]
if len(imported) != 1:
raise RuntimeError("Nie mozna zaimportowac tekstury " + name)
texture = imported[0]
texture.set_editor_property("srgb", role == "Color")
texture.set_editor_property("flip_green_channel", role == "Normal")
texture.set_editor_property("compression_settings", unreal.TextureCompressionSettings.TC_NORMALMAP
if role == "Normal" else unreal.TextureCompressionSettings.TC_DEFAULT
if role == "Color" else unreal.TextureCompressionSettings.TC_MASKS)
unreal.EditorAssetLibrary.save_loaded_asset(texture)
textures[key] = texture
return texture
materials = {sanitized(item.get("name", "")): item for item in document.get("materials", [])}
lib = unreal.MaterialEditingLibrary
for slot in mesh.static_materials:
material = slot.material_interface
if material is None or material.get_name() not in materials:
continue
spec = materials[material.get_name()]
pbr = spec.get("pbrMetallicRoughness", {})
lib.set_material_instance_vector_parameter_value(
material, "BaseColorFactor", unreal.LinearColor(*pbr.get("baseColorFactor", [1, 1, 1, 1])))
lib.set_material_instance_vector_parameter_value(
material, "EmissiveFactor", unreal.LinearColor(*spec.get("emissiveFactor", [0, 0, 0]), 1))
lib.set_material_instance_scalar_parameter_value(material, "MetallicFactor", pbr.get("metallicFactor", 1))
lib.set_material_instance_scalar_parameter_value(material, "RoughnessFactor", pbr.get("roughnessFactor", 1))
maps = {"BaseColorTexture": pbr.get("baseColorTexture"),
"MetallicRoughnessTexture": pbr.get("metallicRoughnessTexture"),
"NormalTexture": spec.get("normalTexture"),
"OcclusionTexture": spec.get("occlusionTexture"),
"EmissiveTexture": spec.get("emissiveTexture")}
for parameter, info in maps.items():
lib.set_material_instance_static_switch_parameter_value(material, "bHas" + parameter, info is not None)
lib.update_material_instance(material)
for parameter, info in maps.items():
if info is None:
continue
texture = load_texture(info, parameter)
# UE 5.7's setter returns False even when it succeeds; verify by reading.
lib.set_material_instance_texture_parameter_value(material, parameter, texture)
if lib.get_material_instance_texture_parameter_value(material, parameter) != texture:
raise RuntimeError(f"Nie mozna przypisac {parameter} w {material.get_name()}.")
lib.update_material_instance(material)
unreal.EditorAssetLibrary.save_loaded_asset(material)
+68
View File
@@ -0,0 +1,68 @@
"""Automated static mesh imports using the engine's enabled import pipelines."""
from pathlib import Path
import unreal
from .paths import GENERATED, slug
def import_model(filename, name="Character"):
source = Path(filename).resolve()
if not source.is_file():
raise FileNotFoundError(f"Brak modelu: {source}")
if source.suffix.lower() not in {".glb", ".gltf", ".fbx", ".obj"}:
raise ValueError("Model musi byc plikiem GLB, glTF, FBX lub OBJ.")
destination = f"{GENERATED}/Imports/{slug(name)}"
task = unreal.AssetImportTask()
task.set_editor_property("filename", str(source))
task.set_editor_property("destination_path", destination)
task.set_editor_property("destination_name", name)
task.set_editor_property("automated", True)
task.set_editor_property("replace_existing", True)
task.set_editor_property("replace_existing_settings", True)
task.set_editor_property("save", True)
task.set_editor_property("async_", False)
if source.suffix.lower() in {".glb", ".gltf"}:
# Defaults reuse existing materials without updating them. Our generated
# import folder belongs to the source file, so apply source properties.
template = unreal.load_asset("/Interchange/Pipelines/DefaultGLTFAssetsPipeline")
pipeline = unreal.load_asset(GENERATED + "/Pipelines/GLTFWorkshop")
if pipeline is None:
pipeline = unreal.AssetToolsHelpers.get_asset_tools().duplicate_asset(
"GLTFWorkshop", GENERATED + "/Pipelines", template)
pipeline.set_editor_property("reimport_strategy", unreal.ReimportStrategyFlags.APPLY_PIPELINE_PROPERTIES)
pipeline.material_pipeline.set_editor_property("import_materials", True)
# PBR images are imported explicitly into WorkshopTextures below. This
# avoids Interchange's dependency-preserving material reimport behavior.
pipeline.material_pipeline.texture_pipeline.set_editor_property("import_textures", False)
pipeline.set_editor_property("scene_name_sub_folder", True)
pipeline.set_editor_property("asset_type_sub_folders", True)
pipeline.material_pipeline.set_editor_property(
"search_location", unreal.InterchangeMaterialSearchLocation.DO_NOT_SEARCH)
pipeline.material_pipeline.set_editor_property(
"material_import", unreal.InterchangeMaterialImportOption.IMPORT_AS_MATERIAL_INSTANCES)
unreal.EditorAssetLibrary.save_loaded_asset(pipeline)
options = unreal.InterchangePipelineStackOverride()
options.set_editor_property("override_pipelines", [
unreal.SoftObjectPath(pipeline.get_path_name()),
unreal.SoftObjectPath("/Interchange/Pipelines/DefaultGLTFPipeline.DefaultGLTFPipeline")])
task.set_editor_property("options", options)
if source.suffix.lower() == ".fbx":
options = unreal.FbxImportUI()
options.set_editor_property("import_as_skeletal", False)
options.set_editor_property("mesh_type_to_import", unreal.FBXImportType.FBXIT_STATIC_MESH)
options.set_editor_property("automated_import_should_detect_type", False)
options.static_mesh_import_data.set_editor_property("combine_meshes", True)
task.set_editor_property("options", options)
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
meshes = [obj for obj in task.get_objects() if isinstance(obj, unreal.StaticMesh)]
if len(meshes) != 1:
raise RuntimeError(
f"Import {source.name}: oczekiwano jednego StaticMesh, otrzymano {len(meshes)}. "
"Polacz obiekty przed eksportem albo uzyj workshop.blender.import_character().")
if source.suffix.lower() in {".glb", ".gltf"}:
# UE preserves material edits on mesh reimport. In this workflow Blender
# owns the look; explicitly synchronize the supported PBR parameters.
from ._gltf import link_textures
link_textures(source, meshes[0], destination)
return meshes[0]
+25
View File
@@ -0,0 +1,25 @@
"""Export saved Blender files in a separate process, then import them in UE."""
from pathlib import Path
import subprocess
from .paths import PROJECT, executable, slug
def export(filename, name="Character", collection="Character", blender=None):
source = Path(filename).resolve()
if not source.is_file() or source.suffix.lower() != ".blend":
raise FileNotFoundError(f"Brak pliku .blend: {source}")
destination = PROJECT / "build" / slug(name) / (name + ".glb")
destination.parent.mkdir(parents=True, exist_ok=True)
command = [executable("blender", blender), "--background", str(source),
"--python-exit-code", "1", "--python", str(PROJECT / "tools" / "blender_export.py"),
"--", "--output", str(destination), "--collection", collection]
result = subprocess.run(command, capture_output=True, text=True, timeout=180)
if result.returncode or not destination.is_file():
raise RuntimeError("Eksport Blendera nie powiodl sie:\n" + (result.stdout + result.stderr)[-6000:])
return destination
def import_character(filename, name="Character", collection="Character", blender=None):
from .assets import import_model
return import_model(export(filename, name, collection, blender), name)
+30
View File
@@ -0,0 +1,30 @@
import unreal
def look_at(actor, target):
actor.set_actor_rotation(unreal.MathLibrary.find_look_at_rotation(
actor.get_actor_location(), unreal.Vector(*target)), False)
def create(scene, location=(450, -550, 300), target=(0, 0, 100), focal_length=45):
actor = scene.spawn(unreal.CineCameraActor, "Camera", location)
component = actor.get_cine_camera_component()
filmback = component.get_editor_property("filmback")
filmback.set_editor_property("sensor_width", 36.0)
filmback.set_editor_property("sensor_height", 20.25)
component.set_editor_property("filmback", filmback)
component.set_editor_property("current_focal_length", float(focal_length))
focus = component.get_editor_property("focus_settings")
focus.set_editor_property("focus_method", unreal.CameraFocusMethod.DISABLE)
component.set_editor_property("focus_settings", focus)
settings = component.get_editor_property("post_process_settings")
settings.set_editor_property("override_auto_exposure_bias", True)
settings.set_editor_property("auto_exposure_bias", 0.0)
settings.set_editor_property("override_auto_exposure_min_brightness", True)
settings.set_editor_property("override_auto_exposure_max_brightness", True)
# Extended luminance range interprets these as EV100, not linear brightness.
settings.set_editor_property("auto_exposure_min_brightness", 8.0)
settings.set_editor_property("auto_exposure_max_brightness", 8.0)
component.set_editor_property("post_process_settings", settings)
look_at(actor, target)
return actor
+23
View File
@@ -0,0 +1,23 @@
import unreal
from .camera import look_at
def area(scene, name="Light", location=(200, -200, 350), target=(0, 0, 100),
intensity=12000, color=(1, 1, 1), size=180):
actor = scene.spawn(unreal.RectLight, name, location)
light = actor.get_component_by_class(unreal.RectLightComponent)
light.set_mobility(unreal.ComponentMobility.MOVABLE)
light.set_editor_property("intensity", float(intensity))
light.set_editor_property("source_width", float(size))
light.set_editor_property("source_height", float(size))
light.set_editor_property("attenuation_radius", 2000.0)
light.set_light_color(unreal.LinearColor(*color, 1.0))
look_at(actor, target)
return actor
def studio(scene, key_color=(1.0, 0.82, 0.65), fill_color=(0.5, 0.7, 1.0)):
return [area(scene, "Key", (200, -250, 350), color=key_color),
area(scene, "Fill", (100, 300, 250), intensity=7000, color=fill_color),
area(scene, "Rim", (-200, 0, 300), intensity=15000)]
+34
View File
@@ -0,0 +1,34 @@
"""Small opaque PBR materials, generated without external assets."""
import unreal
from .paths import GENERATED, slug
def color(name, rgb=(0.15, 0.5, 0.8), roughness=0.55, metallic=0.0):
path = f"{GENERATED}/Materials/M_{slug(name)}"
material = unreal.load_asset(path)
if material is None:
material = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
"M_" + name, GENERATED + "/Materials", unreal.Material, unreal.MaterialFactoryNew())
if not isinstance(material, unreal.Material):
raise TypeError(f"{path} nie jest materialem.")
lib = unreal.MaterialEditingLibrary
lib.delete_all_material_expressions(material)
base = lib.create_material_expression(material, unreal.MaterialExpressionConstant3Vector, -300, 0)
base.set_editor_property("constant", unreal.LinearColor(*rgb, 1.0))
lib.connect_material_property(base, "", unreal.MaterialProperty.MP_BASE_COLOR)
for offset, value, prop in [(160, roughness, unreal.MaterialProperty.MP_ROUGHNESS),
(280, metallic, unreal.MaterialProperty.MP_METALLIC)]:
node = lib.create_material_expression(material, unreal.MaterialExpressionConstant, -300, offset)
node.set_editor_property("r", float(value))
lib.connect_material_property(node, "", prop)
lib.recompile_material(material)
unreal.EditorAssetLibrary.save_loaded_asset(material)
return material
def assign(actor, material, slot=0):
component = actor.get_component_by_class(unreal.StaticMeshComponent)
if component is None:
raise TypeError("Material mozna przypisac do aktora ze StaticMeshComponent.")
component.set_material(slot, material)
+23
View File
@@ -0,0 +1,23 @@
"""Paths and process helpers shared by UE, Blender launchers and plain Python."""
import os
from pathlib import Path
import re
import shutil
PROJECT = Path(__file__).resolve().parents[3]
GENERATED = "/Game/Workshop/Generated"
def slug(value):
if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", value):
raise ValueError("Nazwa musi zaczynac sie litera i zawierac tylko litery ASCII, cyfry lub _.")
return value
def executable(name, explicit=None):
candidate = explicit or os.environ.get("WORKSHOP_" + name.upper()) or shutil.which(name)
if candidate and Path(candidate).is_file():
return str(Path(candidate).resolve())
raise FileNotFoundError(
f"Nie znaleziono {name}. Dodaj program do PATH lub ustaw WORKSHOP_{name.upper()} "
"na pelna sciezke do pliku wykonywalnego.")
+16
View File
@@ -0,0 +1,16 @@
"""Small append-only progress events shared with the external launcher."""
import json
import os
from pathlib import Path
def report(state, **details):
target = os.environ.get("WORKSHOP_PROGRESS")
if not target:
return
try:
with Path(target).open("a", encoding="utf-8") as stream:
stream.write(json.dumps(dict(state=state, **details), ensure_ascii=False) + "\n")
except OSError:
# Progress reporting must not interrupt a render or hide its result.
pass
+183
View File
@@ -0,0 +1,183 @@
"""MRQ rendering with retained delegates, isolated output and completion status."""
from datetime import datetime
from dataclasses import dataclass
import json
import os
from pathlib import Path
import uuid
import unreal
from .paths import PROJECT, executable
from .video import encode
from .progress import report
_active = None
PRESETS = {"preview": (640, 360, 1, 8), "final": (1280, 720, 4, 32)}
@dataclass
class Renderer:
"""Reusable render settings. Dimensions in pixels; warmup in frames."""
width: int = 640
height: int = 360
samples: int = 1
warmup: int = 8
mp4: bool = True
ffmpeg: str = None
def __post_init__(self):
self._validate()
def _validate(self):
for name, minimum in (("width", 1), ("height", 1), ("samples", 1), ("warmup", 0)):
value = getattr(self, name)
if type(value) is not int or value < minimum:
raise ValueError(f"{name} musi byc liczba calkowita >= {minimum}.")
@classmethod
def from_preset(cls, name="preview", **overrides):
if name not in PRESETS:
raise ValueError("Nieznany preset. Dostepne: " + ", ".join(PRESETS))
settings = dict(zip(("width", "height", "samples", "warmup"), PRESETS[name]))
settings.update(overrides)
return cls(**settings)
def render(self, sequence, *, single_frame=None):
self._validate()
return _start(sequence, self, single_frame=single_frame)
def is_rendering():
return _active is not None
def _write_status(data):
target = os.environ.get("WORKSHOP_STATUS")
if target:
path = Path(target)
temporary = path.with_suffix(".tmp")
temporary.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
temporary.replace(path)
def _on_error(executor, pipeline, fatal, message):
if _active is not None:
_active["errors"].append(str(message))
unreal.log_error("[Workshop] MRQ: " + str(message))
def _on_finished(executor, success):
global _active
state = _active
if state is None:
return
result = {"success": False, "output": str(state["output"]), "settings": state["settings"]}
try:
if not success or state["errors"]:
raise RuntimeError("Render nie powiodl sie: " + "; ".join(state["errors"]))
frames = state["output"] / "frames"
count = len(list(frames.glob("frame_*.png")))
if count != state["frames"]:
raise RuntimeError(f"Zapisano {count} klatek zamiast {state['frames']}.")
if state["mp4"]:
report("encoding")
result["video"] = str(encode(frames, state["fps"], ffmpeg=state["ffmpeg"],
expected_frames=state["frames"]))
result["success"] = True
unreal.log("[Workshop] Gotowe: " + str(state["output"]))
except Exception as exc:
result["error"] = str(exc)
unreal.log_error("[Workshop] " + str(exc))
finally:
(state["output"] / "result.json").write_text(json.dumps(result, indent=2), encoding="utf-8")
_write_status(result)
_active = None
if os.environ.get("WORKSHOP_BATCH") == "1":
report("closing")
unreal.EditorPythonScripting.set_keep_python_script_alive(False)
def _begin_render(delta_seconds):
"""Start after the script and pending Slate window updates have returned."""
if _active is None:
return
_active["ticks"] -= 1
if _active["ticks"] > 0:
return
unreal.unregister_slate_post_tick_callback(_active.pop("tick_handle"))
try:
subsystem = unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem)
report("rendering", output=str(_active["output"]), frames=_active["frames"])
# This installs the Sequencer playback-context guard needed during PIE.
subsystem.render_queue_instance_with_executor_instance(_active["queue"], _active["executor"])
except Exception as exc:
_active["errors"].append(str(exc))
_on_finished(_active["executor"], False)
def start(sequence, preset="preview", mp4=True, ffmpeg=None, single_frame=None):
"""Shortcut preserved for existing workshop scripts."""
return Renderer.from_preset(preset, mp4=mp4, ffmpeg=ffmpeg).render(sequence, single_frame=single_frame)
def _start(sequence, renderer, single_frame=None):
global _active
if _active is not None:
raise RuntimeError("Trwa juz render warsztatowy.")
if single_frame is not None and not 0 <= single_frame < sequence.frames:
raise ValueError("Klatka poza zakresem sekwencji.")
mp4 = renderer.mp4 and single_frame is None
ffmpeg_path = executable("ffmpeg", renderer.ffmpeg) if mp4 else None
subsystem = unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem)
if subsystem.is_rendering():
raise RuntimeError("Movie Render Queue jest zajete.")
report("preparing_render")
# Own queue: do not clear the user's Movie Render Queue.
queue = unreal.MoviePipelineQueue()
job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob)
sequence.save(open_editor=False)
unreal.LevelSequenceEditorBlueprintLibrary.close_level_sequence()
job.map = unreal.SoftObjectPath(sequence.scene.map_path)
job.sequence = unreal.SoftObjectPath(sequence.asset.get_path_name())
job.job_name = sequence.scene.name
width, height, samples, warmup = renderer.width, renderer.height, renderer.samples, renderer.warmup
stamp = datetime.now().strftime("%Y%m%d_%H%M%S") + "_" + uuid.uuid4().hex[:6]
output = PROJECT / "renders" / sequence.scene.name / stamp
(output / "frames").mkdir(parents=True)
config = job.get_configuration()
settings = config.find_or_add_setting_by_class(unreal.MoviePipelineOutputSetting)
settings.output_directory = unreal.DirectoryPath(str(output / "frames"))
settings.file_name_format = "frame_{frame_number_rel}"
settings.zero_pad_frame_numbers = 4
settings.output_resolution = unreal.IntPoint(width, height)
settings.use_custom_frame_rate = True
settings.output_frame_rate = unreal.FrameRate(sequence.fps, 1)
settings.use_custom_playback_range = True
settings.custom_start_frame = single_frame if single_frame is not None else 0
settings.custom_end_frame = single_frame + 1 if single_frame is not None else sequence.frames
settings.flush_disk_writes_per_shot = True
config.find_or_add_setting_by_class(unreal.MoviePipelineImageSequenceOutput_PNG)
config.find_or_add_setting_by_class(unreal.MoviePipelineDeferredPassBase)
aa = config.find_or_add_setting_by_class(unreal.MoviePipelineAntiAliasingSetting)
aa.spatial_sample_count = samples
aa.temporal_sample_count = 1
aa.engine_warm_up_count = warmup
aa.render_warm_up_count = warmup
aa.render_warm_up_frames = True
executor = unreal.MoviePipelinePIEExecutor()
executor.on_executor_errored_delegate.add_callable_unique(_on_error)
executor.on_executor_finished_delegate.add_callable_unique(_on_finished)
_active = dict(executor=executor, queue=queue, output=output, errors=[], fps=sequence.fps,
frames=1 if single_frame is not None else sequence.frames,
mp4=mp4, ffmpeg=ffmpeg_path, ticks=2,
settings=dict(width=width, height=height, samples=samples, warmup=warmup, fps=sequence.fps))
try:
_write_status({"state": "rendering", "output": str(output)})
_active["tick_handle"] = unreal.register_slate_post_tick_callback(_begin_render)
except Exception:
_active = None
raise
unreal.log("[Workshop] Render: " + str(output))
return output
+86
View File
@@ -0,0 +1,86 @@
"""An isolated generated map, simple meshes and a ready-to-use character."""
import unreal
from .paths import GENERATED, slug
class Scene:
"""Owns one map. Building again replaces only that generated map's actors."""
def __init__(self, name="MyShot"):
from . import render
if render.is_rendering() or unreal.get_editor_subsystem(unreal.MoviePipelineQueueSubsystem).is_rendering():
raise RuntimeError("Poczekaj na zakonczenie renderu przed przebudowa sceny.")
# Discard Sequencer's live bindings before replacing their actors.
current_sequence = unreal.LevelSequenceEditorBlueprintLibrary.get_current_level_sequence()
if current_sequence and current_sequence.get_path_name().startswith(GENERATED + "/"):
unreal.LevelSequenceEditorBlueprintLibrary.close_level_sequence()
self.name = slug(name)
self.map_path = f"{GENERATED}/{name}/Map"
self.subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
editor = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
current = editor.get_current_level()
current_path = current.get_outer().get_path_name().split(".")[0] if current else ""
if current_path == self.map_path:
# This map is wholly generated by this exercise, never a user's other map.
for actor in self.subsystem.get_all_level_actors():
if "Workshop" in [str(tag) for tag in actor.tags]:
self.subsystem.destroy_actor(actor)
else:
if unreal.EditorLoadingAndSavingUtils.get_dirty_map_packages():
raise RuntimeError("Zapisz zmiany aktualnej mapy przed utworzeniem sceny warsztatowej.")
if unreal.EditorAssetLibrary.does_asset_exist(self.map_path):
if not editor.load_level(self.map_path):
raise RuntimeError("Nie mozna otworzyc " + self.map_path)
for actor in self.subsystem.get_all_level_actors():
if "Workshop" in [str(tag) for tag in actor.tags]:
self.subsystem.destroy_actor(actor)
elif not editor.new_level(self.map_path):
raise RuntimeError("Nie mozna utworzyc " + self.map_path)
def spawn(self, actor_class, name, location=(0, 0, 0), rotation=(0, 0, 0)):
actor = self.subsystem.spawn_actor_from_class(actor_class, unreal.Vector(*location),
unreal.Rotator(*rotation))
if actor is None:
raise RuntimeError("Nie mozna utworzyc aktora " + name)
actor.set_actor_label(name)
actor.tags = ["Workshop"]
actor.set_folder_path("Workshop")
return actor
def mesh(self, asset, name="Model", location=(0, 0, 0), scale=(1, 1, 1), material=None):
mesh = unreal.load_asset(asset) if isinstance(asset, str) else asset
if not isinstance(mesh, unreal.StaticMesh):
raise TypeError(f"Oczekiwano StaticMesh: {asset}")
actor = self.spawn(unreal.StaticMeshActor, name, location)
actor.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
actor.static_mesh_component.set_static_mesh(mesh)
actor.set_actor_scale3d(unreal.Vector(*scale))
if material is not None:
actor.static_mesh_component.set_material(0, material)
return actor
def primitive(self, shape="Cube", **kwargs):
if shape not in {"Cube", "Sphere", "Cylinder", "Cone", "Plane"}:
raise ValueError("Ksztalt: Cube, Sphere, Cylinder, Cone lub Plane.")
return self.mesh(f"/Engine/BasicShapes/{shape}.{shape}", **kwargs)
def character(self, material=None, head_scale=1.0):
root = self.spawn(unreal.StaticMeshActor, "Character")
root.static_mesh_component.set_mobility(unreal.ComponentMobility.MOVABLE)
for shape, name, position, scale in [
("Cube", "Body", (0, 0, 90), (0.55, 0.8, 0.9)),
("Sphere", "Head", (0, 0, 175), (0.7 * head_scale,) * 3),
("Cube", "LeftLeg", (0, -23, 25), (0.35, 0.3, 0.5)),
("Cube", "RightLeg", (0, 23, 25), (0.35, 0.3, 0.5)),
("Sphere", "Nose", (34, 0, 177), (0.24, 0.2, 0.2)),
]:
actor = self.primitive(shape, name=name, location=position, scale=scale, material=material)
actor.attach_to_actor(root, "", unreal.AttachmentRule.KEEP_WORLD,
unreal.AttachmentRule.KEEP_WORLD, unreal.AttachmentRule.KEEP_WORLD, False)
return root
def save(self):
editor = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
if not editor.save_current_level():
raise RuntimeError("Nie mozna zapisac mapy " + self.map_path)
+81
View File
@@ -0,0 +1,81 @@
"""Frame-based Sequencer helpers. Ranges are [start, end), rotations roll/pitch/yaw."""
import unreal
from .paths import GENERATED, slug
class Sequence:
def __init__(self, scene, name="Shot", fps=24, frames=48):
if not isinstance(fps, int) or fps < 1 or not isinstance(frames, int) or frames < 1:
raise ValueError("FPS i liczba klatek musza byc dodatnimi liczbami calkowitymi.")
self.scene, self.fps, self.frames = scene, fps, frames
path = f"{GENERATED}/{scene.name}/LS_{slug(name)}"
self.asset = unreal.load_asset(path)
if self.asset and unreal.LevelSequenceEditorBlueprintLibrary.get_current_level_sequence() == self.asset:
unreal.LevelSequenceEditorBlueprintLibrary.close_level_sequence()
if self.asset is None:
self.asset = unreal.AssetToolsHelpers.get_asset_tools().create_asset(
"LS_" + name, f"{GENERATED}/{scene.name}", unreal.LevelSequence,
unreal.LevelSequenceFactoryNew())
for binding in self.asset.get_bindings():
binding.remove()
for track in self.asset.get_tracks():
self.asset.remove_track(track)
self.asset.set_display_rate(unreal.FrameRate(fps, 1))
self.asset.set_playback_start(0)
self.asset.set_playback_end(frames)
self._bindings = {}
self._transforms = {}
def bind(self, actor):
key = actor.get_path_name()
if key not in self._bindings:
self._bindings[key] = self.asset.add_possessable(actor)
return self._bindings[key]
def camera(self, actor):
binding = self.bind(actor)
tracks = self.asset.find_tracks_by_type(unreal.MovieSceneCameraCutTrack)
track = tracks[0] if tracks else self.asset.add_track(unreal.MovieSceneCameraCutTrack)
for old in track.get_sections():
track.remove_section(old)
section = track.add_section()
section.set_range(0, self.frames)
binding_id = unreal.MovieSceneObjectBindingID()
binding_id.set_editor_property("guid", binding.get_id())
section.set_camera_binding_id(binding_id)
def transform_key(self, actor, frame, location=None, rotation=None, scale=None, interpolation="linear"):
if not isinstance(frame, int) or not 0 <= frame < self.frames:
raise ValueError(f"Klatka musi nalezec do 0..{self.frames - 1}.")
modes = {"linear": unreal.MovieSceneKeyInterpolation.LINEAR,
"auto": unreal.MovieSceneKeyInterpolation.AUTO,
"constant": unreal.MovieSceneKeyInterpolation.CONSTANT}
if interpolation not in modes:
raise ValueError("Interpolacja: linear, auto lub constant.")
key = actor.get_path_name()
if key not in self._transforms:
track = self.bind(actor).add_track(unreal.MovieScene3DTransformTrack)
section = track.add_section()
section.set_range(0, self.frames)
channels = section.get_all_channels()
initial = actor.get_actor_transform()
r = initial.rotation.rotator()
defaults = [*initial.translation.to_tuple(), r.roll, r.pitch, r.yaw,
*initial.scale3d.to_tuple()]
for channel, value in zip(channels, defaults):
channel.set_default(float(value))
self._transforms[key] = channels
channels = self._transforms[key]
for offset, values in [(0, location), (3, rotation), (6, scale)]:
if values is not None:
if len(values) != 3:
raise ValueError("Transformacja wymaga trzech wartosci.")
for channel, value in zip(channels[offset:offset + 3], values):
channel.add_key(unreal.FrameNumber(frame), float(value), interpolation=modes[interpolation])
def save(self, open_editor=True):
self.scene.save()
unreal.EditorAssetLibrary.save_loaded_asset(self.asset)
if open_editor:
unreal.LevelSequenceEditorBlueprintLibrary.open_level_sequence(self.asset)
+27
View File
@@ -0,0 +1,27 @@
"""FFmpeg encoding; this module also works outside Unreal."""
from pathlib import Path
import subprocess
from .paths import executable
def encode(frames, fps, output=None, ffmpeg=None, expected_frames=None):
frames = Path(frames).resolve()
if fps <= 0:
raise ValueError("FPS musi byc dodatni.")
images = sorted(frames.glob("frame_*.png"))
count = len(images)
if not count or any(p.name != f"frame_{i:04d}.png" for i, p in enumerate(images)):
raise RuntimeError(f"Brak kompletnej numeracji frame_0000.png, frame_0001.png... w {frames}")
if expected_frames is not None and count != expected_frames:
raise RuntimeError(f"Render zapisal {count} klatek zamiast {expected_frames}.")
output = Path(output) if output else frames.parent / "film.mp4"
output.parent.mkdir(parents=True, exist_ok=True)
command = [executable("ffmpeg", ffmpeg), "-hide_banner", "-loglevel", "error", "-y",
"-framerate", str(fps), "-start_number", "0", "-i", str(frames / "frame_%04d.png"),
"-frames:v", str(count), "-c:v", "libx264", "-crf", "20", "-pix_fmt", "yuv420p",
"-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2", "-movflags", "+faststart", str(output)]
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode:
raise RuntimeError("FFmpeg: " + result.stderr[-4000:])
return output.resolve()
Binary file not shown.
Binary file not shown.
+30 -96
View File
@@ -1,98 +1,32 @@
{
"FileVersion": 3,
"EngineAssociation": "5.8",
"Category": "",
"Description": "",
"Modules": [
{
"Name": "ExampleProject",
"Type": "Runtime",
"LoadingPhase": "Default"
}
],
"Plugins": [
{
"Name": "Bridge",
"Enabled": true
},
{
"Name": "Dataflow",
"Enabled": true
},
{
"Name": "Fab",
"Enabled": true
},
{
"Name": "MeshPainting",
"Enabled": true
},
{
"Name": "PCG",
"Enabled": true
},
{
"Name": "PluginUtils",
"Enabled": true
},
{
"Name": "PluginBrowser",
"Enabled": true
},
{
"Name": "GLTFExporter",
"Enabled": true
},
{
"Name": "InterchangeEditor",
"Enabled": true,
"SupportedTargetPlatforms": [
"Win64",
"Linux",
"Mac"
]
},
{
"Name": "MetaHumanSDK",
"Enabled": false
},
{
"Name": "Interchange",
"Enabled": true,
"SupportedTargetPlatforms": [
"Win64",
"Linux",
"Mac"
]
},
{
"Name": "SkeletalMeshModelingTools",
"Enabled": true
},
{
"Name": "ChaosEditor",
"Enabled": true
},
{
"Name": "InterchangeTests",
"Enabled": true,
"SupportedTargetPlatforms": [
"Win64",
"Linux",
"Mac"
]
},
{
"Name": "EditorScriptingUtilities",
"Enabled": true
},
{
"Name": "MovieRenderPipeline",
"Enabled": true
},
{
"Name": "SequencerScripting",
"Enabled": true
}
]
"FileVersion": 3,
"EngineAssociation": "5.7",
"Category": "Education",
"Description": "Python rendering workshop",
"Plugins": [
{
"Name": "PythonScriptPlugin",
"Enabled": true
},
{
"Name": "EditorScriptingUtilities",
"Enabled": true
},
{
"Name": "SequencerScripting",
"Enabled": true
},
{
"Name": "MovieRenderPipeline",
"Enabled": true
},
{
"Name": "Interchange",
"Enabled": true
},
{
"Name": "InterchangeEditor",
"Enabled": true
}
]
}
-16
View File
@@ -1,16 +0,0 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0.0",
"FriendlyName": "Demo Render Bridge",
"Description": "Minimalny przyklad wywolania C++ z Unreal Python.",
"Category": "Examples",
"CanContainContent": false,
"Modules": [
{
"Name": "DemoPlugin",
"Type": "Editor",
"LoadingPhase": "Default"
}
]
}
-48
View File
@@ -1,48 +0,0 @@
# Demo Render Bridge: minimalny prawdziwy plugin UE C++ -> Python
Plugin pokazuje rzeczywisty przeplyw:
```text
TextureRenderTarget2D
-> Python wywoluje UFUNCTION
-> C++: FImageUtils::GetRenderTargetImage
-> C++: FImageUtils::CompressImage(".png")
-> TArray<uint8> wraca do Pythona
-> Python zapisuje bytes na dysku
```
## Struktura
```text
DemoPlugin.uplugin
Source/DemoPlugin/DemoPlugin.Build.cs
Source/DemoPlugin/Public/DemoRenderBridge.h
Source/DemoPlugin/Private/DemoRenderBridge.cpp
Source/DemoPlugin/Private/DemoPlugin.cpp
use_plugin.py
```
## Jak uruchomic
1. Skopiuj caly katalog jako `<Projekt>/Plugins/DemoPlugin`.
2. Upewnij sie, ze projekt ma modul C++ i wlaczony `Python Editor Script Plugin`.
3. Otworz projekt i zaakceptuj kompilacje pluginu albo zbuduj projekt w IDE.
4. Utworz asset `TextureRenderTarget2D` pod `/Game/Demo/RT_Demo` lub zmien
sciezke `RENDER_TARGET_ASSET` w `use_plugin.py`.
5. W Unreal Editor wybierz `Tools -> Execute Python Script` i uruchom
`use_plugin.py`.
6. Wynik pojawi sie jako `Saved/demo_from_cpp.png`.
## Co jest mostem do Pythona
- `UCLASS()` rejestruje klase w refleksji.
- `UFUNCTION(BlueprintCallable)` eksportuje funkcje.
- `UDemoRenderBridge` staje sie `unreal.DemoRenderBridge`.
- `RenderTargetToPng` staje sie `render_target_to_png`.
- `UTextureRenderTarget2D*` przyjmuje obiekt zaladowany w Pythonie.
- `TArray<uint8>` wraca jako kolekcja konwertowalna przez `bytes(...)`.
To jest odchudzona wersja prawdziwego render bridge'a: pomija wybor formatu,
luminancje, specjalne kodowanie RGB, transport sieciowy i rozbudowana obsluge
bledow. Zachowuje jednak faktyczne wywolania API C++ Unreala i prawdziwy
kontrakt widoczny z Unreal Python.
@@ -1,17 +0,0 @@
using UnrealBuildTool;
public class DemoPlugin : ModuleRules
{
public DemoPlugin(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new[]
{
"Core",
"CoreUObject",
"Engine",
"ImageCore"
});
}
}
@@ -1,8 +0,0 @@
#include "Modules/ModuleManager.h"
IMPLEMENT_MODULE(FDefaultModuleImpl, DemoPlugin)
//This registers that it is the implementation of DemoPlugin module
//that can be loaded by UE
//Here could be startup scripts or sth
@@ -1,51 +0,0 @@
#include "DemoRenderBridge.h"
#include "Engine/TextureRenderTarget2D.h"
#include "ImageCore.h"
#include "ImageUtils.h"
bool UDemoRenderBridge::RenderTargetToPng(
UTextureRenderTarget2D* RenderTarget,
TArray<uint8>& OutPngBytes,
FString& OutError)
{
OutPngBytes.Reset();
OutError.Reset();
if (!IsInGameThread())
{
OutError = TEXT("Function must be ran on Game Thread");
return false;
}
if (!IsValid(RenderTarget))
{
OutError = TEXT("Render Target reference is invalid");
return false;
}
// Faktyczne wywolanie API C++ Unreala: odczyt Render Targetu do FImage.
FImage Image;
if (!FImageUtils::GetRenderTargetImage(RenderTarget, Image))
{
OutError = TEXT("Couldnt read Render Target");
return false;
}
// Faktyczne wywolanie API C++ Unreala: kompresja FImage do PNG w pamieci.
TArray64<uint8> Encoded;
if (!FImageUtils::CompressImage(Encoded, TEXT(".png"), Image, 0))
{
OutError = TEXT("Couldnt encode png");
return false;
}
if (Encoded.Num() > MAX_int32)
{
OutError = TEXT("Result too large");
return false;
}
OutPngBytes.Append(Encoded.GetData(), static_cast<int32>(Encoded.Num()));
return true;
}
@@ -1,23 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "DemoRenderBridge.generated.h"
class UTextureRenderTarget2D;
/** Funkcje C++ dostepne w Blueprintach i Python API */
UCLASS()
class DEMOPLUGIN_API UDemoRenderBridge : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/** Odczytuje Render Target z GPU i zwraca zakodowany PNG w pamieci. */
UFUNCTION(BlueprintCallable, Category = "Demo|C++ Bridge")
static bool RenderTargetToPng(
UTextureRenderTarget2D* RenderTarget,
TArray<uint8>& OutPngBytes,
FString& OutError);
};
-19
View File
@@ -1,19 +0,0 @@
from pathlib import Path
import unreal
RENDER_TARGET_ASSET = "/Game/NewTextureRenderTarget2D"
OUTPUT_FILE = Path(unreal.Paths.project_saved_dir()) / "demo_from_cpp.png"
render_target = unreal.load_asset(RENDER_TARGET_ASSET)
# Parametry Out funkcji UFUNCTION wracaja w Pythonie jako tuple.
png_bytes, error = unreal.DemoRenderBridge.render_target_to_png(
render_target
)
if error:
raise RuntimeError(error)
OUTPUT_FILE.write_bytes(bytes(png_bytes))
unreal.log(f"preview saved as {OUTPUT_FILE}")
+192 -1
View File
@@ -1,2 +1,193 @@
# PythonInUnreal
# Python w Unrealu — warsztat na godzinę
UE **5.7**, Windows / Linux. Biblioteka `ue_workshop` automatyzuje scenę,
Sequencer, Movie Render Queue i składanie MP4. Nie wymaga kompilacji C++ ani pip.
Pierwsze ćwiczenie korzysta wyłącznie z brył silnika. Blender jest opcjonalny.
Jeśli używasz wcześniejszej kopii projektu z C++ i edytor zgłasza
`Target Upgrade Required`, zamknij UE i usuń pozostałości starej kompilacji:
`Binaries/`, `Intermediate/Build/` oraz `Intermediate/TargetInfo.json`.
Nowy projekt nie ma własnego targetu C++ i nie wymaga aktualizacji `Target.cs`.
## Przygotowanie przed spotkaniem
1. Zainstaluj UE 5.7 i otwórz `ExampleProject.uproject`. Poczekaj na kompilację shaderów.
2. Zainstaluj FFmpeg z enkoderem `libx264` i dodaj do PATH, jeżeli chcesz MP4.
Bez FFmpeg można renderować PNG: `render.start(shot, mp4=False)`.
3. Zwykły Python 3.10+ jest potrzebny tylko do launchera z terminala.
4. Sprawdź `python tools/run_workshop.py --check` i wykonaj ćwiczenie próbnie.
W projekcie są już włączone Python Editor Script Plugin, Editor Scripting Utilities,
Sequencer Scripting, Movie Render Queue oraz Interchange. Python jest narzędziem
automatyzacji edytora; nie służy tu do gameplayu.
## Pierwszy skrypt w edytorze
Zapisz aktualną mapę. Wybierz **File → Execute Python Script** i wskaż
`exercises/my_shot.py`. Możesz też wpisać w konsoli UE:
```text
py "C:/sciezka/do/projektu/exercises/my_shot.py"
```
Powstanie prosta postać, podłoga, trzy światła, kamera i dwusekundowa animacja.
Obejrzyj ją w Sequencerze. Zmień kolor, `head_scale`, kamerę albo klucze i uruchom
plik ponownie. Skrypt jest odczytywany ponownie z dysku.
Ustaw `RENDER = True`, aby wyrenderować film. Domyślny preset `preview` to
640×360, 24 FPS, 48 klatek. `final` to 1280×720 i więcej próbek.
Szybkie sprawdzenie obrazu: zamiast pełnego renderu użyj
`renderer.render(shot, single_frame=24)`.
Własne ustawienia wpisujesz bezpośrednio w skrypcie uczestnika:
```python
from ue_workshop import Renderer
renderer = Renderer(width=1920, height=1080, samples=8, warmup=32, mp4=True)
renderer.render(shot)
# Albo zamiast filmu: renderer.render(shot, single_frame=24)
```
`width` i `height` to piksele, `samples` to liczba próbek przestrzennych na klatkę,
a `warmup` to liczba klatek rozgrzewki. Wymiary i próbki muszą być dodatnimi
liczbami całkowitymi, rozgrzewka może wynosić zero. FPS pochodzi z sekwencji.
Można też użyć `Renderer.from_preset("final", width=1920, height=1080)`:
argumenty nadpisują wybrane wartości presetu. Obiektu użyjesz ponownie do kolejnych
sekwencji po zakończeniu poprzedniego renderu. Ustawienia są zapisane w `result.json`.
Dotychczasowe `render.start(...)` nadal działa jako skrót do presetów.
Wyniki: `renders/MyShot/<czas_i_id>/frames/`, `film.mp4`, `result.json`.
Każdy render ma nowy katalog. Nieudane kodowanie zachowuje klatki.
MP4 powstaje dopiero po zakończeniu MRQ; nie zamykaj wtedy edytora.
Na czas przebudowy sceny i renderowania biblioteka zamyka kartę Sequencera.
Render startuje po powrocie ze skryptu. Sekwencję można ponownie otworzyć
z Content Browser; ponowne uruchomienie ćwiczenia też ją otwiera.
## Z terminala, bez okna edytora
Drugi plik obok ćwiczenia uruchamia `my_shot.py` bez okna UE, czeka na zakończenie
renderu oraz kodowania MP4 i automatycznie zamyka uruchomiony proces edytora:
```text
python exercises/my_shot_headless.py
```
Uruchamiaj go **zwykłym Pythonem z terminala**, nie przez Execute Python Script
w Unreal. Korzysta z obiektu `renderer` i ustawień zapisanych w `my_shot.py`.
Wymusza render również przy `RENDER=False`. Wynik trafia do tego samego
`renders/MyShot/<czas_i_id>/film.mp4`. Można dopisać `--editor`, `--timeout`
lub `--check`; ścieżka skryptu jest ustalana względem pliku launchera.
Domyślnie konsola pokazuje etapy: uruchamianie UE, przygotowanie sceny,
renderowanie, składanie MP4 i zamykanie UE, a na końcu wynik i ścieżkę do logu.
Podczas renderowania podaje liczbę zapisanych PNG; przy dłuższym oczekiwaniu
co 10 sekund potwierdza, że proces nadal działa. To nie jest szacowany procent
całego zadania — rozgrzewka, zapis klatek i kodowanie mają różne czasy wykonania.
Pełne logi trafiają do `Saved/Workshop/<id>.log`, także przy błędzie lub timeout.
Żeby widzieć logi UE na żywo w terminalu, dodaj:
```text
python exercises/my_shot_headless.py --verbose
```
Ścieżki filmu i logu są klikalne w obsługujących to terminalach, m.in.
Windows Terminal i terminalu VS Code (zwykle Ctrl+klik). W pozostałych konsolach
oraz przy przekierowaniu wyjścia do pliku wypisywane są zwykłe ścieżki.
Można wymusić linki opcją `--links always` lub wyłączyć je przez `--links never`.
Ogólny launcher do pozostałych skryptów:
```text
python tools/run_workshop.py --check
python tools/run_workshop.py exercises/my_shot.py --render
python tools/run_workshop.py exercises/my_shot.py --single-frame 24
```
Bez `--render` launcher tylko wykonuje skrypt i zapisuje scenę (chyba że sam skrypt
uruchamia render). Zwraca kod 0 przy powodzeniu i 1 przy błędzie. Domyślny limit
to 900 sekund; można zmienić `--timeout`. Nie używaj `-nullrhi` do renderowania.
Tryb bez okna nadal wymaga GPU, sterowników i na Linuksie działającego Vulkan.
Obsługa Linuksa jest przygotowana w kodzie; trzeba sprawdzić ją na docelowej maszynie.
Ścieżki programów można ustawić zmiennymi środowiskowymi:
```powershell
$env:WORKSHOP_UNREAL = 'C:\Program Files\Epic Games\UE_5.7\Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
$env:WORKSHOP_BLENDER = 'C:\Program Files\Blender Foundation\Blender 5.1\blender.exe'
$env:WORKSHOP_FFMPEG = 'C:\tools\ffmpeg\bin\ffmpeg.exe'
```
```bash
export WORKSHOP_UNREAL=/opt/UnrealEngine/Engine/Binaries/Linux/UnrealEditor
export WORKSHOP_BLENDER=/usr/bin/blender
export WORKSHOP_FFMPEG=/usr/bin/ffmpeg
python3 tools/run_workshop.py --check --blender
```
Ścieżki nie są zapisane na sztywno w bibliotece. Launcher ma domyślną lokalizację
instalacji UE 5.7 na Windows; na Linuksie korzysta ze zmiennej lub PATH.
## Blender → UE → film
Przykładowy model znajduje się w `source_assets/character/character.blend`.
Można go też odtworzyć (generator nie nadpisuje istniejącego pliku):
```text
blender --background --python-exit-code 1 --python tools/create_character.py
```
Otwórz model w Blenderze, powiększ obiekt `Head`, zmień kolor materiału i **zapisz**.
Uruchom `examples/blender_character.py` w UE albo:
```text
python tools/run_workshop.py examples/blender_character.py
```
Skrypt uruchamia osobny proces Blendera, eksportuje kolekcję `Character` do GLB,
aktualizuje model i materiały w UE, buduje sekwencję i renderuje MP4.
Blender nie musi być otwarty. Eksport czyta ostatni zapis z dysku.
Umowa dla modelu: statyczne siatki w kolekcji `Character`, metry w Blenderze,
stopy na Z=0, przód w +X. Modyfikatory są uwzględniane, obiekty łączone do jednej
siatki z zachowaniem slotów materiałów. Origin eksportu to zero świata.
Podstawowe PBR: Principled BSDF, kolory, roughness, metallic, obsługiwane tekstury.
Tekstury PNG/JPEG są wyciągane z GLB i aktualizowane w wydzielonym katalogu
`WorkshopTextures`. Parametry PBR synchronizujemy jawnie, ponieważ standardowy
reimport siatki w UE zachowuje wcześniejsze materiały. Zmiany w tych importowanych
materiałach wykonuj w Blenderze; skrypt nadpisuje wspierane parametry ich źródłem.
Dowolne grafy proceduralne, rig, animacje szkieletowe, włosy i shader skóry nie
wchodzą w zakres v1. Materiały proceduralne należy wypalić do tekstur.
## Układ i zasady
- `Content/Python/ue_workshop/` — biblioteka, automatycznie dostępna w Pythonie UE.
- `exercises/my_shot.py` — jedyny wymagany plik uczestnika; zrób kopię przed edycją.
- `examples/` — dodatkowa kamera i Blender; ćwiczenie startowe jest też rozwiązaniem bazowym.
- `tools/` — launcher, eksport/generator Blendera i testy integracyjne.
- `docs/` — ściąga API i scenariusz spotkania.
- `Content/Workshop/Generated/` — mapy, sekwencje, importy i materiały generowane w UE.
- `build/`, `renders/` — pliki pośrednie i wyniki, pomijane przez Git.
- `archive/legacy_python/` — stare skrypty; poza ścieżką importu UE.
- `DemoAssets/` — zachowane źródłowe modele z poprzedniego projektu, opcjonalne.
`Scene("MyShot")` zarządza mapą `/Game/Workshop/Generated/MyShot/Map`.
Ponowne wykonanie usuwa w niej tylko aktorów z tagiem `Workshop`, a sekwencję
odbudowuje. Własną scenę nazwij inaczej, np. `Scene("AniaShot")`.
Generowana sekwencja jest w całości zastępowana: ręczne zmiany zachowaj w jej kopii.
Mapy spoza warsztatu nie są czyszczone; przy niezapisanej obcej mapie skrypt prosi o zapis.
Nie usuwaj całego folderu Generated podczas pracy — zawiera aktualnie używane assety.
## Weryfikacja
```text
python -m unittest discover -s tests -v
python tools/run_workshop.py tools/smoke_scene.py
python tools/run_workshop.py tools/smoke_blender.py
python tools/run_workshop.py tools/smoke_renderer.py
```
Pierwszy test integracyjny wykonuje ćwiczenie dwa razy, sprawdza duplikaty,
renderuje dwie klatki i koduje MP4. Drugi wymaga Blendera i sprawdza powtórny
import zmienionej geometrii oraz materiałów i render.
Opis sprawdzonego środowiska: `docs/validation.md`.
-15
View File
@@ -1,15 +0,0 @@
// Fill out your copyright notice in the Description page of Project Settings.
using UnrealBuildTool;
using System.Collections.Generic;
public class ExampleProjectTarget : TargetRules
{
public ExampleProjectTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
DefaultBuildSettings = BuildSettingsVersion.V7;
ExtraModuleNames.AddRange( new string[] { "ExampleProject" } );
}
}
@@ -1,23 +0,0 @@
// Fill out your copyright notice in the Description page of Project Settings.
using UnrealBuildTool;
public class ExampleProject : ModuleRules
{
public ExampleProject(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
PrivateDependencyModuleNames.AddRange(new string[] { });
// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// Uncomment if you are using online features
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
}
}
-6
View File
@@ -1,6 +0,0 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "ExampleProject.h"
#include "Modules/ModuleManager.h"
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, ExampleProject, "ExampleProject" );
-6
View File
@@ -1,6 +0,0 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
-12
View File
@@ -1,12 +0,0 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "dummy.h"
dummy::dummy()
{
}
dummy::~dummy()
{
}
-15
View File
@@ -1,15 +0,0 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
/**
*
*/
class EXAMPLEPROJECT_API dummy
{
public:
dummy();
~dummy();
};
-15
View File
@@ -1,15 +0,0 @@
// Fill out your copyright notice in the Description page of Project Settings.
using UnrealBuildTool;
using System.Collections.Generic;
public class ExampleProjectEditorTarget : TargetRules
{
public ExampleProjectEditorTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Editor;
DefaultBuildSettings = BuildSettingsVersion.V7;
ExtraModuleNames.AddRange( new string[] { "ExampleProject" } );
}
}
+3
View File
@@ -0,0 +1,3 @@
Stare skrypty zostaly zachowane jako material dodatkowy. Nie sa czescia
nowego warsztatu i moga wymagac assetow/uslug z poprzedniej wersji projektu.
Zachowano rowniez lokalna zmiane w legacy_python/basic_scripting_demo/3.py.
@@ -1,5 +1,5 @@
import unreal
sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = sub.get_all_level_actors()
import unreal
sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = sub.get_all_level_actors()
unreal.log(f"{len(actors)} actors in level")
@@ -1,16 +1,16 @@
import unreal
OLD = unreal.load_asset("/Game/ImportedAssets/Trees/SM_tree04.SM_tree04")
NEW = unreal.load_asset("/Game/ImportedAssets/Trees/SM_tree01.SM_tree01")
sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = sub.get_all_level_actors()
old_mesh_count = 0
for actor in actors:
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
if comp and comp.static_mesh == OLD:
old_mesh_count += 1
import unreal
OLD = unreal.load_asset("/Game/ImportedAssets/Trees/SM_tree04.SM_tree04")
NEW = unreal.load_asset("/Game/ImportedAssets/Trees/SM_tree01.SM_tree01")
sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = sub.get_all_level_actors()
old_mesh_count = 0
for actor in actors:
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
if comp and comp.static_mesh == OLD:
old_mesh_count += 1
print(f"Found {old_mesh_count} instances of the old mesh")
@@ -1,19 +1,20 @@
import unreal
OLD = unreal.load_asset("/Game/ImportedAssets/Trees/SM_tree04.SM_tree04")
NEW = unreal.load_asset("/Game/ImportedAssets/Trees/SM_tree01.SM_tree01")
sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = sub.get_all_level_actors()
old_mesh_count = 0
with unreal.ScopedEditorTransaction("Swap meshes"):
for actor in actors:
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
if comp and comp.static_mesh == OLD:
comp.set_editor_property("static_mesh", NEW)
# comp.set_static_mesh(NEW) -- tak tez mozna ale undo nie zadziala bo zmiana nie jest robiona przez edytor wiec jej on nie rejestruje
old_mesh_count += 1
import unreal
OLD = unreal.load_asset("/Game/ImportedAssets/Trees/SM_tree04.SM_tree04")
NEW = unreal.load_asset("/Game/ImportedAssets/Trees/SM_tree01.SM_tree01")
sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
actors = sub.get_all_level_actors()
old_mesh_count = 0
with unreal.ScopedEditorTransaction("Swap meshes"):
for actor in actors:
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
if comp and comp.static_mesh == OLD:
comp.set_editor_property("static_mesh", NEW)
# comp.set_static_mesh(NEW) -- tak tez mozna ale undo nie zadziala
# bo zmiana nie jest robiona przez edytor wiec jej on nie rejestruje
old_mesh_count += 1
unreal.log(f"Swapped {old_mesh_count} meshes")
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+39
View File
@@ -0,0 +1,39 @@
# Ściąga — ue_workshop
Import: `from ue_workshop import Scene, Sequence, Renderer, camera, lighting, materials, assets, blender, render`
| Operacja | Przykład |
|---|---|
| Własna mapa | `scene = Scene("MyShot")` |
| Kolor PBR (wartości liniowe 0–1) | `mat = materials.color("Red", (0.8, 0.02, 0.01), roughness=0.4)` |
| Prosta postać | `actor = scene.character(material=mat, head_scale=1.3)` |
| Bryła | `scene.primitive("Sphere", name="Ball", location=(0, 100, 50), material=mat)` |
| Import statycznego modelu | `mesh = assets.import_model("C:/models/model.glb", name="MyModel")` |
| Eksport + import Blendera | `mesh = blender.import_character("C:/models/character.blend")` |
| Model na scenie | `actor = scene.mesh(mesh, name="Hero")` |
| Światła studyjne | `lights = lighting.studio(scene)` |
| Pojedyncze światło | `lighting.area(scene, location=(200, -200, 350), intensity=10000, color=(1, 0.5, 0.2))` |
| Kamera | `cam = camera.create(scene, location=(450, -550, 300), target=(0, 0, 100), focal_length=45)` |
| Zmiana kierunku | `camera.look_at(cam, (0, 0, 150))` |
| Sekwencja | `shot = Sequence(scene, fps=24, frames=48)` |
| Aktywna kamera | `shot.camera(cam)` |
| Klucz pozycji | `shot.transform_key(actor, 0, location=(0, 0, 0))` |
| Klucz rotacji | `shot.transform_key(actor, 47, rotation=(0, 0, 360))` |
| Klucz skali | `shot.transform_key(actor, 24, scale=(1.2, 1.2, 1.2), interpolation="auto")` |
| Zapis + otwarcie Sequencera | `shot.save()` |
| Własny renderer | `renderer = Renderer(width=1920, height=1080, samples=8, warmup=32, mp4=True)` |
| Preset z własnymi wartościami | `renderer = Renderer.from_preset("final", samples=8)` |
| Render z własnymi ustawieniami | `renderer.render(shot)` |
| Podgląd tym samym rendererem | `renderer.render(shot, single_frame=24)` |
| PNG + MP4 | `render.start(shot, preset="preview")` |
| Tylko PNG | `render.start(shot, mp4=False)` |
| Jedna klatka | `render.start(shot, single_frame=24)` |
UE: centymetry, Z w górę. Rotacja w kluczach: **roll, pitch, yaw**, stopnie.
Klatki: **0..frames-1**, koniec zakresu wyłączny. Interpolacja: `linear`, `auto`, `constant`.
Cały obrót: klucze 0 i 360, nie 0 i 0. Kamera i modele to oryginalne obiekty UE:
możesz wywoływać ich metody, np. `actor.set_actor_label("MyHero")`.
`shot.asset` to oryginalny LevelSequence, `shot.bind(actor)` zwraca binding UE.
Przy pojedynczym podglądzie nie uruchamiaj dodatkowo pełnego renderu:
jednocześnie może pracować tylko jeden renderer warsztatowy.
+33
View File
@@ -0,0 +1,33 @@
# Weryfikacja v1 — 31 października 2026
Sprawdzone lokalnie: Windows, Unreal Engine 5.7, Blender 5.1.2 i FFmpeg.
Nie uruchamiano testów na Linuksie ani na Blenderze 4.x.
- Testy poza UE: 7 zaliczonych, w tym rzeczywiste kodowanie dwóch PNG do MP4,
odczyt częściowo zapisanych komunikatów i raportowanie liczby klatek.
- Postęp headless: sprawdzone etapy od startu UE do jego zamknięcia,
licznik zapisanych PNG i cichy zapis pełnych logów do pliku.
- Obiekt `Renderer`: własne 320×180, dwie próbki, rozgrzewka 0; sprawdzone
właściwości MRQ i rzeczywiste wymiary PNG. Walidacja błędnych argumentów
i nadpisywanie presetu. Launcher używa obiektu ze skryptu: sprawdzone 400×224.
- Dwukrotne wykonanie ćwiczenia w UE: bez dodatkowych aktorów, bindingów i Camera Cuts.
- Render MRQ w procesie edytora uruchomionym bez okna: 48 PNG oraz MP4, 24 FPS.
- `python exercises/my_shot_headless.py`: pełne ćwiczenie 1920×1080, 4 próbki,
rozgrzewka 32, 48 PNG i MP4. UE zamknął się automatycznie; oba kody zakończenia 0.
- Podgląd pojedynczej klatki z terminala (`--single-frame 24`): zaliczony.
- Odroczone zamknięcie edytora po renderze: kod zakończenia UE i launchera 0.
- Zwykły `UnrealEditor.exe`: projekt otwiera się bez żądania aktualizacji targetu
po usunięciu pozostałych receiptów i cache kompilacji UE 5.8.
- Wizualnie sprawdzony kadr, ekspozycja i wygenerowana postać.
- Pełny edytor z działającym UI Sequencera: otwarcie sekwencji, ticki UI,
przebudowa sceny i dwa kolejne rendery po 48 klatek do MP4 w jednym procesie.
Renderer startuje przez MoviePipelineQueueSubsystem po powrocie ze skryptu;
Sequencer jest zamykany przed przebudową bindingów oraz rozpoczęciem PIE.
- Blender: eksport zapisanej kolekcji, trzy materiały, aktualizacja istniejącego
StaticMesh pod tą samą ścieżką po zmianie geometrii.
- Zmiana koloru materiału oraz dodanie osadzonej tekstury: sprawdzone wartości
parametrów UE, przypisanie tekstury i render do MP4.
Uruchamianie sprawdzeń jest opisane w README. Wyniki każdego renderu są zapisane
w jego `result.json`; logi UE znajdują się w `Saved/Logs`.
Skrypty integracyjne generują własne mapy i assety w `Workshop/Generated`.
+25
View File
@@ -0,0 +1,25 @@
# Spotkanie — 60 minut
Przed spotkaniem: UE 5.7, pobrany projekt, skompilowane shadery, sprawdzony render.
FFmpeg potrzebny do MP4, Blender opcjonalnie. Przygotuj gotowy film na wypadek
wolniejszego sprzętu. Nie poświęcaj części praktycznej na instalowanie narzędzi.
| Minuty | Działanie |
|---|---|
| 0–15 | Python w edytorze, wykonanie pliku, aktor i asset, pokaz Blender → film. |
| 15–23 | `my_shot.py`: model, scena, klucze, Camera Cuts, render. Pokaż wynik w UI. |
| 23–53 | Własna kopia ćwiczenia, zmiany i krótki render. |
| 53–60 | Wyniki, pytania, pokaz launchera bez okna. |
Propozycje zadań, bez obowiązku wykonania wszystkich:
1. Zmień kolory postaci i oświetlenia, powiększ głowę.
2. Zmień pozycję i ogniskową kamery. Zrób pojedynczy podgląd.
3. Dodaj klucz w środku sekwencji: podskok albo zmianę skali.
4. Animuj kamerę — przykład w `examples/animated_camera.py`.
5. Opcjonalnie: zmień model w Blenderze i wykonaj `examples/blender_character.py`.
Punkt ratunkowy: świeża kopia `exercises/my_shot.py` z Git. Do pierwszych prób
zostaw `RENDER=False` i oglądaj Sequencera. Do renderu włącz `RENDER=True`.
Przy powrocie do skryptu pamiętaj wyłączyć drugi render, jeżeli dodajesz wywołanie
`render.start` ręcznie. Pokaż gdzie są PNG oraz `result.json`.
+13
View File
@@ -0,0 +1,13 @@
"""Wariant: nieruchoma postac i animowana kamera."""
from ue_workshop import Scene, Sequence, camera, lighting, materials
scene = Scene("CameraShot")
scene.character(material=materials.color("Orange", (0.8, 0.16, 0.03)))
scene.primitive("Cube", name="Floor", location=(0, 0, -10), scale=(20, 20, 0.2))
lighting.studio(scene)
cam = camera.create(scene, location=(450, -550, 300))
shot = Sequence(scene)
shot.camera(cam)
shot.transform_key(cam, 0, location=(450, -550, 300), interpolation="auto")
shot.transform_key(cam, 47, location=(280, -340, 230), interpolation="auto")
shot.save()
+18
View File
@@ -0,0 +1,18 @@
"""Najpierw wygeneruj source_assets/character/character.blend; instrukcja w README."""
from ue_workshop import Scene, Sequence, Renderer, blender, camera, lighting, materials
from ue_workshop.paths import PROJECT
mesh = blender.import_character(PROJECT / "source_assets/character/character.blend")
scene = Scene("BlenderShot")
character = scene.mesh(mesh, name="Character")
floor = materials.color("Floor", rgb=(0.07, 0.08, 0.11))
scene.primitive("Cube", name="Floor", location=(0, 0, -10), scale=(20, 20, 0.2), material=floor)
lighting.studio(scene)
cam = camera.create(scene)
shot = Sequence(scene, frames=48)
shot.camera(cam)
shot.transform_key(character, 0, rotation=(0, 0, 0))
shot.transform_key(character, 47, rotation=(0, 0, 360))
shot.save()
renderer = Renderer(width=640, height=360, samples=1, warmup=8)
renderer.render(shot)
+45
View File
@@ -0,0 +1,45 @@
"""Uruchom plik w UE: File > Execute Python Script. Zmieniaj i uruchamiaj ponownie."""
import importlib
import sys
# Ponowny import bibliotek (ue cachuje zaimportowaną biblioteke i jeśli nie bedzie
# jawnego reloadu/restartu edytora to będzie wykonywał stary kod)
if "ue_workshop" in sys.modules:
_render_module = sys.modules.get("ue_workshop.render")
if _render_module is not None and _render_module.is_rendering():
raise RuntimeError("Poczekaj na zakonczenie renderu przed przeladowaniem biblioteki.")
# Najpierw zaleznosci, potem moduly, ktore z nich korzystaja.
for _name in ("paths", "progress", "video", "materials", "camera", "lighting", "assets",
"_gltf", "blender", "scene", "sequence", "render"):
_module = sys.modules.get("ue_workshop." + _name)
if _module is not None:
importlib.reload(_module)
importlib.reload(sys.modules["ue_workshop"])
from ue_workshop import Scene, Sequence, Renderer, camera, lighting, materials
# 1. MODEL I SCENA. Jednostki: centymetry. Os Z wskazuje gore.
scene = Scene("MyShot")
blue = materials.color("Character", rgb=(0.04, 0.35, 0.8), metallic=0.2)
floor = materials.color("Floor", rgb=(0.07, 0.08, 0.11))
character = scene.character(material=blue, head_scale=1.0)
scene.primitive("Cube", name="Floor", location=(0, 0, -10), scale=(20, 20, 0.2), material=floor)
lighting.studio(scene, key_color=(1.0, 0.75, 0.5), fill_color=(0.3, 0.6, 1.0))
cam = camera.create(scene, location=(450, -550, 300), focal_length=45)
# 2. ANIMACJA. 48 klatek przy 24 FPS = 2 sekundy. Ostatnia klatka ma numer 47.
shot = Sequence(scene, fps=24, frames=48)
shot.camera(cam)
# Rotacja: (roll, pitch, yaw), w stopniach.
shot.transform_key(character, 0, rotation=(0, 0, 0))
shot.transform_key(character, 47, rotation=(0, 0, 360))
shot.save()
# 3. WYNIK. Najpierw obejrzyj animacje w Sequencerze, potem zmien RENDER na True.
RENDER = True
renderer = Renderer(width=1920, height=1080, samples=4, warmup=32, mp4=True)
# Mozesz tez zaczac od presetu i nadpisac wybrane wartosci:
# renderer = Renderer.from_preset("final", width=1920, height=1080)
if RENDER:
renderer.render(shot)
# Szybki podglad zamiast filmu: renderer.render(shot, single_frame=24)
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff. Show more