|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
本帖最后由 Maz马 于 2026-8-3 09:37 编辑
视差是一个非常能提升时髦值的东西
无论是用在主界面或是做一些伪3D场景
另外,来自版主的,关于视差的另一个帖子:
用bar制作简单移动视差效果(renpy进阶学习经验五)
https://www.renpy.cn/forum.php?mod=viewthread&tid=1557
26.8.2更新
1.支持多图片字典入参,不再是一张图一个组件
2.不再包装可显示对象,直接在内部包装
3.组件尺寸不再是全屏,而是图片尺寸,可以直观的使用位置特性
4.提供了鼠标感应锚点的模式
5.添加了晃动检测回调
6.兼容安卓陀螺仪,如果不用安卓方案,代码也很干净,放心食用
安卓水平仪支持方案
https://www.renpy.cn/forum.php?mod=viewthread&tid=1843
[RenPy] 纯文本查看 复制代码 # ---------- 视差组件 ----------
init python:
"""
Author: Maz
License: MIT
"""
import pygame
class ParallaxImage(renpy.Displayable):
def __init__(self,layers,mode="screen",callback=None,**kwargs):
super(ParallaxImage,self).__init__()
# 感应模式(感受鼠标偏移的基准):
# "screen" = 屏幕中心 (坐标输入,手机电脑通用)
# "canvas" = 画布中心 (坐标输入,手机电脑通用)
# "rotate" = 水平仪旋转(需要ANDROID_SENSOR,仅支持传感器的设备)
self.mode = mode if mode in ("screen", "canvas", "rotate") else "screen"
# 每层独立
self.layers = [{"child": child,"offset": offset,"x": 0,"y": 0} for child,offset in layers.items()]
# 首次渲染时初始化组件尺寸
self.w = 0
self.h = 0
self.initsize = False
# 输入源(水平仪/鼠标)
self.input_x = 0
self.input_y = 0
self.input_z = 0
# 时间轴
self.old_time = 0
self.delta_time = 0
# 晃动检测
self.shake_time = 0.0 # 采样时间戳
self.shake_timeout = 0.2 # 采样间隔(秒)
self.shake_window = 2.0 # 判定间隔(秒):窗口内波动剧烈视为晃动
self.shake_history = [] # 窗口内采样记录 [(time, x, y, z), ...]
self.shake_threshold = config.screen_width*1.5 # 判定阈值(像素):窗口内三轴最大-最小之和超过视为剧烈
self.shake_callback = callback # 触发晃动时调用的回调
# 晃动检测
def _check_shake(self,st):
# 守卫拦截
if not self.shake_callback:
return
# 采样节流
if st - self.shake_time < self.shake_timeout:
return
self.shake_time = st
# 记录输入源采样,只保留窗口内的
self.shake_history.append((st,self.input_x,self.input_y,self.input_z))
cutoff = st - self.shake_window
self.shake_history = [s for s in self.shake_history if s[0] >= cutoff]
if len(self.shake_history) < 2:
return
# 窗口内波动大于阈值 清空历史触发回调
xs = [s[1] for s in self.shake_history]
ys = [s[2] for s in self.shake_history]
zs = [s[3] for s in self.shake_history]
wave = (max(xs) - min(xs)) + (max(ys) - min(ys)) + (max(zs) - min(zs))
if wave > self.shake_threshold:
self.shake_history = []
if self.shake_callback:
self.shake_callback()
def render(self,width,height,st,at):
# 组件尺寸 首次渲染取图片尺寸,之后每帧直接复用
if not self.initsize:
child_render = renpy.render(renpy.displayable(self.layers[0]["child"]),width,height,st,at)
self.w,self.h = child_render.get_size()
self.initsize = True
render = renpy.Render(self.w, self.h)
# 输入源与归一化基准按模式区分;输入源缺失直接报错
if self.mode == "screen":
pos = renpy.get_mouse_pos()
if pos is None:
raise Exception("ParallaxImage screen 模式需要鼠标/触摸坐标")
mx,my = pos
self.input_x = mx - config.screen_width / 2
self.input_y = my - config.screen_height / 2
self.input_z = 0
half_w = config.screen_width / 2
half_h = config.screen_height / 2
elif self.mode == "canvas":
pos = renpy.get_mouse_pos()
if pos is None:
raise Exception("ParallaxImage canvas 模式需要鼠标/触摸坐标")
mx,my = pos
self.input_x = mx - self.w / 2
self.input_y = my - self.h / 2
self.input_z = 0
half_w = min(self.w / 2, config.screen_width - self.w / 2)
half_h = min(self.h / 2, config.screen_height - self.h / 2)
elif self.mode == "rotate":
if not ANDROID_SENSOR:
raise Exception("ParallaxImage rotate 模式需要 ANDROID_SENSOR(水平仪)")
self.input_x,self.input_y,self.input_z = ANDROID_SENSOR.update()
half_w = config.screen_width / 2
half_h = config.screen_height / 2
# 计算渲染间隔
if self.old_time == 0:
self.old_time = st
self.delta_time = st - self.old_time
self.old_time = st
# 各层共用输入源按各自 offset 绘制
for layer in self.layers:
if layer["offset"] == 0:
# 不位移
layer["x"] = 0
layer["y"] = 0
else:
# 归一化到 ±1 并钳位,保证最大位移 = abs(offset)
norm_x = (self.input_x / half_w) if half_w else 0
norm_y = (self.input_y / half_h) if half_h else 0
norm_x = max(-1.0, min(1.0, norm_x))
norm_y = max(-1.0, min(1.0, norm_y))
target_x = norm_x * layer["offset"]
target_y = norm_y * layer["offset"]
# 缓动公式(系数钳位到 ≤1:避免首帧/卡顿时 k>1 的过冲振荡)
k = 1.5 * self.delta_time * abs(layer["offset"])
k = max(0.0, min(1.0, k))
layer["x"] += (target_x - layer["x"]) * k
layer["y"] += (target_y - layer["y"]) * k
# 画布 = 图片尺寸,中心项抵消,无需再计算
obj_render = renpy.render(renpy.displayable(layer["child"]),width,height,st,at)
render.blit(obj_render,(layer["x"],layer["y"]))
renpy.redraw(self,0)
self._check_shake(st)
return render
# 使用方法:
#
# 定义一个图层字典,格式是 {图片:偏移}
# 设计约定所有图层大小必须一致,组件尺寸 = 统一图片尺寸(制图需预留偏移出血,偏移超出部分被截断)
# define xx = {"sky.png" :-25,
# "mount.png":-15}
#
# 正数=同向移动,负数=反向移动,数值绝对值 = 最大移动像素
#
# 然后可以当成image显示,也可以在screen里直接add,以下是参数含义
#
# layers 你写好的图层
# mode 感受鼠标偏移的模式 屏幕中心"screen"(默认)/ 画布中心"canvas" / 水平仪旋转"rotate"
# callback 扩展的回调,检测到持续晃动时可以调用某些行为,默认是None,如果lambda: renpy.open_url("https://www.pinduoduo.com/")就是"摇一摇打开拼多多"
#
# image xx = ParallaxImage(xx,mode="canvas")
废弃旧版(屎山留底)
[RenPy] 纯文本查看 复制代码 ### 跟随鼠标移动的视差 ###################################################################
init python:
import pygame
class TrackCursor(renpy.Displayable):
def __init__(self,child,paramod=1.0,**kwargs):
super(TrackCursor, self).__init__()
# 图片
self.child = child
# 移动参数:正数=同向移动,负数=反向移动,绝对值越大移动幅度越小
self.paramod = paramod
# 鼠标坐标
self.mouse_x = 0
self.mouse_y = 0
# 渲染坐标
self.x = 0
self.y = 0
# 时间轴
self.old_time = 0
self.delta_time = 0
# 尺寸
self.child_width = 0
self.child_height = 0
# 自适应渲染检查
self.size_initialized = False
# 事件方法
def event(self, ev, x, y, st):
if ev.type == pygame.MOUSEMOTION:
# 计算鼠标相对于屏幕中心的偏移量
center_x = config.screen_width / 2
center_y = config.screen_height / 2
offset_x = (x - center_x) / self.paramod
offset_y = (y - center_y) / self.paramod
self.mouse_x = offset_x
self.mouse_y = offset_y
# 渲染显示对象
def render(self, width, height, st, at):
# 首次渲染时初始化子对象尺寸
if not self.size_initialized:
child_render = renpy.render(self.child, width, height, st, at)
self.child_width, self.child_height = child_render.get_size()
self.size_initialized = True
render = renpy.Render(width, height)
# 计算渲染间隔
if self.old_time == 0:
self.old_time = st
self.delta_time = st - self.old_time
self.old_time = st
# 缓动公式
self.x += (self.mouse_x - self.x) * 1.5 * self.delta_time * abs(self.paramod)
self.y += (self.mouse_y - self.y) * 1.5 * self.delta_time * abs(self.paramod)
# 计算最终绘制位置:图片中心 + 偏移量
draw_x = (width - self.child_width) / 2 + self.x
draw_y = (height - self.child_height) / 2 + self.y
obj_render = renpy.render(self.child, width, height, st, at)
render.blit(obj_render, (draw_x, draw_y))
renpy.redraw(self, 0)
return render
#使用方法:
#两个入参,(图片和偏移系数)
#图片得传入renpy.displayable("xxx")
#偏移系数不能为0,大致来说越靠近0,偏移程度越大。正值跟随鼠标移动,负值反向移动。20-60是一个比较舒适的范围。
#示例:
#image a = TrackCursor(renpy.displayable("background.png"), paramod=-20)
#default b = TrackCursor(renpy.displayable("background.png"), paramod=20)
另外,来自群友@blurred 设计的草案,应用手机传感器根据偏向角的视差(就是安卓没有鼠标,用陀螺仪来检测)
我这里也代为贴上了,由于各个设备不同,因此安卓的偏转参数需要自己测试调整(会自行检测当前设备是PC还是安卓)
[RenPy] 纯文本查看 复制代码 init python:
import pygame
import math
# 倾角传感器管理器
class TiltSensorManager:
def __init__(self):
self.pitch = 0.0 # 俯仰角(前后翻动)
self.roll = 0.0 # 横滚角(左右倾斜)
self.yaw = 0.0 # 偏航角(左右转动)
self.initialized = False
self.sensor_listener = None
self.sensor_manager = None
self.accel_values = [0, 0, 0] # 加速度传感器
self.magnetic_values = [0, 0, 0] # 地磁传感器
# 尝试初始化传感器
self.init_sensors()
def init_sensors(self):
try:
# Android相关
import jnius
PythonActivity = jnius.autoclass('org.renpy.android.PythonSDLActivity')
Context = jnius.autoclass('android.content.Context')
Sensor = jnius.autoclass('android.hardware.Sensor')
SensorManager = jnius.autoclass('android.hardware.SensorManager')
activity = PythonActivity.mActivity
self.sensor_manager = activity.getSystemService(Context.SENSOR_SERVICE)
# 加速度传感器和地磁传感器
self.accelerometer = self.sensor_manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
self.magnetometer = self.sensor_manager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
if self.accelerometer is None or self.magnetometer is None:
print("设备缺少必要的传感器")
return
class SensorListener(jnius.PythonJavaClass):
__javainterfaces__ = ['android/hardware.SensorEventListener']
def __init__(self, manager, **kwargs):
super(SensorListener, self).__init__(**kwargs)
self.manager = manager
@jnius.java_method('(Landroid/hardware/SensorEvent;)V')
def onSensorChanged(self, event):
# 根据传感器类型存储数据
if event.sensor.getType() == Sensor.TYPE_ACCELEROMETER:
self.manager.accel_values = [event.values[0], event.values[1], event.values[2]]
elif event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD:
self.manager.magnetic_values = [event.values[0], event.values[1], event.values[2]]
# 计算倾角
self.manager.calculate_orientation()
@jnius.java_method('(Landroid/hardware/Sensor;I)V')
def onAccuracyChanged(self, sensor, accuracy):
pass
self.sensor_listener = SensorListener(self)
self.sensor_manager.registerListener(
self.sensor_listener,
self.accelerometer,
SensorManager.SENSOR_DELAY_GAME
)
self.sensor_manager.registerListener(
self.sensor_listener,
self.magnetometer,
SensorManager.SENSOR_DELAY_GAME
)
self.initialized = True
print("倾角传感器初始化成功")
except Exception as e:
print(f"无法初始化倾角传感器: {e}")
self.initialized = False
def calculate_orientation(self):
"""根据加速度和地磁数据计算设备方向"""
try:
import jnius
SensorManager = jnius.autoclass('android.hardware.SensorManager')
# 计算旋转矩阵
R = [0] * 9
I = [0] * 9
success = SensorManager.getRotationMatrix(R, I, self.accel_values, self.magnetic_values)
if success:
# 获方向数据
orientation = [0] * 3
SensorManager.getOrientation(R, orientation)
# 转换为角都(弧度转角度)
self.yaw = math.degrees(orientation[0]) # 偏航角(左右转动)
self.pitch = math.degrees(orientation[1]) # 俯仰角(前后翻动)
self.roll = math.degrees(orientation[2]) # 横滚角(左右倾斜)
except Exception as e:
print(f"计算方向时出错: {e}")
def stop(self):
if self.sensor_manager and self.sensor_listener:
try:
self.sensor_manager.unregisterListener(self.sensor_listener)
except:
pass
# 创建全局倾角传感器管理器
tilt_sensor_manager = None
if renpy.variant("mobile") and not (renpy.variant("pc") or renpy.variant("web")):
tilt_sensor_manager = TiltSensorManager()
# 老马出品的视差
class TrackCursor(renpy.Displayable):
def __init__(self, child, paramod=1.0, sensitivity=0.5, threshold=5.0,
flip_sensitivity=1.0, dead_zone=10.0, **kwargs):
super(TrackCursor, self).__init__()
# 图片
self.child = child
# 移动参数
self.paramod = paramod
# 灵敏度控制
self.sensitivity = sensitivity * 1.5
# 倾斜阈值(度),超过此值才开始移动
self.threshold = threshold
# 翻转灵敏度(控制翻转方向的影响程度)
self.flip_sensitivity = flip_sensitivity
# 死区角度(度),在此范围内不响应翻转
self.dead_zone = dead_zone
# 输入数据
self.input_x = 0
self.input_y = 0
# 渲染坐标
self.x = 0
self.y = 0
# 时间轴
self.old_time = 0
self.delta_time = 0
# 尺寸
self.child_width = 0
self.child_height = 0
# 自适应渲染检查
self.size_initialized = False
# 判断平台
self.is_mobile = renpy.variant("mobile") and not (renpy.variant("pc") or renpy.variant("web"))
# 基准角度(初始位置)
self.base_yaw = 0
self.base_pitch = 0
self.calibrated = False
# 翻转状态跟踪
self.last_flip_direction = 0 # 0: 未翻转, 1: 正向翻转, -1: 反向翻转
def event(self, ev, x, y, st):
if not self.is_mobile and ev.type == pygame.MOUSEMOTION:
# 计算鼠标相对于屏幕中心的偏移量
center_x = config.screen_width / 150
center_y = config.screen_height / 5.5
offset_x = (x - center_x) / self.paramod
offset_y = (y - center_y) / self.paramod
self.input_x = offset_x
self.input_y = offset_y
elif ev.type == pygame.MOUSEBUTTONDOWN and self.is_mobile:
self.calibrate_sensors()
def calibrate_sensors(self):
"""校准传感器,设置当前姿态为基准"""
if tilt_sensor_manager and tilt_sensor_manager.initialized:
self.base_yaw = tilt_sensor_manager.yaw
self.base_pitch = tilt_sensor_manager.pitch
self.calibrated = True
print(f"传感器已校准: yaw={self.base_yaw}, pitch={self.base_pitch}")
def calculate_flip_direction(self, angle_diff):
"""计算翻转方向并应用死区"""
if abs(angle_diff) < self.dead_zone:
return 0 # 在死区内,不响应
# 计算翻转方向(1: 正向, -1: 反向)
direction = 1 if angle_diff > 0 else -1
# 如果翻转方向改变,应用翻转灵敏度
if direction != self.last_flip_direction:
self.last_flip_direction = direction
return angle_diff * self.flip_sensitivity
return angle_diff
# 渲染显示对象
def render(self, width, height, st, at):
# 首次渲染时初始化子对象尺寸
if not self.size_initialized:
child_render = renpy.render(self.child, width, height, st, at)
self.child_width, self.child_height = child_render.get_size()
self.size_initialized = True
render = renpy.Render(width, height)
# 计算渲染间隔
if self.old_time == 0:
self.old_time = st
self.delta_time = st - self.old_time
self.old_time = st
# 根据平台获取输入数据
if self.is_mobile and tilt_sensor_manager and tilt_sensor_manager.initialized:
if not self.calibrated:
self.calibrate_sensors()
# 计算相对于基准的角度差异
yaw_diff = tilt_sensor_manager.yaw - self.base_yaw
pitch_diff = tilt_sensor_manager.pitch - self.base_pitch
# 处理偏航角(左右转动)的360度跳变问题
if yaw_diff > 180:
yaw_diff -= 360
elif yaw_diff < -180:
yaw_diff += 360
# 应用翻转方向检测和死区
yaw_diff = self.calculate_flip_direction(yaw_diff)
pitch_diff = self.calculate_flip_direction(pitch_diff)
if abs(yaw_diff) > self.threshold:
# 偏航角(左右转动)控制左右移动
self.input_x = yaw_diff * self.sensitivity
else:
self.input_x = 0
if abs(pitch_diff) > self.threshold:
# 俯仰角(前后翻动)控制上下移动
self.input_y = pitch_diff * self.sensitivity
else:
self.input_y = 0
elif not self.is_mobile:
pass
else:
self.input_x = 0
self.input_y = 0
# 缓动公式
self.x += (self.input_x - self.x) * 1.5 * self.delta_time * abs(self.paramod)
self.y += (self.input_y - self.y) * 1.5 * self.delta_time * abs(self.paramod)
draw_x = (width - self.child_width) / 2 + self.x
draw_y = (height - self.child_height) / 2 + self.y
obj_render = renpy.render(self.child, width, height, st, at)
render.blit(obj_render, (draw_x, draw_y))
renpy.redraw(self, 0)
return render
# image sky = TrackCursor(renpy.displayable("sky.png"), paramod=-25, sensitivity=1.0, flip_sensitivity=1.5, dead_zone=5.0)
# image mount = TrackCursor(renpy.displayable("mount.png"), paramod=-15, sensitivity=1.0, flip_sensitivity=1.2, dead_zone=8.0)
# paramod # 移动幅度参数(PC)
# sensitivity # 基本灵敏度
# threshold # 倾斜阈值(度)超过这个角度才会发生偏转
# flip_sensitivity # 翻转灵敏度
# dead_zone # 死区角度(度)在这个角度范围内需要偏转threshold才会相应偏转,在这个范围外翻转任意角度都会偏转
#点击头像 查看我写的更多屎
粉身碎骨浑不怕,要留答辩在人间
|
评分
-
查看全部评分
|